Custom auth vs Supabase Auth: when to use which

Status: đźź© COMPLETE Last updated: 2026-06-21 Plain-English tagline: Supabase ships with a complete auth system (emails, OAuth, magic links, passkeys). Sometimes you should use it; sometimes a custom session table is simpler. This is when each fits.


What this decides

When you build a Supabase project, you choose where authentication lives:

  • Supabase Auth — the built-in auth.users system, with email/password, magic links, OAuth providers, passkeys, MFA, etc. Issues JWTs that auth.uid() reads in RLS policies.
  • Custom auth — your own users table, your own sessions table, your own login flow. You validate credentials and issue session cookies yourself.

For background: Authentication vs authorization 🟩, Sessions and cookies 🟩, JWT 🟩, Supabase 🟩 🟦.


The short answer

Default to Supabase Auth for any project where users have email addresses and you want OAuth / passwordless / MFA.

Reach for custom auth when your situation doesn’t fit the email-centric model:

  • Users don’t have email addresses (kids, internal codes, niche identifiers)
  • You need a very specific login UX that Supabase Auth doesn’t support
  • You’re building something simple enough that custom is genuinely less code

Bible Quest is the canonical custom-auth case: kids log in with auto-generated 8-character access codes (no email). Supabase Auth couldn’t model this cleanly. A custom sessions table did.


The factors that matter

  1. Do users have email addresses? Yes → Supabase Auth fits naturally. No → custom is easier.
  2. Do you need social login (Google, Apple, GitHub, etc.)? Yes → Supabase Auth (one config switch per provider). No → either works.
  3. Do you need MFA / passkeys? Yes → Supabase Auth (built-in). No → either works.
  4. How much auth surface do you need? Lots (signup, login, password reset, email verification, account deletion) → Supabase Auth saves you weeks. Minimal (one type of login) → custom may be ~50 lines.
  5. Are you using RLS with auth.uid()? Yes → Supabase Auth makes this trivial. Custom auth means you do user-identity differently (server-side via service_role).
  6. How comfortable are you with auth security? Auth bugs are catastrophic. Supabase Auth is battle-tested by thousands of apps. Custom auth means you’re the security expert.

When to pick SUPABASE AUTH

  • Most consumer webapps — sign up with email, optionally social login, password reset via magic link. Standard pattern; Supabase Auth nails it.
  • B2B SaaS — Supabase Auth supports SSO (SAML/OIDC) on paid tiers, which enterprise buyers expect.
  • You want passwordless from day one — magic links and passkeys are first-class.
  • You want OAuth — Google, GitHub, Apple, Discord, and ~20 more providers configured via dashboard toggle.
  • You want MFA — TOTP + SMS supported out of the box.
  • You need standard auth events to trigger backend work — Supabase emits webhooks on signup/login that you can hook into.
  • You’re building anything where “users have email addresses” is reasonable — which is most apps.

Setup is roughly: enable the providers you want in the Supabase dashboard, install @supabase/auth-helpers-nextjs (or the newer @supabase/ssr), and use the signInWith* methods.

Reference: How-to: Add Supabase auth đźź©.


When to pick CUSTOM AUTH

  • Users don’t have email addresses. Bible Quest’s kids get access codes. Internal employee tools may use SSO codes. A guest-checkout flow may use phone numbers without email.
  • Your login UX is unusual. “Tap a card to log in,” “scan a QR code,” “your phone is your identity” — these don’t fit Supabase Auth’s model.
  • You’re building something genuinely tiny where the full Supabase Auth surface is overkill. A “type the family password” landing page might be 30 lines of custom code.
  • You need full control over the user record. Supabase Auth’s auth.users table has fixed columns; you can’t add fields (you add a separate profiles table linked by user_id). Custom auth can put everything in one table.
  • You want server-side sessions (cookies, no JWT in the client). Supabase Auth uses JWTs; custom auth can use server-side sessions directly.

Custom auth pattern (Bible Quest):

CREATE TABLE users (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  quest_name text NOT NULL,
  access_code text UNIQUE NOT NULL,
  role user_role NOT NULL DEFAULT 'kid'
);
 
CREATE TABLE sessions (
  token uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id uuid NOT NULL REFERENCES users(id),
  expires_at timestamptz NOT NULL
);

Then a lib/auth.ts with createSession, getCurrentUser, logout, requireAdmin — about 100 lines for everything Bible Quest needs.


What you give up with custom auth

Be honest about what you’re not getting:

  • Password reset flows — you implement them yourself, or you don’t have them.
  • Email verification — same.
  • Audit logs of login events — Supabase Auth logs these; you’d build them.
  • Rate limiting on login attempts — you’d add this yourself (or use a layer like Cloudflare).
  • Session revocation / “log out everywhere” — possible with custom but requires explicit support.
  • auth.uid() in RLS — your queries can’t use it. You either route all queries through server code with service_role, or you write a custom function that reads the session.

For Bible Quest, none of these mattered (kids don’t need password reset; the use case is internal). For most apps, they do.


The hybrid pattern (rare)

Some projects use Supabase Auth for some users and custom auth for others:

  • A public app with Supabase Auth for end users + an admin tool with custom auth for staff
  • A B2C app with Supabase Auth for consumers + a service-account model for partners

This adds complexity. Avoid unless the use case truly demands it.


What if I’ve already chosen?

“I started with custom auth and need to migrate to Supabase Auth”: you can backfill auth.users rows that point at your existing users records. Keep the existing IDs as id and add a user_metadata.legacy_user_id to link. Users get a “verify your email to claim your account” flow once.

“I started with Supabase Auth and want custom”: rarely worth it. The migration cost (re-issuing all auth, replacing all auth.uid() calls with custom session lookups, rewriting RLS) is large. Stay put unless you have a compelling reason.

“I used both and now they’re tangled”: consolidate. One auth system per app is much easier to reason about.


See also


Sources