Skip to content

auth · side by side

Clerk vs Supabase Auth for a Next.js app

Both fill the auth slot, so a generated repo carries one or the other, never both. Every line below is read out of the two manifests.

Short answer

Pick Clerk if

Teams who want finished auth UI and Clerk's hosted user dashboard on day one, with organisations there when they need them. The trade: the user record lives in Clerk's database. With a database selected, a webhook keeps a copy in yours.

Pick Supabase Auth if

Teams already on Supabase Postgres who want row-level security as the authorization model. With the policies right, a buggy query cannot return another tenant's rows.

Clerk is tested with Neon. Supabase Auth isn't yet.

Side by side

Price, obligations, and the surface each one adds. No row is written by hand. This is manifest.yaml, rendered.

Clerk compared with Supabase Auth on pricing, fit, trade-offs, required companions, environment variables, dependencies, MCP servers and repo footprint.
From the manifestOption AClerkOption BSupabase Auth
In one lineClerk

Hosted auth with prebuilt sign-in UI in your design's colours and a user sync webhook.

Supabase Auth

Postgres-native auth where the database, not the API layer, is the last line of defence.

PricingClerk

Free Hobby plan up to 50,000 monthly retained users per app. Pro is $25/month, or $20/month billed yearly, then $0.02 per extra user. MFA needs Pro. Pro includes one enterprise SSO connection.

Supabase Auth

Free: 50,000 monthly active users. Pro: 100,000 included, then $0.00325 per MAU. Anonymous sign-ins are included. SAML SSO needs Pro: 50 SSO users included, then $0.015 each.

Best forClerk

Teams who want finished auth UI and Clerk's hosted user dashboard on day one, with organisations there when they need them. The trade: the user record lives in Clerk's database. With a database selected, a webhook keeps a copy in yours.

Supabase Auth

Teams already on Supabase Postgres who want row-level security as the authorization model. With the policies right, a buggy query cannot return another tenant's rows.

Trade-offsVerbatim from the manifestClerk
  • Prebuilt <SignIn /> and <SignUp /> components cover email, OAuth and account management out of the box. MFA needs the Pro plan.
  • Users live in Clerk, not in your Postgres. Anything that joins users to your tables needs the webhook mirror this battery ships with a database. The mirror is eventually consistent: a signup can reach your app a beat before the webhook lands.
  • Roles live in publicMetadata by default. Convenient, but it is vendor state. Moving to a local roles table later is a migration, not a config change.
  • Every server render that needs auth runs behind clerkMiddleware. Forget the proxy matcher and auth() throws at runtime instead of failing the build.
  • Pricing counts monthly retained users: people who come back 24+ hours after signing up. A large free consumer product costs more here than self-hosted auth.
  • Lock-in is real but bounded. Clerk exports users, and with a database selected this battery already keeps a copy in your own tables, keyed on your own id.
Supabase Auth
  • Authorization lives in SQL policies, not in TypeScript. That is the whole point, and it is also the learning curve: you debug permissions with explain and set role, not a debugger.
  • Auth is coupled to the Supabase project. Moving the database off Supabase means moving users, JWT signing and every policy at the same time.
  • Cookie-based sessions in the App Router need a proxy refresh. Skip it and users get logged out after an hour with no error anywhere.
  • Roles live in app_metadata, which only the service-role key can write. Good for security, awkward for self-service role changes.
  • The service-role key bypasses every policy. One import of it into a client component and the whole database is public.
Required companionsAdded for you, with a reasonClerk

Nothing. It stands on its own.

Supabase Auth
  • Supabase
Recommended alongsideSuggested, never added for youClerk
  • A database battery
Supabase Auth
  • A file storage battery
Env vars you will manageEvery one documented in docs/onboard.mdClerk

4 variables · 2 required

  • CLERK_SECRET_KEY
  • NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
  • NEXT_PUBLIC_CLERK_SIGN_IN_URL
  • NEXT_PUBLIC_CLERK_SIGN_UP_URL
Supabase Auth

0 variables

None.

Dependencies addedClerk
  • @clerk/nextjs ^7.9.7
  • server-only ^0.0.1
Supabase Auth
  • @supabase/ssr ^0.12.7
  • @supabase/supabase-js ^2.117.0
  • server-only ^0.0.1
  • supabase ^2.2.1 (dev)
MCP serversWritten into .mcp.jsonClerk

None. No extra agent tools from this one.

Supabase Auth

None. No extra agent tools from this one.

Footprint in your repoClerk

29 files, plus 8 injections into shared stack files

Supabase Auth

60 files, plus 8 injections into shared stack files

What changes in your repo

The paths each battery contributes, diffed. A path in the third list is written by both, so swapping rewrites that file rather than adding one.

