Skip to main content
ASoc
Comparison

The Auth0 Alternative for a Postgres-Backed Next.js Stack

Six RLS policies, all for select, zero write policies anywhere. What authorization looks like when it's a property of the table instead of a service beside it.

The ASoc Team9 min read

The realistic Auth0 alternative for a template-based SaaS is not another auth-as-a-service vendor — it's the database you're already running. Auth0 sells login as a separate product because most stacks have nowhere else to put authorization. A Postgres-backed stack already has that place: Row-Level Security, evaluated on every query, with no second system to keep in sync with the first.

We didn't build this storefront's commerce layer on Auth0, so this isn't a benchmark against it — the fee structure and dashboard experience below are its own published facts, not something we measured. What we can show is the alternative in full: the actual policies, the actual server code, and the one line that has to be right on every request or the whole model fails silently.

Why this comparison even makes sense

Auth0 solves a real problem: identity is hard, and a dedicated vendor amortizes password resets, social logins, MFA, brute-force protection and compliance audits across every customer using it. That's worth paying for when your team doesn't want to own any of it.

The trade-off is where the "alternative" argument starts. Auth0's authorization model lives beside your data, not inside it — a Rule or Action decides who's allowed to do something, and your application (or your database) has to trust what that decision handed back. Supabase Auth, wired to Postgres RLS, collapses that into one system: the same database that stores the row also decides who can read it, on every single query, with no second service to be out of sync with.

Auth0Supabase Auth + RLS
Where authorization runsRules/Actions, separate from the data layerPolicies attached to the table itself
Pricing shapePer monthly active user, tieredIncluded in the Postgres instance you're already paying for
Server-side session modelJWT + your own verification codegetClaims() against the same Postgres-backed session
Enforced even from a raw SQL client?No — depends on your app code checking firstYes — RLS applies to any connection, including psql
Setup for a Next.js App Router templateUniversal Login + SDK + server-side verificationCookie-based SSR client, already idiomatic for Server Components
What you're actually buyingIdentity infrastructure you don't want to ownOne less system, if you already run Postgres

If your team has no database opinions and wants identity to be someone else's operational problem, Auth0 (or Clerk, Cognito, WorkOS) is a reasonable, well-supported choice — that's not in dispute here. This post is for the specific reader typing "auth0 alternative" while already committed to a Postgres-backed Next.js stack, where the honest answer is "you may not need a second vendor at all."

What our actual policy set looks like

Six tables, six policies, all for select, and every one shaped the same way:

-- supabase/migrations/0001_commerce_init.sql
create policy "own profile"   on public.profiles          for select using ((select auth.uid()) = id);
create policy "own orders"    on public.orders            for select using ((select auth.uid()) = user_id);
create policy "own slots"     on public.entitlement_slots for select using ((select auth.uid()) = user_id);
create policy "own downloads" on public.download_events   for select using ((select auth.uid()) = user_id);
-- supabase/migrations/0008_refund_requests.sql
create policy "own deliveries"      on public.download_deliveries for select using ((select auth.uid()) = user_id);
create policy "own refund requests" on public.refund_requests     for select using ((select auth.uid()) = user_id);

There is no corresponding for insert or for update policy anywhere in the schema. Every write goes through a security definer RPC on the server instead — a buyer can read their own orders directly, but cannot write to that table under any circumstance the client controls, because no policy grants it. Supabase's own advisor lints this schema and reports zero findings: no table without RLS, no function with a mutable search_path, no permissive policy overlap.

That's the structural difference from a Rules/Actions model. An Auth0 Action can be written to enforce the same "only your own rows" logic, but it's logic your application (or a downstream API) has to remember to call before touching the database — it isn't a property the database itself refuses to violate. If a new internal script, a background job, or a future service connects to Postgres directly, RLS still applies to it. An Auth0 Action does not run for a connection that skips your API.

The session code, in full

// src/lib/supabase/server.ts
export async function createClient() {
  const cookieStore = await cookies();
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll: () => cookieStore.getAll(),
        setAll: (items) =>
          items.forEach(({ name, value, options }) =>
            cookieStore.set(name, value, options),
          ),
      },
    },
  );
}

This is the entire server-side session client — no SDK initialization step, no separate token-exchange endpoint, because the cookie is the session and Postgres's RLS reads the same JWT the cookie carries. Google sign-in is one function call on top of it:

// src/lib/actions/auth.ts
const { data, error } = await supabase.auth.signInWithOAuth({ provider: "google", /* ... */ });

