Authentication
Authentication is a singlesignIn() call: the user signs one message with their wallet and the SDK obtains a session from the server. After sign-in, the user has a privacy address — a public identifier (separate from their Ethereum address) that others use to send them private transfers — and your app can read their balances and history. The user’s privacy keys are not derived until something needs them: the first send, withdrawal, or shield unlocks the vault on demand, and a wallet-signature vault does that with one more wallet signature. See Key Management for details on key derivation.
Your app manages one thing, the session. It never manages whether keys are in memory.
Choosing an Auth Method
Before integrating authentication, choose how your app verifies users. See the auth method comparison table in App Setup. For development, the SDK authenticates directly — no backend needed. For production, you’ll route authentication through your backend using a token provider.User Experience
From the user’s perspective, authentication looks like this:- Your app calls
signIn() - User signs one message in their wallet
- User is signed in — balances and history load, with no further prompts
Wallet Popups
The sign-in message names the deployment (chain and pool) and states that it authorizes access to the account’s financial data and to editing contacts and labels. Servers that predate view sessions still accept the older message; against them
signIn() unlocks the vault immediately, so every read works either way.Returning Users
When a user returns and their session is still saved,signIn() signs nothing: the saved session reads the account straight away. Keys are never persisted by the session; the first write unlocks as usual. Persistence is on by default and can be turned off with session: { persist: false }.
Approval-only accounts are an exception: a new device, or a device without resident keys, signs once for login and once to authorize the one-time escrow export. The export signature is bound to a short-lived, single-use nonce; an unexpired bearer token alone cannot release permanent keys.
Locked and unlocked
sdk.auth.state is the one thing to render from:
Subscribe to
sdk.on('authChange', state => ...) to follow it. In React, useAuth().status reports 'locked' for a signed-in session whose keys aren’t resident — gate your signed-in shell on that too, not only on 'authenticated'. auth.lock(), and the idle or absolute auto-lock configured with idleTtlSecs and absoluteTtlSecs, drop the keys and keep the session: balances stay on screen and the next write prompts again. auth.signOut() ends everything.
Basic Usage
Approval-only EOA multichain authentication
SetapprovalOnly: true on a root SDK instance. The SDK discovers chainId and the chain’s AuthRegistry from that server’s /api/v1/info; applications must not supply an AuthRegistry address.
An EOA has exactly one global approval-only identity. Every chain must contain the account derived from that identity’s same owner and salt. Deployments fail closed with a conflict if an EOA has multiple approval-only accounts or if a chain contains a different account ID; such accounts require an operator-assisted migration and cannot be selected through this API.
When the identity exists globally but not on the current chain, authentication returns a normal EOA transaction request:
accountCreationRequired, so retrying after a slow wallet transaction or indexer delay always performs a fresh login.
Multiple chains
Approval-only mode currently uses one root SDK per chain;forChain() rejects this mode because approval plans and submission state are owned by the root KeyVault. For each chain:
- Create a root SDK with that chain’s
serverUrlandapprovalOnly: true. - Switch the wallet to the server-advertised chain before submitting a prepared transaction.
- Authenticate and retain that root only while using the chain.
- Call
logout()anddispose()before discarding it. Do not share a root instance or its session between chains.
Approval-only trust model
Approval-only multichain recovery is an explicit exception to split-key vault confidentiality. The TEE stores a server-decryptable viewing key and can read the nullifying key associated with the identity. Always Encrypted protects these values from a SQL operator, but a compromised TEE process can recover them and becomes the account’s privacy root. These keys reveal transaction history and ownership relationships but do not authorize spends: approval-only accounts still require an on-chain approval for every spend.PIN / Password Unlock
If the vault is protected by a PIN or password (persistence.unlock set to 'pin' or 'password'), the SDK asks for it through the prompts.credential handler you configure, at the moment it needs it:
- First time — when the vault is created, to encrypt it
- Returning user — the first write after sign-in, to decrypt it
action ('setup' or 'unlock'), unlockType, and attempt, which counts up on each wrong entry so your dialog can say “try again”. The SDK does the re-asking, so your handler needs no loop of its own; after ten attempts it stops and the unlock fails with INVALID_CREDENTIAL. Without a handler, a PIN vault fails with CREDENTIAL_REQUIRED. Reject with an error whose code is USER_CANCELLED when the user dismisses the dialog; the action is dropped and the app’s input is untouched.
Where to register prompts
PrivacyBoostConfig.prompts is convenient when you have the handlers at construction. Often you don’t: the UI that answers a prompt mounts after the SDK is created, changes with locale or route, and remounts. Register them where they’re rendered instead:
useAuthPrompts() does the registration, re-registration and cleanup for you, and is the place to put the promise bridge between the SDK’s callback and your modal:
New accounts: inline, or driven by your app
Creating an account is not a question with an answer — it’s a flow: show twelve words, wait for the user to store them properly, then create the vault. So it has two shapes. Inline. Wireprompts.recoveryPhrase and signIn() does the whole thing in one call, showing the phrase through your handler:
recoveryPhrase handler, and signIn() stops and hands you the phrase instead. Nothing exists yet — not on the server, not on chain:
complete() is safe to retry. A failure leaves the phrase staged and creates nothing, so you can keep the user on the same screen and let them try again — and it will be the same account, not a new one. That property is why this exists: retrying through signIn() alone could otherwise hand the user a second phrase and silently strand the first.
setup.abandon() discards the phrase, so the next signIn() starts a fresh account. Starting over is deliberate, precisely because a written-down phrase has to stay valid.
Only a brand-new account ever produces
setupRequired. A returning user always resolves to signedIn, so the branch runs once in an account’s lifetime.In a browser on the default iframe key vault it never runs at all: handing the phrase to your page is exactly what that vault exists to prevent, so with no recoveryPhrase handler the vault draws its own phrase screen and sign-in resolves signedIn. The app-driven flow is the one to use in Node, in local mode, and on iOS, Android and React Native.What belongs in a prompt
A prompt answers a question with a value. Keep to that:- Don’t call the SDK from inside one. An unlock is in progress and holds a single-flight gate, so a handler that calls an execution method deadlocks against it until the prompt times out.
- Always settle. A dialog dismissed without resolving or rejecting is bounded — a few minutes for a question, longer for a first-time recovery phrase, and shorter still when the prompt was raised from inside an operation that has its own deadline — and then fails with
PROMPT_TIMED_OUT. The operation is stuck until then, so don’t rely on it. - Don’t clear the user’s input on the failure path.
USER_CANCELLEDpromises the app’s input is intact; that promise is yours to keep on your side.
If persistence uses
biometric or passkey unlock, the platform prompt appears automatically — no extra code needed.Token Providers
A token provider is a function you pass toauthenticate() that routes the login request through your backend. Your backend adds credentials (API secret, Privy token, or custom JWT) before forwarding to Privacy Boost. This keeps secrets out of client-side code.
When do you need one? Only if your app uses API secret, Privy, or custom JWT authentication. For direct auth (development), no token provider is needed. See App Setup for details.
How it works
Implementation
{ token: string, expiresIn: number }.
Unlocking ahead of time
Execution methods unlock on their own, so an app never has to.auth.ensureUnlocked() exists for when the prompt should land at a moment you choose rather than inside the operation — a “Confirm in your wallet” step you want to render first, or warming the vault while a review screen is up. It’s a no-op once the keys are resident.
ensureUnlocked() on iOS, Android and React Native), and useAuth() exposes it in React.
Locking and Signing Out
Use
lock() for a “lock wallet” control or when the app goes to the background; the idleTtlSecs and absoluteTtlSecs timeouts do the same on their own. Use signOut() for a full sign-out.
Migrating from authenticate()
authenticate() and its result union (credentialRequired, mnemonicGenerated, recoveryRequired, accountCreationRequired) still work and are deprecated. A session it opens unlocks eagerly and ends on any lock, exactly as before. The four continuation cases map one-to-one onto the four prompts:
prompts; the SDK sends the account-creation transaction itself with the signed-in wallet, and a new user’s phrase remains available through revealRecoveryPhrase().
One thing to re-check while migrating: anything that gates a read on isAuthenticated. That flag still means “keys are in memory”, which a returning user’s signIn() deliberately does not give them. Gate reads on status === 'signedIn' (in React, useAuth().state, or the signedIn flag on the wallet store) and keep isAuthenticated only where resident keys are genuinely required.
Next Steps
If you’re using a production auth method, set up your integration next:API Secret
Server-to-server authentication with client credentials
Custom JWT
Auth0, Firebase, Supabase, Clerk, or any OIDC provider
Privy
Social login and embedded wallets via Privy
Dynamic
Wallet connection and embedded wallets via Dynamic
- Key Management — Choose a key source and configure persistence
- Error Handling — Handle auth and operation errors
- Keys & Encryption — Deep dive into how auth keys, viewing keys, and privacy addresses work