TypeScript

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 packages
pnpm add @solana/keychain-core # Core interfaces (required for custom signers)
pnpm add @solana/keychain-memory # Local keypair signer
pnpm add @solana/keychain-vault # HashiCorp Vault
pnpm add @solana/keychain-aws-kms # AWS KMS
pnpm add @solana/keychain-gcp-kms # GCP KMS
pnpm add @solana/keychain-privy # Privy
pnpm add @solana/keychain-turnkey # Turnkey
pnpm add @solana/keychain-fireblocks # Fireblocks
pnpm add @solana/keychain-cdp # Coinbase Developer Platform
pnpm add @solana/keychain-crossmint # Crossmint
pnpm add @solana/keychain-dfns # Dfns
pnpm add @solana/keychain-openfort # Openfort
pnpm add @solana/keychain-para # Para
pnpm add @solana/keychain-utila # Utila
pnpm 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

Use createKeychainSigner with a discriminated config to create any backend:

import { createKeychainSigner } from "@solana/keychain";
import { signTransactionWithSigners } from "@solana/signers"; // requires @solana/signers >= 8.1
const signer = await createKeychainSigner({
backend: "privy",
appId: "your-app-id",
appSecret: "your-app-secret",
walletId: "your-wallet-id"
});
// Sign an already-compiled transaction
const 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 builder
const transaction = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(signer, tx)
// ... set a lifetime and add instructions
);
// Sign with the standard signers API
const 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 TransactionPartialSigner
client.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:

ModeConfigTypeMethod
Black boxchain unsetFordefiBlackBoxSignersignTransactions()
Native autochain set, pushMode omitted or "auto"FordefiNativeSignersignAndSendTransactions()
Native manualchain set, pushMode: "manual"FordefiNativeManualSignermodifyAndSignTransactions()

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 (Kit TransactionPartialSigner): returns signatures for a transaction you own.
  • SolanaModifyingSigner (Kit TransactionModifyingSigner): rewrites the transaction, signs it, and returns the new transaction without broadcasting.
  • SolanaSendingSigner (Kit TransactionSendingSigner): signs and broadcasts through its provider.
  • SolanaMessageSigner (Kit MessagePartialSigner): 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.

BackendShape
memory, vault, aws-kms, gcp-kms, privy, turnkey, fireblocks, cdp, dfns, para, openfort, fordefi (black box)SolanaTransactionSigner & SolanaMessageSigner
utilaSolanaTransactionSigner (no message signing)
fordefi (native manual)SolanaModifyingSigner & SolanaMessageSigner
fordefi (native auto)SolanaSendingSigner & SolanaMessageSigner
crossmintSolanaSendingSigner (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?

© 2026 Solana Foundation. All rights reserved.