Skip to main content
ASoc
Comparison

Supabase vs Firebase for a Template-Based SaaS

Authorization is where they differ most and what you cannot change later. Row-Level Security vs Security Rules, the Server Component fit, and the 68 KiB our own auth SDK cost.

The ASoc Team12 min read

Pick Supabase when your product has relational data and a web-first audience — orders that belong to customers, entitlements that belong to accounts. Pick Firebase when you are mobile-first and need offline sync more than you need joins. For a SaaS built on a bought template, Supabase usually wins on a narrower point: templates ship as Next.js code, and Supabase's model fits Server Components without a translation layer.

We built our own storefront's commerce layer on Supabase — auth, entitlements, gated downloads, row-level security, the lot. This post uses what that cost us rather than restating either vendor's marketing. We did not build the same thing twice on Firebase, and where that limits the comparison, it says so.

The decision in one table

AxisSupabaseFirebase
DatabasePostgreSQL, relationalFirestore (document) + Data Connect (Postgres)
Query modelSQL, joins, views, CTEsDocument reads, limited querying
AuthorizationRow-Level Security, in the databaseSecurity Rules, in a config language
AuthEmail, OAuth, magic link, SAMLBroader — phone, anonymous, more providers
Offline supportNone first-classExcellent, the core strength
RealtimePostgres replication, per-tableBest in class, sub-10 ms document reads
Server-side rendering fitNative — SSR clients, cookie sessionsWorkable, less idiomatic
Local developmentFull stack in Docker, real PostgresEmulator suite
PortabilityOpen source, it is just PostgresProprietary; migrating means rewriting
Schema migrationsSQL files, versioned in gitSchemaless; migrations are your problem
Cost shapePer instance, predictablePer read/write/delete, scales with traffic
Right forRelational, web-first productsMobile-first, offline, realtime products

If you read one row: authorization. It is where the two products differ most, it is what you will spend the most time on, and it is the hardest thing to change later.

Why the database model decides more than it looks like it does

The usual framing is SQL versus NoSQL and it undersells the consequence. Take a boring SaaS question: which customers on the Team plan have not used feature X in 30 days?

In Postgres that is a query — a join, a where, a group by. You write it in the SQL editor in about four minutes and you are done.

In Firestore there is no join. You either denormalize that relationship into the documents ahead of time — deciding, before you have the question, that you will need it — or you read one collection, then read another per result, in a loop. The first costs write complexity and consistency risk; the second costs money proportional to your data size and will not work at scale.

Firestore's constraint is deliberate: every query it permits is guaranteed to scale, because it refuses the ones that do not. That is a real engineering property and it is why Firebase handles enormous mobile workloads. It is also why analytical and administrative questions — which is most of what a SaaS backend does after launch — are awkward there.

Firebase Data Connect narrows this considerably by putting managed Postgres behind a GraphQL layer. It changes the comparison from "SQL vs NoSQL" to "two ways to reach Postgres," and if you are already in the Google ecosystem it is worth evaluating on its own terms. It does not change the authorization comparison below, which is the deeper one.

Authorization: the part that takes the time

Both products let you write authorization rules that run outside your application, so a compromised client cannot read what it should not. They put those rules in very different places.

Firebase uses Security Rules — a purpose-built language, deployed as a file, evaluated per request:

match /orders/{orderId} {
  allow read: if request.auth != null
              && resource.data.userId == request.auth.uid;
  allow write: if false;
}

Supabase uses Postgres Row-Level Security — policies attached to tables, evaluated by the database:

alter table public.orders enable row level security;

create policy "orders_select_own"
  on public.orders for select
  to authenticated
  using (user_id = (select auth.uid()));

They read similarly. The difference is what happens next.

RLS applies to every connection to that table — your app, a background job, a psql session, a future service in another language. It is a property of the data, not of the client. Security Rules apply to requests through the Firebase client SDKs; the Admin SDK bypasses them entirely by design, so your server code is outside the model and any authorization there is code you write and test yourself.

That cuts both ways. RLS is stronger, and it is also easier to get quietly wrong in ways that are hard to see — a policy that is subtly permissive looks identical to one that is not.

