The database moved to a container of our own; the platform is gone. This takes out what was left of it — and, where the leftovers were load bearing, moves rather than deletes. Moved, not deleted: supabase/migrations/ -> db/migrations/ the schema's source of truth supabase/build-org.ts -> scripts/build-org.ts lib/supabase/types.ts -> lib/types.ts 52 import sites repointed The bookkeeping needed care. It lived in `supabase_migrations.schema_migrations`, and simply renaming the schema would have left the runner facing an empty table: it would have called all 67 migrations pending and replayed them against a database that is long since current. So the runner now creates `migrationen.schema_migrations` and, once, copies the old rows across — guarded so a second run does nothing and a fresh database skips it entirely. Only then does migration 20260907100000 drop the old schema. Deleted: the CLI config, the seed, the historical schema/function dumps (nothing read them), scripts/umzug-von-supabase.sh (the move is done), and both Supabase packages plus the CLI. Nothing in the application imported them — the build now succeeds with no environment variables at all, which is the proof. Integration tests: six of them signed in through Supabase Auth and asserted against the anon key and the service role. That model is gone, so the tests were not portable — they are deleted. session-context and employee-status-filter already ran on pg and are untouched; om-reporting is ported to a direct connection because it guards a real risk (the reporting line rule exists twice, once in SQL and once in TypeScript). CI: the integration job started a Supabase stack. It now runs a postgres service, applies deploy/db-init and every migration to an empty database — that was the valuable part, and it still holds — then checks that a second run is a no-op, which is what proves the bookkeeping works. Docs: security-review.md audited a service-role key, a cookie adapter and auth.users, none of which exist. Restating findings about removed components would suggest today's system had been reviewed; it has not. It now records what was removed and says a fresh review is due. data-model.md was already marked obsolete and described the pre-OM schema; azure-migration.md was a plan for a route not taken. Both deleted. Verified: npm ci, typecheck, lint, 445 tests, build — all clean without the packages. Integration tests skip cleanly with no database. Migration SQL and the runner are reviewed but NOT executed: no Docker here, and the old instance no longer resolves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
126 lines
5.1 KiB
YAML
126 lines
5.1 KiB
YAML
services:
|
|
# Die Datenbank gehört zum Projekt und läuft hier mit.
|
|
#
|
|
# Was das Schema von der Plattform brauchte, bringt deploy/db-init mit:
|
|
# die Anwendungsrolle ohne BYPASSRLS, zwei Erweiterungen und eine Attrappe
|
|
# des `auth`-Schemas für die frühen Migrationen. Nachgewiesen ist, dass
|
|
# alle Migrationen auf einer leeren Datenbank durchlaufen und dabei
|
|
# Spalte für Spalte dasselbe Schema ergeben wie bisher.
|
|
db:
|
|
image: postgres:17-alpine
|
|
restart: unless-stopped
|
|
environment:
|
|
POSTGRES_DB: ${POSTGRES_DB:-alpenwerk}
|
|
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD fehlt in .env}
|
|
# Vom Startskript gelesen; damit steht das Passwort der Anwendungsrolle
|
|
# nicht in einer Datei im Repository.
|
|
APP_DB_PASSWORD: ${APP_DB_PASSWORD:?APP_DB_PASSWORD fehlt in .env}
|
|
# Ohne das legt initdb die Datenbank mit der Locale des Abbilds an;
|
|
# Sortierung von Umlauten ist bei einer Mitarbeiterliste keine
|
|
# Nebensache.
|
|
LANG: de_AT.UTF-8
|
|
POSTGRES_INITDB_ARGS: "--locale-provider=icu --icu-locale=de-AT --encoding=UTF8"
|
|
volumes:
|
|
- db-daten:/var/lib/postgresql/data
|
|
# Läuft nur beim allerersten Start, solange das Datenverzeichnis leer
|
|
# ist. Spätere Änderungen wirken erst auf einem frischen Band.
|
|
- ./deploy/db-init:/docker-entrypoint-initdb.d:ro
|
|
# Bewusst **kein** `ports:`. Die Datenbank ist nur im Compose-Netz
|
|
# erreichbar, nicht vom Host und erst recht nicht von aussen. Wer von
|
|
# aussen heranmuss (Sicherung, Migration), tut das über `docker compose
|
|
# exec db` — dann gibt es keinen offenen Port, den jemand vergisst.
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-alpenwerk}"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 10
|
|
start_period: 30s
|
|
|
|
# ── Werkzeuge, die nicht mitlaufen ───────────────────────────────────
|
|
#
|
|
# Beide stehen im Profil `tools` und starten deshalb bei `docker compose
|
|
# up` **nicht** mit. Sie werden einzeln gerufen (`docker compose run --rm
|
|
# migrate`) und sind der Grund, warum auf dem Host weder Node noch ein
|
|
# psql installiert sein muss — nur Docker.
|
|
|
|
# Spielt ausstehende Migrationen gegen den db-Container ein.
|
|
migrate:
|
|
profiles: ["tools"]
|
|
image: node:22-alpine
|
|
working_dir: /repo
|
|
volumes:
|
|
- .:/repo
|
|
environment:
|
|
# Der direkte Zugang: im Compose-Netz gibt es keinen Pooler, und
|
|
# scripts/migrate.mjs erwartet ohnehin die unmittelbare Verbindung.
|
|
MIGRATE_DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-alpenwerk}
|
|
DATABASE_SSL: "false"
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
# `pg` liegt im Repository; fehlt node_modules auf dem Server, wird es
|
|
# einmalig nachgeladen, ohne package.json anzufassen.
|
|
entrypoint:
|
|
- sh
|
|
- -c
|
|
- 'test -d node_modules/pg || npm install --no-save --no-audit --no-fund pg@8 >/dev/null; node scripts/migrate.mjs "$@"'
|
|
- --
|
|
|
|
# psql und pg_dump, ohne sie auf dem Host zu installieren.
|
|
psql:
|
|
profiles: ["tools"]
|
|
image: postgres:17-alpine
|
|
volumes:
|
|
- .:/repo
|
|
working_dir: /repo
|
|
environment:
|
|
PGPASSWORD: ${POSTGRES_PASSWORD}
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
entrypoint: ["psql", "-v", "ON_ERROR_STOP=1", "-h", "db", "-U", "${POSTGRES_USER:-postgres}", "-d", "${POSTGRES_DB:-alpenwerk}"]
|
|
|
|
app:
|
|
# Ohne Build-Argumente: alles, was die Anwendung braucht — DATABASE_URL,
|
|
# AUTH_* — liest sie zur Laufzeit aus .env. Das Abbild ist damit für jede
|
|
# Umgebung dasselbe.
|
|
build:
|
|
context: .
|
|
restart: unless-stopped
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
# Nur auf der Loopback-Adresse, nicht auf allen Schnittstellen. Erreichbar
|
|
# ist die App damit ausschliesslich über den Reverse Proxy, der TLS
|
|
# beendet — sonst stünde daneben derselbe Dienst unverschlüsselt offen,
|
|
# und ein Fehler in der Firewall genügte.
|
|
ports:
|
|
- "127.0.0.1:3000:3000"
|
|
env_file:
|
|
- .env
|
|
|
|
# Der nächtliche Lauf: ruft täglich um 03:00 Uhr
|
|
# /api/cron/apply-pending-changes auf, damit fällig gewordene Versetzungen,
|
|
# Beförderungen, Karenzen und Reorganisationen wirksam werden. Ausgewiesen
|
|
# über CRON_SECRET — es gibt dabei keine angemeldete Person.
|
|
cron:
|
|
image: alpine:3.20
|
|
restart: unless-stopped
|
|
depends_on:
|
|
app:
|
|
condition: service_healthy
|
|
env_file:
|
|
- .env
|
|
entrypoint: ["/bin/sh", "-c"]
|
|
command:
|
|
- |
|
|
echo "0 3 * * * /bin/sh -c 'wget -q -O- --header=\"Authorization: Bearer \$$CRON_SECRET\" http://app:3000/api/cron/apply-pending-changes >> /var/log/cron.log 2>&1'" > /etc/crontabs/root
|
|
crond -f -d 8
|
|
|
|
volumes:
|
|
# Benannt, nicht als Pfad auf dem Host: `docker compose down` lässt es
|
|
# stehen, und nur ein ausdrückliches `down -v` löscht es. Der Unterschied
|
|
# zwischen „Dienst neu gestartet" und „Personaldaten weg".
|
|
db-daten:
|