Zum Hauptinhalt springen
lanforge.eu

Meine Termindaten gehören mir: Wie ich mein Buchungssystem selbst gebaut habe, in einer Woche, mit KI

Selbst gebaut statt eingekauft: warum „hugo-booker" meine Termindaten in eigener Hand behält #

Für die Terminbuchung auf meiner Website habe ich es zuerst so gemacht, wie es fast alle machen: einen der bekannten Cloud-Dienste eingebunden. Ein Klick, fertig. Das war im Frühjahr, und es war damals die richtige Entscheidung, denn ich musste erst einmal online gehen und hatte für mehr schlicht keine Zeit. Eine Seite ohne Terminbuchung nützt niemandem, und die perfekte Lösung, die man nie baut, auch nicht.

Wohl war mir dabei trotzdem nie. Denn bei jeder Buchung liefen die Daten meiner Besucher über die Server eines fremden Anbieters. Wer bei mir einen Termin bucht, vertraut mir seine Daten an, nicht irgendeinem SaaS-Konzern. Also habe ich die Übergangslösung jetzt ersetzt und das Buchungssystem selbst gebaut: hugo-booker, ein kleiner Go-Dienst, der sich in meine Hugo-Seite einbettet, die Anfragedaten auf meiner eigenen Infrastruktur verarbeitet und den bestätigten Termin per CalDAV in meinen Kalender schreibt.

Worum es mir wirklich geht: Datenhoheit und Unabhängigkeit #

Das ist für mich kein Nebenaspekt, sondern der eigentliche Grund für das ganze Projekt.

Volle Kontrolle über die Daten. Name, E-Mail, Telefonnummer, Anliegen: diese Anfragedaten liegen auf meiner eigenen Infrastruktur. Ich entscheide, was gespeichert wird, wie lange, und was wieder verschwindet. Nichts wird im Hintergrund getrackt, ausgewertet oder weiterverkauft. Bei einem fremden Dienst weiß man das nie so genau; bei meinem eigenen Code weiß ich es ganz genau, denn ich habe ihn geschrieben.

Datensparsamkeit, die im Code steht. Die Anfragedaten werden sieben Tage nach dem Termin automatisch gelöscht, und zwar unabhängig davon, ob die Anfrage bestätigt, abgelehnt oder nie bearbeitet wurde. Das ist keine Absichtserklärung in einer Datenschutzerklärung, sondern ein Hintergrundprozess, der beim Start und danach alle sechs Stunden aufräumt; die Frist lässt sich über eine einzige Umgebungsvariable ändern. Was ein bestätigter Termin an Information braucht, steht danach im Kalender, nicht in einer wachsenden Datei mit Namen und Telefonnummern. Was gar nicht mehr da ist, kann auch nicht abfließen, gehackt oder weiterverwendet werden.

Keine Besucherdaten im Schaufenster. Der Kalender auf dieser Seite ist öffentlich, die Termine darin sind es nicht. Wer eine Anfrage stellt, taucht dort ausschließlich als „Angefragt" beziehungsweise „Gebucht" auf, nie mit Namen. In meinem eigenen Kalender steht der Name sehr wohl, sonst wüsste ich ja nicht, wer kommt. Die Trennung läuft technisch über die Kennung des Termins und nicht über seinen Titel: Selbst wenn ich den Eintrag in der Kalender-App umbenenne, bleibt die Maskierung nach außen bestehen. Ein automatischer Test wacht darüber, damit das auch nach der nächsten Änderung noch gilt.

Unabhängig vom Buchungsdienst. Kein Preismodell, das sich über Nacht ändert. Keine API, die plötzlich abgekündigt wird. Keine AGB-Updates, kein Feature, das hinter eine teurere Stufe wandert, kein Dienst, der einfach eingestellt wird. Was ich baue, läuft, solange ich es laufen lassen will, auf meinem eigenen Server, zu meinen Bedingungen. Diese Unabhängigkeit ist mir mehr wert als jede Bequemlichkeit, die ein fertiger Dienst bietet.

Kurz: Die Kontrolle über diese Daten soll bei mir und meinen Besuchern liegen, bei niemandem sonst. Genau diesen letzten Schritt habe ich inzwischen abgeschlossen, und darum geht es im nächsten Abschnitt.