Three things we settled on, which apply to any Supabase project:

  • Read-own-only, and no write policies at all. Every one of our tables grants select scoped to the owner and grants nothing else to anon or authenticated. Every write goes through the service-role client on the server. The client cannot mutate the database even if a bug hands it the chance, because there is no policy under which the write is legal.
  • Anything that must not race goes in a SECURITY DEFINER function. Redeeming an entitlement slot is a check-then-write; done in application code it is a race, and two concurrent requests can both pass the check. Ours is one RPC — pinned search_path, execute revoked from anon and authenticated, called only by the server — so the check and the write are one atomic statement.
  • Run the linter. Supabase's database advisors flag unindexed foreign keys, tables without RLS, functions with mutable search_path, and permissive policy overlaps. Ours reports zero lints, and getting there found real problems. It is the closest thing either platform has to a security test suite, and it is free.

Firebase's equivalent discipline is the emulator suite plus a rules test file, which is a genuinely good testing story — arguably better than Postgres's, where testing a policy means connecting as a role and asserting on rows. Neither platform makes this effortless.

The Next.js part, which is where templates live

If you are wiring a backend into a bought template, the template is Next.js App Router code — Server Components by default, Server Actions, a request-scoped session. This is where the two diverge in day-to-day feel.

Supabase publishes SSR clients built around cookie-based sessions, so a Server Component reads the user directly:

// src/lib/supabase/server.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";

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),
          ),
      },
    },
  );
}

One rule that is easy to miss and matters: use getClaims(), never getSession(), on the server. getSession() returns whatever is in the cookie without verifying it, and a cookie is client-controlled. getClaims() validates the JWT signature. Our proxy refreshes the session on every request and calls getClaims() for exactly this reason. The Supabase docs warn about it; it is still the single most common mistake in Supabase + Next.js code we have read.

Firebase's client SDK is built around a long-lived listener (onAuthStateChanged), which is a browser-shaped idea. Server-side you use the Admin SDK with session cookies — it works, and there is documented guidance, but it is two SDKs with two auth models rather than one.

The bundle cost, measured

Here is a number from our own audit that neither vendor publishes.

Our site-wide header imported the Supabase browser client at module scope. That put @supabase/ssr plus auth-js~68 KiB gzipped, 255 KiB parsed — in the initial bundle of every page, including the home page, the blog and the docs, none of which have any account UI at all. Moving it behind a dynamic import removed it from four page types.

The lesson generalizes to either platform: an auth SDK imported at module scope in a shared layout lands in every page's bundle. Firebase's modular v9+ SDK is tree-shakeable and its auth module is broadly comparable in size; we have not measured it on an equivalent build, so treat that as an architectural warning rather than a benchmark. Check what is actually in your bundle either way — this is the kind of thing that never shows up until someone opens the analyzer.

Cost, in the shape that actually bites

Supabase prices per instance: a fixed monthly fee for compute and storage, mostly independent of how many queries you run. Firebase prices per operation — per document read, write and delete.

The consequence is not "Firebase is expensive." It is that Firebase's cost is coupled to your access patterns, and your access patterns change when you are not looking. A dashboard that reads a 200-document collection to compute a count is 200 reads per page load. The same page in Postgres is one count(*). Nothing warns you; the bill arrives a month later.

Supabase's failure mode is the opposite and more predictable: you outgrow an instance size and upgrade it. You can see it coming in the metrics.

For a template-based SaaS with modest traffic, both platforms have free tiers that cover a launch. The relevant question is which curve you would rather be on at 10× your current usage, and for read-heavy relational workloads that is usually the flat one.

Where Firebase is straightforwardly the better answer

This is not a close call in either direction; the products are good at different things.

  • Offline-first applications. Firestore's local persistence and conflict handling are the reason to choose Firebase, and Supabase has no equivalent. If your users work on trains, in warehouses, or on bad connections and expect the app to keep functioning, this decides it.
  • Mobile-first products. The native SDKs, crash reporting, analytics, remote config, A/B testing and push notifications are one integrated platform. Assembling that from parts around Supabase is real work.
  • High-frequency realtime at scale. Chat, presence, collaborative cursors, live multiplayer. Supabase Realtime is capable and built on Postgres replication; Firestore was designed for this from the start.
  • You are already in Google Cloud. IAM, billing and support in one place is worth more than a feature comparison suggests.

And the honest one: if your team knows Firebase and does not know SQL, the productivity difference on the first release may exceed every architectural argument above.

