Supabase MFA with an authenticator app
Supabase Auth's TOTP factor behind an animated code screen: enrol with the QR Supabase gives you, then challenge and verify the six digits to lift the session to aal2.
Loading live demoNot downloaded yet? Full refund within 14 days. Broken files get fixed or refunded. Refunds
- Download right after paying
- Next.js + Cursor/v0 ready
- Use it in client projects
By buying you agree to the Terms and ask for instant access, so you lose the 14-day withdrawal right. When we still refundWe'll also email you when a new piece lands (about once a week). Unsubscribe in one click, anytime.
Paid through Stripe. We never see your card.
Ready forCursorv0Lovableshadcn CLI
Use it in Next.js
"use client";
import { useRouter } from "next/navigation";
import { StepUpGate } from "@/components/StepUpGate";
import { createClient } from "@/lib/supabase/client";
const supabase = createClient();
export function MfaGate({ factorId, children }: { factorId: string; children: React.ReactNode }) {
const router = useRouter();
return (
<StepUpGate
title="Two-factor check"
description="Enter the 6-digit code from your authenticator app."
onVerify={async (code) => {
const { error } = await supabase.auth.mfa.challengeAndVerify({ factorId, code });
return !error; // the session is now aal2
}}
onVerified={() => router.refresh()} // server components re-read the upgraded session
>
{children}
</StepUpGate>
);
}import { createClient } from "@/lib/supabase/server";
import { MfaGate } from "./MfaGate";
// The code screen while this session still needs its second factor, or null once it's done.
export async function requireAal2() {
const supabase = await createClient();
const { data: aal } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel();
// enrolled but not verified in this session: ask for the code
if (aal?.currentLevel === "aal1" && aal.nextLevel === "aal2") {
const { data } = await supabase.auth.mfa.listFactors();
const factorId = data?.totp[0]?.id;
if (factorId) return <MfaGate factorId={factorId}>{null}</MfaGate>;
}
return null;
}import { requireAal2 } from "../require-aal2";
export default async function BillingPage() {
const gate = await requireAal2();
if (gate) return gate; // stop here: nothing below runs or reaches the browser at aal1
// read private data only after the check
return <main>Your balance and invoices</main>;
}const { data, error } = await supabase.auth.mfa.enroll({ factorType: "totp" });
// data.totp.qr_code is an SVG image: <img src={data.totp.qr_code} />
// then confirm with the first code, which also activates the factor:
await supabase.auth.mfa.challengeAndVerify({ factorId: data.id, code });Good to know
- The check sits at the top of each private page, not in a layout. Next.js still sends a page to the browser when its layout leaves {children} out, so a layout gate would leak the page at aal1. We caught that in our own test. After router.refresh() the server sees aal2 and renders the page.
- lib/supabase/client.ts and server.ts are the usual helpers from Supabase's Next.js guide (createBrowserClient and createServerClient from @supabase/ssr).
- Turn on TOTP under Authentication > Multi-Factor in the Supabase dashboard. It's free on every plan.
- Protect the data itself too: add a row level security policy that checks (auth.jwt() ->> 'aal') = 'aal2' on tables that need the second factor.
- A wrong code makes challengeAndVerify return an error, the gate shakes, and the visitor can type again.
- No onResend is passed, so the gate hides its Resend link. An authenticator app has nothing to resend. Not now shows a short neutral line; set cancelMessage to use your own words. Bought before October 22, 2026? Download the zip again to get both.
- Tested on Oct 21, 2026 in a fresh Next.js 15.5 app (React 19.1, @supabase/ssr 0.7, supabase-js 2.118) against a local mock of the Supabase Auth API, at 1440 and 390 px wide. At aal1 the page HTML holds only the code screen. A wrong code shakes the gate, the right one moves the session cookie to aal2 and the page appears. Enrolment returned a QR that renders. Not yet run on a live Supabase project.
Install
- Copy from the zipPut
react/src/components + react/src/dataintosrc/, thennpm i framer-motion @supabase/supabase-js @supabase/ssr. - Or ask your AIPaste PROMPT.md into Cursor, Lovable or v0.
Props
- onVerify
- (code) → Promise<boolean>, true unlocks
- onVerified
- after the unlock animation
- title / description
- the dialog's text
- length
- number of cells, 6 by default
- autoSubmit
- submit when the last cell fills
How qodesign compares
- qodesign vs Magic UIMagic UI is best for: landing pages built with shadcn/ui.
- qodesign vs Aceternity UIAceternity UI is best for: teams building many marketing pages who want one big library.
All comparisons, with prices read on each library's own site.
Questions
Can I use it for Supabase phone (SMS) MFA too?
Yes. Use factorType "phone", call challenge to send the SMS, and wire onResend to a new challenge.
Is the check done on the server?
Supabase does it. challengeAndVerify sends the code to Supabase Auth, which upgrades the session cookie, so a faked true in the browser gets nobody past an aal2 policy.
Can Cursor or v0 add it for me?
Yes. Each download has a PROMPT.md. Paste it into Cursor, Lovable, v0 or Claude Code and it fits the piece into your app.
Can I use it in client work?
Yes, in personal and client projects. You can't resell the code itself as a kit.