Upgrade guide

Upgrade your Web3.js project to Kit

Why upgrade?

Kit is a complete rewrite of the Web3.js library. It is designed to be more composable, customizable, and efficient than its predecessor. Its functional design enables the entire library to be tree-shaken, drastically reducing your bundle size. It also takes advantage of modern JavaScript features — such as native Ed25519 key support and bigint for large values — resulting in an even smaller bundle, better performance and most importantly, a reduced attack surface for your application.

Tree-shaking illustrationThe left side shows a bunch of imports from Web3.js all being bundled despite only a couple being used. The right side shows an equivalent set of imports for Kit but only the used functions are bundled.

Unlike Web3.js, Kit doesn't rely on JavaScript classes or other non-tree-shakeable features. This brings us to our first key difference.

Where's my Connection class?

In Web3.js, the Connection class serves as a central entry point, making the library's API easier to discover via a single object. However, this comes at a cost: using the Connection class forces you to bundle every method it provides, even if you only use a few. As a result, your users must download the entire library, even when most of it goes unused.

To avoid this, Kit does not include a single entry-point class like Connection. Instead, it offers a set of functions that you can import and use as needed. Two key functions replace most of the Connection class's functionality: createSolanaRpc and createSolanaRpcSubscriptions.

The former, createSolanaRpc, returns an Rpc object for making RPC requests to a specified endpoint. Read more about RPCs here.

Web3.js
import { Connection, PublicKey } from '@solana/web3.js';
 
// Create a `Connection` object.
const connection = new Connection('https://api.devnet.solana.com', {
    commitment: 'confirmed',
});
 
// Send RPC requests.
const wallet = new PublicKey('1234..5678');
const balance = await connection.getBalance(wallet);
Kit
import { ,  } from '@solana/kit';
 
// Create an RPC proxy object.
const  = ('https://api.devnet.solana.com');
 
// Send RPC requests.
const  = ('1234..5678');
const { :  } = await .().();

The latter, createSolanaRpcSubscriptions, returns an RpcSubscriptions object, which lets you to subscribe to events on the Solana network. Read more about RPC Subscriptions here.

Web3.js
import { Connection, PublicKey } from '@solana/web3.js';
 
// Create a `Connection` object with a WebSocket endpoint.
const connection = new Connection('https://api.devnet.solana.com', {
    wsEndpoint: 'wss://api.devnet.solana.com',
    commitment: 'confirmed',
});
 
// Subscribe to RPC events and listen to notifications.
const wallet = new PublicKey('1234..5678');
connection.onAccountChange(wallet, (accountInfo) => {
    console.log(accountInfo);
});
Kit
import { ,  } from '@solana/kit';
 
// Create an RPC subscriptions proxy object.
const  = ('wss://api.devnet.solana.com');
 
// Use an `AbortController` to cancel the subscriptions.
const  = new ();
 
// Subscribe to RPC events.
const  = ('1234..5678');
const  = await 
    .(, { : 'confirmed' })
    .({ : . });
 
try {
    // Listen to event notifications.
    for await (const  of ) {
        .();
    }
} catch () {
    // Gracefully handle subscription disconnects.
}

Note that, although Rpc and RpcSubscriptions look like classes, they are actually Proxy objects. This means they dynamically construct RPC requests or subscriptions based on method names and parameters. TypeScript is then used to provide type safety for the RPC API, making it easy to discover available RPC methods while keeping the library lightweight. Regardless of whether your RPC supports 1 method or 100, the bundle size remains unchanged.

Introducing Kit clients

The functional approach above gives you maximum treeshakability, but it can feel verbose compared to Web3.js's Connection. Kit addresses this with plugin-based clients — a single object that bundles the functionality you need while still letting you choose what goes in. You cherry-pick plugins, so you typically only bundle what you actually use.

Web3.js
import { Connection, PublicKey } from '@solana/web3.js';
 
// Create a `Connection` object.
const connection = new Connection('https://api.devnet.solana.com');
 
// Use the connection.
const wallet = new PublicKey('1234..5678');
const balance = await connection.getBalance(wallet);
Kit
import { , ,  } from '@solana/kit';
import {  } from '@solana/kit-plugin-rpc';
import {  } from '@solana/kit-plugin-signer';
import {  } from '@solana-program/system';
 