Mistakes and how they show up

MistakeWhat happensFix
getSession() on the serverTrusting an unverified, client-controlled cookiegetClaims() — it validates the JWT
RLS enabled, no policies writtenEvery read returns zero rows, silentlyWrite the select policy; test as the role
Table created without RLSFully public via the anon keyEnable RLS on creation; the advisor catches it
Client-side writes with a permissive policyUsers can mutate their own rows arbitrarilyNo write policies; writes via service role
Check-then-write in application codeRace condition under concurrencyOne SECURITY DEFINER RPC
Service-role key in a client componentTotal database compromiseServer-only; never NEXT_PUBLIC_
Auth SDK imported in a shared layout~68 KiB gzipped on every pageDynamic import at the call site
Firestore reads in a loop for a joinCost scales with data, not with usersDenormalize deliberately, or use Postgres
No search_path pin on a definer functionPrivilege escalation via schema shadowingset search_path = '', schema-qualify
Firebase Admin SDK assumed to respect rulesIt bypasses them by designAuthorize explicitly in server code

Frequently asked questions

Is Supabase production-ready for a paid product? Yes, with the same caveat as any managed service: understand your backup and recovery story before you take money. It is Postgres, so the operational knowledge is transferable and the escape hatch is real — you can dump the database and run it anywhere, which is not true of Firestore. We run our commerce layer on it: accounts, entitlements, an audit trail and gated downloads from a private storage bucket.

Can I switch later if I choose wrong? Asymmetrically. Supabase → anywhere-Postgres is a dump and restore, because the lock-in is limited to auth and storage conventions. Firestore → relational is a rewrite: the data model, the queries and the authorization rules all have to be re-expressed, and denormalized documents have to be decomposed. Weight this by how uncertain you are, not by how likely a move seems today.

Which is better for a Next.js template specifically? Supabase, for a narrow, practical reason: templates that include auth wire it as cookie sessions read in Server Components, which is exactly Supabase's SSR model. Firebase's auth is client-listener-shaped, so integrating it into a Server Component template means adding the Admin SDK and a session-cookie exchange. Both work. One is less code you did not write.

Do I need row-level security if all my writes go through the server? Yes. RLS is defence in depth, and the specific thing it defends against is the anon key being used directly against the REST API — which is public by design, because that key ships in your client bundle. Without RLS, "server-only writes" holds exactly until someone reads your JavaScript. Enable it on every table, including the ones you think are internal.

What about the free tier? Both are generous enough to launch on, and both pause or throttle inactive free projects. The thing to check before launching a paid product is not the limits but the failure behaviour: what happens when you exceed them. Neither should be the reason you pick one.

Templates that already have a storefront to put behind it

A backend decision is easier once the frontend exists and you can see the shape of the data it needs.

ASoc Bazaar is a curated multi-vendor marketplace — furniture, electronics and watches in one cart — and multi-vendor is the case that makes the relational argument concrete, because vendors, products, orders and payouts are a join graph rather than a document. ASoc Arcade is a digital game store with an instant-key catalog, which is an entitlement model: who owns what, redeemed when, downloadable how — the same problem our own commerce layer solves. ASoc Sage is a vitamins and supplements storefront across eight categories, the recurring-order shape where customer history matters more than realtime.

Browse the full set of Next.js shop templates, or the Tailwind shop templates. For the payment layer that sits on top of whichever backend you choose, selling a digital product with LemonSqueezy covers checkout, the signed webhook and the entitlement check that gates the download.

Keep reading

Comparison8 min read

Supabase vs MongoDB: The Question Isn't the App, It's the Dataset

Our 8,114-line, 111-product catalog is document-shaped — and lives in a typed array, not a database. The commerce layer is relational, on Postgres. Why the split decides, not the app.

Read more
Comparison10 min read

Supabase vs Neon: Both Are Postgres, So the Database Isn't the Call

Six RLS policies, all built on auth.uid() — a function Postgres does not have. An audit of exactly which of our own code depends on the platform rather than the database.

Read more
Comparison9 min read

Supabase vs PlanetScale: 7 Foreign Keys Into a Table MySQL Doesn't Have

The dialect differences port. The 7 foreign keys into auth.users and 6 policies built on auth.uid() don't — inventoried line by line from this storefront's 8 migrations.

Read more