Supabase in production
Your Supabase app works locally and breaks in production
Supabase gives you a real Postgres database, auth, storage and realtime in one project, which is why the AI tools reach for it. But the defaults are tuned for building, not for traffic. The database has a hard connection ceiling. Queries are cut off after a few seconds. The function runtime has a CPU budget your laptop never enforced. If your app worked all week and fell over the day real users arrived, one of the six failures below is the likely cause. None of them shows up while the only user is you.
Fixed £4,500 · 3 days · pass-or-refund · next slot w/c 3 August
What actually breaks
- Serverless traffic exhausts your Postgres connectionsYour API routes run as serverless functions, and each instance opens its own Postgres connection. The AI copied the direct connection string, port 5432, built for one long-lived server. Postgres accepts a fixed number of connections, set by your compute size, and serverless instances hold theirs open. A traffic burst exhausts the slots and every request after that fails. Supabase's transaction-mode pooler on port 6543 exists for this, but it does not support prepared statements, so swapping the string alone can trade one error for another.
- Queries that were instant in dev start dying at the 3 second statement timeoutSupabase caps query time by role: 3 seconds for requests using the anon key, 8 seconds for authenticated users. On your laptop, with a few hundred rows, nothing comes close. In production the table grows, a filter misses an index, and Postgres cancels the statement mid query. Your API call returns error 57014, the page shows nothing, and the same query works fine when you test it in the SQL editor as postgres, which gets two minutes.
- Edge functions get two seconds of CPU per requestEdge functions run on Deno with hard ceilings: two seconds of CPU time per request, 256MB of memory, and a wall clock limit of 150 seconds on the free plan. Waiting on a model API is fine, because async waiting does not count as CPU time. Parsing a large upload, resizing an image or hashing a password in the function can blow the budget, and the platform kills the request with a resource limit error. Locally, none of these ceilings exists, so you never saw it fail.
- Storage buckets are public forever or locked to everyoneFiles live in buckets, and a bucket is either public or private. Public means anyone with the link can read every file in it, no login needed: the wrong setting for invoices, exports or ID documents. Private buckets deny everything by default; uploads and downloads only work once you write policies on the storage.objects table, which are separate from the policies on your data tables. The quick way to make a demo work is one public bucket, and that is often the choice the AI made. Check which yours is.
- The dashboard and your migrations disagree about the schemaSupabase gives you two ways to change the schema: migration files pushed by the CLI, and the dashboard's table editor, which edits production directly. The CLI tracks what it has applied in a history table; dashboard edits never appear there. Once someone adds a column through the dashboard, your local database and production quietly diverge, and the next push fails against a history that no longer matches. The fix is one route for schema changes: migrations, every time.
- Realtime holds for a demo, not for a launchLive updates run over WebSockets, and the free plan allows 200 concurrent connections and 100 messages per second across the whole project. Every open browser tab holds its own connection. One user in the demo never touches these numbers; a launch does. Go over and Supabase refuses new connections with a too_many_connections error, and if message throughput exceeds the plan it disconnects clients until traffic drops. To your users that looks like the live features dying at the exact moment the app got popular.
Every one of these leaves evidence
Connection exhaustion shows up in the database logs as refused connections. The email failure announces itself: sign-ups from outside your project team get an error back from the auth API, and the send never happens. Edge function breaches are named in the function logs as resource limit errors. Bucket exposure takes one look at the storage settings. Schema drift is a diff between your migrations folder and what production reports. Realtime overages arrive as named errors on the WebSocket. The diagnosis is not guesswork: read each layer's own logs and settings in order, confirm which of the six are live in your project, then fix them in the order that unblocks real users first. In practice a broken project usually has more than one.
What “done” looks like
We agree one of these in writing before you pay anything. If it passes, the sprint passed. If it doesn't, you get a full refund and keep the work.
- Concurrent traffic no longer exhausts the database: connections run through the transaction pooler and a load test passes cleanly.
- A stranger can sign up and receive the confirmation email, sent through a production SMTP provider.
- One schema everywhere: migrations apply cleanly and production matches what you test against locally.
- Edge functions stay inside their CPU and memory budget on real payloads, and storage buckets are private unless public is the point.
We agree the acceptance test before any work starts. If it doesn't pass within three working days, you get a full refund and keep every change I've committed. The risk is mine, not yours.