Vorname und Nachname getrennt zu fuehren versprach eine Ordnung, die es hier nie gab: sortiert wird ueber den ganzen Namen, angezeigt wird der ganze Name, gesucht wird ueber beides zugleich. Dafuer musste jede Maske zwei Felder anbieten und jede Anzeige sie wieder zusammensetzen — und wer schlicht "Schorsch" heisst, stand vor der Frage, welches der beiden Felder das ist. Das Feld name gibt es an riders laengst; es lag nur brach, weil die Anwendung firstname und lastname benutzte. Es uebernimmt deren Inhalt — kein neues Feld, keine zweite Spalte, nichts, was auf bestehenden Instanzen erst entstehen muesste. Zurueck geht es nur ungenau: Die Umkehrung trennt am ersten Leerzeichen und raet damit bei jedem Doppelvornamen falsch. Das ist der Preis dafuer, dass die Trennung ueberhaupt verschwindet. fullName heisst jetzt riderName und ist nur noch ein Feldzugriff — die Funktion bleibt als die eine Stelle, an der ein namenloser Fahrer zu einem leeren String wird statt als undefined durch die Oberflaeche zu geistern. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr |
||
|---|---|---|
| .. | ||
| pb_hooks | ||
| pb_migrations | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yaml | ||
| Dockerfile | ||
| entrypoint.sh | ||
| README.md | ||
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, runs, 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:
- Jeder Route-Handler läuft in einer eigenen JS-Laufzeit. Funktionen, die
in derselben Datei neben
routerAddstehen, sind im Handler nicht sichtbar (ReferenceError). Gemeinsamer Code gehört deshalb in ein Modul, das der Handler sich perrequire(`${__hooks}/…`)selbst holt. - Was
record.get()bei einer Mehrfach-Relation liefert, ist ein Go-Slice und kein JS-Array —indexOfund Konsorten sind darauf nicht verlässlich. Anhängen geht über den Modifierrecord.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.