Password reset
Password recovery is three steps: request a recovery email, enter the code from the email, change the password. Under the hood, verifyOtp (with type: 'recovery') and updatePassword are reused, so there’s no separate “recovery mode” in AuthClient — after verifyOtp the user is in a regular signed-in session, just with an access token from a recovery grant.
await auth.requestPasswordReset({ email });
// user enters the code from the email:
await auth.verifyOtp({ email, token: code, type: 'recovery' });
// now we have a valid session — change the password:
await auth.updatePassword({ password: newPassword });requestPasswordReset
Triggers a recovery email. Like sendOtp, always returns ok (anti-enumeration).
Signature
auth.requestPasswordReset(input: {
email: string;
}): Promise<void>What’s in the email — a code or a link? The SDK expects a 6-digit code. The platform’s default email template sends a code; if a link arrives instead (legacy templates), extract token=... from it in your UI before calling verifyOtp.
updatePassword
Changes the password of the current signed-in session. Works after verifyOtp({ type: 'recovery' }) (a recovery session) and from a regular signed-in session (the “change password in settings” feature).
auth.updatePassword(input: { password: string }): Promise<void>When there’s no session
The method throws PaywallError('not_authenticated') before the network request. That means:
- the user didn’t pass
verifyOtp(or did, butAuthClienthadn’t stored the session — no such races exist, the method awaits); - the session expired and the refresh token is invalid.
In real UI this is a rare path: there are seconds between “enter code” and “enter new password”.
When the password is weak
The platform’s auth backend validates length (minimum 6) and applies any additional policy rules. A weak password yields PaywallError with code: 'weak_password', status: 400.
try {
await auth.updatePassword({ password: newPassword });
} catch (e) {
if (e instanceof PaywallError && e.code === 'weak_password') {
showError('Password is too weak');
return;
}
throw e;
}After updatePassword the session is NOT recreated. The access token stays the one issued after verifyOtp/regular signin. This is deliberate: there’s no need to sign out a user who just changed their password. If you want “sign out from all devices after a password change” — that’s a separate feature on the backend; the SDK doesn’t ship it.
Full “Forgot password” flow
Step 1: “Forgot password?” → request email
UI asks for the email and calls requestPasswordReset:
await auth.requestPasswordReset({ email });
showStep('enter-recovery-code');Step 2: enter the code
try {
await auth.verifyOtp({ email, token: code, type: 'recovery' });
showStep('enter-new-password');
} catch (e) {
if (e instanceof PaywallError && e.code === 'invalid_otp') {
showError('Wrong or expired code');
return;
}
throw e;
}Step 3: enter the new password
try {
await auth.updatePassword({ password: newPassword });
showStep('done');
} catch (e) {
if (e instanceof PaywallError && e.code === 'weak_password') {
showError('Password is too weak — add more characters');
return;
}
throw e;
}Step 4: done
After updatePassword the user is already signed in (AuthClient.getCachedUser() returns them) — redirect straight into the app, no need to force a re-login.
Alt-flow: “change password in settings”
If the user is already signed in and just wants to change their password — only updatePassword is needed. No recovery email.
// on the "Settings → Security" screen:
async function changePassword(currentPassword: string, newPassword: string) {
// (optional) re-verify the current password via signInWithEmail
const email = auth.getCachedUser()?.email;
if (!email) throw new Error('not authenticated');
await auth.signInWithEmail({ email, password: currentPassword });
// if signIn fails — current password is wrong; surface the error
await auth.updatePassword({ password: newPassword });
}Re-verifying via signInWithEmail is needed because updatePassword itself doesn’t require the current password — the access token is enough. Re-auth protects against “user left a laptop unlocked, someone changed the password”.
Security
- Anti-enumeration in UI. Show neutral messages on every step. After
requestPasswordReset— “If that email exists, we sent a message”, not “Email sent” (the latter reveals registration). - Recovery token is short-lived. The recovery session has the same lifetime as a regular access token (1 hour by default). If the user leaves the “enter new password” screen for half an hour —
updatePasswordstill works; for an hour and a half — they have to start over. - Hold the email in state between steps. Don’t make the user type it again — that’s friction and a typo opportunity.
Next steps
- Email OTP —
verifyOtpand the general code mechanics. - Session management —
signOut,onAuthChange, what the user sees after a password change.