The one rule that has to hold everywhere this client is used: call getClaims(), never getSession(), when deciding who's allowed to do something. getSession() returns whatever the cookie says without verifying its signature, and a cookie is client-controlled — trusting it for an authorization decision is the RLS-and-Postgres equivalent of trusting an Auth0 Action's output without checking it came from Auth0. Our proxy calls getClaims() on every request specifically so this can't be gotten wrong per-route. The full session-refresh path is covered in the Supabase-vs-Firebase comparison, including the measured 68 KiB gzipped the auth SDK costs when it's imported somewhere it shouldn't be — a bundle-cost lesson that applies to any auth SDK, Auth0's included.

Where this doesn't hold

Being straight about the limits matters more here than in most comparisons, because we're arguing against a category we haven't operated:

  • Multi-tenant B2B with per-organization SSO. Auth0 (and purpose-built alternatives like WorkOS or Frontegg) have mature primitives for "customer brings their own identity provider." Rolling that yourself on RLS alone is real, ongoing engineering work.
  • Enterprise compliance requirements — SOC 2 vendor attestations, specific MFA policies mandated by a customer's security team. A dedicated identity vendor's paperwork is sometimes the product you're actually buying.
  • A team with no Postgres experience. RLS policies are SQL. If nobody on the team is comfortable reading or writing SQL, the "one less system" argument inverts — you've traded a vendor's UI for a query language the team has to learn under pressure.
  • We have not measured Auth0's pricing at any specific scale, and its MAU-based tiers change often enough that a number here would be stale before this post's updated date. Read Auth0's own pricing page for the current tiers rather than trusting a secondhand figure — ours or anyone else's.

Mistakes and how they show up

MistakeSymptomFix
getSession() for an authorization decisionTrusting an unverified, client-controlled cookiegetClaims() — it validates the JWT
RLS enabled with no policies writtenEvery read silently returns zero rowsWrite the select policy explicitly, then test as the role
Assuming an Action/Rule protects the database itselfA direct DB connection bypasses it entirelyRLS is a property of the table, not of the request path
Client-side writes with a permissive policyUsers mutate their own rows in ways you didn't intendNo write policies at all; writes via a security definer RPC
Comparing vendor pricing from memoryA stale number in a published postCite the vendor's current page, or state plainly that you didn't measure it
Treating "identity" and "authorization" as one problemLogin works, but access control is bolted on separatelyDecide up front whether authorization lives beside the data or inside it

Frequently asked questions

Is Supabase Auth actually a drop-in Auth0 replacement? For identity primitives (email/password, OAuth, magic links), largely yes. For enterprise SSO federation and some compliance tooling, no — those are places Auth0 and its enterprise-focused peers have more built out today.

Do I lose anything by not using a dedicated identity vendor? Mainly operational surface you'd otherwise own: brute-force protection tuning, breach-password detection, anomaly detection on logins. Supabase Auth covers the basics; a dedicated vendor's differentiation is usually in that second tier of hardening.

Can RLS alone replace what an Auth0 Action does? For row-level data access, yes — that's the exact case RLS is built for. For things that aren't about database rows (custom claims added to a token, external API calls during login), you'd reach for a Postgres function or an edge function instead, which is more code than an Action's declarative editor, in exchange for not adding a vendor.

What's the actual cost comparison? We can't honestly give you one — we haven't run the same product on both, and Auth0's tiers move. What we can say is the structural claim: RLS-based authorization is included in a Postgres instance you're very likely already paying for, so the marginal cost of this specific piece is close to zero once you're already committed to Postgres.

Templates with the full auth flow already wired

ASoc Scholar is a 210+ page multi-purpose admin with a full auth flow across its React, Next.js, Vue and Angular editions — the account and roles modules are exactly where an authorization model like this one lives. ASoc Lura is a multi-vertical admin suite spanning the same four frameworks, with its own app shell and auth screens per module. ASoc Admin is the flagship dashboard with 13 modules and full auth pages, built to pair with a real backend rather than mocked data.

Browse the full set of React admin templates, Next.js admin templates, or Tailwind admin templates. For the fuller Postgres-versus-document-database decision this sits on top of, see Supabase vs Firebase for a template-based SaaS.

Keep reading

Comparison8 min read

esbuild vs. Vite: What This Repo's Own Lockfile Says

Neither is a dependency here — but the Vite version vitest pulls in has already dropped esbuild for Rolldown, proven straight from the lockfile.

Read more