Appearance
Email Recovery (Passkey + Email)
Email recovery lets a user recover an existing NEAR account by:
- Deriving a new deterministic device key from a passkey (TouchID/FaceID).
- Sending a recovery email that your email recovery pipeline verifies.
- Finalizing on-chain registration and storing the new device locally.
This guide documents the SDK surface (setRecoveryEmails, getRecoveryEmails, startEmailRecovery, finalizeEmailRecovery) and required configuration.
Prerequisites
- The account already has at least one recovery email configured on-chain (the email address the user must send from).
- The account has enough NEAR to pay for the finalization transaction (
minBalanceYoctois checked before prompting). - Your account is using the EmailRecoverer contract (local or global) and exposes
get_recovery_attempt(request_id)for polling recovery status (the SDK can attach it automatically when setting recovery emails).
Configure Email Recovery
Configure relayer.emailRecovery in your SDK config:
tsx
import { TatchiPasskeyProvider } from '@tatchi-xyz/sdk/react/provider'
import { PASSKEY_MANAGER_DEFAULT_CONFIGS } from '@tatchi-xyz/sdk/react'
export function AppShell() {
return (
<TatchiPasskeyProvider
config={{
...PASSKEY_MANAGER_DEFAULT_CONFIGS,
relayer: {
...PASSKEY_MANAGER_DEFAULT_CONFIGS.relayer,
emailRecovery: {
...PASSKEY_MANAGER_DEFAULT_CONFIGS.relayer.emailRecovery,
// Inbox that receives recovery emails (mailto "to")
mailtoAddress: '[email protected]',
},
},
}}
>
{/* ... */}
</TatchiPasskeyProvider>
)
}Configure Recovery Emails (One-time Setup)
Email recovery requires the account to have a recovery email list on-chain. You typically do this while the user is already logged in on an existing device.
ts
// Adds/updates the recovery email list on-chain (and stores a local mapping in IndexedDB).
await tatchi.setRecoveryEmails('alice.testnet', ['[email protected]'], {
onEvent: (ev) => console.log('setRecoveryEmails', ev),
});You can display the currently configured recovery emails (best-effort: known emails are shown when this device has the local mapping; otherwise the hash is shown):
ts
const recoveryEmails = await tatchi.getRecoveryEmails('alice.testnet');
console.log(recoveryEmails); // [{ hashHex, email }]Relayer Integration
The relayer exposes POST /recover-email (see Relay Server Deployment). It accepts a ForwardableEmailPayload containing the full raw RFC822 email (raw) and parsed headers (must include subject).
Encrypted TEE Recovery
Encrypted email recovery lets the relayer submit DKIM verification without putting plaintext email on-chain. The relayer encrypts the raw RFC822 email to the Outlayer TEE public key and calls the per-account EmailRecoverer contract.
The per-account EmailRecoverer contract stores a recovery attempt keyed by request_id, which the frontend polls via get_recovery_attempt(request_id).
Route usage:
POST /recover-email- Optional mode override: JSON
explicitMode/explicit_modeor headerx-email-recovery-mode - Defaults to
tee-encryptedwhen no mode is provided
Direct SDK usage:
ts
import { AuthService } from '@tatchi-xyz/sdk/server'
const service = new AuthService({ /* relayer config */ })
const result = await service.emailRecovery?.verifyEncryptedEmailAndRecover({
accountId: 'alice.testnet',
emailBlob: rawEmailRfc822,
})ZK-Email Recovery
zk-email recovery uses a prover server to generate a proof from the raw RFC822 email, then submits the proof to the per-account recovery contract.
Prover API:
GET /healthz->{ "status": "ok" }POST /prove-emailwith{ "rawEmail": "<full RFC822 email>" }->{ proof, publicSignals }
Health checks:
- The SDK performs a lightweight
/healthzcheck before/prove-emailby default. - You can disable or tune this via
zkEmailProver.healthCheckin the relayer config.
AuthService config:
ts
import { AuthService } from '@tatchi-xyz/sdk/server'
const service = new AuthService({
/* relayer config */
zkEmailProver: { baseUrl: 'http://127.0.0.1:5588', timeoutMs: 60_000 },
})To force zk-email on the route:
- JSON:
explicitMode: "zk-email"(orexplicit_mode) - Header:
x-email-recovery-mode: zk-email
The relayer can also infer mode from the first non-empty body line (zk-email, tee-encrypted, onchain-public), but explicit mode is recommended for programmatic callers.
Async mode (avoid prover timeouts):
- Header:
Prefer: respond-async(or query?async=1) - Response:
202with{ "success": true, "queued": true, "accountId": "..." }
Client Flow
startEmailRecovery creates the new device key and returns a mailto: URL. finalizeEmailRecovery polls for on-chain verification completion and finalizes registration.
ts
const { mailtoUrl, nearPublicKey } = await tatchi.startEmailRecovery({
accountId: 'alice.testnet',
options: { onEvent: (ev) => console.log(ev) },
})
window.open(mailtoUrl, '_blank', 'noopener,noreferrer')
await tatchi.finalizeEmailRecovery({
accountId: 'alice.testnet',
nearPublicKey, // optional if you are resuming from pending state
options: { onEvent: (ev) => console.log(ev) },
})The user must send the email from one of the recovery emails configured on-chain (via setRecoveryEmails). If they send from a different address, DKIM/policy verification will fail and you should restart the flow.
Customizing the confirmation prompt
Email recovery runs inside the same wallet confirmation system as other sensitive flows. You can override the confirmer copy and confirmation UX:
ts
await tatchi.startEmailRecovery({
accountId: 'alice.testnet',
options: {
confirmerText: {
title: 'Recover account',
body: 'Approve to create a new device key and start email recovery.',
},
confirmationConfig: {
behavior: 'requireClick',
},
},
});Cancelling / restarting
If the user backs out after opening the mail client (or you want a “Start over” button), you can cancel the in-flight flow:
ts
await tatchi.cancelEmailRecovery({ accountId: 'alice.testnet' });Email Subject Format
The SDK generates:
recover-<REQUEST_ID> <ACCOUNT_ID> <NEW_PUBLIC_KEY>
Example:
recover-AB12CD alice.testnet ed25519:...
Resume and Retry
- Pending recovery state is stored in IndexedDB; after a reload you can call
finalizeEmailRecovery({ accountId })to resume. - If verification fails (wrong sender, DKIM failure, malformed subject), restart the flow with
startEmailRecovery.
Tracking requestId
The SDK generates a short requestId and embeds it in the email subject. It is also included in progress events so you can correlate UI state with relayer and contract logs:
ts
await tatchi.startEmailRecovery({
accountId,
options: {
onEvent: (ev) => {
if (ev?.data && 'requestId' in ev.data) {
console.log('email recovery requestId', ev.data.requestId)
}
},
},
})