# ADR-0003: Vendor a trimmed Supabase compose stack against external Postgres Date: 2026-07-08 Status: Accepted ## Context The Supabase stack on app-host was a placeholder skeleton. The official self-hosting bundle assumes its own bundled `db` container and ships nine+ services (realtime, storage, imgproxy, edge functions, supavisor pooler, Logflare analytics, …), most of which the UVI pilot does not use. Meanwhile our Postgres lives on a separate NixOS host (db-host) with TimescaleDB. Kong deserves a note, since "why is it even there" came up: Kong is the single front door for `api.gebos.online` (Caddy → Kong on loopback :8000). It routes paths to internal services (`/auth/v1` → GoTrue, `/rest/v1` → PostgREST) and enforces the perimeter (key-auth + ACLs), so unauthenticated internet traffic never reaches the backends. Replacing it would mean re-implementing routing/auth/CORS in Caddy by hand instead of inheriting Supabase's tested config. ## Decision Vendor the upstream `docker/docker-compose.yml` (and `kong.yml` + `kong-entrypoint.sh`) into `nix/supabase/`, keeping only **studio, kong, auth (GoTrue), rest (PostgREST), meta (postgres-meta)**, with these deviations — each also documented in the files' header comments: 1. **No `db` service.** Everything connects to `${POSTGRES_HOST}:${POSTGRES_PORT}` (db-host); `depends_on: db` dropped. 2. **No realtime / storage / imgproxy / functions / supavisor / analytics.** Re-vendor from upstream when actually needed. 3. **Studio binds to loopback only, and Kong's catch-all dashboard route is removed.** Upstream fronts Studio with Kong basic-auth on `/`; we expose no route to it at all. Access is `ssh -L 3000:127.0.0.1:3000` — possession of the SSH key is the credential. `supabase_dashboard_password` is therefore currently unused. The public API surface is exactly `/auth/v1`, `/rest/v1`, `/graphql/v1` and two `.well-known` endpoints. 4. **meta connects as `supabase_admin`, not `postgres`.** Upstream's `postgres` role *is* its cluster superuser; on db-host `postgres` keeps a separate admin password, so `supabase_admin` (made SUPERUSER by ADR-0002) fills that role with the shared service password. 5. **`kong-entrypoint.sh` is vendored byte-identical.** Kong's declarative YAML cannot read env vars, so the entrypoint substitutes `$VARS` (the API keys) into the config at container start; keeping it unmodified makes re-vendoring a plain copy. Variable layering (the answer to "where does `POSTGRES_HOST` come from"): * non-secret deployment config (`POSTGRES_HOST/PORT/DB`, `STUDIO_BIND`) — systemd unit `environment` in `gebos-supabase.nix`, fed by module options; * secrets (`POSTGRES_PASSWORD`, `JWT_SECRET`, `ANON_KEY`, `SERVICE_ROLE_KEY`) — sops-rendered `EnvironmentFile`; * gebos constants (public URLs, org name) — hardcoded in the vendored compose. ## Consequences * Deliberately small attack surface and dependency set; Studio reachable only through SSH. * Upstream updates are a re-copy plus re-applying the five listed deviations; all drift is concentrated in `docker-compose.yml` and `kong.yml` headers. * GoTrue has **no SMTP configured yet** — magic-link login cannot send email until the `SMTP_*` secrets are added (TODO marked in the compose file). * `anon`/`authenticated` still hold zero grants; PostgREST serves nothing until the RLS/grants migration lands (future ADR).