Quickstart
One runnable path from zero to a working passkey wallet — create, sign, and recover a Stellar smart account, ES256-only and low-S by default.
One runnable path from zero to a working passkey wallet: create a smart account from a biometric, sign a transaction with it, and recover it on a new device. ES256-only, low-S by default, no seed phrase.
What you'll build
1 · Create
A passkey registers and deploys a Stellar smart-account wallet — one Face ID / Touch ID prompt.
2 · Sign
Approve a transaction; the assertion is DER → raw, low-S normalized, then assembled into Soroban auth.
3 · Recover
On a fresh device, a discoverable prompt + the indexer find every account the passkey controls.
Interactive create preview
The real component runs in mock mode on the demo. Open the demo →
Install
The SDK is the only required dependency:
pnpm add @soropass/corenpm i @soropass/coreTo go through Stellar Wallets Kit (Path B), add the kit and the module:
pnpm add @creit.tech/stellar-wallets-kit @soropass/wallets-kit-module@stellar/stellar-sdk is a peer dependency — it's never bundled into the SDK output. Install it alongside if your app doesn't already depend on it.
Path A — headless / direct
Call the three ceremonies straight from @soropass/core. Lead with the mock path: it runs in CI with no authenticator and no network, and the live path has the exact same shape.
import { createPasskeyKit } from '@soropass/core/testing';
// Deterministic in-memory authenticator + backend — no network, no authenticator.
const kit = createPasskeyKit({ mode: 'mock', rpId: 'localhost' });
const account = await kit.createPasskey({ userName: 'alice' });
console.log(account.contractId); // C-address
const signedEntry = await kit.signAuthEntry(entryXdr);
const accounts = await kit.recover(); // 0, 1, or many
// Swap mode 'mock' → 'live' (with real adapters) and the path stays green.import { createPasskey } from '@soropass/core/create';
import { signTransaction, browserPasskeySigner } from '@soropass/core/sign';
import { recover, eventsIndexer } from '@soropass/core';
// 1. Create — registers an ES256 passkey and deploys the smart account.
const { contractId, credentialId } = await createPasskey({
rpId: 'example.com',
rpName: 'Example',
userName: 'alice',
deployer, // <- the only thing you supply (AccountDeployer)
});
// 2. Sign — the button press is the WebAuthn user gesture.
const signedXdr = await signTransaction(txXdr, {
networkPassphrase,
sign: browserPasskeySigner({ rpId: 'example.com', allowCredentials: [credentialId] }),
});
// 3. Recover — discoverable prompt + indexer, on any device.
const accounts = await recover({
rpId: 'example.com',
indexer: eventsIndexer({ rpcUrl, factoryContractId }),
});The deployer (an AccountDeployer) is the only piece the adopter supplies — it deploys the smart account for a new passkey against your factory. Everything else (DER → raw, low-S, the Soroban auth struct) is handled inside the SDK.
Path B — through the kit (recommended)
Register a PasskeyModule and passkey becomes one more wallet in @creit.tech/stellar-wallets-kit — your existing getAddress / signTransaction calls are unchanged. StellarWalletsKit is a static class; never new it.
import { PasskeyModule, PASSKEY_ID } from '@soropass/wallets-kit-module';
import { eventsIndexer } from '@soropass/core';
import { StellarWalletsKit } from '@creit.tech/stellar-wallets-kit/sdk';
import { Networks } from '@creit.tech/stellar-wallets-kit';
const passkey = new PasskeyModule({
rpId: 'example.com',
rpName: 'Example',
networkPassphrase: Networks.TESTNET,
indexer: eventsIndexer({ rpcUrl, factoryContractId }),
deployer,
});
StellarWalletsKit.init({ network: Networks.TESTNET, modules: [passkey] });
// createAccount lives on the MODULE, not the kit:
const account = await passkey.createAccount('alice');
// Then drive everything through the static kit:
StellarWalletsKit.setWallet(PASSKEY_ID);
const { address } = await StellarWalletsKit.getAddress();
const { signedTxXdr } = await StellarWalletsKit.signTransaction(txXdr, { networkPassphrase });Two facts to keep in mind. module.isAvailable() resolves isUVPAA within a 500ms budget — that's what gates whether passkey appears in the wallet picker. And raise the resource budget after simulate(): on-chain secp256r1_verify is under-budgeted by simulation, so a transaction sent at the simulated budget will fail.
Handle errors
Every ceremony throws a KitError with a code from the frozen 10-code taxonomy. Narrow with isKitError(e), then switch on e.code:
import { isKitError, KIT_ERROR_CODES } from '@soropass/core/types';
// KIT_ERROR_CODES (frozen, 10):
// USER_CANCELLED, ES256_NOT_SUPPORTED, RP_ID_MISMATCH, ORIGIN_MISMATCH,
// CHALLENGE_MISMATCH, INVALID_SIGNATURE_DER, INVALID_PUBLIC_KEY,
// CONTRACT_AUTH_FAILED, NETWORK_ERROR, UNSUPPORTED_AUTHENTICATOR
try {
await signTransaction(txXdr, { networkPassphrase, sign });
} catch (e) {
if (isKitError(e)) {
switch (e.code) {
case 'USER_CANCELLED': return; // user dismissed the sheet
case 'NETWORK_ERROR': /* retry */ break;
case 'ES256_NOT_SUPPORTED': /* wrong algorithm */ break;
}
}
throw e;
}The full taxonomy with per-code copy lives in the KitError taxonomy. The styled screens already render the right message per code.
Test in CI (no authenticator)
Two options for deterministic, zero-network tests: the createPasskeyKit({ mode: 'mock' }) facade shown above, or the lower-level primitives when you need to drive the raw ceremonies yourself.
import { mockAuthenticator, createInMemoryBackend } from '@soropass/core/testing';
// Deterministic P-256 keypair + credentialId from a seed; a real attestationObject.
const auth = mockAuthenticator({ rpId: 'localhost', seed: 'alice' });
// Zero-IO deployer + indexer + submission sharing one registry.
const backend = createInMemoryBackend();
// Run create → sign → recover with backend.deployer / backend.indexer,
// and auth as the WebAuthn client / signer — no network, no real authenticator.What's next
Create screen
The drop-in styled create screen and its states.
SDK · Sign
Soroban auth assembly, challenge binding, low-S.
Recover & Connect
Silent reconnect and the new-device recovery path.
Adapters
Pluggable submission + indexer; the zero-infra defaults.
KitError taxonomy
All 10 frozen codes and what to show for each.
Compatibility
The living matrix: where passkeys work today.
Want a styled flow instead of wiring the screens yourself? Mount one from @soropass/ui/styled — see the component docs.