LazorKit LogoLazorKit
React SDK

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

PropTypeDefaultSince
rpcUrlstringhttps://api.devnet.solana.com
portalUrlstringhttps://portal.lazor.sh
paymasterConfig{ paymasterUrl: string; apiKey?: string }https://kora.devnet.lazorkit.com
v1PaymasterConfig{ paymasterUrl: string; apiKey?: string }paymasterConfig3.0.0
cluster'mainnet' | 'devnet'read from rpcUrl, else mainnet3.0.0
onConfirmWallet'builtin' | 'throw' | ConfirmWalletHandler'builtin'3.0.0
trustedAuthoritiesstring[] — your own Ed25519 keys, base58none3.0.0
watchMintsstring[] — SPL Token mints you receivenone3.0.0
keyStorage'auto' | 'memory''auto'3.3.0
childrenReactNode—
  • 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. apiKey is sent as x-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 whenever paymasterConfig points at a v2 relayer and you have v1 users. See Migrating from v1.
  • cluster — which cluster rpcUrl serves. 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 — how connect treats a returning user's wallet. connect uses 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:

KeyWhat
lazorkit-wallet-store, lazorkit-wallet, CREDENTIAL_ID, SMART_WALLET_ADDRESS, PUBLIC_KEYThe connected wallet (credential id, passkey public key, wallet PDA, vault, protocol version) and the provider config. disconnect clears the wallet.
lazorkit-configThe 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:

  1. A non-extractable WebCrypto Ed25519 key in IndexedDB (database lazorkit-keys), which signs with crypto.subtle. A script on the page can make it sign while the page is open, but cannot copy it out.
  2. 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).
  3. 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 a createSession still 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 Standard standard:disconnect delete the session key the same way, whatever keyStorage is, and keep the authority key (3.3.1+; earlier releases left the session key). adapter.disconnect({ keepSessionKeys: true }) keeps it; standard:disconnect takes 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 with keepSessionKeys) went on signing.
  • revokeSession() deletes the session key once the revoke lands. removeAuthority deletes 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

app/layout.tsx
import { Providers } from './providers';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return <html><body><Providers>{children}</Providers></body></html>;
}
app/providers.tsx
'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>