stammtisch-hersbruck/backend/README.md
Daniel Michelberger 893509be9d refactor: Aus Runs werden Stages, bis in die Datenbank
"Run" hiess in dieser App immer schon der abgesteckte Abschnitt eines
Events, auf dem gefahren und gestoppt wird — also das, was im Rennsport
Stage heisst. "Run" ist daneben der einzelne Durchgang eines Fahrers,
und genau der steht hier als `times`. Zwei Bedeutungen fuer ein Wort, an
einer Stelle, an der beide vorkommen.

Die Migration benennt um statt neu anzulegen: Collection und Feld
behalten ihre IDs, PocketBase benennt Tabelle und Spalte um, die Daten
bleiben stehen. Gesucht wird ueber die Collection-ID und nicht ueber den
Namen, damit sie auch auf einer frischen Datenbank durchlaeuft, deren
Snapshot die Collection bereits `stages` nennt.

Im Frontend wandert `times.run` zu `times.stage`, der Store heisst
stages.svelte.ts, und aus /events/[id]/runs/[runId] wird
/events/[id]/stages/[stageId].

Nicht umbenannt: `running`, `allRunning` und RunningTimesToast. Das sind
laufende Zeiten und keine Stages — dieselben Buchstaben, andere Sache.

Und die Artikel: Der Run war maskulin, die Stage ist feminin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
2026-09-07 22:16:22 +02:00

8.1 KiB

stammtisch-hersbruck.de — Backend (PocketBase)

PocketBase-Instanz für Events, Läufe, Fahrer, Zeiten und Teams.

Lokal starten

.env aus der Vorlage anlegen (die .env selbst steht in .gitignore):

cp .env.example .env

und die Werte eintragen — .env.example beschreibt jede Variable. Dann:

docker compose up -d --build

Admin-UI: http://127.0.0.1:8090/_/ — Login mit SUPERUSER_EMAIL/SUPERUSER_PASSWORD.

Damit das Frontend gegen diese Instanz läuft, in ../frontend/.env PUBLIC_PB_URL=http://127.0.0.1:8090 setzen.

Schema

Das Schema liegt als versionierte Migration in pb_migrations/ und wird beim Start automatisch angewendet. Es enthält die sechs fachlichen Collections users, teams, events, stages, riders, times.

1754400000_init_schema.js ist ein Snapshot der produktiven Instanz. Er wurde per GET /api/collections abgezogen und ruft importCollections mit deleteMissing = false auf — die Migration legt an und aktualisiert, löscht aber nichts, was nicht im Snapshot steht.

Schema-Änderungen aus dem Admin-UI holen

pb_migrations/ wird nicht in den Container gemountet — die Migrationen kommen über COPY ins Image (siehe Dockerfile). Das ist Absicht: Ein Bind-Mount überdeckt das Image-Verzeichnis, und auf einem Deployment-Host ohne ausgechecktes Repo legt Docker dort ein leeres Verzeichnis an. PocketBase findet dann keine Migration und startet mit einer Datenbank ohne Collections.

Die Kehrseite: Eine im Admin-UI erzeugte Migration liegt zunächst nur im Container und muss von Hand herausgeholt werden:

# Welche Dateien sind dazugekommen?
docker compose exec pocketbase ls /pb/pb_migrations

# Die neue Datei ins Repo kopieren und committen
docker compose cp pocketbase:/pb/pb_migrations/<datei> ./pb_migrations/

Ohne diesen Schritt ist die Änderung beim nächsten --build verloren.

Ein frisch gestarteter Container hat damit dasselbe Schema wie die produktive Instanz — aber keine Daten.

Regeln: der Login-Vorspann ist Pflicht

Jede Regel, die @request.auth.id erwähnt, beginnt mit @request.auth.id != "" && (…). Das ist kein Stilmittel, sondern schließt ein Loch:

Eine leere Relation ist in PocketBase gleich dem leeren @request.auth.id einer anonymen Anfrage. team.admins.id ?= @request.auth.id ist damit für jedes Team ohne Admins wahr — und ohne Admins ist jedes frisch angelegte Team. Dasselbe gilt für jede optionale Relation: created_by, stewards, rider.user.

Nachgewiesen an einer lokalen Instanz mit dem Schema aus diesem Repo: Ein POST auf /api/collections/riders/records ohne jeden Token legte den Fahrer an. Betroffen waren nach demselben Muster teams (update), riders, trails, trail_versions, trail_flags, trail_markers, trail_comments, event_series und event_participants.

1754501700_rules_require_login.js setzt den Vorspann vor jede betroffene Regel. Er kann nichts erlauben, was vorher verboten war — er nimmt nur die anonyme Anfrage heraus, die keine dieser Regeln je erfüllen sollte. Gelesen werden die Regeln aus der Datenbank, eine Handänderung aus dem Admin-UI wird also abgesichert und nicht überschrieben.

Nach dem Deploy einmal gegenprüfen — das muss scheitern:

curl -X POST https://api.stammtisch-hersbruck.de/api/collections/riders/records \
  -H 'Content-Type: application/json' \
  -d '{"team":"<TEAM_ID>","name":"Test"}'

