Multi-factor authentication
MFA is opt-in per project: Authentication → Settings → Multi-factor authentication. Users add an authenticator app (TOTP, 6 digits, 30 s). Once they did, password sign-in asks for a code.
// enroll (signed in): render data.totp.uri as a QR code, or show data.totp.secretconst { data: f } = await base.auth.mfa.enroll({ friendlyName: "Phone" })const { data: v } = await base.auth.mfa.verify({ factorId: f!.id, code: "123456" })showOnce(v!.recoveryCodes) // 10 single-use codes, only returned here
// sign in: password first, then the second stepconst r = await base.auth.signInWithPassword({ email, password })if (r.data.mfaRequired) { await base.auth.mfa.challenge({ ticket: r.data.ticket!, code }) // or { recoveryCode }}
// step up a session that signed in another way (magic link, OAuth...)const { data: aal } = await base.auth.mfa.getAuthenticatorAssuranceLevel()if (aal!.currentLevel !== aal!.nextLevel) await base.auth.mfa.verify({ code })
await base.auth.mfa.listFactors() // never returns secretsawait base.auth.mfa.unenroll({ factorId }) // a verified factor needs an aal2 sessionawait base.auth.mfa.regenerateRecoveryCodes() // aal2 sessionAll MFA calls return { data, error }; error.code is mfa_disabled until the project enables MFA.
Enforce it with RLS
Section titled “Enforce it with RLS”Tokens carry aal (aal1 or aal2). Enforce MFA on the data, not only in the app:
create policy "payouts need MFA" on public.payouts as restrictive for insert to authenticated with check ((select auth.aal()) = 'aal2');The dashboard policy editor has a Require MFA (aal2) option in Custom mode.
- The level belongs to the session: a refresh keeps
aal2, a new sign-in starts ataal1. - With “required for all” users, sessions of users without a factor carry
mfa_enrollment_required: trueso you can prompt them to enroll. Enforcement on data is still youraal2policy. - A code cannot be used twice (
mfa_code_reused). Five wrong codes in five minutes lock the second step for five minutes (429, doubling up to one hour). Recovery codes count toward the same limit. - Lost device: open the user in Authentication → Users and choose Reset MFA.