Feature spec
AdvancedCategory
auth-security
Build with AI
4–8 hours with Lovable or V0
Custom build
2–3 weeks custom dev
Running cost
$0–25/mo for TOTP-only; $25–300/mo if SMS MFA widely adopted
Works on
Everything it takes to ship Multi Factor Authentication — parts, prompts, and real costs.
MFA means a user proves identity with at least two factors. For a web app, ship TOTP (authenticator app) plus email OTP as your two baseline options using Supabase Auth's built-in MFA module. With Lovable or V0 and a detailed prompt, you can have enrollment, verification, and step-up auth working in 4–8 hours. Running cost is near zero for TOTP; SMS adds roughly $0.10/verification if you choose that path.
What Multi-Factor Authentication Actually Means for Your App
Multi-factor authentication asks users to prove identity with something they know (password) AND something they have (phone with authenticator app, or inbox for an OTP email). The key concept Supabase introduces is AAL — Authentication Assurance Level. AAL1 means the user passed password-only login. AAL2 means they also passed at least one enrolled MFA factor. Your app can check the current AAL level before granting access to sensitive routes or operations. Step-up auth takes this further: instead of requiring MFA only at login, you re-challenge on specific actions (delete account, export data, change payment method). This is what separates a robust MFA implementation from one that looks good until an attacker steals a session token.
What users consider table stakes in 2026
- At least two MFA factor types available — TOTP authenticator app and email OTP at minimum
- Enrollment wizard that walks through each step: choose factor, set up, verify with a live code before saving
- Step-up auth on sensitive actions — not just at login — re-prompting even if the user is already authenticated
- Admin setting to require MFA for users with high-privilege roles (admin, finance)
- Recovery path: 10 single-use backup codes generated at enrollment, shown once, stored hashed
- Activity log showing every successful and failed MFA attempt with timestamp and factor type
Anatomy of the Feature
Six components working together. Supabase Auth handles the factor storage internally; you build the UI flows and the AAL enforcement layer around it.
Supabase Auth MFA module
BackendSupabase Auth provides enrollMfa(), challengeMfa(), and verifyMfa() methods. Each enrolled factor is stored as an enrolledFactor record tied to the user. The module tracks AAL — after password login the session is AAL1; after a successful MFA challenge it becomes AAL2. All factor storage is handled internally in auth.mfa_factors — you do not manage this table directly.
Note: AAL2 is what you assert on protected routes. Check supabase.auth.mfa.getAuthenticatorAssuranceLevel() before rendering sensitive pages or accepting sensitive mutations.
TOTP factor
BackendRFC 6238 time-based 6-digit codes. Supabase Auth generates the TOTP secret server-side during enrollMfa(). The client renders it as a QR code using the qrcode.react React component or the qrcode npm package. The otpauth:// URI must include issuer name and user email for authenticator apps to display a meaningful account name.
Note: Use the exact URI format: otpauth://totp/YourApp:user@email.com?secret=BASE32SECRET&issuer=YourApp. Validate by scanning with both Google Authenticator and Authy before shipping.
Email OTP factor
BackendA Supabase Edge Function generates a 6-digit code, hashes it, stores hash + expiry in the mfa_otp table, then delivers it via Resend or Supabase SMTP. Code expires in 10 minutes. The Edge Function enforces rate limiting — max 3 OTP requests per user per 10-minute window to prevent inbox flooding.
Note: The code send must happen in an Edge Function, not in a client-side React component. Client-side OTP sending has no rate limiting and is a trivial spam vector.
SMS OTP factor (optional)
ServiceTwilio Verify API handles code generation, delivery, and verification in a single integration. Lovable's native Twilio connector makes wiring it to an Edge Function straightforward. Reserve SMS as a fallback for users who cannot use authenticator apps — TOTP is free, SMS costs $0.05/SMS sent plus $0.05/verification (approx).
Note: Do not make SMS the default. TOTP is phishing-resistant; SMS is vulnerable to SIM-swap attacks and adds ongoing cost per verification.
Factor enrollment UI
UIA multi-step wizard built with shadcn/ui Dialog and a Stepper component. Step 1: choose factor type (TOTP or email OTP). Step 2: setup — show QR code and manual secret for TOTP, or confirm email address for email OTP. Step 3: enter the live code to verify enrollment before saving. Step 4: show and download backup codes. Each step must validate before allowing Next.
Note: Never save an enrolled factor without verifying it. AI tools sometimes skip the verification step and enroll the factor on QR code display alone — this means a typo in the secret goes undetected.
Step-up auth gate
UIA React component (or Next.js middleware) that reads the current AAL level from Supabase Auth before allowing access to sensitive routes or triggering sensitive mutations. If the current session is AAL1 but the action requires AAL2, it intercepts and shows a fresh MFA challenge. Applies to /settings/security, delete account, export data, change email, and any payment modifications.
Note: Step-up auth must also be enforced in Edge Functions for sensitive mutations — check auth.jwt() ->> 'aal' = 'aal2' in the function middleware. A UI-only gate is bypassed by direct API calls.
The data model
Supabase Auth manages enrolled factors in auth.mfa_factors internally. You need three additional tables: mfa_otp for email OTP codes, backup_codes for recovery, and an audit_log for compliance. Run this in the Supabase SQL editor:
1create table public.mfa_otp (2 id uuid primary key default gen_random_uuid(),3 user_id uuid references auth.users(id) on delete cascade not null,4 code_hash text not null,5 factor_type text not null default 'email',6 expires_at timestamptz not null,7 used_at timestamptz,8 created_at timestamptz not null default now()9);1011alter table public.mfa_otp enable row level security;1213create policy "Users can read own OTP rows"14 on public.mfa_otp for select15 using (auth.uid() = user_id);1617create table public.backup_codes (18 id uuid primary key default gen_random_uuid(),19 user_id uuid references auth.users(id) on delete cascade not null,20 code_hash text not null,21 used_at timestamptz,22 created_at timestamptz not null default now()23);2425alter table public.backup_codes enable row level security;2627create policy "Users can read own backup codes"28 on public.backup_codes for select29 using (auth.uid() = user_id);3031create table public.mfa_audit_log (32 id uuid primary key default gen_random_uuid(),33 user_id uuid references auth.users(id) on delete cascade not null,34 event text not null,35 factor_type text,36 success boolean not null default true,37 ip_address text,38 created_at timestamptz not null default now()39);4041alter table public.mfa_audit_log enable row level security;4243create policy "Users can read own audit log"44 on public.mfa_audit_log for select45 using (auth.uid() = user_id);4647create index mfa_otp_user_expires_idx on public.mfa_otp (user_id, expires_at);48create index backup_codes_user_idx on public.backup_codes (user_id) where used_at is null;49create index mfa_audit_user_idx on public.mfa_audit_log (user_id, created_at desc);Heads up: The backup_codes partial index (WHERE used_at IS NULL) makes unused-code lookups fast even after a user has consumed most of their recovery codes over time. Insert into mfa_audit_log via an Edge Function using the service role key — the anon key cannot bypass RLS for audit writes from other users' sessions.
Build it — pick your path
Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.
Full control over all factor types, AAL enforcement, adaptive MFA triggers, compliance audit logs, and recovery flows. Required for apps with NIST 800-63B, SOC 2, or HIPAA requirements.
Step by step
- 1Implement all Supabase Auth MFA calls (enrollMfa, challengeMfa, verifyMfa) with full error handling for each factor type and failure mode
- 2Write Next.js middleware that enforces AAL at the route level with explicit per-route configuration
- 3Build step-up auth as a reusable React context that any component or Server Action can invoke
- 4Implement adaptive MFA: evaluate risk signals (new device fingerprint, VPN detection, unusual geography) and only challenge when risk score exceeds threshold
- 5Write automated compliance audit log pipeline: every MFA event written to append-only table with IP, user agent, and factor type
Where this path bites
- Security-critical code that requires expert review before shipping to production — budget for external penetration testing
- Adaptive MFA risk scoring requires a data pipeline and decision model that adds weeks of engineering beyond basic MFA
Third-party services you'll need
TOTP MFA is effectively free to run — Supabase Auth includes it. You only incur costs if you add SMS OTP or route email OTP through a paid transactional email service at scale:
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| Supabase Auth (TOTP MFA) | Factor enrollment, challenge, and verification; AAL level tracking; stores enrolled factors in auth.mfa_factors | Included in all Supabase plans — no per-MFA-verification charge | Pro $25/mo for production uptime SLA |
| Twilio Verify (SMS OTP) | SMS code generation, delivery, and verification for users who opt for SMS MFA | Free trial with $15 credit | $0.05/SMS sent + $0.05/verification (approx) |
| Resend (email OTP delivery) | Transactional email delivery for email OTP codes and recovery code notifications | 3,000 emails/mo free | $20/mo thereafter |
| Clerk MFA | Alternative all-in-one auth provider with built-in MFA management UI if you prefer not to manage Supabase Auth MFA manually | Free tier limited | Pro plan $25/mo includes MFA |
Swipe the table sideways to see pricing.
What it costs to run
Drag through the tiers to see how your monthly bill scales with users — no surprises later.
Estimated monthly running cost
TOTP + email OTP via Supabase free tier; Resend free tier covers OTP email volume; no SMS cost if TOTP-only offering
Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.
What breaks when AI tools build this
The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.
MFA enrollment exists but AAL level is never checked
Symptom: Lovable and V0 frequently generate the enrollment wizard and verification screens but never add the route-level AAL check. Users enroll MFA and feel secure — but they can skip MFA challenge entirely by navigating directly to /dashboard after a password-only login. The AAL is aal1 and no code stops them.
Fix: After generating MFA UI, explicitly verify the AAL check: call supabase.auth.mfa.getAuthenticatorAssuranceLevel() on every protected page load and redirect to the MFA challenge route if currentLevel is aal1 and nextLevel is aal2. Add this to Next.js middleware for blanket route enforcement.
Step-up auth missing from sensitive mutations
Symptom: The login MFA challenge is implemented correctly, but destructive actions — delete account, change email, export all data — don't re-challenge. A stolen session cookie allows an attacker to wipe the account without ever touching the MFA flow.
Fix: Every sensitive Edge Function or Server Action must independently check auth.jwt() ->> 'aal' = 'aal2' before proceeding. A UI gate alone is insufficient — direct API calls bypass it. Make the AAL check the first line of every sensitive function.
Email OTP sent from client without rate limiting
Symptom: AI generates OTP send logic inside a React onClick handler or a client component. There is nothing preventing an attacker from calling the send endpoint hundreds of times, flooding a victim's inbox and potentially triggering Resend's abuse detection, which suspends your account.
Fix: Route all OTP sends through a Supabase Edge Function. Inside the function, query mfa_otp for requests in the last 10 minutes from that user_id. If count >= 3, return a 429 error without sending. The rate limit enforcement must live in the database, not the client.
Recovery codes remain valid after first use
Symptom: AI generates recovery code storage and verification but omits the used_at check. A recovery code, once used to regain access, stays in the table as a valid code. Anyone who observed the code being entered can reuse it indefinitely.
Fix: Before accepting a recovery code, query WHERE code_hash = hash AND used_at IS NULL. If no row matches, reject. On success, SET used_at = now() in the same database transaction as the session grant — never in a separate call that might not complete.
QR code URI encoding breaks authenticator apps
Symptom: The otpauth:// URI that drives QR code enrollment is very format-sensitive. AI sometimes generates the URI with spaces not encoded as %20, colons not percent-encoded in the account name, or the issuer parameter missing. Google Authenticator silently fails to add the account with no error message shown to the user.
Fix: Use the exact format otpauth://totp/IssuerName:user@email.com?secret=BASE32SECRET&issuer=IssuerName. Never hand-roll the URI — let the Supabase Auth enrollMfa() return value provide the URI string. Test by scanning with Google Authenticator AND Authy before shipping; they fail differently.
Best practices
Make TOTP the default recommended factor — it is free to run, phishing-resistant, and works offline on the user's phone without cell service
Show backup codes only once at enrollment and display a prominent warning: 'Save these now — you cannot view them again'
Implement step-up auth for every destructive or high-value action, not just the login flow; a stolen session token should never be enough to delete an account
Rate-limit OTP sends in the Edge Function at the database level, not in client code — client-side rate limiting is trivially bypassed
Write every MFA event (enroll, verify success, verify failure, backup code use) to the audit log with timestamp and factor type; compliance requirements will ask for this
When a user has no enrolled factors and MFA is required for their role, send a weekly reminder email with a deadline before locking the account — not a hard immediate lockout
Test the full enrollment flow with both Google Authenticator and Authy before shipping — QR code parsing behavior differs between the two apps
Provide a 'remove factor' path in settings so users who change phones are not locked out; require step-up auth before allowing factor removal
When You Need Custom MFA Development
Supabase Auth MFA with Lovable or V0 handles TOTP and email OTP well for most apps. Certain requirements exceed what AI tools can reliably produce:
- FIDO2/WebAuthn hardware key support (YubiKey, passkeys) as an MFA factor alongside TOTP — the WebAuthn challenge flow requires custom Edge Functions and careful counter management
- Adaptive MFA that only challenges on risk signals — new device fingerprint, VPN exit node, geographic anomaly — requiring a real-time risk scoring pipeline, not a static enrollment check
- NIST 800-63B AAL2 or AAL3 compliance, SOC 2 Type II, or HIPAA Technical Safeguards — these require penetration-tested implementations and detailed audit documentation
- Admin-forced MFA enrollment with escalating enforcement: grace period, in-app reminders, login nag, account suspension — a multi-state machine that AI tools generate inconsistently
RapidDev builds these features for production
Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.
Frequently asked questions
What is the difference between 2FA and MFA?
Two-factor authentication (2FA) is a specific case of multi-factor authentication using exactly two factors. MFA is the broader term that includes systems with three or more factors, adaptive challenges, or multiple factor types the user can choose between. For most web apps, 2FA and MFA are functionally the same — you prompt for one additional factor after the password.
Is an authenticator app or SMS more secure for MFA?
Authenticator apps (TOTP) are more secure. SMS is vulnerable to SIM-swap attacks where an attacker convinces a carrier to transfer your phone number. TOTP codes are generated offline on the user's device and never transmitted over a carrier network. For most apps, TOTP should be the default recommendation with SMS as an optional fallback.
Can I require MFA for admins but keep it optional for regular users?
Yes. Store a required_mfa flag per user or role. In your middleware or Edge Functions, check the AAL requirement based on the user's role. Admin users who haven't reached AAL2 get redirected to the MFA challenge; regular users do not. Implement the enforcement in the database and Edge Functions, not just in the UI.
What happens if a user loses access to all their MFA factors?
This is why backup codes exist. At enrollment, generate 10 single-use recovery codes and instruct the user to save them offline. If backup codes are also lost, you need a manual identity verification process — typically an admin override that requires the user to submit a support ticket and verify identity before codes are regenerated. Design this before you ship MFA, not after the first lockout.
What is step-up authentication and do I need it?
Step-up auth means requiring a fresh MFA challenge for specific sensitive actions, even if the user already passed MFA at login. The login session might be hours old. Requiring re-authentication before deleting an account, changing the primary email, or exporting all data means a stolen cookie alone is not enough to do damage. If your app handles financial data, user-generated content with real value, or business records, you need step-up auth.
Which MFA method should I implement first for my MVP?
TOTP authenticator app. It costs nothing to run, works without cell service, is phishing-resistant, and Supabase Auth supports it natively. Email OTP is a reasonable second option for users who resist installing an authenticator app. Skip SMS for your MVP — the $0.10/verification adds up and SIM-swap risk makes it the weakest option.
Does MFA slow down the login experience significantly?
Only if you require it on every login. Most apps show the MFA challenge once per new device and skip it for known devices using a trust token. The extra 10–15 seconds to open an authenticator app is a well-accepted tradeoff for security-conscious users. Make TOTP the default since users can typically retrieve a code faster than waiting for an SMS.
Can I add MFA to an existing app without forcing all users to re-enroll?
Yes. Roll it out as optional first: add the enrollment flow and let users opt in. After adoption climbs, set a deadline for admin-role users to enroll before enforcement kicks in. Never force immediate hard enrollment on existing users who are already mid-session — it causes lockouts and support tickets. The Supabase Auth MFA enrollment flow is additive and does not require existing sessions to be invalidated.
Need this feature production-ready?
RapidDev builds multi factor authentication into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.