Add infrastructure skeleton for review (refs #21)
check / flake-check (pull_request) Has been cancelled

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.).
This commit is contained in:
Klaus
2026-05-27 17:23:02 +00:00
parent 71d74d3b83
commit 2d57db8182
34 changed files with 1005 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
# Supabase compose stack
Vendored from the official Supabase self-hosting bundle, with two deliberate
changes:
1. The bundled `db` service is removed. Every service that talks to Postgres
reads `POSTGRES_HOST` / `POSTGRES_PORT` from the environment and connects to
the external `db-host`. The systemd unit (`nix/modules/gebos-supabase.nix`)
injects these from `/etc/gebos/secrets.env`.
2. Studio is bound to `127.0.0.1` only. There is **no public route** to it.
To use it from your laptop:
```
ssh -L 3000:127.0.0.1:3000 app-host
open http://localhost:3000
```
This is intentional — Studio runs with the `service_role` JWT and bypasses
RLS. Putting it on the public internet behind only HTTP basic auth (the
default) is too thin.
## What's in here
- `docker-compose.yml` — the stack itself
- `init.sql` — schemas, roles, and extensions Postgres needs before the
compose services come up. Loaded by `nix/modules/gebos-postgres.nix`.
- `kong.yml` — Kong's declarative routing (to be vendored alongside the compose
file when we copy it in).
## Updating the vendored compose
When upstream Supabase ships a new compose layout, re-vendor by:
1. Copy upstream `docker/docker-compose.yml` over `docker-compose.yml`.
2. Remove the `db:` service block.
3. Replace every `db:5432` / `postgres:5432` reference with
`${POSTGRES_HOST}:${POSTGRES_PORT}`.
4. Bind Studio to `${STUDIO_BIND}:3000` instead of `0.0.0.0:3000`.
5. Commit, deploy, smoke-test through the SSH tunnel.
+67
View File
@@ -0,0 +1,67 @@
# Gebos — vendored Supabase compose stack.
#
# TODO: copy the full official compose from
# 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)
#
# 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.
services:
kong:
image: kong:2.8.1
restart: unless-stopped
ports:
- "127.0.0.1:8000:8000/tcp" # Caddy proxies api.ge-bos.de here
environment:
KONG_DATABASE: "off"
KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml
volumes:
- ./kong.yml:/home/kong/kong.yml:ro
auth:
image: supabase/gotrue:v2.158.1
restart: unless-stopped
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.ge-bos.de
GOTRUE_JWT_SECRET: ${JWT_SECRET}
GOTRUE_JWT_EXP: "3600"
API_EXTERNAL_URL: https://api.ge-bos.de
rest:
image: postgrest/postgrest:v12.0.2
restart: unless-stopped
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_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.ge-bos.de
DASHBOARD_USERNAME: gebos
DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD}
meta:
image: supabase/postgres-meta:v0.83.2
restart: unless-stopped
environment:
PG_META_DB_HOST: ${POSTGRES_HOST}
PG_META_DB_PORT: ${POSTGRES_PORT}
PG_META_DB_NAME: ${POSTGRES_DB}
PG_META_DB_USER: supabase_admin
PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
+81
View File
@@ -0,0 +1,81 @@
-- Gebos Postgres bootstrap.
-- Runs once via services.postgresql.initialScript on db-host.
--
-- Sets up:
-- * Extensions Supabase + TimescaleDB need
-- * Supabase's expected schemas and admin roles
-- * The gebos_ingest role (BYPASSRLS) used by the MQTT ingester
-- * A telemetry hypertable with tenant_id column for RLS
--
-- TODO: split into ordered files once it grows. For the skeleton, one file is fine.
create extension if not exists timescaledb;
create extension if not exists pgcrypto;
create extension if not exists pgjwt;
create extension if not exists pg_graphql;
create extension if not exists pgsodium;
-- create extension if not exists supabase_vault; -- TODO: vendor or package
-- Supabase schemas
create schema if not exists auth;
create schema if not exists storage;
create schema if not exists _analytics;
create schema if not exists _supavisor;
-- Roles Supabase services connect as. Passwords come from secrets.env, not here.
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 noinherit login;
grant anon, authenticated, service_role to authenticator;
exception when duplicate_object then null; end $$;
do $$ begin
create role supabase_admin login createrole createdb replication bypassrls;
exception when duplicate_object then null; end $$;
do $$ begin
create role supabase_auth_admin login createrole;
grant usage on schema auth to supabase_auth_admin;
exception when duplicate_object then null; end $$;
-- Ingester writes telemetry; bypasses RLS because at write time no user is
-- authenticated. Trust boundary is the broker ACL ("who can publish to what
-- tenant's topic"), not the database.
do $$ begin
create role gebos_ingest login bypassrls;
exception when duplicate_object then null; end $$;
-- Telemetry hypertable. Tenant lives on the row so standard RLS works.
create table if not exists public.telemetry (
ts timestamptz not null,
tenant_id uuid not null,
device_id uuid not null,
metric text not null,
value double precision,
payload jsonb
);
select create_hypertable('public.telemetry', 'ts', if_not_exists => true);
create index if not exists telemetry_tenant_device_ts_idx
on public.telemetry (tenant_id, device_id, ts desc);
alter table public.telemetry enable row level security;
create policy telemetry_tenant_isolation on public.telemetry
for select
using (tenant_id = current_setting('app.tenant_id', true)::uuid);
grant select on public.telemetry to authenticated;
grant insert on public.telemetry to gebos_ingest;
+48
View File
@@ -0,0 +1,48 @@
_format_version: "2.1"
# Declarative Kong config — vanilla Supabase routing, no surprises.
# Lives at /home/kong/kong.yml inside the container.
consumers:
- username: anon
keyauth_credentials:
- key: ${ANON_KEY}
- username: service_role
keyauth_credentials:
- key: ${SERVICE_ROLE_KEY}
services:
- name: auth-v1
url: http://auth:9999/
routes:
- name: auth-v1-route
strip_path: true
paths: [ /auth/v1/ ]
plugins:
- name: cors
- name: rest-v1
url: http://rest:3000/
routes:
- name: rest-v1-route
strip_path: true
paths: [ /rest/v1/ ]
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: true
key_names: [ apikey ]
- name: rpc
url: http://rest:3000/rpc/
routes:
- name: rpc-route
strip_path: true
paths: [ /rpc/ ]
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: true
key_names: [ apikey ]