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.
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
| Axis | Supabase | Firebase |
|---|---|---|
| Database | PostgreSQL, relational | Firestore (document) + Data Connect (Postgres) |
| Query model | SQL, joins, views, CTEs | Document reads, limited querying |
| Authorization | Row-Level Security, in the database | Security Rules, in a config language |
| Auth | Email, OAuth, magic link, SAML | Broader — phone, anonymous, more providers |
| Offline support | None first-class | Excellent, the core strength |
| Realtime | Postgres replication, per-table | Best in class, sub-10 ms document reads |
| Server-side rendering fit | Native — SSR clients, cookie sessions | Workable, less idiomatic |
| Local development | Full stack in Docker, real Postgres | Emulator suite |
| Portability | Open source, it is just Postgres | Proprietary; migrating means rewriting |
| Schema migrations | SQL files, versioned in git | Schemaless; migrations are your problem |
| Cost shape | Per instance, predictable | Per read/write/delete, scales with traffic |
| Right for | Relational, web-first products | Mobile-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
selectscoped to the owner and grants nothing else toanonorauthenticated. 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 DEFINERfunction. 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 — pinnedsearch_path,executerevoked fromanonandauthenticated, 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
| Mistake | What happens | Fix |
|---|---|---|
getSession() on the server | Trusting an unverified, client-controlled cookie | getClaims() — it validates the JWT |
| RLS enabled, no policies written | Every read returns zero rows, silently | Write the select policy; test as the role |
| Table created without RLS | Fully public via the anon key | Enable RLS on creation; the advisor catches it |
| Client-side writes with a permissive policy | Users can mutate their own rows arbitrarily | No write policies; writes via service role |
| Check-then-write in application code | Race condition under concurrency | One SECURITY DEFINER RPC |
| Service-role key in a client component | Total database compromise | Server-only; never NEXT_PUBLIC_ |
| Auth SDK imported in a shared layout | ~68 KiB gzipped on every page | Dynamic import at the call site |
| Firestore reads in a loop for a join | Cost scales with data, not with users | Denormalize deliberately, or use Postgres |
No search_path pin on a definer function | Privilege escalation via schema shadowing | set search_path = '', schema-qualify |
| Firebase Admin SDK assumed to respect rules | It bypasses them by design | Authorize 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.