// Create a client with the plugins you need.
const  = await ();
const  = ().(()).(()).(());
 
// Use the client.
const  = ('1234..5678');
const { :  } = await ..().();

A Kit client is an immutable JavaScript object built by chaining .use() calls. Each plugin adds capabilities to the client — like signer roles, RPC access, transaction sending, or typed program instructions. Start with createClient() from @solana/kit, add signer plugins such as signer(payer), then apply RPC bundles like solanaDevnetRpc() to install RPC, subscriptions, transaction planning, sending, and devnet airdrops. You may then extend it with program plugins like systemProgram() and tokenProgram() to add typed access to the accounts and instructions of those programs.

The rest of this guide uses a client for the Kit examples. To learn more about the plugin system, see Plugins.

Fetching and decoding accounts

Fetching onchain accounts is as simple as calling the appropriate RPC method on the client. Kit also provides helper functions like fetchEncodedAccount, which returns a MaybeAccount object with an exists boolean to indicate whether the account is present onchain:

Web3.js
import { PublicKey } from '@solana/web3.js';
 
const wallet = new PublicKey('1234..5678');
const account = await connection.getAccountInfo(wallet);
Kit
import { , ,  } from '@solana/kit';
 
const  = ('1234..5678');
const  = await (., );
();
. satisfies ;

To decode raw account data, Kit provides a composable serialization library called codecs. You can define a codec for any account type by combining primitives:

Kit
import {
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
} from '@solana/kit';
 
type  = {
    : number;
    : number;
    : string;
    : ;
};
 
const  = ([
    ['discriminator', ()], // A single-byte account discriminator.
    ['wallet', ()], // A 32-byte account address.
    ['age', ()], // An 8-bit unsigned integer.
    ['name', ((), ())], // A UTF-8 string with a 32-bit length prefix.
]);
 
const  = .({
    : 42,
    : 0,
    : 'Alice',
    : ('1234..5678'),
});

You can read more about codecs here.

Once you have a codec, you can use decodeAccount to transform an encoded account into a decoded one:

Kit
import { , ,  } from '@solana/kit';
 
const  = ('1234..5678');
const  = await (, );
const  = (, );
 
if (.) {
    . satisfies ;
}

Using program libraries

Fortunately, you don't need to create a custom codec for every account. Kit provides client libraries for many popular Solana programs, generated via Codama, ensuring a consistent structure across them. These libraries can be used as standalone imports or as client plugins.

As client plugins, they add typed .accounts and .instructions namespaces to your client. For example, here's how to fetch and decode a Nonce account using the System program:

Web3.js
import { NonceAccount, PublicKey } from '@solana/web3.js';
 
const wallet = new PublicKey('1234..5678');
const accountInfo = await connection.getAccountInfo(wallet);
const nonce = NonceAccount.fromAccountData(accountInfo.data);
Kit
import {  } from '@solana/kit';
 
const  = await ....(('1234..5678'));

They can also be used as standalone functions without a client:

Kit
import {  } from '@solana/kit';
import {  } from '@solana-program/system';
 
const  = await (, ('1234..5678'));

Check out the "Available plugins" page for a list of available program libraries compatible with Kit.

Creating instructions

Program libraries also provide functions for creating instructions. With a client, you can create instructions through the program's typed namespace:

Web3.js
import { PublicKey, SystemProgram } from '@solana/web3.js';
 
const transferSol = SystemProgram.transfer({
    fromPubkey: new PublicKey('2222..2222'),
    toPubkey: new PublicKey('3333..3333'),
    lamports: 1_000_000_000, // 1 SOL.
});
Kit
import {  } from '@solana/kit';
 
const  = ...({
    : .,
    : ('3333..3333'),
    : 1_000_000_000, // 1 SOL.
});

Without a client, you can use the standalone instruction helpers directly. In this case, Kit uses TransactionSigner objects for accounts that need to sign. This allows signers to be extracted from built transactions later, enabling automatic transaction signing without manually tracking which keys need to sign.

Kit
import { ,  } from '@solana/kit';
import {  } from '@solana-program/system';
 
