Add infrastructure skeleton for review (refs #21)
check / flake-check (pull_request) Has been cancelled
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:
@@ -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.
|
||||
@@ -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}
|
||||
@@ -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;
|
||||
@@ -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 ]
|
||||
Reference in New Issue
Block a user