Skip to main content
ASoc
Comparison

AWS Amplify vs. Firebase: Neither Bundled Backend Is What We Run

8 migrations, 6 RLS policies built on auth.uid(), and a write path that deliberately isn't rule-gated like either platform's default posture.

The ASoc Team9 min read

AWS Amplify and Firebase both sell the same trade: adopt their bundled auth, database and hosting stack, and skip building a backend yourself. Amplify wraps Cognito, AppSync/DynamoDB and Amplify Hosting; Firebase wraps Firebase Auth, Firestore and Firebase Hosting. Neither is what runs this storefront. It runs Postgres — via Supabase, with SQL row-level security instead of either platform's own rule language — and the choice was about which parts of "bundled" were worth keeping.

What each platform actually bundles

Both platforms answer "auth + database + hosting" with one proprietary stack. The differences that matter for a real app aren't the marketing copy — they're the data model and the security model, because those two determine what you can and can't do later without a rewrite.

AWS AmplifyFirebaseWhat this storefront runs
Data modelDynamoDB (NoSQL) via AppSync/GraphQL, or Amplify Data's schema layerFirestore (NoSQL document store)Postgres (relational, plain SQL)
AuthCognito user poolsFirebase AuthenticationSupabase Auth (GoTrue) over the same Postgres database
Access-control modelIAM policies + Amplify's @auth authorization rules on the schemaFirestore Security Rules — a separate declarative DSLRow-Level Security — SQL POLICY statements on the actual tables
HostingAmplify HostingFirebase HostingVercel
Payments/taxNot bundled — a separate processor is required either wayNot bundled — sameNot bundled — LemonSqueezy as merchant of record

The last row is the one point where all three agree: none of them is a checkout. Every path here ends at a third-party payments provider regardless of which backend platform holds the data.

Why the security model is the real fork in the road

Amplify's @auth directives and Firestore's Security Rules are both declarative, both genuinely powerful, and both their own language — you learn Amplify's rule grammar or Firestore's rule grammar, and what you can express is bounded by what that grammar supports. Querying across the boundary those rules protect (a join, an aggregate that spans documents/records owned by different users) is where both diverge hardest from a relational database, because neither NoSQL store's query layer was built around joins in the first place.

Row-Level Security is a Postgres feature, not a platform's abstraction on top of one, which means the six policies this storefront's schema actually has are ordinary SQL:

-- supabase/migrations/0001_commerce_init.sql
alter table public.profiles enable row level security;
alter table public.orders   enable row level security;

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

auth.uid() reads the authenticated user's ID out of the request's JWT, and the policy runs as part of the query plan — a select * from orders issued by an authenticated client only ever sees rows where user_id matches, enforced by Postgres itself, not by application code checking ownership after the fetch. Six of these across eight migrations (0001 through 0008) cover every table a buyer's own client is allowed to read: profiles, orders, entitlement_slots, download_events, download_deliveries, refund_requests. Every one of them still joins, filters and aggregates like any other Postgres table, because RLS is a filter on a normal table, not a different kind of database.

The write side is deliberately not symmetric

Here's the part a straight RLS pitch usually skips: this schema has zero write policies for anonymous or authenticated roles. Every insert, update and delete goes through server code using the service-role key, which bypasses RLS entirely:

// src/lib/supabase/admin.ts
export function createAdminClient() {
  return createSbClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.SUPABASE_SERVICE_ROLE_KEY!,
    { auth: { persistSession: false, autoRefreshToken: false } },
  );
}

Two of the eight migrations add SECURITY DEFINER functions — Postgres functions that run with the privileges of the function's owner rather than the caller's — for the operations that need atomicity a plain RLS-gated insert can't give (an entitlement redemption, a download rate-limit check-and-increment). This is a genuinely different shape from Amplify or Firebase's default posture, where the same declarative rules that gate reads typically gate writes too, on the same schema, in the same rule language. Here the read boundary is declarative (RLS policies, checked by Postgres per-query) and the write boundary is explicit (server code, checked by review), which is a deliberate choice: reads are the common case exposed directly to clients, writes are rare enough and consequential enough to route through code a human wrote and can audit.

The session-validation pitfall every one of these platforms has, worded differently