const  = await ();
const  = ({
    ,
    : ('3333..3333'),
    : 1_000_000_000, // 1 SOL.
});

Sending transactions

In Web3.js, sending a transaction involves creating a Transaction object, adding instructions, signing it, and sending it. Kit clients reduce this to a single call:

Web3.js
import {
    Keypair,
    PublicKey,
    sendAndConfirmTransaction,
    SystemProgram,
    Transaction,
} from '@solana/web3.js';
 
const payer = Keypair.generate();
 
const transaction = new Transaction().add(
    SystemProgram.transfer({
        fromPubkey: payer.publicKey,
        toPubkey: new PublicKey('3333..3333'),
        lamports: 1_000_000_000,
    }),
);
 
const signature = await sendAndConfirmTransaction(connection, transaction, [payer]);
Kit
import {  } from '@solana/kit';
 
const  = await ..
    .({
        : .,
        : ('3333..3333'),
        : 1_000_000_000,
    })
    .();
 
const  = ..;

With a client, building the transaction message, setting the fee payer, fetching a recent blockhash, signing, sending, and confirming are all handled for you.

You can also send multiple instructions as a single atomic transaction using client.sendTransaction([...]):

Kit
import {  } from '@solana/kit';
 
await .([
    ...({
        : .,
        : ('3333..3333'),
        : 500_000_000,
    }),
    ...({
        : .,
        : ('4444..4444'),
        : 500_000_000,
    }),
]);

For full control over each of these steps, you can build transactions manually. Kit uses an immutable transaction model where each helper returns a new transaction object with an updated type. The pipe function lets you chain these helpers together:

Kit
import {
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
} from '@solana/kit';
import {  } from '@solana-program/system';
import { ,  } from '@solana-program/token';
 
// Create signers — Kit uses the native CryptoKeyPair API for secure key management.
const [, ] = await .([(), ()]);
 
// Create instructions — signers are attached directly to instructions.
const  = ({
    ,
    : ,
    ,
    ,
    : ,
});
const  = ({
    : .,
    : ('1234..5678'),
    : 2,
});
 
// Build the transaction message.
const { :  } = await .().();
const  = (
    ({ : 0 }),
    () => (, ),
    () => (, ),
    () => ([, ], ),
);
 
// Sign — signers are extracted automatically from the transaction message.
const  = await ();
 
// Send and confirm.
();
const  = ({ ,  });
const  = ();
await (, { : 'confirmed' });

A few things to note about the manual approach:

  • Signers: Kit uses TransactionSigner objects (highlighted above) instead of raw keypairs. Since signers are attached to instructions and the fee payer, signTransactionMessageWithSigners can sign the transaction automatically without needing to pass signers separately. Read more about signers here.
  • Immutability: Each transaction helper returns a new object with a narrower type, ensuring that required fields (fee payer, lifetime, etc.) are set before the transaction can be signed or sent. The pipe function chains these transformations together.
  • Signatures: Transaction signatures are deterministically known before sending. The getSignatureFromTransaction helper extracts the signature from the signed transaction, and sendAndConfirm does not return it.

Closing words

We've now explored the key differences between Web3.js and Kit, giving you a solid foundation for upgrading your project. Since Kit is a complete rewrite, there's no simple step-by-step migration guide, and the process may take some time.

To ease the transition, Kit provides a @solana/compat package, which allows for incremental migration. This package includes functions that convert core types — such as public keys and transactions — between Web3.js and Kit. This means you can start integrating Kit without having to refactor your entire project at once.

Kit
import { ,  } from '@solana/web3.js';
import {  } from '@solana/kit';
import {
    ,
    ,
    ,
    ,
} from '@solana/compat';
 
// Convert `PublicKeys`.
const  = (new ('1234..5678'));
 
// Convert `Keypairs`.
const  = await (.());
const  = await ();
 
// Convert `TransactionInstruction`.
const  = ();
 
// Convert `VersionedTransaction`.
const  = ();

If you're using a Kit client, the upgrade is even more straightforward — the client-based API is conceptually closer to Web3.js's Connection model while still giving you the benefits of Kit's modular architecture. Check out the Getting Started tutorial to see the client-based approach in action or explore Plugins to learn more about the plugin system.

On this page