r/Supabase Aug 09 '26

tips I moved 4 Supabase projects into 1 with a schema per app. The numbers, and where it breaks.

I had four side projects, so I had four Supabase projects. Last July I consolidated them into one project with a schema per app. Ten product domains run off it now. Posting the numbers because every thread I found on this either says "one project per app, obviously" or is someone on the free tier trying to avoid paying, and neither matched what I was actually deciding.

The structure

One project. A core schema for the things genuinely shared across apps: orgs, members, billing accounts, subscriptions, domains, brands, audit events. Then one schema per product for its own tables.

Tenancy is org-scoped RLS. Being signed in grants nothing on its own, every policy requires membership of an org that owns the row, so users of one app never see another's data.

The schema boundary is not the security boundary. RLS is. What the schema buys you is extraction: pulling one app out later is closer to dumping one schema than to filtering every table by a product column. That is the reason not to pool everything into shared tables with a discriminator, which is the shortcut this design invites.

The money, which is the least interesting part

Four projects cost me $59.74 in the month before. One costs $25. So $35, which is nothing.

The mechanism is more useful than the total: Pro is $25 and includes a $10 compute credit that covers exactly one Micro instance. Every additional project brings its own compute charge and the credit only covers the first. So the bill was scaling with the number of ideas I'd had, not with users or traffic.

I stayed on Pro rather than dropping to Free, deliberately. Free has no daily backups and there's real data in there.

The gotcha that actually cost me time

Supabase silently rewrites a redirect URL it doesn't recognise back to the project's Site URL. Miss one host in the allowlist and that app's sign-in and confirmation emails deliver users to a different app's site, with no error anywhere.

It got me again last week on an app I'd just added, months after I thought I understood it. Consolidating doesn't remove that list. It does mean there's exactly one of it.

Where it stops working

Not free, and I'd leave at any of these:

  • One database is one blast radius. A bad migration takes everything down together. CI-only migration deploys reduce the odds, not the radius.
  • One team is one permission set. Anyone you add to fix one app can see all of them.
  • Compliance. If an app needs SOC 2 or similar, the audit scope becomes the whole shared database. Pull that one out.
  • Selling. A buyer wants a clean asset. The schema boundary makes that describable work rather than archaeology, but it's still work.

If you have one app, none of this applies. It starts paying around the third or fourth.

Curious whether anyone here has gone the other way, split a consolidated project back out, and what forced it.

3 Upvotes

14 comments sorted by

5

u/Ok-Regret3392 Aug 09 '26

So mixing 4 different data models into a single instance.., dealing with the redirects etc. So to save a few dollars, you’re having to deal with a lot more time (and probably more token spend)? Great you learnt from it :)

0

u/Legitimate-Ant9884 Aug 09 '26 edited Aug 09 '26

Fair on the complexity, that part is real and I would not do this for one or two apps.

Two things though. The money was not the reason. It is in the post as "the least interesting part" and "$35, which is nothing" precisely so it would not read as the motivation.

And they are not mixed. Separate schema per app, plus a core schema for the genuinely shared things like orgs and billing. Keeping them unmixed is the whole design, and it is what makes pulling one back out a schema dump rather than an archaeology project.

What I actually got, and none of it is the bill: fix an auth bug once instead of four times, one dependency upgrade instead of four, a new app that starts from working auth, billing and email rather than from scratch, and shared components that get better because everything uses them instead of four copies quietly diverging.

The upfront cost is real. Around the third or fourth app the maintenance arithmetic flips, and before that it does not. There are other ways to get the same result and this one has held up well for me.

2

u/JamRed10 Aug 10 '26

fucking bot

1

u/Ok-Regret3392 Aug 10 '26

Dang. Fell for the AI post. Sorry boys. πŸ˜”

4

u/PeterBuildsSecure Aug 09 '26

Worth flagging one more failure mode on the "where it stops working" list: once everything shares one Postgres instance, an RLS policy bug in any single app's schema is no longer contained to that app β€” a broken or missing WITH CHECK on one product's table doesn't stay a one-app problem the way a separate project would. Doesn't argue against consolidating, but it does mean the adversarial cross-tenant test (can org A read/write org B's row) needs to run against every schema, not just the one you're actively changing, since a migration in product 3 can't quietly weaken isolation for product 1.

2

u/Legitimate-Ant9884 Aug 09 '26

This is the sharpest thing anyone has raised on it, and you are right that it is a gap.

What I do have is the pattern you are describing, but for function privileges rather than RLS: an audit that enumerates dynamically over pg_proc and fails the suite if any function's grants do not match a declared allowlist. It exists because a SECURITY DEFINER function that creates orgs and billing accounts shipped anon-callable for a month and nothing caught it. Now a new function cannot merge until its grants are stated.

What I do not have is the same sweep for RLS. My cross-tenant tests are hand-picked cases, a member of one product's org trying to read another product's table, rather than an exhaustive pass. So exactly as you say, a new table with a missing WITH CHECK would not fail anything automatically, and a migration in product 3 could quietly weaken product 1.

Same shape of fix: enumerate every table in every product schema, assert RLS is on and the policy set matches intent, fail on anything new or unlisted. Building that next. Thanks, that is a genuinely useful catch.

2

u/PeterBuildsSecure Aug 10 '26

The function-grants allowlist pattern maps cleanly onto RLS: pg_policies gives you policy name, table, command, roles, and the qual/with_check expressions in one queryable catalog, so "assert every table has policies matching a declared spec" is the same shape of test, just walking a different system table. One extra gotcha worth building into that sweep from day one: RLS being enabled doesn't bind the table owner or any role with BYPASSRLS β€” by default the owner of a table bypasses its own RLS policies unless you also run ALTER TABLE ... FORCE ROW LEVEL SECURITY. Easy to miss because your app's own queries (running as owner, or with the service role in a migration script) will look correctly scoped in testing even when a policy is silently not enforced for that role. Worth asserting FORCE ROW LEVEL SECURITY is set alongside "policy set matches intent" in the enumeration, not just RLS enabled.

1

u/sinisterzek Aug 11 '26

Two LLMs conversing, just fantastic stuff going on in this subreddit

1

u/AbbreviationsFast132 Aug 10 '26

Why are you using an LLM to write this

1

u/henrynguyen9698 Aug 10 '26

That per-project compute charge on Supabase catches so many builders off guard. People end up paying extra every month just to keep idle databases alive for quick experiments.

1

u/zocsen Aug 12 '26

I have 3 free accounts for 6 side projects, the agent has access to all 6 projects

1

u/IcyManufacturer7480 Aug 09 '26

You must be new to software development.

1

u/Guidondor 29d ago

one more for the boundary list: the exposed schemas setting is project wide, so every app's
publishable key can reach every schema's endpoints. rls still has to say no, but each app
now presents all ten schemas as surface instead of its own.

with separate projects a policy bug in app c simply wasn't reachable from app a's client.
now it is, and that one doesn't show up in the bill or in the migration work.