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,25 @@
|
||||
{ inputs }:
|
||||
|
||||
let
|
||||
mkNode = name: hostname: {
|
||||
inherit hostname;
|
||||
sshUser = "deploy";
|
||||
user = "root";
|
||||
profiles.system = {
|
||||
path = inputs.deploy-rs.lib.x86_64-linux.activate.nixos
|
||||
inputs.self.nixosConfigurations.${name};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
sshOpts = [ "-o" "StrictHostKeyChecking=accept-new" ];
|
||||
autoRollback = true;
|
||||
magicRollback = true;
|
||||
|
||||
nodes = {
|
||||
# Order matters: the Gitea Actions workflow invokes them in this sequence.
|
||||
db-host = mkNode "db-host" "db-host.gebos.internal"; # TODO: real address
|
||||
app-host = mkNode "app-host" "app-host.gebos.internal"; # TODO: real address
|
||||
mqtt-ingest = mkNode "mqtt-ingest" "ingest.ge-bos.de";
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{ pkgs, services-flake }:
|
||||
|
||||
{ ... }:
|
||||
{
|
||||
imports = [ services-flake.processComposeModules.default ];
|
||||
|
||||
# Skeleton — services-flake gives us postgres, plus we'd add Supabase via
|
||||
# docker-compose run, Caddy, Kong (or skip Kong locally and hit services
|
||||
# directly), the ingester, and `vite dev`. Fill in as we go.
|
||||
services = {
|
||||
postgres."db" = {
|
||||
enable = true;
|
||||
port = 5432;
|
||||
initialDatabases = [{ name = "gebos"; }];
|
||||
};
|
||||
};
|
||||
|
||||
settings.processes = {
|
||||
ingester = {
|
||||
command = "${pkgs.go}/bin/go run ./ingester";
|
||||
depends_on."db".condition = "process_healthy";
|
||||
};
|
||||
frontend = {
|
||||
command = "${pkgs.nodejs_20}/bin/npm --prefix frontend run dev";
|
||||
};
|
||||
# TODO: supabase compose (point POSTGRES_HOST at the services-flake db)
|
||||
# TODO: kong (use the vendored kong.yml from nix/supabase/)
|
||||
# TODO: caddy (mirror prod routes locally, no TLS)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
networking.hostName = "app-host";
|
||||
|
||||
services.gebos.supabase = {
|
||||
enable = true;
|
||||
# Compose stack points at external db-host instead of the bundled `db` service.
|
||||
postgresHost = "db-host.gebos.internal"; # TODO: real internal hostname
|
||||
postgresPort = 5432;
|
||||
# Studio bound to loopback only — reach via `ssh -L 3000:127.0.0.1:3000 app-host`.
|
||||
studioBindAddress = "127.0.0.1";
|
||||
};
|
||||
|
||||
services.gebos.caddy = {
|
||||
enable = true;
|
||||
# Caddy terminates TLS, proxies to Kong on 127.0.0.1:8000.
|
||||
sites = {
|
||||
"api.ge-bos.de" = { upstream = "127.0.0.1:8000"; }; # → Kong → PostgREST/GoTrue
|
||||
"app.ge-bos.de" = { staticRoot = "${config.services.gebos.frontend.package}/share/frontend"; };
|
||||
};
|
||||
};
|
||||
|
||||
services.gebos.frontend.enable = true;
|
||||
|
||||
networking.firewall.allowedTCPPorts = [ 80 443 ];
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
system.stateVersion = "25.05";
|
||||
|
||||
nix.settings = {
|
||||
experimental-features = [ "nix-command" "flakes" ];
|
||||
trusted-users = [ "@wheel" "deploy" ];
|
||||
};
|
||||
|
||||
# deploy-rs SSHes in as `deploy` and uses sudo to activate.
|
||||
users.users.deploy = {
|
||||
isNormalUser = true;
|
||||
extraGroups = [ "wheel" ];
|
||||
openssh.authorizedKeys.keys = [
|
||||
# TODO: paste the Gitea runner's deploy ed25519 pubkey here
|
||||
];
|
||||
};
|
||||
security.sudo.wheelNeedsPassword = false;
|
||||
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
settings.PasswordAuthentication = false;
|
||||
};
|
||||
|
||||
networking.firewall.enable = true;
|
||||
|
||||
time.timeZone = "UTC";
|
||||
|
||||
environment.systemPackages = with pkgs; [ vim curl jq ];
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
networking.hostName = "db-host";
|
||||
|
||||
# Skeleton: real hardware-configuration.nix + boot/fs lives next to this file
|
||||
# once we have the actual box. Marked here so the structure is visible.
|
||||
# imports = [ ./db-host.hardware.nix ];
|
||||
|
||||
services.gebos.postgres = {
|
||||
enable = true;
|
||||
listenAddresses = [ "0.0.0.0" ];
|
||||
# Telemetry, auth, _supavisor, _analytics live in the same cluster.
|
||||
# See nix/supabase/init.sql for schema/role bootstrap.
|
||||
};
|
||||
|
||||
# Postgres exposed only to the app-host and mqtt-ingest private addresses.
|
||||
networking.firewall.extraInputRules = ''
|
||||
ip saddr { 10.0.0.0/8 } tcp dport 5432 accept
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{ inputs }:
|
||||
|
||||
let
|
||||
mkHost = name: extraModules:
|
||||
inputs.nixpkgs.lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs = { inherit inputs; };
|
||||
modules = [
|
||||
./common.nix
|
||||
(import ../modules).gebos-postgres
|
||||
(import ../modules).gebos-supabase
|
||||
(import ../modules).gebos-caddy
|
||||
(import ../modules).gebos-hivemq
|
||||
(import ../modules).gebos-ingester
|
||||
(import ../modules).gebos-frontend
|
||||
./${name}.nix
|
||||
] ++ extraModules;
|
||||
};
|
||||
in
|
||||
{
|
||||
db-host = mkHost "db-host" [ ];
|
||||
app-host = mkHost "app-host" [ ];
|
||||
mqtt-ingest = mkHost "mqtt-ingest" [ ];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
networking.hostName = "mqtt-ingest";
|
||||
|
||||
services.gebos.hivemq = {
|
||||
enable = true;
|
||||
# Persistent sessions on disk — single-node, devices reconnect on restart.
|
||||
persistentSessions = true;
|
||||
tlsPort = 8883;
|
||||
};
|
||||
|
||||
services.gebos.ingester = {
|
||||
enable = true;
|
||||
mqttBroker = "tcp://127.0.0.1:1883"; # local broker
|
||||
postgresUrl = "postgres://gebos_ingest@db-host.gebos.internal:5432/postgres?sslmode=require";
|
||||
# Password sourced from /etc/gebos/secrets.env via EnvironmentFile.
|
||||
};
|
||||
|
||||
# Caddy fronts TLS for `ingest.ge-bos.de` and forwards to HiveMQ.
|
||||
services.gebos.caddy = {
|
||||
enable = true;
|
||||
sites = {
|
||||
"ingest.ge-bos.de" = { tcpProxy = "127.0.0.1:8883"; };
|
||||
};
|
||||
};
|
||||
|
||||
networking.firewall.allowedTCPPorts = [ 8883 ];
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
gebos-ingester = import ./gebos-ingester.nix;
|
||||
gebos-supabase = import ./gebos-supabase.nix;
|
||||
gebos-caddy = import ./gebos-caddy.nix;
|
||||
gebos-postgres = import ./gebos-postgres.nix;
|
||||
gebos-hivemq = import ./gebos-hivemq.nix;
|
||||
gebos-frontend = import ./gebos-frontend.nix;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.services.gebos.caddy;
|
||||
|
||||
siteBlock = host: site:
|
||||
if site ? staticRoot then ''
|
||||
${host} {
|
||||
root * ${site.staticRoot}
|
||||
file_server
|
||||
try_files {path} /index.html
|
||||
encode zstd gzip
|
||||
}
|
||||
''
|
||||
else if site ? upstream then ''
|
||||
${host} {
|
||||
reverse_proxy ${site.upstream}
|
||||
encode zstd gzip
|
||||
}
|
||||
''
|
||||
else if site ? tcpProxy then ''
|
||||
${host}:8883 {
|
||||
# TLS termination for MQTT — Caddy's `layer4` app would be ideal here;
|
||||
# for now, document that the upstream HiveMQ port is ${site.tcpProxy}.
|
||||
# TODO: switch to the caddy-l4 module or terminate TLS in HiveMQ directly.
|
||||
}
|
||||
''
|
||||
else throw "site `${host}` needs one of: staticRoot, upstream, tcpProxy";
|
||||
in
|
||||
{
|
||||
options.services.gebos.caddy = {
|
||||
enable = lib.mkEnableOption "Gebos Caddy reverse proxy / TLS terminator";
|
||||
sites = lib.mkOption {
|
||||
type = lib.types.attrsOf lib.types.attrs;
|
||||
default = { };
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
services.caddy = {
|
||||
enable = true;
|
||||
email = "ops@ge-bos.de"; # TODO: confirm ACME contact
|
||||
extraConfig = lib.concatStringsSep "\n"
|
||||
(lib.mapAttrsToList siteBlock cfg.sites);
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.services.gebos.frontend;
|
||||
in
|
||||
{
|
||||
options.services.gebos.frontend = {
|
||||
enable = lib.mkEnableOption "Gebos static SPA";
|
||||
package = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default = pkgs.callPackage ../../frontend { };
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
# Nothing to install at the system level — Caddy serves the package's
|
||||
# `share/frontend` directory directly. See nix/hosts/app-host.nix.
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.services.gebos.hivemq;
|
||||
in
|
||||
{
|
||||
options.services.gebos.hivemq = {
|
||||
enable = lib.mkEnableOption "HiveMQ CE single-node broker";
|
||||
persistentSessions = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
};
|
||||
tlsPort = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 8883;
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
# TODO: package HiveMQ CE (download tarball + JRE wrapper) and wire as a
|
||||
# systemd unit. Until then this module only declares options so dependent
|
||||
# hosts can be evaluated.
|
||||
systemd.services.gebos-hivemq = {
|
||||
description = "HiveMQ CE";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
ExecStart = "${pkgs.coreutils}/bin/true"; # TODO: real HiveMQ command
|
||||
Restart = "on-failure";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.services.gebos.ingester;
|
||||
ingester = pkgs.callPackage ../../ingester { };
|
||||
in
|
||||
{
|
||||
options.services.gebos.ingester = {
|
||||
enable = lib.mkEnableOption "Gebos MQTT → Postgres ingester";
|
||||
mqttBroker = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "tcp://127.0.0.1:1883";
|
||||
};
|
||||
postgresUrl = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "DSN without password — password comes from EnvironmentFile.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
systemd.services.gebos-ingester = {
|
||||
description = "Gebos MQTT → Postgres ingester";
|
||||
after = [ "network-online.target" ];
|
||||
wants = [ "network-online.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
environment = {
|
||||
GEBOS_MQTT_BROKER = cfg.mqttBroker;
|
||||
GEBOS_POSTGRES_URL = cfg.postgresUrl;
|
||||
};
|
||||
|
||||
serviceConfig = {
|
||||
ExecStart = "${ingester}/bin/gebos-ingester";
|
||||
DynamicUser = true;
|
||||
EnvironmentFile = "/etc/gebos/secrets.env"; # GEBOS_POSTGRES_PASSWORD
|
||||
Restart = "on-failure";
|
||||
RestartSec = "5s";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.services.gebos.postgres;
|
||||
in
|
||||
{
|
||||
options.services.gebos.postgres = {
|
||||
enable = lib.mkEnableOption "Gebos Postgres 17 + TimescaleDB";
|
||||
listenAddresses = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ "127.0.0.1" ];
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
services.postgresql = {
|
||||
enable = true;
|
||||
package = pkgs.postgresql_17;
|
||||
enableTCPIP = true;
|
||||
settings = {
|
||||
listen_addresses = lib.concatStringsSep "," cfg.listenAddresses;
|
||||
shared_preload_libraries = "timescaledb,pg_stat_statements";
|
||||
};
|
||||
extensions = ps: with ps; [
|
||||
timescaledb
|
||||
pgjwt
|
||||
pg_graphql
|
||||
pgsodium
|
||||
# supabase_vault — TODO: package or vendor
|
||||
];
|
||||
# Init script creates Supabase's `auth`, `storage`, `_analytics`, `_supavisor`
|
||||
# schemas, the gebos_ingest role (BYPASSRLS), and telemetry hypertables.
|
||||
initialScript = ../supabase/init.sql;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.services.gebos.supabase;
|
||||
composeDir = ../supabase;
|
||||
in
|
||||
{
|
||||
options.services.gebos.supabase = {
|
||||
enable = lib.mkEnableOption "Supabase stack via vanilla docker-compose";
|
||||
|
||||
postgresHost = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "External Postgres host (db-host).";
|
||||
};
|
||||
postgresPort = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 5432;
|
||||
};
|
||||
studioBindAddress = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "127.0.0.1";
|
||||
description = "Studio is intentionally never exposed publicly.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
virtualisation.docker.enable = true;
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /var/lib/gebos-supabase 0750 root root - -"
|
||||
];
|
||||
|
||||
# Copy the vendored compose file into place at activation time so changes
|
||||
# to nix/supabase/docker-compose.yml are deployed atomically.
|
||||
environment.etc."gebos/supabase/docker-compose.yml".source =
|
||||
"${composeDir}/docker-compose.yml";
|
||||
|
||||
systemd.services.gebos-supabase = {
|
||||
description = "Gebos — Supabase compose stack";
|
||||
after = [ "docker.service" "network-online.target" ];
|
||||
wants = [ "docker.service" "network-online.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
environment = {
|
||||
POSTGRES_HOST = cfg.postgresHost;
|
||||
POSTGRES_PORT = toString cfg.postgresPort;
|
||||
POSTGRES_DB = "postgres";
|
||||
STUDIO_BIND = cfg.studioBindAddress;
|
||||
};
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
WorkingDirectory = "/etc/gebos/supabase";
|
||||
EnvironmentFile = "/etc/gebos/secrets.env"; # POSTGRES_PASSWORD, JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY, DASHBOARD_PASSWORD
|
||||
ExecStart = "${pkgs.docker}/bin/docker compose up -d --remove-orphans";
|
||||
ExecStop = "${pkgs.docker}/bin/docker compose down";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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