Infrastructure skeleton — flake, three hosts, Supabase compose, ingester, deploy-rs #23

Merged
lars merged 3 commits from klaus/skeleton-infra into main 2026-06-25 14:54:06 +00:00
Collaborator

Skeleton for review before any real implementation — refs #21 (comment #41).

Everything matches what we locked in the design thread. No host actually boots yet, no closure actually builds end-to-end (the vendored Supabase compose, HiveMQ packaging, hardware configs, and real internal hostnames are TODOs marked in-line). The point is for you to walk the structure and tell me where it's wrong before I fill it in.

What's here

  • flake.nix — one root flake. Inputs: nixpkgs unstable, flake-parts, deploy-rs, process-compose-flake, services-flake. Exposes packages.{ingester, frontend}, nixosConfigurations.{db-host, app-host, mqtt-ingest}, nixosModules, deploy, devShells.default, and process-compose.dev.
  • nix/hosts/ — three host configs + common.nix (ssh, deploy user with passwordless sudo for deploy-rs, flake settings).
  • nix/modules/ — one NixOS module per service: gebos-postgres, gebos-supabase, gebos-caddy, gebos-hivemq, gebos-ingester, gebos-frontend.
  • nix/supabase/ — vendored compose file (skeleton, not the full upstream yet — the README documents how to re-vendor), declarative kong.yml, and init.sql that bootstraps extensions, roles, schemas, and the public.telemetry hypertable with RLS.
  • nix/dev/process-compose.nix — local dev stack stub (Postgres + ingester + frontend wired; Supabase / Kong / Caddy marked TODO).
  • nix/deploy.nix — deploy-rs node map, db-hostapp-hostmqtt-ingest.
  • ingester/ — Go MQTT→Postgres skeleton. main.go is ~30 lines with the algorithm sketched in comments; target is <200 LOC for the real version.
  • frontend/ — Vite + React stub. src/supabase.ts initializes supabase-js against the single api.ge-bos.de origin.
  • .gitea/workflows/{deploy,check}.yml — flake check on PR, deploy-rs from nix runner on push to main.

Key wiring choices baked in

  • Studio is bound to 127.0.0.1 on app-host. No public route. Access via ssh -L 3000:127.0.0.1:3000 app-host. The compose binding uses ${STUDIO_BIND}:3000, defaulted to 127.0.0.1 by the NixOS module.
  • One Postgres on db-host. Supabase compose has its bundled db removed, services read POSTGRES_HOST / POSTGRES_PORT from the systemd env.
  • PostgREST + /rpc/ as the public REST surface, no Huma. The Go ingester is internal-only on mqtt-ingest, writes as gebos_ingest (BYPASSRLS).
  • Caddy → Kong → upstream. Caddy at :443, reverse-proxies api.ge-bos.de to 127.0.0.1:8000 (Kong), which fans out /auth/v1, /rest/v1, /rpc to the right service.
  • Secrets at /etc/gebos/secrets.env, loaded via systemd EnvironmentFile=. Each unit that needs creds declares it; nothing in the Nix store.
  • Topic-encoded tenancy. telemetry table has tenant_id column, RLS policy USING (tenant_id = current_setting('app.tenant_id')::uuid). Backend sets the GUC from the JWT before each query.

What's deliberately not done yet

Marked with TODO in-line; the big ones:

  • Real hardware-config / boot loader / filesystems per host (depends on the actual boxes).
  • HiveMQ CE packaging (the unit currently runs true so the host eval succeeds).
  • Vendoring the full upstream Supabase compose — only the services we'll keep are sketched.
  • caddy-l4 for MQTT TLS at the edge (or terminating TLS inside HiveMQ — open question).
  • npmDepsHash / vendorHash set to placeholders; first build will print the real values.
  • The Gitea runner's deploy ed25519 pubkey in nix/hosts/common.nix.

Test plan

  • nix flake check once flake.lock is generated (Gitea runner first run).
  • nix build .#ingester once go.sum exists.
  • nix build .#frontend once package-lock.json exists.
  • One end-to-end deploy-rs dry-run against test VMs before we point at prod hostnames.
  • Walk through the structure here and call out anything that should move, split, or disappear.

Closes once we converge on the layout and I follow up with real implementation PRs.

— Klaus

