Upgrading from 1.x? See the 2.0 migration guide.
Installation
Install the umbrella package or individual signers as needed:
# Umbrella package (includes all signers)pnpm add @solana/keychain# Or install individual packagespnpm add @solana/keychain-core # Core interfaces (required for custom signers)pnpm add @solana/keychain-memory # Local keypair signerpnpm add @solana/keychain-vault # HashiCorp Vaultpnpm add @solana/keychain-aws-kms # AWS KMSpnpm add @solana/keychain-gcp-kms # GCP KMSpnpm add @solana/keychain-privy # Privypnpm add @solana/keychain-turnkey # Turnkeypnpm add @solana/keychain-fireblocks # Fireblockspnpm add @solana/keychain-cdp # Coinbase Developer Platformpnpm add @solana/keychain-crossmint # Crossmintpnpm add @solana/keychain-dfns # Dfnspnpm add @solana/keychain-openfort # Openfortpnpm add @solana/keychain-para # Parapnpm add @solana/keychain-utila # Utilapnpm add @solana/keychain-fordefi # Fordefi# Kit client plugins (keychainSigner / keychainPayer / keychainIdentity)pnpm add @solana/keychain-kit-plugin
Requires @solana/kit 8.1 or later. v1 transactions
(createTransactionMessage({ version: 1 })) are supported, except by Fireblocks
with useProgramCall: true, which accepts legacy and v0 only.
Basic Usage
Unified Factory (Recommended for apps wanting to support multiple signers)
Use createKeychainSigner with a discriminated config to create any backend:
import { createKeychainSigner } from "@solana/keychain";import { signTransactionWithSigners } from "@solana/signers"; // requires @solana/signers >= 8.1const signer = await createKeychainSigner({backend: "privy",appId: "your-app-id",appSecret: "your-app-secret",walletId: "your-wallet-id"});// Sign an already-compiled transactionconst signedTx = await signTransactionWithSigners([signer],compiledTransaction);
Or install an individual signer package for a smaller dependency footprint:
import { createPrivySigner } from "@solana/keychain-privy";const signer = await createPrivySigner({appId: "your-app-id",appSecret: "your-app-secret",walletId: "your-wallet-id"});
Using with Transaction Messages
Every backend except Crossmint and Fordefi native mode returns a
SolanaTransactionSigner, which is a Kit TransactionPartialSigner and works
with the @solana/signers partial-signer helpers below. Managed-broadcast
backends are covered in Signer Capabilities.
import type { SolanaTransactionSigner } from "@solana/keychain-core";import { signTransactionMessageWithSigners } from "@solana/signers";import {createTransactionMessage,setTransactionMessageFeePayerSigner,pipe} from "@solana/kit";async function signWithKeychain(signer: SolanaTransactionSigner) {// Check availability (useful for remote signers)if (!(await signer.isAvailable())) {throw new Error("Signer offline");}// Use with @solana/kit transaction builderconst transaction = pipe(createTransactionMessage({ version: 0 }),(tx) => setTransactionMessageFeePayerSigner(signer, tx)// ... set a lifetime and add instructions);// Sign with the standard signers APIconst signedTx = await signTransactionMessageWithSigners(transaction);return signedTx;}
Sign and Send with Any Signer
signAndSendTransaction from @solana/keychain-core gets a transaction on
chain whatever the signer's shape. Sending signers broadcast through their
provider; every other shape uses the sendTransaction you pass in (core has no
RPC dependency):
import { signAndSendTransaction } from "@solana/keychain-core";const signature = await signAndSendTransaction(signer, transaction, {sendTransaction: (tx) =>sendAndConfirmTransaction(tx, { commitment: "confirmed" })});
Using with a Kit Client (keychainSigner plugin)
@solana/keychain-kit-plugin installs a keychain signer directly on a
Kit client, wiring it up as the payer and
identity so you skip the manual pipe setup shown above:
import { createClient } from "@solana/kit";import { keychainSigner } from "@solana/keychain-kit-plugin";const client = await createClient().use(keychainSigner({backend: "privy",appId: process.env.PRIVY_APP_ID!,appSecret: process.env.PRIVY_APP_SECRET!,walletId: process.env.PRIVY_WALLET_ID!}));client.payer; // SolanaTransactionSigner, also a Kit TransactionPartialSignerclient.identity; // same signer instance
The plugin accepts the same backend-tagged config as createKeychainSigner,
except Crossmint and Fordefi native mode (chain set). Those backends rewrite
the transaction server-side, so they cannot act as a client payer or
identity; the plugin rejects them at compile time and at runtime. Only the
backend it dispatches to is bundled (backend packages load via dynamic
import()).
Use keychainPayer or keychainIdentity to set just one role, and mix backends
on a single client:
import { createClient } from "@solana/kit";import { keychainIdentity, keychainPayer } from "@solana/keychain-kit-plugin";const client = await createClient().use(keychainPayer({backend: "memory",privateKeyPath: `${process.env.HOME}/.config/solana/id.json`})).use(keychainIdentity({ backend: "turnkey", ...turnkeyConfig }));
Already have a SolanaTransactionSigner? It is a valid Kit
TransactionPartialSigner, so you can install it with
@solana/kit-plugin-signer
instead (pnpm add @solana/kit-plugin-signer):
import { createClient } from "@solana/kit";import { signer } from "@solana/kit-plugin-signer";import { createKeychainSigner } from "@solana/keychain";const mySigner = await createKeychainSigner({backend: "vault",...vaultConfig});const client = createClient().use(signer(mySigner));
Cancellation and Unconfirmed Broadcasts
signTransactions, modifyAndSignTransactions, signAndSendTransactions and
signMessages all accept { abortSignal } as their second argument. Aborting
stops waiting; it does not undo work a provider already accepted.
On Crossmint and Fordefi native auto mode, a failure does not mean nothing
landed. When the provider may have accepted the transaction, the error code is
SignerErrorCode.BROADCAST_UNCONFIRMED ("SIGNER_BROADCAST_UNCONFIRMED").
context.providerTransactionId is set when the create was accepted, and
context.idempotencyKey carries the key derived from the message. Resending the
byte-identical transaction is safe; a rebuilt one (for example with a new
blockhash) is a new transfer. providerMayHaveAccepted(error) and
providerStatus(error) from @solana/keychain-core help classify the error.
Reconcile with the provider before retrying.
Backend Configuration
Memory
Provide exactly one of keyPair, privateKey, privateKeyString or
privateKeyPath:
import { createMemorySigner } from "@solana/keychain-memory";const signer = await createMemorySigner({privateKeyPath: "/path/to/id.json"});
HashiCorp Vault
import { createVaultSigner } from "@solana/keychain-vault";const signer = createVaultSigner({vaultAddr: "https://vault.example.com:8200",vaultToken: "hvs.xxxxx",keyName: "my-solana-key",publicKey: "base58_public_key"});
AWS KMS
import { createAwsKmsSigner } from "@solana/keychain-aws-kms";const signer = createAwsKmsSigner({keyId: "alias/my-solana-key",publicKey: "base58_public_key",region: "us-east-1" // optional});
GCP KMS
import { createGcpKmsSigner } from "@solana/keychain-gcp-kms";const signer = createGcpKmsSigner({keyName:"projects/my-project/locations/us-east1/keyRings/my-ring/cryptoKeys/my-key/cryptoKeyVersions/1",publicKey: "base58_public_key"});
Privy
import { createPrivySigner } from "@solana/keychain-privy";const signer = await createPrivySigner({appId: "app_id",appSecret: "app_secret",walletId: "wallet_id"});
Turnkey
import { createTurnkeySigner } from "@solana/keychain-turnkey";const signer = createTurnkeySigner({apiPublicKey: "api_public_key",apiPrivateKey: "api_private_key",organizationId: "org_id",privateKeyId: "private_key_id",publicKey: "base58_public_key"});
Fireblocks
import { createFireblocksSigner } from "@solana/keychain-fireblocks";const signer = await createFireblocksSigner({apiKey: "api_key",privateKeyPem: "-----BEGIN RSA PRIVATE KEY-----\n...",vaultAccountId: "0",assetId: "SOL" // or "SOL_TEST" for devnet});
CDP (Coinbase Developer Platform)
import { createCdpSigner } from "@solana/keychain-cdp";const signer = await createCdpSigner({cdpApiKeyId: "api_key_id",cdpApiKeySecret: "api_key_secret",cdpWalletSecret: "wallet_secret",address: "base58_address",network: "solana" // or "solana-devnet"; required for address lookup tables});
Crossmint
import { createCrossmintSigner } from "@solana/keychain-crossmint";const signer = await createCrossmintSigner({apiKey: "api_key",walletLocator: "wallet_locator"});
Crossmint is sending-only. Its API always executes an approved transaction
server-side and sponsors gas, so the signer exposes only
signAndSendTransactions(): it has no signTransactions() or signMessages(),
and it cannot be installed with @solana/keychain-kit-plugin. The returned
signature identifies the transaction Crossmint landed.
const [signature] = await signer.signAndSendTransactions([transaction]);// or: await signAndSendTransactionMessageWithSigners(transactionMessage);
Dfns
import { createDfnsSigner } from "@solana/keychain-dfns";const signer = await createDfnsSigner({authToken: "auth_token",credId: "cred_id",privateKeyPem: "-----BEGIN EC PRIVATE KEY-----\n...",walletId: "wallet_id"});
Openfort
import { createOpenfortSigner } from "@solana/keychain-openfort";const signer = await createOpenfortSigner({secretKey: "sk_...",walletSecret: "wallet_secret",accountId: "account_id"});
Para
import { createParaSigner } from "@solana/keychain-para";const signer = await createParaSigner({apiKey: "api_key",walletId: "wallet_id"});
Utila
Utila signs transactions only; it has no signMessages().
import { createUtilaSigner } from "@solana/keychain-utila";const signer = await createUtilaSigner({serviceAccountEmail: "sa@example.com",serviceAccountPrivateKeyPem: "-----BEGIN PRIVATE KEY-----\n...",vaultId: "vault_id",walletId: "wallet_id",network: "networks/solana-devnet"});
Fordefi
Fordefi has three signing modes, fixed at construction by chain and
pushMode. Each mode exposes exactly one transaction method, and all three sign
messages:
| Mode | Config | Type | Method |
|---|---|---|---|
| Black box | chain unset | FordefiBlackBoxSigner | signTransactions() |
| Native auto | chain set, pushMode omitted or "auto" | FordefiNativeSigner | signAndSendTransactions() |
| Native manual | chain set, pushMode: "manual" | FordefiNativeManualSigner | modifyAndSignTransactions() |
API requests are authenticated with an ECDSA P-256 request signature: supply the
signing key as privateKeyPem, or keep it in a KMS/HSM by passing a
requestSigner.
Native auto mode (recommended for on-chain use): set chain, and Fordefi
signs and broadcasts the transaction itself. Because Fordefi may replace the
blockhash or fees before signing, the signer is a Kit
TransactionSendingSigner: use it with
signAndSendTransactionMessageWithSigners, not the partial-signer flow shown
above. Native auto mode accepts only an unsigned transaction whose sole required
signer is the vault, and it cannot be used with @solana/keychain-kit-plugin.
import { createFordefiSigner } from "@solana/keychain-fordefi";import { signAndSendTransactionMessageWithSigners } from "@solana/signers";import { readFileSync } from "node:fs";const signer = await createFordefiSigner({accessToken: process.env.FORDEFI_ACCESS_TOKEN!,vaultId: process.env.FORDEFI_VAULT_ID!,publicKey: process.env.FORDEFI_PUBLIC_KEY!, // Solana vault address (base58)privateKeyPem: readFileSync("./secret/private.pem", "utf8"),chain: "solana_devnet", // or "solana_mainnet"fee: { type: "custom", priority_fee: "1000" } // optional});const signature =await signAndSendTransactionMessageWithSigners(transactionMessage);
Native manual mode: set chain and pushMode: "manual". Fordefi rewrites
the message (recent blockhash, Compute Budget fee instructions) and signs it,
but does not broadcast. modifyAndSignTransactions() returns the transaction
Fordefi signed, which replaces the one you submitted: always continue from the
returned transaction. Fordefi must be the fee payer and must sign before any
other required signer. The rewrite is not diffed, so inspect the result before
broadcasting.
import { createFordefiSigner } from "@solana/keychain-fordefi";import { readFileSync } from "node:fs";const signer = await createFordefiSigner({accessToken: process.env.FORDEFI_ACCESS_TOKEN!,vaultId: process.env.FORDEFI_VAULT_ID!,publicKey: process.env.FORDEFI_PUBLIC_KEY!,privateKeyPem: readFileSync("./secret/private.pem", "utf8"),chain: "solana_devnet",pushMode: "manual"});const [signedTransaction] = await signer.modifyAndSignTransactions([transaction]);
Black box mode: omit chain. Fordefi signs raw bytes and does not
broadcast; you assemble and submit the transaction yourself. Use this with a
Fordefi black box vault.
import { createFordefiSigner } from "@solana/keychain-fordefi";import { readFileSync } from "node:fs";const signer = await createFordefiSigner({accessToken: process.env.FORDEFI_ACCESS_TOKEN!,vaultId: process.env.FORDEFI_BB_VAULT_ID!,publicKey: process.env.FORDEFI_BB_PUBLIC_KEY!,privateKeyPem: readFileSync("./secret/private.pem", "utf8")});
Signer Capabilities
@solana/keychain-core defines one interface per Kit signer shape. Each adds
isAvailable(): Promise<boolean> to the matching @solana/signers interface:
SolanaTransactionSigner(KitTransactionPartialSigner): returns signatures for a transaction you own.SolanaModifyingSigner(KitTransactionModifyingSigner): rewrites the transaction, signs it, and returns the new transaction without broadcasting.SolanaSendingSigner(KitTransactionSendingSigner): signs and broadcasts through its provider.SolanaMessageSigner(KitMessagePartialSigner): signs off-chain messages. This is separate from the transaction shapes.
SolanaSigner is the union of the three transaction shapes. Each factory's
return type names its backend's exact shape. When the config is only known at
runtime, narrow with isSolanaTransactionSigner, isSolanaModifyingSigner,
isSolanaSendingSigner or isSolanaMessageSigner. signerCapabilities(signer)
reports all four at once.
| Backend | Shape |
|---|---|
| memory, vault, aws-kms, gcp-kms, privy, turnkey, fireblocks, cdp, dfns, para, openfort, fordefi (black box) | SolanaTransactionSigner & SolanaMessageSigner |
| utila | SolanaTransactionSigner (no message signing) |
| fordefi (native manual) | SolanaModifyingSigner & SolanaMessageSigner |
| fordefi (native auto) | SolanaSendingSigner & SolanaMessageSigner |
| crossmint | SolanaSendingSigner (no message signing) |
import {isSolanaSendingSigner,isSolanaTransactionSigner} from "@solana/keychain-core";if (isSolanaSendingSigner(signer)) {await signer.signAndSendTransactions([transaction]);} else if (isSolanaTransactionSigner(signer)) {await signer.signTransactions([transaction]);}
Building Custom Signers
Implement the capability interfaces your backend supports:
import type {SolanaMessageSigner,SolanaTransactionSigner} from "@solana/keychain-core";import type { Address } from "@solana/addresses";import type {MessagePartialSignerConfig,SignableMessage,TransactionPartialSignerConfig} from "@solana/signers";import type { Transaction } from "@solana/transactions";class MyCustomSigner implements SolanaTransactionSigner, SolanaMessageSigner {constructor(readonly address: Address) {}async isAvailable(): Promise<boolean> {return await myBackend.healthCheck();}async signMessages(messages: readonly SignableMessage[],config?: MessagePartialSignerConfig) {return await myBackend.signMessages(messages, config?.abortSignal);}async signTransactions(transactions: readonly Transaction[],config?: TransactionPartialSignerConfig) {return await myBackend.signTransactions(transactions, config?.abortSignal);}}
Implement only what your backend supports. Kit classifies signers by which methods exist, so never add a method that just throws.
See the Adding Signers guide to integrate additional key management services.
Is this page helpful?