Skip to content

Email Recovery (Passkey + Email)

Email recovery lets a user recover an existing NEAR account by:

  1. Deriving a new deterministic device key from a passkey (TouchID/FaceID).
  2. Sending a recovery email that your email recovery pipeline verifies.
  3. 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 (minBalanceYocto is 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_mode or header x-email-recovery-mode
  • Defaults to tee-encrypted when 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-email with { "rawEmail": "<full RFC822 email>" } -> { proof, publicSignals }

Health checks:

  • The SDK performs a lightweight /healthz check before /prove-email by default.
  • You can disable or tune this via zkEmailProver.healthCheck in 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" (or explicit_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: 202 with { "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)
      }
    },
  },
})