Reference
Auth Patterns
Technical walkthrough of all 7 authentication flows built in this project. Each pattern covers UI state, server actions, Supabase API calls, and redirect handling — end-to-end.
01
Email + Password
Classic credential auth with Zod validation, sign-in and sign-up flows, and optional email confirmation.
Sign In
ServerValidate form
useActionState wires the form to the signIn() Server Action. Zod emailPasswordSchema validates email + password rules on the server.
signInWithPassword
Supabase checks credentials. On error, the action returns { error } which is rendered in the error banner. Email and password fields never touch the client.
Redirect on success
redirect('/?success=1') is called outside the try-catch to avoid the NEXT_REDIRECT error. The SuccessToast component reads ?success=1 from the URL.
Sign Up
ServerValidate form
Same Zod schema enforces min 8 chars, uppercase, lowercase, and digit. Error message is returned and displayed inline.
signUp with emailRedirectTo
Calls supabase.auth.signUp() with emailRedirectTo pointing to /auth/callback. If email confirmation is disabled, a session is returned immediately.
Email confirmation
If confirmation is required, the action returns a success message. The user clicks the link in their inbox, which hits /auth/callback.
Callback → session
GET /auth/callback extracts the code param, calls exchangeCodeForSession, and redirects to /?success=1.
02
Email OTP
Passwordless two-step flow — email address then 8-digit one-time code. No password to store or leak.
Enter email
Step state is managed with useState. Submitting the email form calls requestOtp() via useTransition. On success, the UI transitions to the code entry step.
requestOtp — send code
Calls supabase.auth.signInWithOtp with shouldCreateUser: true. Supabase emails an 8-digit numeric OTP. Returns { ok: true, email } to advance the step.
Enter 8-digit code
OtpInput (variant='boxes') auto-advances focus and calls onComplete when all 8 boxes are filled. The completed value is passed directly to verifyOtp().
verifyOtp — exchange code
Strips non-digits, validates length, then calls supabase.auth.verifyOtp with type: 'email'. On success redirect() fires. On failure, { error } is returned and the code boxes are cleared.
03
Two-Factor Auth (TOTP)
TOTP enrolment via Supabase MFA — QR code scan, code activation, then ongoing login verification.
enrollTotp — generate QR
Called automatically on mount via useEffect. Unenrolls any existing TOTP factors first (clean slate), then calls supabase.auth.mfa.enroll(). Returns factorId, QR data URI, and plain-text secret.
Scan QR code
The QR data URI is rendered as an <Image> inside a white-background card. A 'Can't scan?' toggle reveals the plain-text secret for manual entry in the authenticator app.
activateTotp — activate factor
The user enters the 6-digit TOTP code from their app. The action calls mfa.challenge() to get a challengeId, then mfa.verify() to confirm the code. Returns { ok: true } to advance the UI to the login step.
Login prompt
After activation, the step switches to 'login'. The UI shows a success badge and a fresh OtpInput. The user enters their current TOTP code to complete sign-in.
loginWithTotp — verify and redirect
Identical challenge + verify pattern to activation, but calls redirect('/?success=1') on success instead of returning.
Supabase MFA requires an existing authenticated session before enrolling TOTP. In a real app this is a post-login step, not a standalone sign-in flow.
04
Magic Link
One-click passwordless login — user enters email, receives a sign-in link, clicks it, and lands authenticated.
Enter email
useActionState hooks the form to sendMagicLink(). The button shows a loading state while the action is in flight.
sendMagicLink — dispatch email
Calls supabase.auth.signInWithOtp with emailRedirectTo pointing to /auth/callback. Supabase sends a sign-in link with a one-time code embedded in the URL. Returns { ok: true, email }.
Waiting state UI
When state.ok is true, the form is replaced by an envelope illustration with a pulsing dot, step-by-step instructions, and the destination email address. Link expiry is noted (60 minutes).
GET /auth/callback — session exchange
The link lands on /auth/callback with a code param. The route handler calls exchangeCodeForSession(code) which establishes the session, then redirects to /?success=1.
05
Passkey (WebAuthn)
Biometric authentication using the WebAuthn API via SimpleWebAuthn v11. Challenge is stored in an httpOnly cookie to prevent CSRF.
Register
ServerEnter email + click Create
Email is required to associate the credential. The fingerprint frame glows blue with an animate-pulse overlay while the device prompt is active.
getRegistrationOptions
Fetches existing credentials for the email to populate excludeCredentials (prevents duplicate registration). Challenge is stored in an httpOnly cookie with maxAge: 60.
startRegistration
@simplewebauthn/browser.startRegistration() triggers the device biometric prompt. The browser creates a public/private key pair on-device. Returns a RegistrationResponseJSON.
verifyRegistration + save
Reads challenge from the cookie, verifies the response, then inserts credential_id, public_key (base64url), counter, and transports into the Supabase passkeys table. Redirects on success.
Authenticate
ServerClick Sign in
No email required for authentication. The discoverable credential flow lets the device surface all registered passkeys for this relying party (rpID).
getAuthenticationOptions
Generates options without allowCredentials — this is the discoverable flow. The browser automatically shows all passkeys for the rpID. Challenge stored in httpOnly cookie.
startAuthentication
@simplewebauthn/browser.startAuthentication() shows the device passkey picker. The user selects a passkey and authenticates with biometrics. Returns AuthenticationResponseJSON.
verifyAuthentication + update counter
Looks up the credential by response.id, verifies the signature, then updates the counter in the DB to prevent replay attacks. Redirects on success.
Credential data (credential_id, public_key, counter) is stored in a Supabase passkeys table. The service role key is used for DB access since WebAuthn does not go through Supabase Auth directly.
07
Passcode
Client-side numeric PIN pad — no server, no Supabase. A pure UX demo showcasing the OtpInput component and keypad interaction.
Numeric keypad
A 3×4 grid of buttons (1–9, blank, 0, backspace). Each tap appends a digit to the code string via useState. The backspace key slices the last character.
OtpInput display
OtpInput with variant='underline' and mask=true renders the code as bullet dots (like a PIN pad). Also accepts keyboard input via its own onChange handler.
Auto-submit at 6 digits
Both the keypad and OtpInput's onComplete call verify() when code.length reaches 6. A 600ms setTimeout simulates a server round-trip and shows the loading state.
Success or error
If the code matches CORRECT ('123456'), router.push('/?success=1') navigates away. Otherwise, error state is set (red dot on OtpInput), the code is cleared, and the user can retry.
This is a demo pattern only. In production, PIN verification must happen server-side against a hashed value — never hardcode the correct PIN in client code.
06
Social OAuth
One-click sign-in via Google or GitHub. Supabase handles the OAuth dance; the app only needs to initiate and finalize.
Click provider button
Each provider button calls signInWithProvider(id) via useTransition. The provider id ('google' | 'github') is passed directly to Supabase.
signInWithOAuth — get redirect URL
Calls supabase.auth.signInWithOAuth with the provider and redirectTo: /auth/callback. Supabase returns the full OAuth URL including state param. The action calls redirect(data.url) which triggers a NEXT_REDIRECT.
OAuth provider consent
The browser navigates to the provider's OAuth page (Google / GitHub). After the user grants access, the provider redirects back to /auth/callback with a code param.
GET /auth/callback — session exchange
The route handler calls exchangeCodeForSession(code). Supabase exchanges the code for a session and sets the auth cookie. The user is redirected to /?success=1.
Google and GitHub providers must be enabled in the Supabase dashboard under Authentication → Providers. Set the OAuth app's redirect URI to your-origin/auth/callback.