Skip to main content
ASoc
Comparison

Supabase vs. AWS: What Five Services Replace One Dependency

Supabase bundles Postgres, auth, storage and atomic RPCs behind one SDK. Mapped against this app's real files, here's what RDS, Cognito, S3 and Lambda would cost to assemble instead.

The ASoc Team9 min read

Supabase is one platform — Postgres, auth, storage and functions behind a single client library. AWS is not a backend at all; it's a catalog you assemble one from, service by service. This storefront runs on Supabase, so the honest comparison isn't "which is better" — it's what you'd actually have to wire up on AWS to replace the five things this app gets from one dependency, counted against the real files that use each one.

The short answer

Supabase bundles a managed Postgres database, an auth service, private file storage and a way to run atomic server-side logic, all reachable from one SDK with one set of credentials. AWS gives you the components to build the same thing yourself — RDS, Cognito, S3, Lambda — each with its own console, its own IAM policy shape, and no wiring between them until you write it. Pick Supabase when you want that wiring done for you and can live inside its opinions. Pick AWS when you're already standardized on it, need a service Supabase doesn't offer, or the assembly cost buys you configuration Supabase's opinions don't allow.

What each side actually is

Supabase (this app)The AWS assembly
DatabaseManaged Postgres, one connection stringRDS for PostgreSQL — you provision instance size, patching window, backup retention
AuthBundled auth service, JWTs verified with getClaims()Cognito User Pool issues JWTs; verifying them in a Route Handler is your own middleware
AuthorizationRow-level security, evaluated by Postgres itselfRLS is still available on RDS Postgres, but nothing wires a Cognito claim into a policy automatically — you write that mapping
File storageA private bucket with server-issued signed URLsS3, plus either presigned URLs from your own code or a CloudFront signed-URL setup
Atomic server logicA SQL function, SECURITY DEFINER, called over the same connectionA Lambda function, its own deploy unit, calling back into RDS for the transaction
Client surfaceOne SDK: @supabase/supabase-js / @supabase/ssrOne SDK per service: @aws-sdk/client-rds-data, amazon-cognito-identity-js, @aws-sdk/client-s3, plus the Lambda runtime itself

Every row on the AWS side is a real, capable service. None of them talks to the others by default.

What this app's five Supabase pieces actually do

Counted in this checkout, not estimated.

Database — 8 migrations, one schema. supabase/migrations/ holds 8 files (0001_commerce_init.sql through 0008_refund_requests.sql), each one a plain SQL migration against a single Postgres instance. On RDS, this is the same SQL — Postgres is Postgres — but provisioning, patch scheduling, and connection pooling are now a console you configure yourself, not a bucket item on the Supabase dashboard.

Auth — three clients, one verification rule.

src/lib/supabase/client.ts   — browser client (anon key), identity only
src/lib/supabase/server.ts   — server client, bound to request cookies, every real read
src/lib/supabase/admin.ts    — service-role client, bypasses RLS, server-only
src/proxy.ts                 — refreshes the session every request, via getClaims()

src/proxy.ts is 42 lines. It calls supabase.auth.getClaims() — never getSession(), because getSession() trusts a cookie without verifying the JWT against Supabase's servers, and getClaims() doesn't. On Cognito, the equivalent exists (a JWKS-verified token), but nothing hands it to you pre-wired into a Next.js proxy — the verification middleware is code you write once and then own.

Authorization — 6 row-level-security policies, all for select:

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

auth.uid() reads straight out of the verified JWT Supabase's Postgres extension already has on the connection. RDS Postgres supports the identical create policy ... using (...) syntax — RLS is a Postgres feature, not a Supabase one — but auth.uid() doesn't exist there. You'd write a Lambda authorizer that takes the Cognito claim, sets it as a Postgres session variable per connection (SET LOCAL app.user_id = ...), and reference that variable in the policy instead. Same capability, one more layer you own.

Storage — a private bucket, a signed URL, and nothing else touching it. supabase/migrations/0001_commerce_init.sql documents the releases bucket as private, and src/app/api/download/route.ts is the only code path that calls .storage.from("releases").createSignedUrl(path, SIGNED_URL_TTL_SECONDS, ...) — one call, a 60-second TTL, after authorizeDownload() in src/lib/download.ts has already confirmed the caller owns the edition. On S3 this is the same idea — a presigned URL, or a CloudFront signed URL if you want edge caching — but it's your own SDK call, your own bucket policy, and (if you want the edge-cached version) a key-pair setup in CloudFront that Supabase's dashboard just doesn't require you to think about.

Atomic writes — 3 SECURITY DEFINER functions, one job each:

create_order_with_slots()      — supabase/migrations/0003_commerce_rpcs.sql
refund_order()                 — supabase/migrations/0003_commerce_rpcs.sql (redefined in 0008)
record_download_within_limit() — supabase/migrations/0007_atomic_download_rate_limit.sql

