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
+91 -1
View File
@@ -37,8 +37,98 @@ in
# supabase_vault — TODO: package or vendor
];
# Init script creates Supabase's `auth`, `storage`, `_analytics`, `_supavisor`
# schemas, the gebos_ingest role (BYPASSRLS), and telemetry hypertables.
# schemas and the Supabase admin/API roles. It only runs at first initdb;
# everything else arrives via manual supabase migrations (see
# supabase/README.md) and the password oneshot below.
initialScript = ../supabase/init.sql;
# The firewall limits 5432 to 10.0.0.0/8, but NixOS's default pg_hba
# only covers local sockets and loopback — without this line every
# connection from app-host / mqtt-ingest is rejected before password
# auth even starts.
authentication = ''
host all all 10.0.0.0/8 scram-sha-256
'';
};
# Role passwords are data, not configuration: NixOS has no declarative
# option for them (anything reachable from the config lands world-readable
# in /nix/store, and initialScript only runs at first initdb). This
# oneshot re-applies them from the sops-rendered env file on every boot /
# deploy, idempotently. It also enforces two attributes the Supabase
# services expect because the upstream supabase/postgres image ships them:
# supabase_admin is SUPERUSER (Studio/postgres-meta connect as it) and
# supabase_auth_admin owns the auth schema (GoTrue runs its own migrations
# there on startup).
#
# After rotating a password in secrets.yaml, re-run with
# `systemctl restart gebos-postgres-passwords` (deploys re-run it only
# when the unit definition itself changed).
systemd.services.gebos-postgres-passwords =
lib.mkIf config.services.gebos.secrets.postgresAdmin {
description = "Gebos sync Postgres role passwords from sops secrets";
after = [ "postgresql.service" ];
requires = [ "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
path = [ config.services.postgresql.package ];
serviceConfig = {
Type = "oneshot";
User = "postgres";
# LoadCredential hands the root-owned 0400 env file to the postgres
# user without widening its permissions.
LoadCredential = "gebos-env:${config.services.gebos.secrets.envFile}";
};
script = ''
set -euo pipefail
set -a; . "$CREDENTIALS_DIRECTORY/gebos-env"; set +a
# All three come from the gebos-env template; the toggles in
# db-host.nix (postgresAdmin + supabase + ingester) must be on.
# --output=/dev/null: the set_config() SELECTs would otherwise echo
# the passwords into the journal.
psql -v ON_ERROR_STOP=1 --output=/dev/null \
-v admin_pw="''${POSTGRES_ADMIN_PASSWORD:?}" \
-v supabase_pw="''${POSTGRES_PASSWORD:?}" \
-v ingest_pw="''${GEBOS_POSTGRES_PASSWORD:?}" \
--dbname postgres <<'SQL'
-- psql :'var' interpolation does not reach inside DO bodies, so
-- stash the values in session GUCs first.
SELECT set_config('gebos.supabase_pw', :'supabase_pw', false);
SELECT set_config('gebos.ingest_pw', :'ingest_pw', false);
ALTER ROLE postgres PASSWORD :'admin_pw';
DO $do$
DECLARE r text;
BEGIN
-- One shared password for the roles the compose services log in
-- as, mirroring upstream's POSTGRES_PASSWORD convention.
FOREACH r IN ARRAY ARRAY['supabase_admin','supabase_auth_admin','authenticator'] LOOP
IF EXISTS (SELECT FROM pg_roles WHERE rolname = r) THEN
EXECUTE format('ALTER ROLE %I PASSWORD %L',
r, current_setting('gebos.supabase_pw'));
END IF;
END LOOP;
IF EXISTS (SELECT FROM pg_roles WHERE rolname = 'supabase_admin') THEN
EXECUTE 'ALTER ROLE supabase_admin SUPERUSER';
END IF;
IF EXISTS (SELECT FROM pg_roles WHERE rolname = 'supabase_auth_admin')
AND EXISTS (SELECT FROM pg_namespace WHERE nspname = 'auth') THEN
EXECUTE 'ALTER SCHEMA auth OWNER TO supabase_auth_admin';
END IF;
-- Created by the schema-v1 migration, so tolerate its absence on
-- a cluster the migration has not reached yet.
IF EXISTS (SELECT FROM pg_roles WHERE rolname = 'gebos_ingest') THEN
EXECUTE format('ALTER ROLE gebos_ingest PASSWORD %L',
current_setting('gebos.ingest_pw'));
END IF;
END
$do$;
SQL
'';
};
};
}
+16 -2
View File
@@ -30,10 +30,16 @@ in
"d /var/lib/gebos-supabase 0750 root root - -"
];
# Copy the vendored compose file into place at activation time so changes
# to nix/supabase/docker-compose.yml are deployed atomically.
# Copy the vendored compose bundle into place at activation time so
# changes under nix/supabase/ are deployed atomically. Kong's declarative
# config is mounted from here by the compose file (relative ./kong.yml
# resolves against WorkingDirectory).
environment.etc."gebos/supabase/docker-compose.yml".source =
"${composeDir}/docker-compose.yml";
environment.etc."gebos/supabase/kong.yml".source =
"${composeDir}/kong.yml";
environment.etc."gebos/supabase/kong-entrypoint.sh".source =
"${composeDir}/kong-entrypoint.sh";
systemd.services.gebos-supabase = {
description = "Gebos Supabase compose stack";
@@ -41,6 +47,14 @@ in
wants = [ "docker.service" "network-online.target" ];
wantedBy = [ "multi-user.target" ];
# `docker compose up -d` only recreates containers whose definition
# changed, but the unit must re-run for it to notice at all.
restartTriggers = [
"${composeDir}/docker-compose.yml"
"${composeDir}/kong.yml"
"${composeDir}/kong-entrypoint.sh"
];
environment = {
POSTGRES_HOST = cfg.postgresHost;
POSTGRES_PORT = toString cfg.postgresPort;