Only with Clerk (19)

  • src/9 files
    • app/3 files
      • (auth)/3 files
        • sign-in/1 file
          • [[...sign-in]]/1 file
            • page.tsx
        • sign-up/1 file
          • [[...sign-up]]/1 file
            • page.tsx
        • layout.tsx
    • components/2 files
      • auth/2 files
        • clerk-root.tsx
        • clerk-user-profile.tsx
    • lib/4 files
      • auth/4 files
        • appearance.ts
        • clerk-env.d.ts
        • publishable-key.ts
        • roles.ts
  • variants/10 files
    • db-sync/5 files
      • scripts/1 file
        • clerk-webhook.ts
      • src/4 files
        • app/1 file
          • api/1 file
            • webhooks/1 file
              • clerk/1 file
                • route.ts
        • lib/3 files
          • auth/3 files
            • user-sync.test.ts
            • user-sync.ts
            • webhook-idempotency.ts
    • orm-drizzle/3 files
      • slots/1 file
        • db-schema.ts
      • src/2 files
        • db/1 file
          • clerk-schema.ts
        • lib/1 file
          • auth/1 file
            • user-store.ts
    • orm-prisma/2 files
      • slots/1 file
        • prisma-models.prisma
      • src/1 file
        • lib/1 file
          • auth/1 file
            • user-store.ts

Only with Supabase Auth (50)

  • scripts/1 file
    • auth/1 file
      • seed.ts
  • src/44 files
    • app/9 files
      • (supabase-auth)/6 files
        • forgot-password/1 file
          • page.tsx
        • no-access/1 file
          • page.tsx
        • reset-password/1 file
          • page.tsx
        • sign-in/1 file
          • page.tsx
        • sign-up/1 file
          • page.tsx
        • layout.tsx
      • auth/3 files
        • callback/1 file
          • route.ts
        • confirm/1 file
          • route.ts
        • sign-out/1 file
          • route.ts
    • components/16 files
      • auth/16 files
        • settings/6 files
          • connected-accounts.tsx
          • email-card.tsx
          • form-feedback.tsx
          • password-form.tsx
          • profile-form.tsx
          • sessions-card.tsx
        • auth-card.tsx
        • check-email.tsx
        • forgot-password-form.tsx
        • oauth-buttons.tsx
        • password-input.tsx
        • provider-icons.tsx
        • reset-password-form.tsx
        • sign-in-form.tsx
        • sign-up-form.tsx
        • supabase-session-listener.tsx
    • lib/19 files
      • auth/19 files
        • actions.ts
        • admin.ts
        • client.ts
        • constants.ts
        • database.types.ts
        • env.ts
        • errors.ts
        • flow.ts
        • form-state.ts
        • identity.test.ts
        • impersonation.ts
        • profile.ts
        • provider-settings.test.ts
        • provider-settings.ts
        • providers.ts
        • proxy.test.ts
        • rls.ts
        • schemas.ts
        • server.ts
  • supabase/2 files
    • migrations/2 files
      • 0100_supabase_auth_profiles.sql
      • 0101_supabase_auth_profile_sync.sql
  • tests/1 file
    • e2e/1 file
      • auth.setup.ts
  • variants/2 files
    • orm-prisma/2 files
      • slots/1 file
        • prisma-external-tables.ts
      • supabase/1 file
        • migrations/1 file
          • 0102_prisma_profiles_without_cross_schema_fk.sql

Same path, different implementation (10)

  • src/8 files
    • components/3 files
      • auth/3 files
        • account-settings.tsx
        • header-auth-actions.tsx
        • sign-out-button.tsx
    • lib/5 files
      • auth/5 files
        • origin.ts
        • proxy.ts
        • redirect.test.ts
        • redirect.ts
        • session.ts
  • tests/2 files
    • e2e/2 files
      • auth.spec.ts
      • auth.ts

Shared stack files Clerk injects into

  • bare-route-groups
  • env-required
  • header-actions
  • legal-processors
  • middleware-matchers
  • providers
  • proxy-handlers
  • verify-checks

Shared stack files Supabase Auth injects into

  • bare-route-groups
  • env-required
  • header-actions
  • legal-processors
  • middleware-matchers
  • providers
  • proxy-handlers
  • verify-checks

Clerk in your .env.local

.env.localbash7 lines
# required
CLERK_SECRET_KEY=sk_test_replace_me
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_replace_me

# optional
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up

Supabase Auth in your .env.local

.env.localbash1 line
# Supabase Auth adds no environment variables.

What changes for your agents

