Einladung & RSVP
Eine responsive Einladung für Vicky und Daniels 30. Geburtstag. Jeder Gast erhält einen persönlichen Link, kann die Originaleinladung als PDF öffnen und eine von drei Antworten abgeben:
- Ich komme nicht
- Ich komme alleine
- Ich komme mit jemandem (inklusive Name der Begleitung)
Der Server benötigt nur Node.js 22.5 oder neuer und keine zusätzlichen Pakete. Als Datenbank dient SQLite über Nodes eingebautes node:sqlite-Modul.
Lokal starten
ADMIN_KEY='ein-langes-geheimes-passwort' \
ORIGIN='http://localhost:3000' \
npm start
Danach läuft die Seite unter http://localhost:3000. Die Verwaltung ist unter http://localhost:3000/admin erreichbar. Ohne ADMIN_KEY oder vorgeschaltetes Authentik bleiben die Admin-Endpunkte aus Sicherheitsgründen deaktiviert.
Viele Links erstellen und Antworten ansehen
Öffne /admin, melde dich mit dem ADMIN_KEY an und füge bis zu 200 Namen auf einmal ein – einen Namen pro Zeile. Die erzeugten Links können einzeln oder gesammelt kopiert werden.
Die Übersicht zeigt:
- ob und wann ein Link erstmals geöffnet wurde
- wann er zuletzt geöffnet wurde und wie oft
- die aktuelle Zu- oder Absage
- den Namen einer Begleitperson
- wann die Antwort zuletzt gespeichert beziehungsweise geändert wurde
- die erwartete Gesamtzahl der kommenden Gäste
Über „CSV herunterladen“ lässt sich die vollständige Liste für Excel, Numbers oder LibreOffice exportieren. Eine Öffnung wird gezählt, sobald das JavaScript der RSVP-Seite die Einladung abruft. Manche besonders aktive Link-Vorschau-Bots können theoretisch ebenfalls eine Öffnung erzeugen.
Persönlichen Einladungslink anlegen
Am einfachsten direkt auf dem Server:
ORIGIN='https://30.brunner.ninja' npm run invite -- "Max Mustermann"
Die Ausgabe enthält den fertigen Link, zum Beispiel:
Max Mustermann: https://30.brunner.ninja/i/zufaelliger-sicherer-token
Alternativ können mehrere Links über die Admin-API erzeugt werden:
curl -X POST 'https://30.brunner.ninja/api/admin/invites' \
-H 'Authorization: Bearer DEIN_ADMIN_KEY' \
-H 'Content-Type: application/json' \
--data '{"names":["Max Mustermann","Erika Musterfrau"]}'
Antworten exportieren
curl 'https://30.brunner.ninja/api/admin/responses.csv' \
-H 'Authorization: Bearer DEIN_ADMIN_KEY' \
-o zusagen.csv
Die CSV-Datei lässt sich direkt mit Excel, Numbers oder LibreOffice öffnen.
Daten und Betrieb
Antworten werden in data/invites.db gespeichert. SQLite läuft im WAL-Modus; das Verzeichnis muss im Produktionsbetrieb beschreibbar und persistent sein. Regelmäßige Backups der Datenbank werden empfohlen.
Für die öffentliche Domain sollte die App hinter einem HTTPS-Reverse-Proxy (beispielsweise Caddy oder nginx) laufen. Benötigte Umgebungsvariablen:
| Variable | Zweck | Standard |
|---|---|---|
PORT |
Interner HTTP-Port | 3000 |
HOST |
Listen-Adresse | 0.0.0.0 |
ORIGIN |
Öffentliche Basis-URL für erzeugte Links | http://localhost:3000 |
DATABASE_FILE |
Pfad zur SQLite-Datenbank | data/invites.db |
ADMIN_KEY |
Geheimer Schlüssel für lokalen Admin-Zugriff | keiner |
TRUST_AUTHENTIK |
Akzeptiert Authentiks verifiziertes Benutzer-Header (true) |
false |
Die RSVP-Seiten liegen unter /i/<token>; von dort kann die persönliche PDF-Einladung geöffnet werden.
Die Einladungsgrafik und PDF sind bewusst nicht im Git-Repository oder Container-Image enthalten. In Kubernetes werden sie aus dem Secret 30-birthday-invitation schreibgeschützt eingebunden und ausschließlich über einen gültigen Gast-Token ausgeliefert. Direkte öffentliche Asset-URLs existieren nicht.
Kubernetes, Traefik und Authentik
Ein vollständiges Beispiel liegt in k8s/invitation.yaml. Es verwendet:
- genau einen Pod (SQLite ist hier bewusst nicht horizontal skaliert)
- einen
128Mi-rook-ceph-block-PVC für/data/invites.db - die Domain
30.brunner.ninja - ausschließlich Traefiks EntryPoint
websecure - das bestehende
default-authentik@kubernetescrd-Middleware nur für/admin*und/api/admin/* - öffentliche Gast- und Asset-Routen ohne Authentik
Recreate-Updates mit einer kurzen Unterbrechung, damit nur ein SQLite-Prozess den Block-PVC verwendet
Zuerst das Image bauen und in die eigene Registry pushen:
docker build -t registry.brunner.ninja/feedc0de/30-birthday:latest .
docker push registry.brunner.ninja/feedc0de/30-birthday:latest
Die Gitea Action in .gitea/workflows/container.yml übernimmt Build, Push und Deployment automatisch. Sie benötigt diese Repository-Secrets:
QUAY_USERNAME:feedc0de+birthday30QUAY_TOKEN: Token dieses Robot-AccountsKUBE_CONFIG_BASE64: Kubeconfig des eingeschränkten30-birthday-deployer-ServiceAccounts
Workload und CI-Berechtigungen werden einmalig mit einem Cluster-Administrator angelegt:
./install.sh
./k8s/create-ci-kubeconfig.sh
Die Action baut registry.brunner.ninja/feedc0de/30-birthday, versieht jedes Image mit dem unveränderlichen Commit-SHA und aktualisiert auf main ausschließlich das bestehende default/30-birthday-Deployment. Ihre Kubernetes-Rolle kann keine Secrets oder anderen Deployments lesen oder verändern.
Der Cluster stellt Authentiks /outpost.goauthentik.io/* bereits zentral bereit. Der App-Service ist nur als ClusterIP erreichbar; daher kann ein externer Client Authentiks Identitätsheader nicht direkt am App-Port einschleusen.