Supabase pricing has a cliff in it. The free tier is real but pauses your project after a week of inactivity, and the next rung is Pro at $25/month. Between "free with an asterisk" and "$25 per month" there is nothing - and that gap is exactly where most side projects, MVPs, and internal tools live.
Self-hosting is the usual answer, but "self-host Supabase" typically means the full eight-container stack and a machine big enough to hold it. So we built a ladder instead: three progressively smaller cuts of the Supabase stack, each memory-tuned under load until it honestly fits a fixed-size plan. All three are open source (MIT) in the deployable.sh catalogue, run locally with plain docker compose up, and deploy to Miget in one step at a flat price:
| Template | Price | RAM | What you get | Dashboard |
|---|---|---|---|---|
| supabase-pico | $5/mo | 512 MiB | Postgres, Auth, REST behind an nginx gateway | From your laptop |
| supabase-nano | $7/mo | 1 GiB | Same APIs plus Kong, Studio and postgres-meta | On the server |
| supabase-lite | $13/mo | 2 GiB | The nano lineup with comfortable headroom | On the server |
| supabase (full) | ~$25/mo | 4 GiB | Everything: Realtime, Storage, edge functions, pooler | On the server |
Disclosure: Miget is our platform and the templates deploy there in one click - but every compose.yaml is vanilla and runs anywhere Docker does. The prices above are Miget's flat plans; nothing in them meters requests, rows, or active users.
What the cuts actually cut
Honesty first, because a $5 Supabase is only impressive if you know what it is not. All three tiers below the full template drop Realtime, Storage, edge functions, and the connection pooler. What remains is the core that most Supabase apps actually exercise: Postgres itself, GoTrue (signups, logins, JWTs), and PostgREST (the auto-generated REST API with row-level security). If your app subscribes to live changes or stores user uploads through Supabase Storage, you want the full template or the cloud - the ladder is for the apps that use Supabase as "Postgres with auth and an instant API," which in our experience is most of them.
The differences inside the ladder:
- pico (512 MiB, $5) is headless: no Studio on the server. The trick that closes the math is replacing Kong with a tiny nginx gateway (16 MiB allowance, 8 MiB peak under load) that does exactly what Kong did for these routes - path routing, apikey allowlist, Authorization fallback, CORS, and the same JWT-secret-to-API-key derivation at startup. The whole stack allocates 400 MiB and peaked at ~217 MiB in testing - real headroom inside the 512 MiB plan. When you need the dashboard, you run Studio locally against the remote database.
- nano (1 GiB, $7) brings the server-side dashboard back: six services - Kong, Auth, REST, Postgres, Studio, postgres-meta - memory-tuned until the whole stack allocates exactly 1024 MiB.
- lite (2 GiB, $13) is the same lineup with margins instead of tuning heroics: 1568 MiB allocated, a deliberately roomy Postgres (512 MiB, so it keeps a real page cache), and the tier we point production guides at.
The engineering that makes the prices honest
Anyone can claim a stack "runs" in 512 MiB by letting it swap or crash on the first traffic spike. These templates were sized the unglamorous way: hard no-swap memory limits per service, each runtime's own memory bounded explicitly, then load tests - concurrent REST readers plus a signup loop and a service-role write loop, with the supabase-js example running alongside - and the limits adjusted until every tier passed cleanly. Each test run issues a batch of 30 requests; "30/30" below means all of them completed, with 0 errors and 0 container restarts.
What healthy looks like, so the failure numbers below have a scale:
| Tier | 16 concurrent | 48 concurrent | Result |
|---|---|---|---|
| pico | 202 ms | 399 ms | 30/30, 0 errors |
| nano | 206 ms | 552 ms | 30/30, 0 errors |
| lite | 216 ms | 379 ms | 30/30, 0 errors |
Tripling the concurrency roughly doubles latency and moves memory by at most 1 MiB per service - at this size the footprint is baseline-dominated, not load-driven, which is why the numbers are stable enough to publish at all.
The findings that actually set the limits:
- "Peak seen" describes the allowance, not the demand. Kong handed 128 MiB reports a 127 MiB peak; handed 160 it reports 151; handed 192 it reports 164. The peak tracks whatever limit it gets (Studio and postgres-meta do the same), so the only meaningful sizing input is the limit below which the service stops performing.
- Kong breaks below 160 MiB - silently. At 128 MiB: no OOM, no restart, nothing in the logs, a healthy-looking 127/128 - and 21 of 30 requests served at ~5-second latencies, against the ~200-550 ms the healthy tiers run. 160 works (at 94% of its allowance); the templates give 192 as margin that makes the failure mode irrelevant rather than merely avoided. Margin, not speed - 192 is not faster than 160.
- GoTrue peaks at ~56 MiB - and an earlier 48 MiB allowance sat below the peak of GoTrue v2.189: a latent OOM, not a tight fit. You only learn that by measuring the version you actually ship.
- PostgREST at 224 MiB was an illusion - GHC expanding into whatever allowance it gets. With
PGRST_DB_POOL=2it measures 16-17 MiB, which is what makes the 64 MiB rung real. - V8 caps were chosen by sweep, not vibes. On one Studio deployment under the same load, only the heap cap changing: 192 MiB cap gave 340 MiB RSS at 433 ms; 160 gave 229 at 379 ms; 128 gave 225 at 325 ms. RSS lands near cap + ~140 MiB, so lite caps Studio at 160 against a 384 allowance and nano at 128 against 320. The tighter caps did not cost latency on these runs - we will not claim they helped, but they certainly did not hurt.
- Postgres runs honest small-tier settings (
shared_buffers=64MB, 30-40 connections, pinned autovacuum/WAL memory) - except on lite, where it deliberately gets 512 MiB so it keeps a real page cache.
One thing that did not work, for the record: KONG_MEM_CACHE_SIZE - the obvious fix, on the theory that Kong's default 128m shared dict was the footprint. Setting it to 64m changed nothing (still 127/128). That is how we learned Kong's memory is not the shared dict, and why the templates deliberately do not set it.
None of this is exotic. It is the boring discipline of measuring instead of guessing, applied service by service until the price point stops being marketing.
When you should not do this
- You use Realtime, Storage, or edge functions as core features. Take the full template (4 GiB, ~$25 - at which point compare seriously with Supabase Pro at the same price, which includes backups and someone else's pager).
- Your database is your business and you have no backup discipline. Managed Supabase does point-in-time recovery for you. Self-hosted, the backup is yours to own - on any tier.
- You are pre-launch and idle pauses do not hurt yet. Supabase's free tier is genuinely fine until the pause bites.
Deploying one
Every template works two ways. Locally:
git clone https://github.com/deployable-sh/stacks
cd stacks/supabase-nano
cp .env.example .env # fill in secrets
docker compose up
On Miget: point a stack at the repo with the template's path, set the same secrets, and the platform layers the sizing file (compose.miget.yaml) on top automatically - that file is where all the memory tuning above lives, and plain docker compose ignores it. Your app connects the same way in both cases, and if it grew out of a Lovable export or any supabase-js frontend, it keeps working - the API surface is standard Supabase.
Start at pico, outgrow it, pg_dump into nano or lite - the database image is Supabase's own Postgres on every tier, so nothing translates or migrates except the plan you pay for. That is the point of the ladder: the bill is a known number at every rung, and the rungs are $5, $7, $13.
Frequently asked questions
Is self-hosting Supabase cheaper than Supabase Cloud?
Below the Pro tier, yes, structurally: the ladder runs $5-$13/month flat for the core API surface, against $25/month for Pro. At the full-stack level ($25 vs $25) the honest answer is that you are no longer buying a lower price - you are buying data ownership and no usage components, and paying with your own ops time.
What is missing compared to Supabase Cloud?
On the ladder tiers: Realtime subscriptions, Storage, edge functions, the pooler - plus everything operational that Pro includes (automated backups, point-in-time recovery, support). The auth, database, and REST API surface is the standard one your supabase-js code already talks to.
Can I move up a tier later?
Yes. All tiers run Supabase's own Postgres image, so moving is a pg_dump/restore into the bigger template (or, on Miget, redeploying the larger stack against the same secrets). Start small deliberately.
Do these templates require Miget?
No. Each is a vanilla compose.yaml that runs anywhere Docker does - the MIT-licensed repo is public. Miget adds the one-step deploy, the flat pricing, and the pre-tuned sizing overlay.
Why does the $5 tier have no Kong?
Because Kong wants 160-192 MiB to route this stack properly (below 160 its throughput quietly collapses, with no error anywhere), while nginx does the same job for these routes in a 16 MiB allowance with an 8 MiB peak. The pico gateway reimplements exactly the routes, key checks, CORS behavior, and API-key derivation the stack needs - that one swap is what makes 512 MiB honest instead of aspirational.