Wire schema v1 into a manual Supabase migration and vendor the real stack
deploy / deploy (push) Successful in 5m57s
check / flake-check (push) Failing after 35m52s

- Move db/schema.sql to supabase/migrations/ as the first supabase CLI
  migration (manual `db push` only, no automated runner); re-add the
  gebos_ingest role + grants there since init.sql never re-runs
- Add gebos-postgres-passwords oneshot on db-host: syncs role passwords
  from sops (LoadCredential, journal-safe), makes supabase_admin
  SUPERUSER and hands the auth schema to supabase_auth_admin to match
  the upstream supabase/postgres image; add pg_hba rule for 10.0.0.0/8
- Vendor the official docker-compose (studio/kong/auth/rest/meta only,
  external Postgres, loopback Studio with no Kong dashboard route) plus
  kong.yml (trimmed) and kong-entrypoint.sh (verbatim); tested: compose
  config, Kong config parse, migration applied on TimescaleDB pg17
- Document decisions as ADRs 0001-0004 (migrations, passwords, vendored
  stack, JWT API keys incl. verify/mint procedure)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Lars Nolden
2026-07-08 22:33:50 +02:00
parent b2a1056255
commit 2ce68cb098
17 changed files with 967 additions and 118 deletions
+179 -35
View File
@@ -1,65 +1,209 @@
# Gebos — vendored Supabase compose stack.
#
# TODO: copy the full official compose from
# Vendored from the official self-hosting bundle
# https://github.com/supabase/supabase/blob/master/docker/docker-compose.yml
# and apply the three modifications described in ./README.md:
# - drop the `db` service
# - replace `db:5432` / `postgres:5432` with ${POSTGRES_HOST}:${POSTGRES_PORT}
# - bind Studio to ${STUDIO_BIND}:3000 (default 127.0.0.1)
# with the deliberate deviations documented in ./README.md:
#
# The skeleton below names the services we'll keep and the env vars each one
# reads, so reviewers can sanity-check the topology before the real compose
# lands.
# * no `db` service — Postgres runs on db-host; every service connects to
# ${POSTGRES_HOST}:${POSTGRES_PORT} and the `depends_on: db` blocks are
# dropped
# * no realtime / storage / imgproxy / functions / supavisor / analytics —
# the pilot needs auth + rest + studio only; re-vendor from upstream when
# one of them becomes needed
# * Studio binds to ${STUDIO_BIND}:3000 (loopback; reach it via SSH tunnel)
# * meta connects as supabase_admin, not `postgres`: upstream's `postgres`
# is its cluster superuser, but on db-host that role carries a different
# password. gebos-postgres-passwords makes supabase_admin SUPERUSER to
# match the upstream image's semantics.
#
# Where variables come from (see nix/modules/gebos-supabase.nix):
# * POSTGRES_HOST / POSTGRES_PORT / POSTGRES_DB / STUDIO_BIND — systemd
# unit `environment` (non-secret deployment config, nix-managed)
# * POSTGRES_PASSWORD / JWT_SECRET / ANON_KEY / SERVICE_ROLE_KEY —
# sops-rendered EnvironmentFile (secrets)
# * gebos constants (public URLs, org name) — hardcoded right here
name: gebos-supabase
services:
kong:
image: kong:2.8.1
studio:
container_name: supabase-studio
image: supabase/studio:2026.07.07-sha-a6a04f2
restart: unless-stopped
ports:
- "127.0.0.1:8000:8000/tcp" # Caddy proxies api.gebos.online here
# Never 0.0.0.0 — Studio performs no authentication of its own in this
# topology (the upstream basic-auth lives on Kong's dashboard route,
# which we do not expose). Access = SSH tunnel to app-host.
- ${STUDIO_BIND}:3000:3000/tcp
healthcheck:
test:
[
"CMD-SHELL",
"node -e \"fetch('http://localhost:3000/api/platform/profile').then((r) => {if (r.status !== 200) throw new Error(r.status)})\""
]
timeout: 10s
interval: 5s
retries: 3
start_period: 20s
environment:
HOSTNAME: "0.0.0.0"
STUDIO_PG_META_URL: http://meta:8080
POSTGRES_HOST: ${POSTGRES_HOST}
POSTGRES_PORT: ${POSTGRES_PORT}
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_USER_READ_WRITE: supabase_admin
PGRST_DB_SCHEMAS: public
DEFAULT_ORGANIZATION_NAME: Gebos
DEFAULT_PROJECT_NAME: Gebos
SUPABASE_URL: http://kong:8000
SUPABASE_PUBLIC_URL: https://api.gebos.online
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
AUTH_JWT_SECRET: ${JWT_SECRET}
# No Logflare/analytics stack in this deployment.
ENABLED_FEATURES_LOGS_ALL: "false"
kong:
container_name: supabase-kong
image: kong/kong:3.9.1
restart: unless-stopped
ports:
- 127.0.0.1:8000:8000/tcp # Caddy proxies api.gebos.online here
healthcheck:
test: ["CMD", "kong", "health"]
interval: 5s
timeout: 5s
retries: 5
volumes:
# The entrypoint substitutes $ENV_VARS in temp.yml and writes the result
# to KONG_DECLARATIVE_CONFIG (Kong has no native env interpolation).
- ./kong.yml:/home/kong/temp.yml:ro
- ./kong-entrypoint.sh:/home/kong/kong-entrypoint.sh:ro
environment:
KONG_DATABASE: "off"
KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml
volumes:
- ./kong.yml:/home/kong/kong.yml:ro
KONG_DECLARATIVE_CONFIG: /usr/local/kong/kong.yml
KONG_ROUTER_FLAVOR: expressions
# https://github.com/supabase/cli/issues/14
KONG_DNS_ORDER: LAST,A,CNAME
KONG_DNS_NOT_FOUND_TTL: 1
KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth,request-termination,ip-restriction,post-function
KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
KONG_PROXY_ACCESS_LOG: /dev/stdout combined
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
# Opaque sb_* API keys — not used yet. Empty values make the entrypoint
# fall back to legacy apikey pass-through and strip the empty
# credentials from the declarative config.
SUPABASE_PUBLISHABLE_KEY: ${SUPABASE_PUBLISHABLE_KEY:-}
SUPABASE_SECRET_KEY: ${SUPABASE_SECRET_KEY:-}
ANON_KEY_ASYMMETRIC: ${ANON_KEY_ASYMMETRIC:-}
SERVICE_ROLE_KEY_ASYMMETRIC: ${SERVICE_ROLE_KEY_ASYMMETRIC:-}
entrypoint: ["/bin/sh", "/home/kong/kong-entrypoint.sh"]
auth:
image: supabase/gotrue:v2.158.1
container_name: supabase-auth
image: supabase/gotrue:v2.189.0
restart: unless-stopped
healthcheck:
test:
[
"CMD",
"wget",
"--no-verbose",
"--tries=1",
"--spider",
"http://localhost:9999/health"
]
timeout: 5s
interval: 5s
retries: 3
environment:
GOTRUE_DB_DRIVER: postgres
GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
GOTRUE_SITE_URL: https://app.gebos.online
GOTRUE_JWT_SECRET: ${JWT_SECRET}
GOTRUE_JWT_EXP: "3600"
GOTRUE_API_HOST: 0.0.0.0
GOTRUE_API_PORT: 9999
API_EXTERNAL_URL: https://api.gebos.online
GOTRUE_DB_DRIVER: postgres
# GoTrue owns the auth schema and runs its own migrations in it on
# startup — supabase_auth_admin's ownership of that schema is enforced
# by the gebos-postgres-passwords oneshot on db-host.
GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
GOTRUE_SITE_URL: https://app.gebos.online
GOTRUE_URI_ALLOW_LIST: ""
# Tenants are invited by an admin (magic link), never self-signed-up.
GOTRUE_DISABLE_SIGNUP: "true"
GOTRUE_JWT_ADMIN_ROLES: service_role
GOTRUE_JWT_AUD: authenticated
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
GOTRUE_JWT_EXP: "3600"
GOTRUE_JWT_SECRET: ${JWT_SECRET}
GOTRUE_JWT_ISSUER: https://api.gebos.online
GOTRUE_EXTERNAL_EMAIL_ENABLED: "true"
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: "false"
GOTRUE_MAILER_AUTOCONFIRM: "false"
# TODO: no SMTP provider configured yet. Magic links (UVI-NFR-08,
# passwordless-only) cannot send until these are filled in — add the
# SMTP secrets to nix/secrets/secrets.yaml and wire them through
# gebos-secrets.nix like the other supabase keys.
GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL:-}
GOTRUE_SMTP_HOST: ${SMTP_HOST:-}
GOTRUE_SMTP_PORT: ${SMTP_PORT:-587}
GOTRUE_SMTP_USER: ${SMTP_USER:-}
GOTRUE_SMTP_PASS: ${SMTP_PASS:-}
GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME:-Gebos}
GOTRUE_MAILER_URLPATHS_INVITE: /auth/v1/verify
GOTRUE_MAILER_URLPATHS_CONFIRMATION: /auth/v1/verify
GOTRUE_MAILER_URLPATHS_RECOVERY: /auth/v1/verify
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: /auth/v1/verify
GOTRUE_EXTERNAL_PHONE_ENABLED: "false"
GOTRUE_SMS_AUTOCONFIRM: "false"
rest:
image: postgrest/postgrest:v12.0.2
container_name: supabase-rest
image: postgrest/postgrest:v14.12
restart: unless-stopped
healthcheck:
test: [ "CMD", "postgrest", "--ready" ]
interval: 5s
timeout: 5s
retries: 3
environment:
PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
PGRST_DB_SCHEMAS: public
PGRST_JWT_SECRET: ${JWT_SECRET}
PGRST_DB_MAX_ROWS: 1000
PGRST_DB_EXTRA_SEARCH_PATH: public
PGRST_DB_ANON_ROLE: anon
studio:
image: supabase/studio:20240326-5e5586d
restart: unless-stopped
ports:
- "${STUDIO_BIND}:3000:3000/tcp" # never bind to 0.0.0.0
environment:
STUDIO_PG_META_URL: http://meta:8080
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
DEFAULT_ORGANIZATION_NAME: Gebos
SUPABASE_URL: https://api.gebos.online
DASHBOARD_USERNAME: gebos
DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD}
PGRST_ADMIN_SERVER_PORT: 3001
PGRST_ADMIN_SERVER_HOST: localhost
PGRST_JWT_SECRET: ${JWT_SECRET}
PGRST_DB_USE_LEGACY_GUCS: "false"
PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET}
PGRST_APP_SETTINGS_JWT_EXP: "3600"
command:
[
"postgrest"
]
meta:
image: supabase/postgres-meta:v0.83.2
container_name: supabase-meta
image: supabase/postgres-meta:v0.96.6
restart: unless-stopped
environment:
PG_META_PORT: 8080
PG_META_DB_HOST: ${POSTGRES_HOST}
PG_META_DB_PORT: ${POSTGRES_PORT}
PG_META_DB_NAME: ${POSTGRES_DB}