SecureStartKit
SecurityFeaturesPricingDocsBlogChangelog
Sign inBuy Now
Getting Started
Installation
Configuration
Deployment

Components

  • Hero
  • Pricing
  • Features

Features

  • Authentication
  • Payments
  • Emails
  • Database
  • Blog
  • Security Headers
  • Claude Code Skills

Recipes

  • Add a Server Action
  • Add a Database Table
  • Add an OAuth Provider
  • Add an Email Template
  • Customize the Auth Flow
  • Add an Admin Metric
  • Enable Bot Protection

Authentication

Supabase Auth in App Router. Server Actions + Zod + rate limiting, RLS deny-all backing identity from session.

What's enforced

Authentication uses Supabase Auth via @supabase/ssr. Every auth mutation is a Server Action with Zod-validated input. Session state is stored in httpOnly cookies; the access token never reaches client JavaScript. Protected routes are gated by proxy.ts (Next.js 16 middleware), which redirects unauthenticated users to /login before the page renders.

Identity inside Server Actions and Server Components comes from getUser() (createServerClientWithCookies then auth.getUser()), which validates the session token server-side. The client-side Supabase client is used only to initiate sign-in / sign-up / OAuth, never to fetch authenticated data.

How the flow works

  1. User submits the login form. The Server Action validates input with Zod.
  2. Supabase Auth verifies credentials (or initiates the OAuth redirect).
  3. Supabase sets an httpOnly session cookie. The browser cannot read it; the cookie travels with subsequent requests.
  4. proxy.ts reads the session cookie on every request to a protected route. Unauthenticated requests redirect to /login?next=<original-path>.
  5. Server Actions and Server Components call getUser() to get the current user. Returns null if the session is missing or expired.
  6. On signup, a database trigger creates a row in profiles automatically. Application code never has to "create the profile" manually.

For the full architectural walkthrough, see Supabase authentication in Next.js App Router (2026).

Auth pages

RoutePurpose
/loginEmail/password + OAuth providers (per config.ts)
/signupNew account with full-name capture
/reset-passwordSend password-reset email
/auth/callbackOAuth and email-verification callback, exchanges the auth code for a session

The /auth/callback route is required for any flow that returns a code via URL (OAuth, magic links, password reset). It calls exchangeCodeForSession server-side, which is the secure pattern; never run the exchange in client code.

Server Actions

All auth mutations live in actions/auth.ts:

ActionPurpose
login(formData)Email/password sign-in
signup(formData)New account registration
loginWithGoogle()Initiates Google OAuth redirect
resetPassword(formData)Sends reset email
logout()Clears session, redirects to /

Each action validates input with Zod, applies rate limiting via the in-memory limiter, and returns a generic error message to the client on failure (the underlying error stays in server logs).

Reading the current user

import { getUser, getUserWithProfile } from '@/lib/supabase/server'

// Just the auth user
const user = await getUser()

// Auth user + profile row
const result = await getUserWithProfile()
if (result) {
  const { user, profile } = result
}

Both helpers run server-side and use the cookie-bound Supabase client. getUserWithProfile joins the profiles row via createAdminClient() (service-role) to get app-specific fields.

For multi-factor and PKCE specifics (which @supabase/ssr uses by default), see Supabase OAuth, magic links, MFA.

Adding an OAuth provider

The summary version:

  1. Enable the provider in the Supabase dashboard (Authentication > Providers) with your OAuth client ID and secret.
  2. Add the provider to config.ts under auth.providers.
  3. Add the login button in components/forms/login-form.tsx that calls a loginWith<Provider>() Server Action.

Full walkthrough: Recipes: Add an OAuth provider.

Why this is shaped this way

Identity is read from the session, not from the request body. A Server Action that takes { userId, ... } as input is the IDOR anti-pattern; the canonical pattern is to read identity from getUser() inside the action and use the request body only for the operation's parameters. This is the same posture documented in the security architecture most SaaS templates skip.