Called from exactly three places — src/lib/lemonsqueezy/webhookDb.ts (twice) and src/app/api/download/route.ts — each over client.rpc("function_name", { ... }), the same connection as every other query. Postgres runs the function body inside one transaction with the function's own privileges, search_path pinned, EXECUTE revoked from anon/authenticated so only server code can call it. The AWS equivalent is a Lambda function that opens its own connection to RDS, opens its own transaction, and either commits or rolls back — the same guarantee, but now split across two systems (the RDS connection pool and Lambda's cold-start lifecycle) instead of living inside Postgres itself.

The one line that's actually the whole argument

Every AWS row above is doable. None of them is wrong. What's different is where the wiring lives: on Supabase, "verify who's asking" and "let Postgres check if they own the row" are two steps inside one system, connected by one JWT that Postgres itself can read (auth.uid()). On AWS, that's Cognito issuing a token, a Lambda authorizer or API Gateway custom authorizer checking it, and a session variable smuggled into an RDS connection so a hand-written RLS policy has something to compare against. Five services, four different consoles, and the join between "authenticated" and "authorized" is code you own instead of a Postgres built-in.

That's the trade, stated plainly: Supabase's assembly cost is paid once, by Supabase, before you ever open the dashboard. AWS's assembly cost is paid by whoever builds this app — every time, on every project, unless a template internally reproduces getClaims()auth.uid() for them.

Where AWS actually wins the argument

None of this is an argument that AWS is worse — it's an argument that Supabase is a pre-assembled form of exactly these AWS services, and pre-assembly has a price on the other side of it: opinions you don't control. AWS wins when you're already running production workloads there and adding a sixth service costs less than a new vendor relationship; when you need something Supabase's managed layer doesn't expose (VPC peering into an existing private network, a specific RDS engine version or extension, Aurora's multi-region replication); or when compliance requires infrastructure your organization already has SOC reports and audit trails for. None of those are "Supabase can't do it" — they're "we've already paid AWS's assembly cost for other reasons, so paying it again here is free."

Mistakes and troubleshooting

SymptomCauseFix
auth.uid() returns null in a Postgres policy on SupabaseThe connection isn't carrying a verified JWT — usually a skipped session refreshConfirm the proxy or session-refresh step ran with getClaims(), not a raw cookie read
Migrating to RDS, RLS policies stop matching any rowauth.uid() is a Supabase-provided function; plain RDS Postgres has no equivalentSet a session variable from your own auth layer (SET LOCAL app.user_id = '<id>') and rewrite policies against it
A Lambda authorizer adds noticeable latency to every requestCold starts on infrequently-invoked authorizer functionsProvisioned concurrency, or cache the authorization decision at the API Gateway layer
Signed URL works locally, 403s from CloudFront in productionPresigned S3 URLs and CloudFront signed URLs use different signing keys and aren't interchangeableDecide up front whether you need edge caching (CloudFront) or a direct-to-S3 URL is enough, and sign for that path specifically
A SECURITY DEFINER function ported to a Lambda loses its transaction guaranteeLambda's connection to RDS is a separate transaction boundary from whatever called itWrap the whole operation in an explicit BEGIN/COMMIT inside the Lambda, or use RDS Data API's transaction support

Frequently asked questions

Is Supabase just AWS with a nicer dashboard? Not quite — Supabase runs its own infrastructure (not resold AWS), but the comparison that matters isn't the hosting layer, it's the assembly. Supabase pre-wires database, auth and storage into one system with one credential model; AWS gives you the pieces and lets you decide how they connect.

Can you self-host Supabase and get AWS-level control? Yes — Supabase is open-source and its self-hosting guide runs the same stack (Postgres, GoTrue for auth, Storage API) on your own infrastructure, AWS included. That trades the managed dashboard for full control, which is a real middle option between the two poles this comparison describes.

Does RLS on RDS Postgres do anything without Cognito wired in? Yes — CREATE POLICY is core Postgres, available on any RDS instance regardless of auth provider. What RDS doesn't give you for free is a function like auth.uid() that reads an authenticated user's ID straight off the connection; you provide that plumbing yourself.

Why does this app call getClaims() instead of getSession()? getSession() reads the session cookie without revalidating it against Supabase's auth server, so a forged or stale cookie can pass. getClaims() verifies the JWT's signature and expiry before trusting it — the same distinction as validating a Cognito token's signature rather than trusting whatever sub claim a client sends.

Templates in this post

ASoc Cover markets a modern insurance brand around a two-minute-quote hero, four cover types, and a live claim-paid dashboard mock. ASoc Echo is an AI customer-support landing page with a live chat-widget preview, a visual flow-builder section, and a conversation-analytics panel. ASoc Edge markets an applied-AI agency around a unified "AI control room" dashboard and an integrations row spanning Messenger, Slack and OpenAI.

Browse the full sets: Next.js landing page templates, Tailwind landing page templates.

Keep reading

Comparison9 min read

Supabase vs. Convex: A Lock You Write vs. a Transaction You Get

Convex's transaction model would have prevented a real TOCTOU race this storefront hit in its Postgres download limiter. Why that's true, and why the fix stayed on Supabase anyway.

Read more
Comparison12 min read

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.

Read more
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