LazorkitProvider
Provider component — initialises the wallet store, the RPC connection and the paymaster config.
Mount once at the top of your client tree. LazorkitProvider creates the wallet store,
builds the RPC connection (at confirmed), and stores the portal and paymaster config.
import { LazorkitProvider } from '@lazorkit/wallet';Client only
This provider uses the portal (a dialog iframe or popup), navigator.credentials,
localStorage and IndexedDB. Mount it inside a client-side boundary — in Next.js App Router, wrap it
in a "use client" file.
Minimum setup
<LazorkitProvider>
<App />
</LazorkitProvider>Defaults: Solana devnet, portal.lazor.sh, LazorKit's devnet Kora paymaster.
Production setup
<LazorkitProvider
rpcUrl={process.env.NEXT_PUBLIC_RPC_URL}
cluster="mainnet"
portalUrl="https://portal.lazor.sh"
paymasterConfig={{
paymasterUrl: process.env.NEXT_PUBLIC_PAYMASTER_URL!,
apiKey: process.env.NEXT_PUBLIC_PAYMASTER_KEY,
}}
v1PaymasterConfig={{ paymasterUrl: process.env.NEXT_PUBLIC_V1_PAYMASTER_URL! }}
>
<App />
</LazorkitProvider>Mainnet runs protocol v1 today and the v2 mainnet program is not deployed: see Networks & versions before you point an app at mainnet.
Props
| Prop | Type | Default | Since |
|---|---|---|---|
rpcUrl | string | https://api.devnet.solana.com | |
portalUrl | string | https://portal.lazor.sh | |
paymasterConfig | { paymasterUrl: string; apiKey?: string } | https://kora.devnet.lazorkit.com | |
v1PaymasterConfig | { paymasterUrl: string; apiKey?: string } | paymasterConfig | 3.0.0 |
cluster | 'mainnet' | 'devnet' | read from rpcUrl, else mainnet | 3.0.0 |
onConfirmWallet | 'builtin' | 'throw' | ConfirmWalletHandler | 'builtin' | 3.0.0 |
trustedAuthorities | string[] — your own Ed25519 keys, base58 | none | 3.0.0 |
watchMints | string[] — SPL Token mints you receive | none | 3.0.0 |
keyStorage | 'auto' | 'memory' | 'auto' | 3.3.0 |
children | ReactNode | — |
portalUrl— where the passkey ceremony runs, and the relying party passkeys are created under (its hostname). Give an absolute URL: the sign dialog posts the stored credential id, passkey public key and wallet address to this URL's origin only, and posts nothing when it has none (3.3.1+; earlier releases posted them to whatever page the dialog's iframe showed).paymasterConfig— the Kora JSON-RPC endpoint for v2 wallets.apiKeyis sent asx-api-key; it ships in your bundle, so it is not a secret.v1PaymasterConfig— the relayer for users whose wallet is still on LazorKit v1: the one your app used before v2. LazorKit's v2 relayer does not sponsor v1, so set this wheneverpaymasterConfigpoints at a v2 relayer and you have v1 users. See Migrating from v1.cluster— which clusterrpcUrlserves. The SDK reads it from the URL (mainnet,devnet,localhost) and takes anything else as mainnet. Set it for your own RPC proxy or a keyed provider URL that does not name the cluster.onConfirmWallet,trustedAuthorities,watchMints— howconnecttreats a returning user's wallet.connectuses a wallet on its own only when it is the one wallet the passkey has signed for and nothing else can spend from it; otherwise it asks the user. See Wallet Confirmation.keyStorage— where the session and authority keys the SDK generates are kept:'auto'uses IndexedDB when it can,'memory'keeps nothing at rest (the key is gone on reload). See below.
The props are typed as LazorkitProviderProps internally; the type is not exported, use
React.ComponentProps<typeof LazorkitProvider>.
What the SDK stores in the browser
In localStorage of your origin:
| Key | What |
|---|---|
lazorkit-wallet-store, lazorkit-wallet, CREDENTIAL_ID, SMART_WALLET_ADDRESS, PUBLIC_KEY | The connected wallet (credential id, passkey public key, wallet PDA, vault, protocol version) and the provider config. disconnect clears the wallet. |
lazorkit-config | The provider config. |
lazorkit:passkey-lane:… | Per passkey: the slot of its last transaction (kept ten minutes) and any send whose outcome is not known yet. See Sending transactions. |
None of it is secret. No key is written to localStorage.
Keys the SDK generates
createSession (without sessionKey) and addAuthority generate an Ed25519 key, and the
SDK keeps it so that signAndSendWithSession / signAndSendWithAuthority sign without a
prompt. It keeps one of each kind: a new session or authority replaces the last one. A
sessionKey you pass is never stored. With keyStorage="auto" (the default) the key is
kept in the best of these the browser has:
- A non-extractable WebCrypto Ed25519 key in IndexedDB (database
lazorkit-keys), which signs withcrypto.subtle. A script on the page can make it sign while the page is open, but cannot copy it out. - Its seed, sealed with AES-GCM under a non-extractable key in the same database,
where the browser has no WebCrypto Ed25519 (iOS 16, Chrome 136 and older) or IndexedDB
cannot hold an Ed25519 key. Any script on the page can have that seed decrypted, so
this only keeps the key out of
localStorage. Once the browser has Ed25519, the seed is moved to (1). - The page's memory, without IndexedDB, outside a secure context, or where IndexedDB cannot hold a WebCrypto key at all. The key is gone on reload; its session stays on chain until it expires.
keyStorage="memory" uses (3) everywhere and keeps nothing at rest. In (1) and (2) the key
is still at rest in the browser profile: malware that can read the profile can copy it.
Each key signs only for its own wallet. It is stored with the wallet it was registered
for, and signAndSendWithSession, signAndSendWithAuthority and revokeSession()
(without sessionPda) use it only while that wallet is connected. Otherwise they reject
with KeyWalletMismatchError; see
Errors › Kept session and authority keys.
The key checks again when it signs and, since 3.4.0, right before each attempt to send
what it signed: a send that loaded the key before any disconnect neither signs nor sends
after it, even if the same wallet is connected again by then.
When a kept key is deleted:
disconnect()deletes the session key, whichever wallet it belongs to, and acreateSessionstill waiting for its transaction keeps no key once it lands.disconnect({ keepSessionKeys: true })keeps it. The authority key is kept, and signs once its wallet is connected again.LazorkitWalletAdapter.disconnect()(a wallet-adapter UI's Disconnect) and the Wallet Standardstandard:disconnectdelete the session key the same way, whateverkeyStorageis, and keep the authority key (3.3.1+; earlier releases left the session key).adapter.disconnect({ keepSessionKeys: true })keeps it;standard:disconnecttakes no options. Since 3.4.0 both also disconnect the store, so a kept key signs only once its wallet is connected again; in 3.3.1 the store stayed connected, and the authority key (and a session key kept withkeepSessionKeys) went on signing.revokeSession()deletes the session key once the revoke lands.removeAuthoritydeletes the authority key when it removes that authority.- A session key whose session has expired is deleted when it is next read.
forgetStoredKeys(), a package export, deletes both wherever they are kept (IndexedDB, the page's memory, plaintext an earlier release left). It rejects if IndexedDB holds keys and could not be cleared.
disconnect acts in its own tab: another tab of the app stays connected, and its
authority key keeps signing there. The keys in IndexedDB are shared by every tab, so call
forgetStoredKeys() at sign-out to leave none behind:
import { forgetStoredKeys, useWallet } from '@lazorkit/wallet';
function SignOutButton() {
const { disconnect } = useWallet();
return (
<button
onClick={async () => {
await disconnect();
await forgetStoredKeys();
}}
>
Sign out
</button>
);
}Changed in 3.3.0
3.2.1 and earlier wrote these secret keys to localStorage as plain text
(lazorkit-session, lazorkit-authority), and disconnect did not remove them. 3.3.0
moves such a key to IndexedDB when the provider mounts, or on first use, and deletes the
plaintext once the move has been written; the move is one-way. Removing those entries,
or localStorage.clear(), no longer removes the keys: call forgetStoredKeys(). See
Upgrading to 3.3.0.
Recipes
Next.js App Router
import { Providers } from './providers';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return <html><body><Providers>{children}</Providers></body></html>;
}'use client';
import { LazorkitProvider } from '@lazorkit/wallet';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<LazorkitProvider rpcUrl={process.env.NEXT_PUBLIC_RPC_URL}>
{children}
</LazorkitProvider>
);
}Switch networks at runtime
import { useState } from 'react';
import { LazorkitProvider } from '@lazorkit/wallet';
export function Providers({ children }: { children: React.ReactNode }) {
const [cluster] = useState<'devnet' | 'mainnet'>('devnet');
const rpcUrl = cluster === 'devnet'
? 'https://api.devnet.solana.com'
: 'https://api.mainnet-beta.solana.com';
return <LazorkitProvider rpcUrl={rpcUrl} cluster={cluster}>{children}</LazorkitProvider>;
}The stored wallet belongs to the cluster it was created on: switching clusters does not move it. Disconnect before switching, or the app keeps showing an address from the other cluster.
Multiple environments
const paymasterConfig = process.env.NODE_ENV === 'production'
? { paymasterUrl: process.env.NEXT_PUBLIC_KORA_MAINNET!, apiKey: process.env.NEXT_PUBLIC_KORA_KEY }
: { paymasterUrl: 'https://kora.devnet.lazorkit.com' };
<LazorkitProvider paymasterConfig={paymasterConfig}>{children}</LazorkitProvider>