Cognito, Firebase Auth and Supabase Auth all issue a session token, and all three have the same trap: reading a cookie that says a user is signed in is not the same as validating the token inside it. This repo's session-refresh code names the distinction directly:

// src/proxy.ts — runs on every request
// Uses getClaims() (validates the JWT) — never getSession().
await supabase.auth.getClaims();

getSession() returns whatever the cookie claims without verifying the signature server-side; getClaims() validates it. The equivalent mistake with Cognito is trusting a client-decoded JWT instead of verifying it against Cognito's public keys; with Firebase it's trusting onAuthStateChanged's client state instead of verifying an ID token server-side with the Admin SDK. Every token-based auth platform has this exact fork, worded in its own SDK's vocabulary — it isn't a Supabase-specific gotcha, and it's worth checking whichever platform you land on has an equally explicit "verify, don't trust" call in its docs.

What actually shipped, measured

  • 8 migrations (0001_commerce_init.sql through 0008_refund_requests.sql), each with a comment naming the security rule it implements (R-2/R-13/R-3, R-9, …) — see ~/.claude/security/secure-backend-playbook.md's R-1…R-17 for what those reference.
  • 6 tables under RLS, 6 read-own policies, all built on auth.uid().
  • 2 SECURITY DEFINER RPCs for the two operations that need atomicity beyond what a policy-gated insert provides.
  • 0 write policies — every write is server-code-gated, not rule-gated.
  • Hosting is Vercel: this storefront's latest build ships 428 routes, 420 statically prerendered, 8 rendered on demand — none of that is Amplify Hosting's or Firebase Hosting's build pipeline, because Next.js on Vercel was the piece that had nothing to do with the Amplify-vs-Firebase question at all.

Troubleshooting

SymptomCauseFix
A query needs to join data owned by two different usersFirestore/DynamoDB's document model doesn't join across owners cheaplyModel the relationship in Postgres and let RLS filter each side, or denormalize deliberately and document why
Amplify Gen 1 → Gen 2 migration breaks the schemaAmplify's own schema-definition format changed between generationsBudget real migration time — this is Amplify's own platform risk, not a third-party dependency's
Firestore Security Rules pass locally, fail in productionRules were tested against the emulator, not the real project's data shapeRun the Firebase emulator suite against production-shaped test data before deploying rules
A client reads data it shouldn't after a session refreshCode path used getSession()/cached client claims instead of validatingVerify server-side — getClaims() here, the equivalent Admin SDK call on Firebase/Cognito
"Do I even need a separate payments provider?"Backend platform confused with a full commerce stackNo — Amplify, Firebase and Supabase all stop at auth/database/hosting; every one of them needs a merchant of record on top

FAQ

Is AWS Amplify or Firebase better for a new project? Depends which lock-in you'd rather accept. Amplify couples you to AWS's IAM and DynamoDB/GraphQL model; Firebase couples you to Firestore's document model and its own rules DSL. Neither is "worse" in the abstract — the real question is whether your data is naturally relational (joins, foreign keys, aggregates across owners), in which case both are a worse fit than plain Postgres.

Why choose Supabase over Amplify or Firebase if it's "just Postgres"? That's the point, not a limitation — RLS policies are ordinary SQL running inside a database you could point any Postgres client at, including ones neither AWS nor Google ships. The cost is doing more setup yourself (this schema's 8 migrations exist because nobody bundled them); the benefit is a portable, unopinionated data model underneath.

Do Amplify or Firebase handle payments? No. Both stop at auth, database and hosting — this storefront's LemonSqueezy integration (merchant of record, checkout, tax) is a separate concern regardless of which backend platform sits underneath it.

What's the single biggest gotcha migrating away from either platform later? The data model, not the code. DynamoDB and Firestore data is shaped around each engine's query constraints (no cheap joins), so it's usually denormalized in ways a relational schema isn't. Moving to Postgres later means re-normalizing, not just changing a connection string.

Templates in this post

ASoc Chain is a DeFi protocol landing page, ASoc Cognition an AI consulting agency page, and ASoc Coin an online banking template — three landing pages sold through the same account/entitlement system this post audits, regardless of which backend platform a buyer's own product ends up running on.

Browse the full sets: Next.js landing page templates, Tailwind landing page templates. For the hosting half of this decision, see Vercel vs. AWS Amplify; for the database half against another Postgres provider, Supabase vs. Neon.

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