Running ~50 apps on one self-hosted Supabase instance. What broke along the way
I’ve been working on hosting a lot of small internal apps (mostly built by non-dev colleagues) on a single self-hosted Supabase, and I’d really like to compare notes with anyone who’s tried something similar.
It’s one VM with Docker Compose and one Postgres cluster, with a database per app. Each app gets its own Auth and PostgREST containers, and Storage, Realtime and Edge Functions are shared. Most of Supabase actually worked unchanged, and supabase-js apps didn’t need any modification, since each app’s API is served on the same subdomain as its frontend. Pretty much all the friction came from running many projects where self-hosted Supabase expects one.
Roles were the first thing that bit me. Postgres roles are global to the cluster, so each app needed its own login roles (app_<id>_authenticator, app_<id>_auth_admin, etc.), cross-database access had to be revoked, and I had to check that Auth, Storage and the migrations didn’t assume fixed role names.
Then connections, and those ran out pretty fast. 50 apps with their own pools exceed max_connections, so I put PgBouncer in transaction mode in front of PostgREST and Storage. Auth and Realtime need session-level connections, so they still connect directly. And since PostgREST no longer gets schema change notifications through the pooler, those have to be relayed separately.
Realtime’s multi-tenant mode needs a superuser to migrate each tenant, and Broadcast from Database only worked for one tenant per instance, so I ended up patching the image. Edge Functions were a bit more annoying, there’s no multi-tenant mode at all. A custom main worker loads each app’s code based on a header set by the reverse proxy, and isolation is V8 isolates rather than containers, which seems fine for code written by colleagues, but I wouldn’t run anything untrusted on it. Studio only manages one project self-hosted, so I’m on a community multi-project fork with a single maintainer, which is a bit of a risk tbh.
There’s one part I haven’t really tested yet, and it’s upgrades. Upgrading Auth means running its migrations on 50 databases and recycling 50 containers without downtime, and I haven’t done a real upgrade on the running setup.
Curious if anyone here has upgraded Auth or Realtime across a lot of databases on a self-hosted instance? And is there a cleaner way to get multi-tenant Broadcast from Database than patching the image?
If you plan to host vibe-coded apps for your company, have a look at marmelab/platform on GitHub. Fork it, try it, and tell me what breaks!
If you want to dive deeper into the technical trade-offs, I wrote a full breakdown here.
8 points · 10 comments
Mentions
Running ~50 apps on one self-hosted Supabase instance. What broke along the way I’ve been working on hosting a lot of small internal apps (mostly built by non-dev colleagues) on a single self-hosted Supabase, and I’d really like to compare notes with anyone who’s tried something similar. It’s one VM with Docker Compose and one Postgres cluster, with a database per app. Each app gets its own Auth and PostgREST containers, and Storage, Realtime and Edge Functions are shared. Most of Supabase actually worked unchanged, and supabase-js apps didn’t need any modification, since each app’s API is served on the same subdomain as its frontend. Pretty much all the friction came from running many projects where self-hosted Supabase expects one. Roles were the first thing that bit me. Postgres roles are global to the cluster, so each app needed its own login roles (app_<id>_authenticator, app_<id>_auth_admin, etc.), cross-database access had to be revoked, and I had to check that Auth, Storage and the migrations didn’t assume fixed role names. Then connections, and those ran out pretty fast. 50 apps with their own pools exceed max_connections, so I put PgBouncer in transaction mode in front of PostgREST and Storage. Auth and Realtime need session-level connections, so they still connect directly. And since PostgREST no longer gets schema change notifications through the pooler, those have to be relayed separately. Realtime’s multi-tenant mode needs a superuser to migrate each tenant, and Broadcast from Database only worked for one tenant per instance, so I ended up patching the image. Edge Functions were a bit more annoying, there’s no multi-tenant mode at all. A custom main worker loads each app’s code based on a header set by the reverse proxy, and isolation is V8 isolates rather than containers, which seems fine for code written by colleagues, but I wouldn’t run anything untrusted on it. Studio only manages one project self-hosted, so I’m on a community multi-project fork with a single maintainer, which is a bit of a risk tbh. There’s one part I haven’t really tested yet, and it’s upgrades. Upgrading Auth means running its migrations on 50 databases and recycling 50 containers without downtime, and I haven’t done a real upgrade on the running setup. Curious if anyone here has upgraded Auth or Realtime across a lot of databases on a self-hosted instance? And is there a cleaner way to get multi-tenant Broadcast from Database than patching the image? If you plan to host vibe-coded apps for your company, have a look at marmelab/platform on GitHub. Fork it, try it, and tell me what breaks! If you want to dive deeper into the technical trade-offs, I wrote a full breakdown here.