267 lines
12 KiB
Nix
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).
|
|
};
|
|
}
|