Skeleton for review before any real implementation — refs #21 (comment [#41](https://git.larsnolden.com/lars/Gebos/issues/21#issuecomment-41)). Everything matches what we locked in the design thread. No host actually boots yet, no closure actually builds end-to-end (the vendored Supabase compose, HiveMQ packaging, hardware configs, and real internal hostnames are TODOs marked in-line). The point is for you to walk the structure and tell me where it's wrong before I fill it in. ## What's here - `flake.nix` — one root flake. Inputs: nixpkgs unstable, flake-parts, deploy-rs, process-compose-flake, services-flake. Exposes `packages.{ingester, frontend}`, `nixosConfigurations.{db-host, app-host, mqtt-ingest}`, `nixosModules`, `deploy`, `devShells.default`, and `process-compose.dev`. - `nix/hosts/` — three host configs + `common.nix` (ssh, `deploy` user with passwordless sudo for deploy-rs, flake settings). - `nix/modules/` — one NixOS module per service: `gebos-postgres`, `gebos-supabase`, `gebos-caddy`, `gebos-hivemq`, `gebos-ingester`, `gebos-frontend`. - `nix/supabase/` — vendored compose file (skeleton, not the full upstream yet — the README documents how to re-vendor), declarative `kong.yml`, and `init.sql` that bootstraps extensions, roles, schemas, and the `public.telemetry` hypertable with RLS. - `nix/dev/process-compose.nix` — local dev stack stub (Postgres + ingester + frontend wired; Supabase / Kong / Caddy marked TODO). - `nix/deploy.nix` — deploy-rs node map, `db-host` → `app-host` → `mqtt-ingest`. - `ingester/` — Go MQTT→Postgres skeleton. `main.go` is ~30 lines with the algorithm sketched in comments; target is <200 LOC for the real version. - `frontend/` — Vite + React stub. `src/supabase.ts` initializes `supabase-js` against the single `api.ge-bos.de` origin. - `.gitea/workflows/{deploy,check}.yml` — flake check on PR, deploy-rs from `nix` runner on push to main. ## Key wiring choices baked in - **Studio is bound to `127.0.0.1` on app-host.** No public route. Access via `ssh -L 3000:127.0.0.1:3000 app-host`. The compose binding uses `${STUDIO_BIND}:3000`, defaulted to `127.0.0.1` by the NixOS module. - **One Postgres** on `db-host`. Supabase compose has its bundled `db` removed, services read `POSTGRES_HOST` / `POSTGRES_PORT` from the systemd env. - **PostgREST + `/rpc/`** as the public REST surface, no Huma. The Go ingester is internal-only on `mqtt-ingest`, writes as `gebos_ingest` (`BYPASSRLS`). - **Caddy → Kong → upstream.** Caddy at `:443`, reverse-proxies `api.ge-bos.de` to `127.0.0.1:8000` (Kong), which fans out `/auth/v1`, `/rest/v1`, `/rpc` to the right service. - **Secrets** at `/etc/gebos/secrets.env`, loaded via systemd `EnvironmentFile=`. Each unit that needs creds declares it; nothing in the Nix store. - **Topic-encoded tenancy.** `telemetry` table has `tenant_id` column, RLS policy `USING (tenant_id = current_setting('app.tenant_id')::uuid)`. Backend sets the GUC from the JWT before each query. ## What's deliberately not done yet Marked with `TODO` in-line; the big ones: - Real hardware-config / boot loader / filesystems per host (depends on the actual boxes). - HiveMQ CE packaging (the unit currently runs `true` so the host eval succeeds). - Vendoring the *full* upstream Supabase compose — only the services we'll keep are sketched. - `caddy-l4` for MQTT TLS at the edge (or terminating TLS inside HiveMQ — open question). - `npmDepsHash` / `vendorHash` set to placeholders; first build will print the real values. - The Gitea runner's deploy ed25519 pubkey in `nix/hosts/common.nix`. ## Test plan - [ ] `nix flake check` once `flake.lock` is generated (Gitea runner first run). - [ ] `nix build .#ingester` once `go.sum` exists. - [ ] `nix build .#frontend` once `package-lock.json` exists. - [ ] One end-to-end `deploy-rs` dry-run against test VMs before we point at prod hostnames. - [ ] Walk through the structure here and call out anything that should move, split, or disappear. Closes once we converge on the layout and I follow up with real implementation PRs. — Klaus
Klaus added 1 commit 2026-05-27 17:23:36 +00:00
Add infrastructure skeleton for review (refs #21)
check / flake-check (pull_request) Has been cancelled
2d57db8182
Lays out the agreed shape from the design discussion: one flake at the
root with three nixosConfigurations (mqtt-ingest, db-host, app-host),
vendored Supabase docker-compose pointed at the external db-host,
Caddy → Kong → PostgREST/GoTrue, a Go MQTT→Postgres ingester stub,
deploy-rs node map, and a Gitea Actions workflow that deploys in
db → app → ingest order on push to main.

No real implementation yet — every host module has TODOs marking the
gaps (real hardware config, actual hostnames, HiveMQ packaging,
vendor the full upstream Supabase compose, etc.).
Owner

@Klaus Quick question. What secrets will be required for deploying the full app?

@Klaus Quick question. What secrets will be required for deploying the full app?
Author
Collaborator

Three buckets, all referenced from this branch:

1. Gitea Actions secret (CI)

  • DEPLOY_SSH_KEY — ed25519 private key for deploy@{db,app,mqtt-ingest} hosts. Used by .gitea/workflows/deploy.yml. The matching pubkey goes into nix/hosts/common.nix (currently a TODO).

2. /etc/gebos/secrets.env on each host (systemd EnvironmentFile=)

Per-host, only the keys that host's units actually read:

db-host — none. Postgres roles are seeded from init.sql at activation, but the role passwords themselves still need to land somewhere (open question — likely the same env file, consumed by a one-shot ALTER ROLE … PASSWORD unit).

app-host (consumed by gebos-supabase.service):

  • POSTGRES_PASSWORD — Postgres password used by supabase_auth_admin, authenticator, supabase_admin connections (auth, rest, studio, meta).
  • JWT_SECRET — HS256 signing secret for gotrue + postgrest. Must be ≥32 chars.
  • ANON_KEY — JWT signed with JWT_SECRET, role=anon. Referenced by kong.yml.
  • SERVICE_ROLE_KEY — JWT signed with JWT_SECRET, role=service_role. Referenced by kong.yml + Studio.
  • DASHBOARD_PASSWORD — Studio basic auth (username pinned to gebos).

mqtt-ingest (consumed by gebos-ingester.service):

  • GEBOS_POSTGRES_PASSWORD — password for the gebos_ingest role (the BYPASSRLS writer).

3. Frontend build inputs

  • VITE_SUPABASE_ANON_KEY — same value as ANON_KEY above, baked into the bundle at nix build .#frontend time. Needs to be wired through as a build arg (not yet plumbed in frontend/).

Not secrets, just adjacent

  • TLS for api.ge-bos.de / app.ge-bos.de / ingest.ge-bos.de — Caddy gets these from Let's Encrypt; nothing to provision.
  • HiveMQ TLS material — open question in the PR description (terminate at caddy-l4 vs. inside HiveMQ). Either way, if HiveMQ terminates, its keystore password belongs in the env file too.

Generation: I'd do openssl rand -hex 32 for JWT_SECRET and the Postgres passwords, then sign the two JWTs offline with that secret (Supabase has a one-liner for this in their self-hosting docs). Want me to add a scripts/gen-secrets.sh that produces a ready-to-scp secrets.env template?

— Klaus

Three buckets, all referenced from this branch: ### 1. Gitea Actions secret (CI) - `DEPLOY_SSH_KEY` — ed25519 private key for `deploy@{db,app,mqtt-ingest}` hosts. Used by `.gitea/workflows/deploy.yml`. The matching pubkey goes into `nix/hosts/common.nix` (currently a TODO). ### 2. `/etc/gebos/secrets.env` on each host (systemd `EnvironmentFile=`) Per-host, only the keys that host's units actually read: **db-host** — none. Postgres roles are seeded from `init.sql` at activation, but the role passwords themselves still need to land somewhere (open question — likely the same env file, consumed by a one-shot `ALTER ROLE … PASSWORD` unit). **app-host** (consumed by `gebos-supabase.service`): - `POSTGRES_PASSWORD` — Postgres password used by `supabase_auth_admin`, `authenticator`, `supabase_admin` connections (auth, rest, studio, meta). - `JWT_SECRET` — HS256 signing secret for gotrue + postgrest. Must be ≥32 chars. - `ANON_KEY` — JWT signed with `JWT_SECRET`, role=`anon`. Referenced by `kong.yml`. - `SERVICE_ROLE_KEY` — JWT signed with `JWT_SECRET`, role=`service_role`. Referenced by `kong.yml` + Studio. - `DASHBOARD_PASSWORD` — Studio basic auth (username pinned to `gebos`). **mqtt-ingest** (consumed by `gebos-ingester.service`): - `GEBOS_POSTGRES_PASSWORD` — password for the `gebos_ingest` role (the `BYPASSRLS` writer). ### 3. Frontend build inputs - `VITE_SUPABASE_ANON_KEY` — same value as `ANON_KEY` above, baked into the bundle at `nix build .#frontend` time. Needs to be wired through as a build arg (not yet plumbed in `frontend/`). ### Not secrets, just adjacent - TLS for `api.ge-bos.de` / `app.ge-bos.de` / `ingest.ge-bos.de` — Caddy gets these from Let's Encrypt; nothing to provision. - HiveMQ TLS material — open question in the PR description (terminate at `caddy-l4` vs. inside HiveMQ). Either way, if HiveMQ terminates, its keystore password belongs in the env file too. Generation: I'd do `openssl rand -hex 32` for `JWT_SECRET` and the Postgres passwords, then sign the two JWTs offline with that secret (Supabase has a one-liner for this in their self-hosting docs). Want me to add a `scripts/gen-secrets.sh` that produces a ready-to-scp `secrets.env` template? — Klaus
Owner

@Klaus What would be a good way to store those secrets in code (encrypted of course)? I'd like to have everything in one place and preferably only manage one secret (the private key to decrypt the other secrets).

@Klaus What would be a good way to store those secrets in code (encrypted of course)? I'd like to have everything in one place and preferably only manage one secret (the private key to decrypt the other secrets).
Author
Collaborator

Standard answer in the Nix world: sops-nix with age recipients. Maps onto what you described almost exactly.

Shape

  • One file in repo, e.g. nix/secrets/secrets.yaml, encrypted with SOPS. Plaintext keys are visible, values are AES-encrypted. Diffs stay readable (you see which secret changed, not its content).
  • Recipients listed in .sops.yaml: one age public key per host + your personal age public key. You only ever hold one private key (the one on your laptop / in a password manager). Each host holds its own — derived from its existing ssh_host_ed25519_key via ssh-to-age, so no extra key material to provision.
  • At activation, the sops-nix NixOS module decrypts the entries that host is a recipient for and drops them at /run/secrets/<name> with the owner/mode you declare. Plaintext never hits the Nix store, never hits disk outside tmpfs.

Fits the current wiring

The PR already loads /etc/gebos/secrets.env via systemd EnvironmentFile=. sops-nix has sops.templates.<name> which renders a file from decrypted placeholders — so you keep the env-file shape, just point EnvironmentFile= at /run/secrets/gebos-env instead. Per-host filtering (db-host gets only its keys, mqtt-ingest gets only GEBOS_POSTGRES_PASSWORD, etc.) falls out of .sops.yaml creation rules.

"One secret to manage"

Your single age private key decrypts secrets.yaml locally for editing (sops nix/secrets/secrets.yaml opens it in $EDITOR, re-encrypts on save). Hosts decrypt independently with their own host keys. Rotating a leaked value = sops edit + commit + redeploy. Rotating a host = sops updatekeys after swapping its age key in .sops.yaml.

Alternatives I considered

  • agenix — same idea, simpler, age-only, no templating. Fine for a handful of secrets but I'd rather have sops's per-host filtering and template rendering given there are already three hosts with different needs.
  • git-crypt — file-level only, no per-host scoping, no NixOS integration. Worse fit.
  • External KMS (Vault, Doppler, AWS SM) — overkill for three boxes, adds a runtime dependency and another thing to bootstrap.

What changes in this PR

If you're on board I'll fold sops-nix into the skeleton before filling in real implementations: add the flake input, the module wiring on each host, a placeholder .sops.yaml + secrets.yaml (encrypted to a throwaway key until you generate yours), and switch gebos-supabase / gebos-ingester to read from /run/secrets/.... The DEPLOY_SSH_KEY Gitea Actions secret stays where it is — that one has to live in Gitea regardless, since CI needs it before any host is touched.

Want me to do that as a follow-up commit on this branch, or as a separate PR once this one lands?

— Klaus

Standard answer in the Nix world: **[sops-nix](https://github.com/Mic92/sops-nix)** with **age** recipients. Maps onto what you described almost exactly. ### Shape - One file in repo, e.g. `nix/secrets/secrets.yaml`, encrypted with [SOPS](https://github.com/getsops/sops). Plaintext keys are visible, values are AES-encrypted. Diffs stay readable (you see *which* secret changed, not its content). - Recipients listed in `.sops.yaml`: one age public key per host + your personal age public key. You only ever hold one private key (the one on your laptop / in a password manager). Each host holds its own — derived from its existing `ssh_host_ed25519_key` via `ssh-to-age`, so no extra key material to provision. - At activation, the `sops-nix` NixOS module decrypts the entries that host is a recipient for and drops them at `/run/secrets/<name>` with the owner/mode you declare. Plaintext never hits the Nix store, never hits disk outside tmpfs. ### Fits the current wiring The PR already loads `/etc/gebos/secrets.env` via systemd `EnvironmentFile=`. sops-nix has `sops.templates.<name>` which renders a file from decrypted placeholders — so you keep the env-file shape, just point `EnvironmentFile=` at `/run/secrets/gebos-env` instead. Per-host filtering (db-host gets only its keys, mqtt-ingest gets only `GEBOS_POSTGRES_PASSWORD`, etc.) falls out of `.sops.yaml` creation rules. ### "One secret to manage" Your single age private key decrypts `secrets.yaml` locally for editing (`sops nix/secrets/secrets.yaml` opens it in `$EDITOR`, re-encrypts on save). Hosts decrypt independently with their own host keys. Rotating a leaked value = `sops` edit + commit + redeploy. Rotating a *host* = `sops updatekeys` after swapping its age key in `.sops.yaml`. ### Alternatives I considered - **agenix** — same idea, simpler, age-only, no templating. Fine for a handful of secrets but I'd rather have sops's per-host filtering and template rendering given there are already three hosts with different needs. - **git-crypt** — file-level only, no per-host scoping, no NixOS integration. Worse fit. - **External KMS (Vault, Doppler, AWS SM)** — overkill for three boxes, adds a runtime dependency and another thing to bootstrap. ### What changes in this PR If you're on board I'll fold sops-nix into the skeleton before filling in real implementations: add the flake input, the module wiring on each host, a placeholder `.sops.yaml` + `secrets.yaml` (encrypted to a throwaway key until you generate yours), and switch `gebos-supabase` / `gebos-ingester` to read from `/run/secrets/...`. The `DEPLOY_SSH_KEY` Gitea Actions secret stays where it is — that one has to live in Gitea regardless, since CI needs it before any host is touched. Want me to do that as a follow-up commit on this branch, or as a separate PR once this one lands? — Klaus
Owner

@Klaus, that sounds great. Please add sops-nix to the current PR. Can we also add default secrets to run locally in development without having to create any? The same goes for other env vars like GEBOS_MQTT_BROKER and GEBOS_POSTGRES_URL . They should have sensible defaults for local dev, if possible.

@Klaus, that sounds great. Please add sops-nix to the current PR. Can we also add default secrets to run locally in development without having to create any? The same goes for other env vars like GEBOS_MQTT_BROKER and GEBOS_POSTGRES_URL . They should have sensible defaults for local dev, if possible.
Klaus added 1 commit 2026-05-30 00:17:18 +00:00
Add sops-nix secrets + dev defaults so local stack runs with zero setup
check / flake-check (pull_request) Has been cancelled
2dd5ed5877
- sops-nix flake input, age + ssh-to-age in devShell, .sops.yaml with
  per-host recipients (real pubkeys still TODO until lars bootstraps).
- nix/modules/gebos-secrets.nix centralises per-host secret subsets and
  renders /run/secrets/gebos-env via sops.templates. Service modules now
  read EnvironmentFile= from services.gebos.secrets.envFile instead of
  the placeholder /etc/gebos/secrets.env.
- Falls back to a writeText env file when nix/secrets/secrets.yaml is
  absent, so `nix flake check` and first-boot eval work pre-bootstrap.
- nix/secrets/README.md walks through age key + sops bootstrap.
- Dev defaults: ingester binary falls back to local tcp/postgres URLs,
  process-compose adds mosquitto and wires GEBOS_MQTT_BROKER /
  GEBOS_POSTGRES_URL / GEBOS_POSTGRES_PASSWORD + VITE_SUPABASE_URL /
  VITE_SUPABASE_ANON_KEY, frontend defaults to 127.0.0.1:8000 in DEV.

Refs #23 (comment #110).
Author
Collaborator

Pushed 2dd5ed5 to this branch.

sops-nix

  • sops-nix flake input; sops, age, ssh-to-age in the devShell.
  • .sops.yaml with placeholder age recipients (one per dev + one per host; age1TODO_… until you run the bootstrap — see below).
  • nix/modules/gebos-secrets.nix is the single point of contact: each host enables the subset it needs (services.gebos.secrets.{supabase,ingester,postgresAdmin}) and the module renders a /run/secrets/gebos-env file from sops.templates. gebos-supabase and gebos-ingester now read EnvironmentFile = config.services.gebos.secrets.envFile — so all the per-unit EnvironmentFile plumbing flows through one place.
  • Wired on the three hosts:
    • app-hostsecrets.supabase = truePOSTGRES_PASSWORD, JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY, DASHBOARD_PASSWORD.
    • mqtt-ingestsecrets.ingester = trueGEBOS_POSTGRES_PASSWORD.
    • db-hostsecrets.postgresAdmin = truePOSTGRES_ADMIN_PASSWORD (consumed by the still-TODO one-shot ALTER ROLE unit).

What's still TODO before sops actually decrypts

nix/secrets/secrets.yaml is not committed (I can't generate genuinely encrypted output without sops/age locally, and a fake-encrypted file would just rot). Instead I added nix/secrets/secrets.yaml.example (plaintext template) and a step-by-step bootstrap in nix/secrets/README.md. The flow when you're ready:

age-keygen -o ~/.config/sops/age/keys.txt          # your private key
ssh-to-age < /etc/ssh/ssh_host_ed25519_key.pub     # on each host → host pubkey
$EDITOR .sops.yaml                                 # paste pubkeys, drop the age1TODO_… lines
cp nix/secrets/secrets.yaml.example nix/secrets/secrets.yaml
$EDITOR nix/secrets/secrets.yaml                    # fill in real values
sops -e -i nix/secrets/secrets.yaml
git add .sops.yaml nix/secrets/secrets.yaml && git commit

gebos-secrets guards on builtins.pathExists ../secrets/secrets.yaml — until the file exists, hosts fall back to a writeText env file with dev-prefixed placeholders. That keeps nix flake check green and lets first-boot eval succeed on a fresh box.

Dev defaults

Goal: nix run .#dev, go run ./ingester, and npm --prefix frontend run dev all work with zero setup.

  • nix/dev/process-compose.nix now spins up mosquitto alongside postgres, and passes explicit GEBOS_MQTT_BROKER / GEBOS_POSTGRES_URL / GEBOS_POSTGRES_PASSWORD to the ingester process, plus VITE_SUPABASE_URL / VITE_SUPABASE_ANON_KEY to the frontend. Credentials are dev-only and committed on purpose (they only work against the local stack).
  • ingester/main.go falls back to the same defaults when env vars are unset and logs which ones it defaulted, so a bare go run ./ingester lights up against the local broker+db without an .env dance. Prod always sets all three via the systemd unit's EnvironmentFile=.
  • nix/modules/gebos-ingester.nixmqttBroker and postgresUrl are now optional with sensible defaults (tcp://127.0.0.1:1883, postgres://gebos_ingest@127.0.0.1:5432/postgres?sslmode=require).
  • frontend/src/supabase.ts defaults to http://127.0.0.1:8000 + the dev anon key when import.meta.env.DEV, prod URL otherwise.

The dev anonKey / serviceRoleKey in process-compose.nix are still placeholders — proper JWTs signed with the dev jwt_secret need the Supabase self-host signing script, which I'll fold in alongside the Supabase compose wiring that's already TODO in the same file.

Heads up

When you do the secrets bootstrap, tighten .sops.yaml — the current creation_rules block puts all keys in one key_group, so every recipient can decrypt every key. Once real pubkeys are in, split into one rule per ^supabase_, ^ingester_, ^postgres_admin_ prefix so each host only gets what it needs. I left a note in the file.

— Klaus

Pushed [`2dd5ed5`](https://git.larsnolden.com/lars/Gebos/pulls/23/commits) to this branch. ## sops-nix - `sops-nix` flake input; `sops`, `age`, `ssh-to-age` in the devShell. - `.sops.yaml` with placeholder age recipients (one per dev + one per host; `age1TODO_…` until you run the bootstrap — see below). - `nix/modules/gebos-secrets.nix` is the single point of contact: each host enables the subset it needs (`services.gebos.secrets.{supabase,ingester,postgresAdmin}`) and the module renders a `/run/secrets/gebos-env` file from `sops.templates`. `gebos-supabase` and `gebos-ingester` now read `EnvironmentFile = config.services.gebos.secrets.envFile` — so all the per-unit `EnvironmentFile` plumbing flows through one place. - Wired on the three hosts: - `app-host` → `secrets.supabase = true` → `POSTGRES_PASSWORD`, `JWT_SECRET`, `ANON_KEY`, `SERVICE_ROLE_KEY`, `DASHBOARD_PASSWORD`. - `mqtt-ingest` → `secrets.ingester = true` → `GEBOS_POSTGRES_PASSWORD`. - `db-host` → `secrets.postgresAdmin = true` → `POSTGRES_ADMIN_PASSWORD` (consumed by the still-TODO one-shot `ALTER ROLE` unit). ### What's still TODO before sops actually decrypts `nix/secrets/secrets.yaml` is **not** committed (I can't generate genuinely encrypted output without `sops`/`age` locally, and a fake-encrypted file would just rot). Instead I added `nix/secrets/secrets.yaml.example` (plaintext template) and a step-by-step bootstrap in [`nix/secrets/README.md`](https://git.larsnolden.com/lars/Gebos/src/branch/klaus/skeleton-infra/nix/secrets/README.md). The flow when you're ready: ```sh age-keygen -o ~/.config/sops/age/keys.txt # your private key ssh-to-age < /etc/ssh/ssh_host_ed25519_key.pub # on each host → host pubkey $EDITOR .sops.yaml # paste pubkeys, drop the age1TODO_… lines cp nix/secrets/secrets.yaml.example nix/secrets/secrets.yaml $EDITOR nix/secrets/secrets.yaml # fill in real values sops -e -i nix/secrets/secrets.yaml git add .sops.yaml nix/secrets/secrets.yaml && git commit ``` `gebos-secrets` guards on `builtins.pathExists ../secrets/secrets.yaml` — until the file exists, hosts fall back to a `writeText` env file with dev-prefixed placeholders. That keeps `nix flake check` green and lets first-boot eval succeed on a fresh box. ## Dev defaults Goal: `nix run .#dev`, `go run ./ingester`, and `npm --prefix frontend run dev` all work with zero setup. - `nix/dev/process-compose.nix` now spins up `mosquitto` alongside `postgres`, and passes explicit `GEBOS_MQTT_BROKER` / `GEBOS_POSTGRES_URL` / `GEBOS_POSTGRES_PASSWORD` to the ingester process, plus `VITE_SUPABASE_URL` / `VITE_SUPABASE_ANON_KEY` to the frontend. Credentials are dev-only and committed on purpose (they only work against the local stack). - `ingester/main.go` falls back to the same defaults when env vars are unset and logs which ones it defaulted, so a bare `go run ./ingester` lights up against the local broker+db without an `.env` dance. Prod always sets all three via the systemd unit's `EnvironmentFile=`. - `nix/modules/gebos-ingester.nix` — `mqttBroker` and `postgresUrl` are now optional with sensible defaults (`tcp://127.0.0.1:1883`, `postgres://gebos_ingest@127.0.0.1:5432/postgres?sslmode=require`). - `frontend/src/supabase.ts` defaults to `http://127.0.0.1:8000` + the dev anon key when `import.meta.env.DEV`, prod URL otherwise. The dev `anonKey` / `serviceRoleKey` in `process-compose.nix` are still placeholders — proper JWTs signed with the dev `jwt_secret` need the Supabase self-host signing script, which I'll fold in alongside the Supabase compose wiring that's already TODO in the same file. ## Heads up When you do the secrets bootstrap, tighten `.sops.yaml` — the current `creation_rules` block puts all keys in one `key_group`, so every recipient can decrypt every key. Once real pubkeys are in, split into one rule per `^supabase_`, `^ingester_`, `^postgres_admin_` prefix so each host only gets what it needs. I left a note in the file. — Klaus
lars added 1 commit 2026-06-01 19:26:10 +00:00
fix nodejs version
check / flake-check (pull_request) Has been cancelled
a84b279747
Owner

@Klaus Update the changes proposed to the new nixos 26.05 release.

@Klaus Update the changes proposed to the new nixos 26.05 release.
lars marked the pull request as ready for review 2026-06-25 14:53:58 +00:00
lars merged commit 1cf710fe5b into main 2026-06-25 14:54:06 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: lars/Gebos#23