Each battery ships rules, skills, subagents and hooks that an agent loads before it touches the code that battery owns. Picking one is also picking how your agents behave in src/lib/auth/**.

Clerk

  • 2

    Skills

  • 3

    Rules

  • 7

    Solution docs

Rules (3)

Authorise from the session, never from the client user object

src/app/** · src/components/** · src/lib/auth/**

Clerk's components wear the design's tokens and only render inside the provider

src/components/auth/** · src/lib/auth/appearance.ts · src/app/(auth)/** · src/components/site/header.tsx

The Clerk webhook verifies its signature before anything else

src/app/api/webhooks/clerk/** · src/lib/auth/user-sync.ts · src/lib/auth/user-store.ts · src/lib/auth/webhook-idempotency.ts

Skills (2)

/add-clerk-role

Add a role to the Clerk-backed role vocabulary, wire it into the session claim, gate a route with it, and assign it safely.

/sync-clerk-user

Extend, backfill or re-verify the Clerk user mirror: the users table and ClerkSyncStore that this battery already ships.

Subagents and hooks

None of its own. The foundation agents and guard hooks still ship.

Supabase Auth

  • 3

    Skills

  • 4

    Rules

  • 7

    Solution docs

Rules (4)

Every table needs row-level security and at least one policy

supabase/** · src/lib/auth/**

getUser() is the trust boundary, getSession() is not

src/lib/auth/** · src/app/** · src/components/**

The service-role key is a root password

src/lib/auth/** · src/app/** · supabase/**

Sign-in flows keep return paths safe and reveal nothing about accounts

src/components/auth/** · src/lib/auth/** · src/app/(supabase-auth)/** · src/app/auth/**

Skills (3)

/add-auth-rls-policy

Add row-level security policies to a Supabase table (owner-scoped, tenant-scoped or admin) with the indexes and the tests that prove they work.

/add-oauth-provider

Switch on Google, GitHub or Microsoft sign-in for Supabase Auth, or add a new provider (Apple, Discord, LinkedIn) with its button, icon, redirect URLs and profile mapping.

/add-profile-field

Add a field people edit on /settings/profile (a bio, a time zone, a company) with Supabase Auth, stored in public.profiles behind a column grant, validated with zod and saved by a server action.

Subagents and hooks

None of its own. The foundation agents and guard hooks still ship.

What each one already knows

Solution docs land in docs/solutions/ in your repo and are published here, so you can read the failure modes before you commit.

Clerk (7)

Supabase Auth (7)

Which one to pick

From meta.bestFor and meta.tradeoffs. If a claim is not in the manifest, it is not on this page.

Pick Clerk when

Teams who want finished auth UI and Clerk's hosted user dashboard on day one, with organisations there when they need them. The trade: the user record lives in Clerk's database. With a database selected, a webhook keeps a copy in yours.

And accept that(6)
  • Prebuilt <SignIn /> and <SignUp /> components cover email, OAuth and account management out of the box. MFA needs the Pro plan.
  • Users live in Clerk, not in your Postgres. Anything that joins users to your tables needs the webhook mirror this battery ships with a database. The mirror is eventually consistent: a signup can reach your app a beat before the webhook lands.
  • Roles live in publicMetadata by default. Convenient, but it is vendor state. Moving to a local roles table later is a migration, not a config change.
  • Every server render that needs auth runs behind clerkMiddleware. Forget the proxy matcher and auth() throws at runtime instead of failing the build.
  • Pricing counts monthly retained users: people who come back 24+ hours after signing up. A large free consumer product costs more here than self-hosted auth.
  • Lock-in is real but bounded. Clerk exports users, and with a database selected this battery already keeps a copy in your own tables, keyed on your own id.

Pick Supabase Auth when

Teams already on Supabase Postgres who want row-level security as the authorization model. With the policies right, a buggy query cannot return another tenant's rows.

And accept that(5)
  • Authorization lives in SQL policies, not in TypeScript. That is the whole point, and it is also the learning curve: you debug permissions with explain and set role, not a debugger.
  • Auth is coupled to the Supabase project. Moving the database off Supabase means moving users, JWT signing and every policy at the same time.
  • Cookie-based sessions in the App Router need a proxy refresh. Skip it and users get logged out after an hour with no error anywhere.
  • Roles live in app_metadata, which only the service-role key can write. Good for security, awkward for self-service role changes.
  • The service-role key bypasses every policy. One import of it into a client component and the whole database is public.

Questions people actually ask

Should I choose Clerk or Supabase Auth?
Clerk is best for teams who want finished auth UI and Clerk's hosted user dashboard on day one, with organisations there when they need them. The trade: the user record lives in Clerk's database. With a database selected, a webhook keeps a copy in yours. Supabase Auth is best for teams already on Supabase Postgres who want row-level security as the authorization model. With the policies right, a buggy query cannot return another tenant's rows. Both fill the auth slot, so a generated repo carries one or the other, never both.
How much do Clerk and Supabase Auth cost?
Clerk: Free Hobby plan up to 50,000 monthly retained users per app. Pro is $25/month, or $20/month billed yearly, then $0.02 per extra user. MFA needs Pro. Pro includes one enterprise SSO connection. Supabase Auth: Free: 50,000 monthly active users. Pro: 100,000 included, then $0.00325 per MAU. Anonymous sign-ins are included. SAML SSO needs Pro: 50 SSO users included, then $0.015 each.
What changes in my repo if I switch from Clerk to Supabase Auth?
Clerk writes 29 files, 4 environment variables and 2 dependencies, and installs 3 path-scoped rules, 2 skills and 7 solution docs. Supabase Auth writes 60 files, 0 environment variables and 4 dependencies, and installs 4 path-scoped rules, 3 skills and 7 solution docs.

Decide once, then build the repo that already knows the decision.

Either way you get that choice’s rules, skills and solution docs installed, plus the guard hooks, an onboarding doc for exactly these env vars, and the Compound Engineering loop. Free and MIT.