Formulare, Webshop und Buchungen
Formulare pflegt das CMS selbst. Webshop und Buchungen liegen seit dem 22. September 2026 in der E-Commerce- und der Termine-App — hier steht, warum und wie die Bausteine dorthin kommen.
Was hier wo liegt
| Ressource | Wo sie lebt | CLI | Verzeichnis |
|---|---|---|---|
| Formulare | im CMS | hcms forms push |
cms/forms/<key>.json |
| Buchungen | Termine-App | — | — |
| Webshop | E-Commerce-App | — (MCP-Werkzeug ecommerce_katalog_abgleichen) |
— |
Bis zum 22. September 2026 führte das CMS beides selbst: eigene Produkte mit eigenem Lagerbestand und eigener Kasse, eigene buchbare Ressourcen mit eigenen Slots. Der Eigentümer hat beides gestrichen.
Formulare
Ein Formular definiert Felder (Text, E-Mail, Auswahl usw.) und wird auf einer Seite eingebunden. Absendungen laufen über die Delivery-API:
GET /forms/{key} # Formulardefinition abrufen
POST /forms/{key}/submit # Formular absenden
Der Submit-Endpunkt akzeptiert entweder ein Bearer-Token oder den öffentlichen ?site=<prefix>-Parameter. Validierungsfehler werden mit 422 VALIDATION_ERROR gemeldet; das öffentliche Limit liegt bei 20 Absendungen pro Minute und IP.
Buchungen — in der Termine-App
Das CMS hatte ein eigenes Buchungssystem, und das war nicht nur eine Doppelung, sondern eine gefährliche: es schrieb nicht in die gemeinsame Belegung. Ein damit reservierter Tisch war für ein Event oder eine Firmenanfrage weiterhin frei. Der Raum war zweimal vergeben, und aufgefallen wäre es beim zweiten Gast.
Die Termine-App führt eine Belegung für alle Geschäftsarten, abgesichert durch eine EXCLUDE-Bedingung in der Datenbank. Die zweite Buchung kommt dort nicht durch — kein Code, den jemand vergessen kann. Die Belegung gilt in beide Richtungen: was eine Website bucht, sperrt den Ort auch für ein Event, und umgekehrt.
Die öffentliche Schnittstelle der Termine-App, die eine Website ruft:
GET /termine/api/public/strecke # Öffentliche Sicht auf die Strecke
GET /termine/api/public/zeiten # Freie Zeiten im Zeitfenster
POST /termine/api/public/reservieren # Platz halten (befristet)
POST /termine/api/public/buchen # Reservierung zur Buchung machen
POST /termine/api/public/zahlung-pruefen # Ist das Geld angekommen?
Drei Schritte, nicht einer: Wer Reservieren und Buchen zusammenlegt, zwingt den Gast, das Formular auszufüllen, bevor klar ist, ob die Zeit überhaupt noch frei ist.
Der Zugangsschlüssel bleibt serverseitig. Die Vorlagen führen dafür eine eigene Route (/api/termine/<vorgang>), die eine Liste erlaubter Vorgänge führt und den Schlüssel dazulegt. Ein Proxy, der weitergibt, was in der Adresse steht, reicht auch das weiter, woran beim Bauen niemand gedacht hat.
Webshop — in der E-Commerce-App
Zwei Läden nebeneinander heissen zwei Wahrheiten über Preis, Steuer und Lagerbestand — und die seltener benutzte driftet. Katalog, Bestand, Rabatte, Versand, Steuer, Kasse und Bestellungen liegen deshalb in der E-Commerce-App.
Was eine Website davon nutzt:
GET /ecommerce/api/v1/laden/{kanal}/katalog # Produktliste
GET /ecommerce/api/v1/laden/{kanal}/produkt/{adresse} # Ein Produkt
GET /ecommerce/api/v1/laden/{kanal}/kasse # Übergabe eröffnen
GET /ecommerce/api/v1/laden/{kanal}/uebergabe?marke=… # Der Gast kommt an
Die Kasse wird übergeben, nicht nachgebaut: Die Website reicht die Korbzeilen an ihre eigene Route (/api/laden/kasse), diese legt den Schlüssel dazu und bekommt eine signierte Übergabe-Adresse. Der Browser folgt ihr; ab dort rechnet die E-Commerce-App Adresse, Versand, Nachlass und Steuer und wickelt die Zahlung ab. Die Marke selbst ist der Ausweis: HMAC-signiert, mit Organisation und Kanal, befristet — und sie öffnet nichts ausser einem Korb.
Ein Adressformular auf der Website wäre eine zweite Kasse. Eine Kartenzahlung endet ohnehin beim Anbieter; Rechtstexte gehören auf die eigene Seite.
Die Bausteine dafür
Die Vorlagen (component-library, shop-storefront, frontend-starter, restaurant-alpenblick) bringen fertige Bausteine mit — product-grid, product-detail, cart, mini-cart, checkout, booking —, und die sind ausdrücklich zum Umgestalten gedacht. Sie lesen ausschliesslich über die Adapter cms/laden.ts und cms/termine.ts; solange das so bleibt, kann die Oberfläche aussehen, wie sie will.
Die nötigen Umgebungsvariablen:
EC_URL=https://<org>.scorebase.ch # ohne /ecommerce
EC_TOKEN=<ksk_… mit „ecommerce.laden.read", für die Kasse zusätzlich „ecommerce.laden.kaufen">
NEXT_PUBLIC_EC_KANAL=web
TERMINE_URL=https://<org>.scorebase.ch # ohne /termine
TERMINE_TOKEN=<tmk_… mit „lesen und buchen">
NEXT_PUBLIC_TERMINE_STRECKE=<Kennung>Kanal und Strecke stehen absichtlich unter NEXT_PUBLIC_: Der Warenkorb im Browser legt sich unter dem Kanal ab, und die Serverseite liest denselben Wert. Zwei Variablen für dieselbe Sache wären eine Falle — eine gesetzt, die andere vergessen, und der Warenkorb gehörte zu einem anderen Kanal als der Katalog. Geheim sind die Tokens, nicht die Kennungen.
Hinweis: Für den Verkauf von Event-Tickets mit Sitzplan gibt es ein eigenes Produkt – siehe die Kategorie Ticketshop. Dessen öffentliche Endpunkte (
/headless/api/v1/ticketing/…) sind von dieser Umstellung nicht betroffen.