Why we mostly use Supabase now.
Postgres, auth, storage, realtime, edge functions — all in one place, all open-source, all running on infrastructure we can leave at any moment. After two years of production deployments, here's the honest cost-benefit and the patterns we've landed on.
For years our default stack was 'a database, an auth service, an object store, a realtime layer, a deploy target, and a lot of glue'. Each piece was good. The glue was where the time went. Every project lost a week stitching auth tokens to the database, configuring CORS on S3 buckets, deciding whether realtime updates went through Pusher or Socket.io, and reconciling user IDs across three services.
Supabase has quietly eaten almost all of that work. Two years into using it in earnest, it's the answer to most of our new-project questions, and the answer for several of our migrations. Here's the full picture.
What Supabase actually is
A hosted bundle of open-source components, mostly built around Postgres. It is not a proprietary database. It is not a no-code platform. It is Postgres plus a small set of services that talk to Postgres, with a thin REST and realtime layer on top, and a hosting business that runs the whole thing for you at sensible prices.
| Concern | What Supabase provides | What it replaces in our old stack |
|---|---|---|
| Database | Managed Postgres 15+ with extensions | MongoDB Atlas / managed Postgres |
| Auth | GoTrue with sessions, JWTs, social providers | Auth0 / custom Express middleware |
| Storage | S3-compatible object storage with policies | AWS S3 + CloudFront + signed URLs |
| Realtime | Postgres logical replication → WebSockets | Pusher / Socket.io / custom Redis |
| Edge functions | Deno runtime, deployed globally | AWS Lambda / Cloudflare Workers |
| Vector search | pgvector extension built in | Pinecone / Weaviate / hand-rolled |
What changed in practice
- 01Auth + database joined at the hip via Postgres RLS (row-level security). A user can only read rows they own — enforced at the database level, not the application level. Removes an entire class of authorisation bug.
- 02Realtime is now free. We used to budget for a realtime service per project. Supabase pipes Postgres changes to subscribed clients with three lines of code.
- 03Storage is properly integrated. Upload to a bucket, store the path in a Postgres column, serve via signed URLs that respect the same RLS as the database. Permissions become coherent across data and files.
- 04Edge functions for the bits that need server-side compute. We use them for webhook handlers, image transforms, and any auth-sensitive third-party API calls.
- 05Local development is excellent. The Supabase CLI spins up the entire stack in Docker — Postgres, auth, storage, realtime, edge functions — for offline-first feature work.
How RLS actually looks in a real schema
Here is a stripped-down example from one of our SaaS projects. A user can read their own bookings; an admin can read everyone's. The whole authorisation layer lives in three lines of SQL — not in the application.
-- Bookings table, keyed to the auth user
create table bookings (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users on delete cascade,
venue text not null,
starts_at timestamptz not null,
inserted_at timestamptz default now()
);
-- Turn RLS on
alter table bookings enable row level security;
-- Users see their own bookings
create policy "Users read own bookings"
on bookings for select
using (auth.uid() = user_id);
-- Users insert their own bookings
create policy "Users create own bookings"
on bookings for insert
with check (auth.uid() = user_id);
-- Admins see everything (admin is a custom JWT claim)
create policy "Admins read all bookings"
on bookings for select
using (auth.jwt() ->> 'role' = 'admin');And from the client side — Nuxt + Supabase
The whole client interaction is one well-typed call. The same query honours the policies above without any application-level checks.
import { createClient } from '@supabase/supabase-js'
import type { Database } from '~/types/supabase'
const config = useRuntimeConfig()
const supabase = createClient<Database>(
config.public.supabaseUrl,
config.public.supabaseAnonKey
)
// RLS means we only get the bookings this user owns
const { data: bookings, error } = await supabase
.from('bookings')
.select('id, venue, starts_at')
.order('starts_at', { ascending: true })
// Realtime subscription — instantly reflect new bookings
supabase
.channel('user-bookings')
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'bookings',
filter: `user_id=eq.${supabase.auth.getUser().data.user?.id}`
}, (payload) => {
bookings.value?.push(payload.new as any)
})
.subscribe()Three lines for the query. Five lines for realtime. The schema enforces authorisation. The client never accidentally over-fetches because the database refuses to.
What we still bring our own of
- 01A real frontend framework — Supabase is the back end, not the app. We pair with Nuxt almost every time.
- 02Email — transactional email is best left to a dedicated provider. We use Resend or Postmark, depending on the project's volume.
- 03Payments — Stripe always. Supabase doesn't try to do this and shouldn't.
- 04Analytics — Plausible or Fathom for marketing sites; PostHog where product analytics matter; nothing baked in via Supabase.
- 05Anything regulated where we need very specific data residency we can prove ourselves — though Supabase's region selection is increasingly granular.
Why it's hard to argue with
It's open-source Postgres underneath. If Supabase goes away tomorrow, we own a Postgres database with a working schema, and we move it elsewhere over a weekend. The auth service (GoTrue), the storage service, the realtime engine — all open-source, all self-hostable. The lock-in is, structurally, almost zero.
Compare to Firebase, where the data model is proprietary, the auth is tied to Google, and the migration story when you outgrow the platform is 'rewrite half the application'. Supabase replaced Firebase for us within six months of trying it.
The lock-in is, structurally, almost zero. That's the strongest argument for any infrastructure choice we've made in a decade. Supabase doesn't ask you to bet your project on Supabase — only on Postgres.
When we don't reach for it
- 01Pure marketing sites with no auth, no database. Static + form endpoint is enough.
- 02Heavy data-warehousing workloads. Supabase is OLTP-shaped; analytics workloads belong elsewhere (BigQuery, ClickHouse).
- 03Projects with very specific regulatory requirements where we need full control of the storage layer.
- 04Anywhere the team has deep operational expertise in a different database (a legacy MySQL shop staying on MySQL is correct, not stubborn).
The pricing reality
Supabase's free tier is genuinely useful (real Postgres, real auth, real storage). The Pro tier at $25/month is plenty for most small SaaS products we ship. Production-grade workloads with serious traffic land somewhere between $100 and $1,000 per month, depending on database compute and bandwidth — typically cheaper than the equivalent AWS managed services and dramatically cheaper than the AWS-plus-Auth0-plus-Pusher-plus-everything stack we used to assemble.
It's not the answer for every project. But it is the answer for most of them, most of the time, now.