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

Server
1
Client

Validate form

useActionState wires the form to the signIn() Server Action. Zod emailPasswordSchema validates email + password rules on the server.

2
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.

3
Server

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

Server
1
Client

Validate form

Same Zod schema enforces min 8 chars, uppercase, lowercase, and digit. Error message is returned and displayed inline.

2
Server

signUp with emailRedirectTo

Calls supabase.auth.signUp() with emailRedirectTo pointing to /auth/callback. If email confirmation is disabled, a session is returned immediately.

3
Server

Email confirmation

If confirmation is required, the action returns a success message. The user clicks the link in their inbox, which hits /auth/callback.

4
Server

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.

1
Client

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.

2
Server

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.

3
Client

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().

4
Server

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.

1
Server

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.

2
Client

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.

3
Server

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.

4
Client

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.

5
Server

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.

05

Passkey (WebAuthn)

Biometric authentication using the WebAuthn API via SimpleWebAuthn v11. Challenge is stored in an httpOnly cookie to prevent CSRF.

Register

Server
1
Client

Enter 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.

2
Server

getRegistrationOptions

Fetches existing credentials for the email to populate excludeCredentials (prevents duplicate registration). Challenge is stored in an httpOnly cookie with maxAge: 60.

3
Client

startRegistration

@simplewebauthn/browser.startRegistration() triggers the device biometric prompt. The browser creates a public/private key pair on-device. Returns a RegistrationResponseJSON.

4
Server

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

Server
1
Client

Click Sign in

No email required for authentication. The discoverable credential flow lets the device surface all registered passkeys for this relying party (rpID).

2
Server

getAuthenticationOptions

Generates options without allowCredentials — this is the discoverable flow. The browser automatically shows all passkeys for the rpID. Challenge stored in httpOnly cookie.

3
Client

startAuthentication

@simplewebauthn/browser.startAuthentication() shows the device passkey picker. The user selects a passkey and authenticates with biometrics. Returns AuthenticationResponseJSON.

4
Server

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.

06

Social OAuth

One-click sign-in via Google or GitHub. Supabase handles the OAuth dance; the app only needs to initiate and finalize.

1
Client

Click provider button

Each provider button calls signInWithProvider(id) via useTransition. The provider id ('google' | 'github') is passed directly to Supabase.

2
Server

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.

3
Server

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.

4
Server

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.

07

Passcode

Client-side numeric PIN pad — no server, no Supabase. A pure UX demo showcasing the OtpInput component and keypad interaction.

1
Client

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.

2
Client

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.

3
Client

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.

4
Client

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.