Einladung & RSVP
Eine responsive Einladung von Vicky und Daniel zu 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 „Antwort setzen“ beziehungsweise „Antwort ändern“ kann die Zu- oder Absage im Adminbereich stellvertretend für einen Gast gespeichert werden. Bei einer Zusage mit Begleitung wird auch deren Name erfasst.
Ü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
Das veröffentlichte Helm Chart befindet sich in chart/. Es owns the
Deployment, Service, probes, SQLite persistence contract, and the public/admin
Ingresses. SQLite must stay at one replica and uses a Recreate strategy.
For this homelab, Argo CD is the sole workload writer. Its cluster values and
existing-claim reference are in the separate
homelab-gitops application.
The live 30-birthday-data PVC is referenced as existingClaim; the chart
does not render or replace it. Invitation assets stay in the existing
30-birthday-invitation Secret, outside public Git. Authentik protects only
admin paths; guest and invitation asset routes remain public.
For another cluster, create the invitation Secret first (the local helper expects private assets excluded from Git), then install the chart:
./k8s/create-invitation-secret.sh
helm repo add brunner.ninja https://brunner.ninja/charts
helm repo update brunner.ninja
helm upgrade --install 30-birthday brunner.ninja/30-birthday \
--namespace default --wait --timeout 5m
Main-branch CI tests the app, publishes an immutable image, packages a chart
with that exact image as appVersion, and promotes only the chart version in
homelab-gitops. CI uses QUAY_USERNAME, QUAY_TOKEN, PACKAGE_USERNAME,
PACKAGE_TOKEN, and GITOPS_TOKEN; it has no Kubernetes credentials.
The cluster exposes Authentik's /outpost.goauthentik.io/* centrally. The app
Service remains ClusterIP, so external clients cannot inject Authentik
identity headers through the app port.