skip to content
fizzgig
sign in
// suite
architectureknow what you builtoperationsknow it’s runningauditknow it’s safe to shipintegrationspricingdocschangelog
// community
sourdough startersproject kicks-off in 1 prompt
sign inrequest access →
// starters/auth/auth-patterns

auth patterns

The six recurring auth shapes (where the check lives, library choice + drift, session storage, webhook signatures, RBAC vs ABAC, OAuth state) and the decisions to make once so the AI stops re-deciding them every session.

● last fed 28 may 20261055 words · 5 min read
// when to use this starter

Fork this at the start of any project that has user accounts. The decisions encoded here are the ones that get expensive to change later — they're load-bearing for every protected route you'll ever add.

authsecuritysessionoauthrbacwebhook

Auth patterns

A sourdough starter for the auth-shaped decisions that need to be made once and then honoured everywhere. The AI's default behaviour is to re-invent auth per route, which is how protected routes ship unprotected.


1. Where the auth check lives

The single most important decision. Three viable options:

  • Middleware (recommended for most cases). One file gates a whole path prefix (/dashboard/*, /api/admin/*). Audit surface = one file. New routes inherit protection by default. Hardest part: getting the matcher right so static assets / public routes don't get blocked.
  • Per-route (in the handler). Each route imports an auth helper and calls it at the top. Audit surface = N files; one missed import = one unprotected route. Use this when the protection rules vary per route (different roles, different scoping).
  • Per-component (in server components). For server-component-rendered protected pages. Often in addition to middleware — middleware blocks the request, the component still verifies the session before rendering.

The decision worth locking: "Auth checks live in middleware for all /dashboard/* and /api/admin/* routes. Per-route checks only when the protection logic varies. Server components additionally call getSession() before rendering protected data — defence in depth."


2. Library choice + drift prevention

Pick one. Mixing auth libraries in one project is a known antipattern — they fight over the session cookie, the AI cargo-cults patterns from one into the other, audit surfaces multiply.

Common choices (2026):

  • Supabase Auth — if you're already on Supabase. Fewest moving parts.
  • Clerk — best out-of-the-box UI; expensive at scale.
  • NextAuth / Auth.js — most flexibility; most ceremony.
  • Lucia — for the "I want to understand every byte" crowd. Pre-built primitives, no opinions.
  • Iron Session / Cookie-only — for the "I just need a session" crowd. Minimal.

The decision worth locking: "We use {one library}. No other auth library is allowed in this codebase. If a new dependency proposes its own auth (e.g. Clerk for the dashboard, NextAuth for the API), reject it — refactor the new dependency's auth into the existing library or don't add the dependency."

auth_flow_trace catches library drift (mixing next-auth + clerk + supabase + auth0 + iron-session + lucia in one source).


3. Session storage

Where does the session live?

  • HTTP-only cookie (recommended). Server reads it; JavaScript can't. Resistant to XSS exfiltration. The library handles this for you in most cases.
  • localStorage / sessionStorage. Tempting because it's easier to read client-side. Don't do it — XSS can exfiltrate everything in localStorage.
  • In-memory only. Refresh = logged out. Useful for some sensitive sub-flows (e.g. fresh auth before a password change), not for the primary session.

The decision worth locking: "Sessions live in HTTP-only secure cookies. The cookie is SameSite=Lax by default; SameSite=Strict for the admin scope. Refresh tokens (if used) are also HTTP-only."


4. Webhook signature verification

Every incoming webhook is a request from the public internet. Verify the signature first, before touching the body.

The pattern, by provider:

  • Stripe — stripe.webhooks.constructEvent(rawBody, signatureHeader, webhookSecret). Raw body bytes, not parsed JSON. Most frameworks parse the body before your handler runs; you need a path to get the unparsed bytes.
  • Supabase Auth webhooks — HMAC SHA-256 of the body with the webhook secret.
  • Svix / Hookdeck / Inngest — provider SDK verification.
  • GitHub webhooks — HMAC SHA-256 of the body with the webhook secret, in X-Hub-Signature-256 header.

The decision worth locking: "Every webhook handler verifies signature first, returns 401 silently on failure (don't echo why), and is idempotent (event ID stored to dedupe replays). Webhooks are added behind app/api/webhooks/<provider>/route.ts — one folder so the audit surface is one place."


5. Authorisation (the layer above authentication)

Once you know who the user is, what can they do? Two patterns:

  • RBAC (Role-Based Access Control) — users have roles (admin, editor, viewer), roles have permissions. Simple, scales to most apps.
  • ABAC (Attribute-Based Access Control) — permissions derived from attributes ("can edit if resource.owner_id == user.id"). More expressive, more complex.

For most projects: RBAC at the route level, ABAC at the resource level. "Admin can access the admin routes (RBAC); within the admin routes, they can only see records belonging to their org (ABAC, enforced by RLS)."

The decision worth locking: "Roles: {list yours}. Role lookups happen in middleware (for route gating) and in RLS policies (for row-level scoping). Roles never live in JWT claims that the client could decode and lie about — always re-read from the database."


6. OAuth state + callback

When you wire third-party OAuth (Google, GitHub, Stripe Connect, etc.):

  • State parameter is mandatory. Without it, you're vulnerable to CSRF on the callback. Most libraries do this; verify yours does.
  • Callback URL is exact-match. Whitelist the production callback explicitly. Don't whitelist *.yourdomain.com — subdomain takeover becomes auth bypass.
  • Validate the token at the OAuth provider, not just locally. The token in the callback URL has been intercepted before; check it's still valid at the provider before issuing your session.
  • Store the OAuth provider's user ID, not the email. Emails change. Provider IDs don't.

The decision worth locking: "OAuth providers we support: {list}. State parameter validated on every callback. Provider user ID is the immutable join key, email is profile data only."


How to feed this starter

Add to it when:

  • You add a new auth provider or library. Document the integration choices.
  • You hit an auth-shaped bug worth recording (e.g. "the SSR createClient vs CSR createClient mixing bug" — common Next.js + Supabase trap).
  • You change one of the locked decisions above. Update + note the supersession + why.

Remove from it when:

  • A pattern stops applying (you retired RBAC in favour of pure ABAC, say).
  • A library you don't use stops being relevant.

Companion starters

  • security-posture — the broader security defaults
  • project-starter — parent shape
  • adr — auth decisions are exactly the shape ADRs are for

Companion tools (Fizzgig)

  • fizzgig__auth_flow_trace — flags unprotected routes, webhook handlers without signature verification, auth library drift
  • fizzgig__form_validation_audit — server validation parity (the boundary between auth-checked-user and trusted-input)
  • fizzgig__rls_checker — row-level scoping for the ABAC layer

All fold into fizzgig__audit at June 2026 launch.

// fork it

Copy the raw markdown and drop it into your project. Edit freely - the value comes from feeding it as you work.

MIT-licensed · attribution welcome but not required
// new to starters?

The canonical article on what a sourdough starter is, why CLAUDE.md isn't one, and how to feed yours.

read the article →
// contribute

Found a shape that works? Markdown PRs welcome on github.

github →
fizzgig

the fluffy guardian of vibe-coded products. growls at insecure code so you don't have to.

all systems operational

suite

architectureoperationsauditintegrationspricingdocschangelog

community

sourdough startersdiscord (soon)github (soon)x / twitter (soon)rss (soon)

company

aboutcontacttermsprivacy
© 2026 fizzgig.
v1.5.0 · 2026-07-26
AI usage: fizzgig uses AI internally (Anthropic Claude, Google Gemini, OpenAI embeddings) to power audit checks, summarise decisions, and rank findings. Content surfaced from these tools is labelled as such in the dashboard. No user data is used to train third-party AI models — see our privacy policy for the full sub-processor list.