Account recovery
If passkeys are your primary sign-in method, you still need a recovery path for users who lose access to them. Common examples include a new device that was never synced, a lost security key, or a wiped password manager.
Mailbox challenges give you an email-based recovery step: prove control of the mailbox on the account, then issue a short-lived recovery session that lets the user re-enrol passkeys or complete other recovery-only actions.
Start the recovery challenge
Section titled “Start the recovery challenge”import { Passlock, isChallengeRateLimitedError } from "@passlock/server";
const passlock = new Passlock({ ... });
// cpatured in a recover account formconst email = "...";
const user = await findUserByEmail(email);if (!user) { throw new Error("Account not found");}
const result = await passlock.createMailboxChallenge({ email: user.email, userId: user.id, purpose: "account-recovery", // any pending challenges for this userId will be nuked invalidateOthers: true,});
if (result.success) { const { challengeId, secret, message } = result.value.challenge;
// save this in the user's session or a secure HTTP only cookie await savePendingChallenge({ purpose: "account-recovery" challengeId, secret, });
await emailCodeToUser({ email, message });} else if (isChallengeRateLimitedError(result.error)) { return showRateLimit(result.error.retryAfterSeconds);} else { throw new Error(result.error.message);}Verify the code and start recovery
Section titled “Verify the code and start recovery”import { Passlock } from "@passlock/server";
const passlock = new Passlock({ ... });
// fetch from the user's session or secure cookieconst pendingChallenge = await loadPendingChallenge({ purpose: "account-recovery" });
const result = await passlock.verifyMailboxChallenge({ challengeId: pendingChallenge.challengeId, secret: pendingChallenge.secret, code: form.code, // the user submitted this});
if (result.success) { const { challenge } = result.value;
// give the user 10 minutes to further verify // their identity and create a new passkey await createRecoverySession({ userId: challenge.userId, expiresInMinutes: 10, });
return redirectTo("/settings/account/recover");} else { return showVerificationError(result.error);}