Der letzte Schritt: der Kalender läuft jetzt auf meiner eigenen Nextcloud #

Zum Start hatte dieses Projekt noch eine Einschränkung, die ich offen genannt habe, weil sie zum Anspruch dazugehört: Der Kalender, in den die Termine geschrieben werden, lag zunächst bei iCloud, also bei Apple. Damit wurden Kalenderdaten in die USA übermittelt. Sobald jemand eine Anfrage stellte, entstand ein vorläufiger Eintrag mit Namen, der zu Apple wanderte. Die übrigen Anfragedaten (E-Mail, Telefonnummer, Anliegen) waren davon nie betroffen, die lagen von Anfang an als Flat Files auf meinem eigenen Server und verschwinden dort nach sieben Tagen automatisch.

Diese Zwischenlösung ist jetzt Geschichte. Der Grund, warum ich sie überhaupt brauchte, war banal: Meine eigene Nextcloud war bisher nur im Homelab erreichbar, der produktive Server konnte sie schlicht nicht auflösen. Diesen Zugang von außen habe ich inzwischen sauber eingerichtet, und damit läuft der Kalender jetzt vollständig über meine eigene Nextcloud. Weil hugo-booker von Anfang an auf CalDAV setzt und nicht auf eine proprietäre Schnittstelle, war der Wechsel tatsächlich reine Konfiguration in der Admin-Oberfläche, kein Umbau am Code: Anbieter-URL und Zugangsdaten ändern, fertig. iCloud brauche ich für die Terminbuchung damit nicht mehr, und auch die Kalendereinträge verlassen meine Infrastruktur nicht mehr. Anfragedaten und Kalender liegen jetzt beide bei mir, ohne Umweg über einen fremden Anbieter.

Zwei Stolpersteine beim Umzug #

Ganz ohne Reibung ging der Wechsel nicht, und da beide Probleme typisch für einen Umzug wie diesen sind, halte ich sie hier fest.

Darstellungsfehler auf Website und Handy. Nextcloud liefert Termindaten in Details (z. B. Statuswerte, Zeitzonen, Freitextfelder) an einigen Stellen anders aus als iCloud es tat. Das äußerte sich zunächst in CSS-Fehlern im Buchungs-Widget: Slots wurden auf der Website falsch gerastert, und auf dem Handy verschob sich die Ansicht zusätzlich durch die kleinere Bildschirmbreite. Ursache war, dass mein Rendering an einigen Stellen stillschweigend das iCloud-spezifische Format der CalDAV-Antworten vorausgesetzt hatte. Die Fixes lagen im Parsing, nicht im CSS selbst: Sobald die Daten wieder in der erwarteten Form vorlagen, stimmte auch die Darstellung.

Verbindungsabbrüche durchs NAT. Die zweite Baustelle war unangenehmer, weil sie sich zeitweise gezeigt hat statt zuverlässig: CalDAV-Anfragen an die eigene Nextcloud im Homelab brachen gelegentlich mit Timeouts ab. Der Grund war das NAT zwischen dem produktiven Server und dem Homelab: Verbindungen, die etwas länger offenblieben, wurden vom NAT-Gateway nach einer gewissen Zeit einfach verworfen, ohne dass eine Seite das sauber mitbekam. Behoben habe ich das über angepasste Keepalive- und Timeout-Werte, sodass Verbindungen regelmäßig genug am Leben gehalten werden, um vom NAT nicht vorzeitig aussortiert zu werden. Seitdem laufen die CalDAV-Anfragen stabil durch.

Was es macht #

Auf der Website erscheint ein Buchungs-Widget: Wochenansicht, freie Slots, Formular. Eine Anfrage landet im Go-Backend, ich bekomme eine Mail, bestätige oder lehne im Admin-Bereich ab, und bei Bestätigung wird der Termin per CalDAV in meinen Kalender geschrieben und liegt automatisch auf dem Handy. Wer gar keinen Termin will, kann über „Nur eine Nachricht schreiben" einfach eine Mail hinterlassen. Das Ganze zweisprachig (DE/EN) und so gebaut, dass es sich als <iframe> oder <div> in jede Seite einbetten lässt.

