Supabase Self-Hosted — Packing a BaaS into One Postgres Pot
Supabase appeared in 2020 with the slogan "open source Firebase." If Firebase is one giant managed box with its own NoSQL · Auth · Storage, Supabase took the path of placing Postgres at the center and stacking small com…
Table of contents
Supabase appeared in 2020 with the slogan "open source Firebase." If Firebase is one giant managed box with its own NoSQL · Auth · Storage, Supabase took the path of placing Postgres at the center and stacking small components on top. Managed (supabase.com) and self-hosted run on the same components.
1. About Supabase
Supabase's "backend service" is in fact a single Postgres + a bundle of microservices in front of it. None of the components keeps its own separate database.
- The Auth user table, Storage object metadata, and Realtime change events all live in different schemas of the same Postgres (
auth·storage·_realtime·_supabase·_analytics).
That is why a single SQL query can deal with permissions, storage, and authentication at once, and why the deploy unit for self-hosted is simple.
2. The 14 components
| Component | Image | Role |
|---|---|---|
| Postgres | supabase/postgres |
The DB itself. pgvector · pg_graphql · pg_cron · pg_net · pgaudit pre-built. |
| Kong | kong |
API gateway. Single external entry, JWT validation · routing. |
| GoTrue | supabase/gotrue |
Auth — email · OAuth · OTP · MFA. |
| PostgREST | postgrest/postgrest |
Auto-REST API from the DB schema. |
| Realtime | supabase/realtime |
Postgres CDC → WebSocket. |
| Storage | supabase/storage-api |
Object storage API. S3 or file backend. |
| imgproxy | darthsim/imgproxy |
Image transforms. |
| postgres-meta | supabase/postgres-meta |
DB schema metadata API. |
| Studio | supabase/studio |
Dashboard UI (Next.js). |
| Edge Runtime | supabase/edge-runtime |
Deno-based serverless functions. |
| Logflare | supabase/logflare |
Log analytics. |
| Vector | timberio/vector |
Container logs → Logflare. |
| Supavisor | supabase/supavisor |
Connection pooler (PgBouncer replacement). |
13~14 containers in one bundle. The official docker-compose is the answer — basing your own setup on the compose under supabase/supabase/docker/ is far less error-prone than authoring from scratch.
3. JWT secret model — the most common stuck point
Supabase's Auth and PostgREST trust only JWTs signed with the same JWT_SECRET. 90% of self-hosted setup pitfalls live here.
Three variables must remain consistent:
JWT_SECRET— A 32+ character random string. The same value across all component env vars.ANON_KEY— A{role: "anon"}JWT signed withJWT_SECRET.SERVICE_ROLE_KEY— A{role: "service_role"}JWT signed withJWT_SECRET.
The demo keys in the official .env.example are signed with a fixed secret (your-super-secret-jwt-token-with-at-least-32-characters-long). Changing only JWT_SECRET while leaving the keys means immediate 401 · 403 on every call.
node -e '
const c = require("crypto");
const s = "put new JWT_SECRET here";
const sign = (p) => {
const h = Buffer.from(JSON.stringify({alg:"HS256",typ:"JWT"})).toString("base64url");
const b = Buffer.from(JSON.stringify(p)).toString("base64url");
return `${h}.${b}.${c.createHmac("sha256",s).update(`${h}.${b}`).digest("base64url")}`;
};
const iat = Math.floor(Date.now()/1000), exp = iat + 60*60*24*365*5;
console.log("ANON_KEY=" + sign({role:"anon", iss:"supabase-demo", iat, exp}));
console.log("SERVICE_ROLE_KEY=" + sign({role:"service_role", iss:"supabase-demo", iat, exp}));
'
4. Storage — the value of file mode
STORAGE_BACKEND selects the backend:
STORAGE_BACKEND=s3— S3-compatible endpoint (managed standard). Self-hosted needs an extra S3 like MinIO.STORAGE_BACKEND=file— Stores files directly on container volumes. Zero external dependency — the simplest option for self-hosted.
Managed Supabase uses s3 mode, but for self-hosted simplicity, file is the answer. publicUrl, signed URLs, RLS, and imgproxy transforms all behave identically.
5. Auth + Inbucket — receiving mail to disk
GoTrue sends mail over SMTP for sign-up and password reset. With no external SMTP, the standard self-hosted pattern is to also bring up a dev SMTP server like Inbucket.
auth:
environment:
GOTRUE_SMTP_HOST: inbucket
GOTRUE_SMTP_PORT: 2500
inbucket:
image: inbucket/inbucket
ports:
- "9000:9000" # Web UI
- "2500:2500" # SMTP
After signing up, opening http://localhost:9000 shows the "Confirm Your Email" message GoTrue sent.
6. Kong — the single entry point
Kong's role:
- Externally only one Kong port (typically 8000 or 54321) is visible.
- Internal per-path routing (
/auth/v1/* → auth,/rest/v1/* → rest,/storage/v1/* → storage,/realtime/v1/* → realtime,/functions/v1/* → functions). - Distinguishes anon · service_role via apikey/JWT headers.
Kong's kong.yml is declarative config. Env-var placeholders ($ANON_KEY) are substituted by an awk-based container entrypoint. When self-hosted Kong dies with /entrypoint.sh: No such file or directory, the cause is usually the kong image bumping versions and moving the entrypoint to /docker-entrypoint.sh (kong:3.x and later).
7. Postgres — why supabase/postgres matters
Standard images like postgres:15-alpine will not run self-hosted Supabase. Required extensions:
pgvector— embeddings.pg_graphql— auto-generated GraphQL.pg_cron— scheduled jobs.pg_net— HTTP calls from inside the DB (webhooks).pgaudit— audit logs.pg_stat_statements.
The supabase/postgres image ships these prebuilt. Installing them onto a standard image causes Realtime · Auth init failures.
8. Frequent stuck points
| Symptom | Cause |
|---|---|
| Every call returns 401/403 | JWT_SECRET vs ANON_KEY / SERVICE_ROLE_KEY mismatch. |
| One or two of the 14 containers stays unhealthy and others hang | depends_on condition: service_healthy chain stalls. Healthcheck timing or permissions. |
Studio healthcheck ECONNREFUSED 127.0.0.1:3000 |
Next.js binds to the container hostname. HOSTNAME=0.0.0.0 is required. |
| Realtime healthcheck always 403 | The _supabase DB tenant seed is not finished at healthcheck time. Can be ignored. |
| Pooler hits Elixir SyntaxError | Windows git autocrlf injects CR into pooler.exs. Convert to LF. |
Kong plugin 'request-termination' not enabled |
Missing in KONG_PLUGINS. Check the full plugin list in the official compose. |
9. Calling with supabase-js
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
"http://localhost:54321",
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
// Auth
await supabase.auth.signUp({ email: "x@local.dev", password: "1234" });
// REST (auto-generated)
const { data } = await supabase.from("posts").select("*").limit(10);
// Storage
await supabase.storage.from("bucket").upload("k.png", file);
// Realtime
supabase
.channel("posts")
.on("postgres_changes", { event: "*", schema: "public", table: "posts" }, console.log)
.subscribe();
// Edge Function
await supabase.functions.invoke("hello", { body: { name: "world" } });
Swapping just one URL between managed and self-hosted runs the same code on both. That is Supabase's biggest promise.
10. Limitations
Memory footprint of 14 containers — idle ~3 GB. On tight laptops, disable analytics · vector, or use the managed offering.
Fast-moving version matrix — components have specific compatible combinations. Following SHA-pinned tags from the official compose is safest.
CDN · domain handling — Storage publicUrl can return container-internal hostnames, so production runs Caddy reverse proxy + SUPABASE_PUBLIC_URL env var for the external domain.
Backup — pg_dump is effectively the backup. Also preserve the storage file backend (/var/lib/storage).
Closing thoughts
Self-hosted Supabase is a bundle of 14 containers, but the data lives in one Postgres. JWT secret consistency + Storage file mode + Inbucket make the most solid shape with zero external dependency. Code compatibility between managed and self-hosted comes down to one URL — accepting 14 containers for that value is the essence of self-hosted.
Next
- firebase-emulator
- api-mocking-wiremock
Supabase official · Self-hosting guide · supabase-js · GoTrue · PostgREST · Realtime · Storage API · Inbucket for reference.
Terms in this content
More in cloud
All in this category →Related posts
Supabase
Supabase emerged as an alternative to Firebase, an open-source backend bundle. PostgreSQL sits at the core, with auth, storage, realtime, and serverless functions stacked around it.
Cloud Emulator Stack — Designing a 4th Environment
> The multi-emulator paths and commands below are an architectural example, not a claim about a particular repository. Select only the providers and cost envelope you actually need.
RDS — Managed Relational Database
Operating a relational DB directly is an area heavy with backup, recovery, HA, tuning, and version upgrades. RDS (Relational Database Service) takes a large share of those operational responsibilities off the user's han…
Supabase Storage — File Upload and Permissions
The Supabase note covered Storage in just one paragraph. This note goes deep on exactly that spot — bucket design, upload/delete code, per-file permissions (RLS), the S3-compatible API, and signed URLs. The practical qu…