Files
Gebos/nix/dev/process-compose.nix
Lars Nolden f4b5926e1f
check / flake-check (push) Successful in 35s
deploy / deploy (push) Successful in 39s
finish local dev setup
2026-07-10 15:59:03 +02:00

267 lines
12 KiB
Nix

{ pkgs, services-flake }:
{ ... }:
let
# Dev-only credentials. Match the defaults baked into ingester/main.go and
# frontend/src/supabase.ts, so `nix run .#dev` and `go run ./ingester` /
# `npm --prefix frontend run dev` all interoperate without any setup.
#
# These values are public on purpose — they ONLY work against the local
# process-compose stack. Real prod values come from sops-nix (see
# nix/secrets/README.md).
dev = {
pgUser = "gebos_ingest";
pgDB = "gebos";
pgPassword = "dev";
pgHost = "127.0.0.1";
pgPort = 5432;
# Unix socket dir, bind-mounted into the compose network's `db` proxy
# (see composeDevOverride below). Relative to the process-compose cwd,
# like services-flake's ./data/db data dir.
pgSocketDir = "./data/db-socket";
mqttHost = "127.0.0.1";
mqttPort = 1883;
# Pre-generated dev JWT pair, signed HS256 with jwtSecret. Regenerate
# with any JWT tool if jwtSecret ever changes; payload is
# {"role":"<anon|service_role>","iss":"supabase-dev","iat":1751328000,"exp":2082758400}.
# Anyone with these tokens can read/write the local dev stack only — not prod.
jwtSecret = "dev-jwt-secret-32chars-minimum-xxxxx";
anonKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLWRldiIsImlhdCI6MTc1MTMyODAwMCwiZXhwIjoyMDgyNzU4NDAwfQ.M6U-nZq9dySoUJEfWHsiB0qabhATWobGaTM1LF86HHU";
serviceRoleKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UtZGV2IiwiaWF0IjoxNzUxMzI4MDAwLCJleHAiOjIwODI3NTg0MDB9.dNeWNshQ9e1pIjxTDYseURlbSPPArVIKa9OuTRZyIH8";
supabaseUrl = "http://127.0.0.1:8000";
frontendPort = 5173; # vite default
};
ingesterEnv = {
GEBOS_MQTT_BROKER = "tcp://${dev.mqttHost}:${toString dev.mqttPort}";
GEBOS_POSTGRES_URL = "postgres://${dev.pgUser}@${dev.pgHost}:${toString dev.pgPort}/${dev.pgDB}?sslmode=disable";
GEBOS_POSTGRES_PASSWORD = dev.pgPassword;
};
frontendEnv = {
VITE_SUPABASE_URL = dev.supabaseUrl;
VITE_SUPABASE_ANON_KEY = dev.anonKey;
};
# services-flake ships no MQTT broker, so run mosquitto as a plain
# process-compose process. Anonymous access on the dev listener only.
mosquittoConf = pkgs.writeText "mosquitto-dev.conf" ''
listener ${toString dev.mqttPort} ${dev.mqttHost}
allow_anonymous true
'';
# ---------------------------------------------------------------------------
# Supabase — the vendored prod compose bundle (nix/supabase/), unchanged,
# plus this dev overlay.
#
# The overlay's job is wiring the containers to the services-flake postgres.
# Bridge→host TCP is a dead end on a typical NixOS dev machine (the host
# firewall's INPUT chain rejects it), so instead a socat `db` service
# forwards TCP 5432 inside the compose network onto the host postgres unix
# socket, bind-mounted from pgSocketDir. Conveniently that restores the
# upstream compose convention of a service literally named `db`, so the
# vendored file works with just POSTGRES_HOST=db. Socket connections match
# pg_hba's `local … trust`, so the dev password is never actually checked.
# ---------------------------------------------------------------------------
composeDevOverride = pkgs.writeText "docker-compose.dev-override.yml" ''
services:
db:
container_name: supabase-dev-db
# Unpinned minor: dev-only TCP→unix-socket shim, nothing depends on
# socat internals.
image: alpine/socat:1.8.0.3
restart: unless-stopped
command: TCP-LISTEN:${toString dev.pgPort},fork,reuseaddr UNIX-CONNECT:/host-postgres/.s.PGSQL.${toString dev.pgPort}
volumes:
- ''${PG_SOCKET_DIR:?set by gebos-dev-supabase-compose}:/host-postgres
studio:
environment:
SUPABASE_PUBLIC_URL: ${dev.supabaseUrl}
auth:
environment:
API_EXTERNAL_URL: ${dev.supabaseUrl}
GOTRUE_SITE_URL: http://localhost:${toString dev.frontendPort}
GOTRUE_JWT_ISSUER: ${dev.supabaseUrl}
# No SMTP locally, so magic-link invites can't send. Allow plain
# self-signup with auto-confirm instead; prod keeps signup disabled.
GOTRUE_DISABLE_SIGNUP: "false"
GOTRUE_MAILER_AUTOCONFIRM: "true"
'';
# One wrapper for `up`/`down` so both resolve the socket-dir bind mount the
# same way (compose needs an absolute path).
supabaseCompose = pkgs.writeShellApplication {
name = "gebos-dev-supabase-compose";
runtimeInputs = [ pkgs.docker pkgs.coreutils ];
text = ''
PG_SOCKET_DIR="$(readlink -f ${dev.pgSocketDir})"
export PG_SOCKET_DIR
exec docker compose \
--project-name gebos-supabase-dev \
-f nix/supabase/docker-compose.yml \
-f ${composeDevOverride} \
"$@"
'';
};
supabaseEnv = {
POSTGRES_HOST = "db"; # the socat proxy above
POSTGRES_PORT = toString dev.pgPort;
POSTGRES_DB = dev.pgDB;
POSTGRES_PASSWORD = dev.pgPassword; # unchecked — see overlay comment
JWT_SECRET = dev.jwtSecret;
ANON_KEY = dev.anonKey;
SERVICE_ROLE_KEY = dev.serviceRoleKey;
STUDIO_BIND = "127.0.0.1";
};
# Everything prod splits across nix/supabase/init.sql (first initdb),
# the gebos-postgres-passwords oneshot, and a manual `supabase db push`,
# collapsed into one idempotent script that runs on every stack start.
dbMigrate = pkgs.writeShellApplication {
name = "gebos-dev-db-migrate";
runtimeInputs = [ pkgs.supabase-cli pkgs.postgresql_17 ];
text = ''
export PGHOST=${dev.pgHost} PGPORT=${toString dev.pgPort} PGDATABASE=${dev.pgDB} PGSSLMODE=disable
# 1. Cluster bootstrap: Supabase roles/schemas the compose services
# expect (mirrors nix/supabase/init.sql, but idempotent because the
# dev data dir may predate this script). Password 'dev' everywhere.
psql -v ON_ERROR_STOP=1 --quiet <<'SQL'
DO $$ BEGIN CREATE ROLE anon NOLOGIN NOINHERIT; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN CREATE ROLE authenticated NOLOGIN NOINHERIT; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN CREATE ROLE service_role NOLOGIN NOINHERIT BYPASSRLS; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN CREATE ROLE authenticator LOGIN NOINHERIT; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
GRANT anon, authenticated, service_role TO authenticator;
DO $$ BEGIN CREATE ROLE supabase_admin LOGIN CREATEROLE CREATEDB BYPASSRLS; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN CREATE ROLE supabase_auth_admin LOGIN CREATEROLE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- On db-host the NixOS postgresql module creates the `postgres`
-- superuser; the services-flake cluster's superuser is the OS user
-- instead, and GoTrue's own migrations GRANT to `postgres` by name.
DO $$ BEGIN CREATE ROLE postgres LOGIN SUPERUSER; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
ALTER ROLE postgres PASSWORD 'dev';
ALTER ROLE supabase_admin SUPERUSER PASSWORD 'dev';
ALTER ROLE supabase_auth_admin PASSWORD 'dev';
ALTER ROLE authenticator PASSWORD 'dev';
-- GoTrue owns `auth` and runs its own migrations there on startup. Its
-- migrator creates tables in the role's default schema, so without the
-- search_path it lands in `public` and dies on PG15+'s revoked CREATE
-- (upstream's supabase/postgres image sets the same role search_path).
CREATE SCHEMA IF NOT EXISTS auth;
ALTER SCHEMA auth OWNER TO supabase_auth_admin;
ALTER ROLE supabase_auth_admin SET search_path TO auth, extensions;
-- The upstream supabase/postgres image keeps shared extensions in an
-- `extensions` schema on the search path; the services assume that.
CREATE SCHEMA IF NOT EXISTS extensions;
CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA extensions;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA extensions;
GRANT USAGE ON SCHEMA extensions TO anon, authenticated, service_role, authenticator, supabase_auth_admin;
ALTER DATABASE gebos SET search_path TO "$user", public, extensions;
SQL
# 2. Schema migrations — same mechanism as prod (supabase/README.md),
# non-interactive, as the local superuser ($USER).
supabase db push --yes --db-url "postgres://$USER@${dev.pgHost}:${toString dev.pgPort}/${dev.pgDB}"
# 3. The schema-v1 migration created gebos_ingest; in prod its password
# comes from the gebos-postgres-passwords oneshot.
psql -v ON_ERROR_STOP=1 --quiet -c "ALTER ROLE ${dev.pgUser} PASSWORD '${dev.pgPassword}'"
# 4. Fixture data — idempotent, dev/tests only (supabase/README.md).
psql -v ON_ERROR_STOP=1 --quiet -f supabase/seed.sql
'';
};
in
{
imports = [ services-flake.processComposeModules.default ];
services = {
postgres."db" = {
enable = true;
port = dev.pgPort;
# TCP stays loopback-only; the compose containers come in through the
# unix socket instead (see composeDevOverride).
socketDir = dev.pgSocketDir;
initialDatabases = [{ name = dev.pgDB; }];
# The schema-v1 migration calls create_hypertable; without the
# extension `supabase db push` rolls back entirely on the dev db.
# The apache variant lacks only TSL features (compression, CAggs
# policies) — hypertables are enough here, and it keeps dev free
# of unfree packages.
extensions = extensions: [ extensions.timescaledb-apache ];
settings.shared_preload_libraries = "timescaledb";
};
};
settings.processes = {
broker = {
command = "${pkgs.mosquitto}/bin/mosquitto -c ${mosquittoConf}";
readiness_probe = {
exec.command = "${pkgs.mosquitto}/bin/mosquitto_sub -h ${dev.mqttHost} -p ${toString dev.mqttPort} -t '$$SYS/#' -C 1 -W 2";
initial_delay_seconds = 1;
period_seconds = 2;
};
};
# Oneshot: bootstrap roles/schemas, apply migrations, seed. Idempotent,
# so it simply re-runs on every stack start.
db-migrate = {
command = "${dbMigrate}/bin/gebos-dev-db-migrate";
depends_on."db".condition = "process_healthy";
};
# Auth (GoTrue) + REST (PostgREST) + Studio + postgres-meta behind Kong
# on ${dev.supabaseUrl} — the vendored prod bundle, see composeDevOverride.
# Needs a running docker daemon on the dev machine.
supabase = {
command = "${supabaseCompose}/bin/gebos-dev-supabase-compose up --remove-orphans";
environment = supabaseEnv;
# `restart: unless-stopped` containers outlive a hard-killed
# process-compose; an explicit `down` covers the clean path.
shutdown.command = "${supabaseCompose}/bin/gebos-dev-supabase-compose down --remove-orphans";
depends_on = {
# GoTrue's own migrations need the auth schema + roles from db-migrate.
"db".condition = "process_healthy";
"db-migrate".condition = "process_completed_successfully";
};
readiness_probe = {
# Kong → key-auth → GoTrue → Postgres: healthy = the whole API path works.
exec.command = "${pkgs.curl}/bin/curl -fsS -H 'apikey: ${dev.anonKey}' ${dev.supabaseUrl}/auth/v1/health";
initial_delay_seconds = 5;
period_seconds = 5;
failure_threshold = 60; # first start pulls the images
};
};
ingester = {
# -C: the go module root is ingester/, not the repo root.
command = "${pkgs.go}/bin/go run -C ingester ./cmd/gebos-ingester";
environment = ingesterEnv;
depends_on = {
# db-migrate (implies db healthy) creates the gebos_ingest role and
# the telemetry tables.
"db-migrate".condition = "process_completed_successfully";
"broker".condition = "process_started";
};
};
frontend = {
command = "${pkgs.nodejs_24}/bin/npm --prefix frontend install && ${pkgs.nodejs_24}/bin/npm --prefix frontend run dev";
environment = frontendEnv;
};
# No caddy locally: prod's Caddy config is generated by the gebos-caddy
# NixOS module, so a hand-mirrored dev caddyfile wouldn't test it — the
# frontend talks to Kong directly (dev.supabaseUrl).
};
}