codingstairs
NotesEDULifeContact
⌕Search⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

Get in touch

Send without signing in. Add your email if you'd like a reply.

  • Leave a message anonymously →
  • ✉ warragon112@gmail.com
  • KakaoTalk Open Chat ↗

© 2026 codingstairs

  • Notes
  • EDU
  • Search
  • Life
  • Contact
  • Legal
  • RSS
  • GitHub
Notes›cloud

Supabase Self-Hosted — Packing a BaaS into One Postgres Pot

Published 2026-04-28· Updated 2026-05-18·0 views

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 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 with JWT_SECRET.
  • SERVICE_ROLE_KEY — A {role: "service_role"} JWT signed with JWT_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:

  1. Externally only one Kong port (typically 8000 or 54321) is visible.
  2. Internal per-path routing (/auth/v1/* → auth, /rest/v1/* → rest, /storage/v1/* → storage, /realtime/v1/* → realtime, /functions/v1/* → functions).
  3. 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.

More in cloud

All in this category →
  • title template single source — don''t let children stamp the site name
  • GitHub Pages — host a repo as a static site
  • Replit — Browser-based dev + deploy in one place
  • HTTP API Mocking — WireMock · MockServer · Prism · MSW
  • Firebase Local Emulator Suite — Running a Firebase Bundle on a Laptop
  • LocalStack and MiniStack — Emulating AWS Locally