Wer eine neue Regel schreibt, setzt den Vorspann mit. Sonst reißt dasselbe Loch an der nächsten Stelle wieder auf.

1754501300 scheitert auf einer frischen Datenbank

Bekannt und nicht behoben: Bei docker compose up --build mit leerem pb_data legt der Snapshot 1754400000 die Collection riders bereits ohne das Feld event an. 1754501300_event_participants.js sucht danach mit findRecordsByFilter('riders', 'event != "") und bricht mit unknown field "event" ab — PocketBase startet dann gar nicht.

Auf der produktiven Instanz tritt das nicht auf, dort gibt es das Feld noch. Angefasst wurde die Migration deshalb nicht; wer lokal von Null startet, braucht diese eine Zeile:

const existing = riders.fields.getByName('event')
    ? app.findRecordsByFilter('riders', 'event != ""', '', 0, 0)
    : []

Hooks (pb_hooks/)

Serverseitiges JavaScript, das PocketBase beim Start lädt (--hooksDir). Wie pb_migrations/ kommt das Verzeichnis per COPY ins Image — eine Änderung wirkt also erst nach docker compose up -d --build.

Bisher liegt dort ein Anliegen: die öffentlichen Routen der Einladungslinks.

Datei Zweck
team_invites.pb.js GET /api/team-invites/{token}, POST …/accept, POST …/join
team_invites_lib.js Gemeinsame Helfer der drei Routen

Warum überhaupt ein Hook? Wer über einen Link beitritt, ist noch nicht angemeldet und muss drei Dinge auf einmal tun, die keine Collection-Regel einzeln erlauben darf: ein Konto anlegen, sich in teams.users eintragen und einen Fahrer anlegen. Ein Hook arbeitet an den Regeln vorbei und kann genau diese Kombination erlauben — gegen einen gültigen Token und sonst nicht.

Zwei Fallstricke, an denen die erste Fassung scheiterte:

  1. Jeder Route-Handler läuft in einer eigenen JS-Laufzeit. Funktionen, die in derselben Datei neben routerAdd stehen, sind im Handler nicht sichtbar (ReferenceError). Gemeinsamer Code gehört deshalb in ein Modul, das der Handler sich per require(`${__hooks}/…`) selbst holt.
  2. Was record.get() bei einer Mehrfach-Relation liefert, ist ein Go-Slice und kein JS-Array — indexOf und Konsorten sind darauf nicht verlässlich. Anhängen geht über den Modifier record.set('users+', id), Prüfen über einen Filter.

Datenpersistenz

Sämtliche Laufzeitdaten liegen in der SQLite-DB unter /pb/pb_data: Datensätze, Uploads, die SMTP-Einstellungen und der Superuser. Ohne gemountetes Volume auf diesem Pfad sind sie nach jedem Redeploy verloren.

Das Volume-Mapping ./pb_data:/pb/pb_data steht in der docker-compose.yaml, damit die Persistenz-Konfiguration versioniert im Repo liegt.

Trügerisches Signal: Die Collections kommen aus der Migration und sind nach einem Redeploy auch dann da, wenn gar kein Volume existiert. Sie taugen nicht als Nachweis funktionierender Persistenz — das zeigt nur ein Datensatz, der in keiner Migration steht.

Mount prüfen:

docker inspect <container> --format '{{json .Mounts}}' | jq

Erscheint dort kein Eintrag mit "Destination": "/pb/pb_data", fehlt die Persistenz.

pb_data/ steht in .gitignore und gehört dort auch hin.

Superuser-Verhalten

entrypoint.sh ruft superuser create auf — bewusst nicht upsert. Existiert der Account bereits, scheitert create mit „Value must be unique" und das Passwort bleibt unangetastet. Eine im Admin-UI vorgenommene Passwortänderung überlebt damit jeden Redeploy.

Situation Verhalten
Erster Start, Variablen gesetzt Superuser wird angelegt
Neustart, Account existiert „Superuser existiert bereits — Passwort bleibt unverändert"
Passwort im UI geändert, dann Neustart UI-Passwort gilt weiter, Env wird ignoriert
Variablen nicht gesetzt Übersprungen, PocketBase gibt Installer-Link im Log aus
Passwort zu kurz (< 8 Zeichen) Container bricht mit Exitcode 1 ab

Passwort später ändern: im Admin-UI, nicht über die Env — eine Änderung der Env-Variable hat keine Wirkung mehr, sobald der Account existiert.

PocketBase aktualisieren

Die Version ist über das Build-Arg PB_VERSION gepinnt (Dockerfile und docker-compose.yaml). Wert hochsetzen, committen, neu bauen.

Ein latest gibt es bewusst nicht: Der GitHub-Asset heißt pocketbase_<version>_linux_amd64.zip, enthält die Version also im Dateinamen. Der Pin ist auch gewollt — sonst zieht jeder Rebuild unangekündigt eine neue Version, inklusive möglicher DB-Schema-Migrationen.

Vor einem Update: Changelog prüfen (https://github.com/pocketbase/pocketbase/releases) und pb_data sichern — PocketBase migriert die DB beim Start automatisch, ein Downgrade ist danach nicht mehr ohne Weiteres möglich.