Ein angefragter Termin ist sofort blockiert, damit derselbe Slot nicht zweimal vergeben werden kann. Er landet außerdem umgehend als vorläufiger Eintrag in meinem Kalender, also mit dem iCal-Status „tentative", den die meisten Kalender-Apps abgesetzt darstellen. Ich sehe eine Anfrage damit auf dem Handy, bevor ich überhaupt am Rechner war. Bestätige ich sie, wird genau dieser Eintrag auf „gebucht" aktualisiert, es entsteht kein zweiter. Lehne ich ab, verschwindet er wieder und die Zeit ist im selben Moment frei. Urlaub und Abwesenheiten pflege ich als Zeiträume im Admin-Bereich, gesperrte Tage sind im Kalender klar als solche zu erkennen.

hugo-booker – Buchungsfluss auf eigener Infrastruktur
Anfrage, Verwaltung, Kalender und Löschung laufen vollständig auf meiner eigenen, selbst betriebenen Infrastruktur, ohne dauerhafte Speicherung der Anfragedaten und ohne externen Cloud-Dienst.

Ein paar bewusste Entscheidungen #

HTMX + html/template statt JS-Framework. Das Frontend rendert serverseitig, HTMX tauscht nur die nötigen Fragmente. Kein Build-Schritt, kein npm, keine 200 KB JavaScript für einen Kalender. Heraus kommt ein einzelnes statisches Go-Binary, Deployment heißt: Container starten.

CalDAV statt proprietärer API. Das passt direkt zum Unabhängigkeits-Gedanken: CalDAV funktioniert mit iCloud, Nextcloud und fast allem anderen. Kein Vendor-Lock-in, und ich kann den Anbieter in der Admin-Oberfläche umstellen, ohne eine Zeile Code zu ändern. Genau das war der Hebel für den Wechsel von iCloud auf meine eigene Nextcloud, der inzwischen abgeschlossen ist. Weil dabei alle Kalender eines Accounts eingelesen werden, lässt sich in den Einstellungen festlegen, welcher davon öffentlich sichtbar ist: Der Geschäftskalender erscheint, private Termine bleiben außen vor und blockieren trotzdem keine Slots, die sie nichts angehen.

Flat Files statt Datenbank. Für die Datenmenge eines persönlichen Buchungstools ist eine Datenbank-Instanz Overkill. JSON-Dateien mit 0600-Rechten reichen: transparent, leicht zu sichern, und alles bleibt unter meiner Kontrolle. Die Kalender-Zugangsdaten liegen dabei AES-256-GCM-verschlüsselt auf Platte. Die Laufzeitdaten liegen bewusst außerhalb des Repositorys, auf einem Volume beziehungsweise einem PVC.

Läuft, wo es soll: als Container #

Weil am Ende ein einziges statisches Binary herauskommt, lässt sich hugo-booker überall dort mitnehmen, wo schon eine Hugo-Umgebung läuft. Als Docker-Image kann es einfach in einer docker-compose.yml neben der Hugo-Seite mitgestartet werden, ein zusätzlicher Service-Eintrag, fertig. Genau so läuft es bei mir produktiv.

Denselben Container fahre ich zusätzlich als Kubernetes-Deployment in meinem Homelab, per Flux im GitOps-Stack. Das ist aber ausdrücklich nicht der Produktivbetrieb, sondern meine Testumgebung: eine vollständige Kopie der Website samt Buchungssystem, auf der ich Änderungen ausprobiere, bevor sie live gehen. Neue Funktionen, ein Theme-Update, eine geänderte Konfiguration, all das läuft dort erst einmal gegen eine eigene Domain, und erst wenn es sich bewährt hat, wandert es auf den produktiven Server.

Praktisch daran ist, dass beide Umgebungen dasselbe Image verwenden. Vom kleinen Single-Host-Setup bis zum Cluster also kein Umbau, man wählt einfach die Betriebsart, die zur jeweiligen Umgebung passt.

In einer Woche fertig, mit einem KI-Partner an der Seite #

Das Programm war in einer Woche fertig. Dass es so schnell ging, lag zu einem großen Teil daran, dass ich nicht allein gebaut habe, sondern mit einer KI (Claude) als Entwicklungspartner. Und ehrlich gesagt war genau das der eigentliche Gamechanger.

