Architektur
Ein Worker, eine D1-Datenbank, ein R2-Bucket. Keine Server, keine Warteschlangen, kein Admin-Login.
Überblick
Projektseite ──GET/POST /buy/:sku──▶ Worker ──Checkout-API──▶ Stripe | Polar
▲ │ Webhook (signiert)
Bestellseite ◀──── 303 ─────────────┘◀── POST /webhook/:provider┘
/order/:provider?checkout_id=… │ idempotent (Event-ID)
├─ /download/:token ─▶ R2 ▼
│ HMAC, 7 Tage, max. 5× D1: orders, licenses,
└─ Lizenzschlüssel CLK1.… download_grants,
(Ed25519, offline prüfbar) webhook_events
Erstattung / Dispute ─▶ Lizenz und Downloads gesperrt
Endpunkte
| Route | Aufgabe |
|---|---|
GET|POST /buy/:sku | Checkout anlegen und weiterleiten; nur Katalog-SKUs, Ratenbegrenzung |
POST /webhook/:provider | Signatur prüfen, Bestellung speichern, ausliefern, nach Erstattung sperren |
GET /order/:provider | Bestellseite mit Download, Lizenz und Rechtshinweisen |
GET /download/:token | Datei aus R2, nur mit gültigem, unverändertem Token |
GET /license/public-key | öffentlicher Schlüssel für die Offline-Prüfung |
GET|POST /license/verify | Online-Prüfung inklusive Sperre und Ablauf |
GET /products.json, /price/:sku | Preise mit Pflichthinweis für deine Seiten |
GET /stats, /healthz | Kennzahlen (mit Token) und Zustand |
Daten in D1
orders: Anbieter, Checkout- und Zahlungsreferenz, SKU, Betrag, Status (bezahlt, ausstehend, erstattet, angefochten), Zeitpunkt der Zustimmung im Direktverkauf.licensesunddownload_grants: je Bestellung höchstens einmal angelegt, mit Sperrvermerk nach Erstattung.webhook_events: bereits verarbeitete Ereignis-IDs, damit Wiederholungen nichts doppelt ausliefern.rate_limits: Zähler unter einem täglich gesalzenen Hash der IP-Adresse; die Adresse selbst wird nie gespeichert.
Schlüssel
Alle Schlüssel stammen aus einem einzigen Secret PAY_SECRET und werden per HKDF getrennt abgeleitet: HMAC-Schlüssel für Download-Links, Salz für IP-Hashes und der Ed25519-Schlüssel für Lizenzen. So genügt ein Secret, und keiner der Zwecke kann einen anderen ersetzen.
Sicherheit
- Webhook-Signaturen und Bearer-Tokens werden in konstanter Zeit verglichen, Zeitstempel dürfen höchstens fünf Minuten abweichen.
- Strikte Prüfung aller Eingaben: SKU-Format, Checkout-IDs, Token-Aufbau, Größenlimits für Anfragen.
- Eine erstattete Bestellung wird auch durch verspätete Ereignisse nie wieder freigeschaltet.
- Alle Antworten mit strenger Content-Security-Policy und
noindex, Seiten ohne Zwischenspeicherung; keine Cookies. Jede Seite verlinkt Impressum und Datenschutz. - Keine Secrets im Code; das Deploy-Skript übergibt sie nur über eine temporäre Datei und erzeugt
PAY_SECRETnur, wenn sicher keins existiert.
Tests
Über 100 automatische Tests: Vitest läuft direkt in der Workers-Laufzeit (Miniflare) mit nachgebildeten Antworten von Stripe, Polar und dem E-Mail-Dienst; node:test deckt Katalog-Generator, Deploy-Skript und die Prüfbibliotheken ab.