Was die Zusammenarbeit konkret gebracht hat:

  • Architektur in einem Gespräch statt stundenlanger Recherche: Aus meiner ASCII-Skizze auf dem Notizzettel wurde direkt ein sauberer Plan mit klaren Modulgrenzen.
  • Boilerplate, das sonst Tage frisst, in Minuten: CalDAV-Parsing inklusive wiederkehrender Termine, JWT-Handling, die AES-GCM-Verschlüsselung, das i18n-Gerüst, Dinge, bei denen ich sonst lange in Doku und Beispielen gegraben hätte.
  • Ein echter Sparringspartner für Entscheidungen: „HTMX oder SPA?", „Datenbank oder Flat Files?", jeweils die Optionen mit Vor- und Nachteilen, sofort, statt mich durch zehn Blogposts zu wühlen.
  • Ein Security-Review, das reale Schwachstellen gefunden und behoben hat: eine E-Mail-Header-Injection über das Nachrichtenformular, ein versehentlich in einer Beispieldatei gelandeter Zugangs-Token und Laufzeitdaten, die im Repository nichts zu suchen hatten. Dazu Rate-Limits gegen Brute-Force und Formular-Spam sowie eine Begrenzung der Request-Größe.

Ein Punkt ist mir dabei besonders im Gedächtnis geblieben, weil er zeigt, wo der eigentliche Nutzen liegt. Beim Review kam die Frage auf, ob eine gefundene Lücke auch den zweiten Weg ins System betrifft. Statt zu spekulieren, wurde die Bibliotheksfunktion mit einem kurzen Testprogramm gegen echte Angriffsstrings geprüft. Ergebnis: Der zweite Weg war sauber, der erste nicht. Genau diese Art von Nachprüfen statt Vermuten hätte ich abends nach Feierabend selten gemacht.

Wichtig ist mir dabei: Die KI hat nicht mein Urteil ersetzt. Die Entscheidungen habe ich getroffen, den Code geprüft, und mein Homelab kenne nur ich. Sie hat auch nicht immer recht gehabt, und dann half nur, gemeinsam in den tatsächlichen Zustand zu schauen statt auf plausible Vermutungen zu bauen. Aber sie hat die Reibung weggenommen, das ständige Nachschlagen, das Tippen von Standardcode, das Abwägen im luftleeren Raum. Was früher gut und gerne ein Monatsprojekt gewesen wäre, war eine Woche. Für jemanden, der so etwas abends nebenbei macht, ist das der Unterschied zwischen „bleibt eine nette Idee" und „läuft in Produktion". Diesen Hebel finde ich bemerkenswert, und deshalb schreibe ich ihn hier auch ganz bewusst dazu.

Fazit #

Ein fokussierter kleiner Go-Dienst, in einer Woche gebaut, der die Übergangslösung aus der Cloud ablöst und meine Termindaten genau dorthin gebracht hat, wo sie hingehören: zu mir. Die Anfragedaten liegen komplett auf meiner Infrastruktur und löschen sich selbst, der Kalender läuft inzwischen ebenfalls über meine eigene Nextcloud, iCloud brauche ich dafür nicht mehr. Ohne laufende Kosten für einen Buchungsdienst, ohne externen Cloud-Dienst, mit voller Kontrolle über jedes Detail. Und mit einer KI als Partner ging es schneller und deutlich leichter, als ich es allein je hinbekommen hätte.


Produktiv läuft hugo-booker per docker-compose auf einem Server, den ich bei einem Hoster gemietet habe, aber selbst installiere, administriere und betreibe; die Konfiguration ist komplett meine. Der Kalender läuft inzwischen ebenfalls über meine eigene Nextcloud, iCloud wird nicht mehr benötigt. Entwickelt und getestet wird auf meiner Testinstanz im Homelab-Cluster (Kubernetes, Flux, GitLab CI), einer Kopie der Website, auf der Änderungen ihre Runde drehen, bevor sie produktiv gehen. Wer einen ähnlichen Weg geht, self-hosted und datensouverän, oder Fragen zu CalDAV-Sync in Go hat: schreibt mir gern.


Dieser Artikel wurde zuerst auf LinkedIn veröffentlicht. Hat Ihnen der Beitrag gefallen? Ich freue mich auf Ihre Meinung und eine spannende Diskussion direkt unter dem Post auf LinkedIn:

Diskussion auf LinkedIn öffnen