Cloudflare-Integration — Design Spec
Datum: 2026-04-14 Status: Entwurf Scope: Stufe 1 — Cloudflare-Anbindung fuer Sites
Ueberblick
Abschnitt betitelt „Ueberblick“Automatisierte Cloudflare-Integration fuer das Onboarding und die Verwaltung von Kunden-Websites. Jede Site bekommt eine Cloudflare-Zone (DNS), ein Pages-Projekt (Hosting) und einen R2-Bucket (Medien). Ziel: Maximale Automatisierung, minimaler manueller Aufwand.
Manueller Schritt pro Site: Git-Repository im Cloudflare Dashboard mit dem Pages-Projekt verbinden und den Deploy Hook kopieren. Alles andere laeuft per API.
Automatisierungsgrad
Abschnitt betitelt „Automatisierungsgrad“| Aufgabe | Methode | Wann |
|---|---|---|
| Zone (Domain) anlegen | Cloudflare API | Bei Site-Erstellung |
| DNS Records seeden | Cloudflare API (Batch) | Bei Site-Erstellung |
| Pages-Projekt erstellen | Cloudflare API | Bei Site-Erstellung |
| Pages Custom Domain setzen | Cloudflare API | Nach NS-Verifikation |
| R2 Bucket erstellen | Cloudflare API | Bei Site-Erstellung |
| R2 CORS konfigurieren | Cloudflare API | Bei Site-Erstellung |
| Tunnel Ingress Rule hinzufuegen | Cloudflare API | Bei Site-Erstellung (fuer Service-API) |
| Git-Repo verbinden | Manuell (Dashboard) | Einmalig nach Erstellung |
| Deploy Hook eintragen | Manuell (in CMS) | Einmalig nach Git-Verbindung |
| NS-Verifikation pruefen | Cloudflare API (Poll) | Nach Domain-Registrar-Aenderung |
| Build ausloesen | CMS → Deploy Hook POST | Bei Content-Aenderung |
| Build-Status pruefen | Cloudflare Pages API (Poll) | Nach Build-Trigger |
Tenant-Key
Abschnitt betitelt „Tenant-Key“Bei Tenant-Erstellung wird ein eindeutiger, sprechender Key generiert:
{firmenname-slug}-{nanoid5}Beispiele: mueller-dach-x7k9p, pizzeria-roma-ab3mf, foto-schmidt-qw2nj
Generierung
Abschnitt betitelt „Generierung“- Firmenname → Slugify (Lowercase, Umlaute aufloesen, Sonderzeichen entfernen, Leerzeichen/Bindestriche normalisieren)
- Suffix: 5-stellige NanoID (Alphabet:
abcdefghijklmnopqrstuvwxyz0123456789, keine Verwechslungsgefahr) - Maximale Gesamtlaenge: 50 Zeichen (Slug wird bei Bedarf gekuerzt)
Verwendung
Abschnitt betitelt „Verwendung“Der Tenant-Key wird als Praefix fuer alle Cloudflare-Ressourcen verwendet:
| Ressource | Namensschema | Beispiel |
|---|---|---|
| R2 Bucket | {tenant-key} | mueller-dach-x7k9p |
| Pages-Projekt | {tenant-key} | mueller-dach-x7k9p (→ mueller-dach-x7k9p.pages.dev) |
| Access Application | {tenant-key} | mueller-dach-x7k9p |
| Access Group | {tenant-key}-access | mueller-dach-x7k9p-access |
| Tunnel Ingress | {tenant-key}.internal | mueller-dach-x7k9p.internal |
Bei Tenants mit mehreren Sites (spaeter): {tenant-key}-{site-slug} (z.B. mueller-dach-x7k9p-blog).
Datenmodell
Abschnitt betitelt „Datenmodell“Neue Spalte auf traffino.tenant (Migration 000006):
| Spalte | Typ | Beschreibung |
|---|---|---|
| resource_key | VARCHAR(50) NOT NULL UNIQUE | Eindeutiger Key fuer externe Ressourcen |
Der resource_key wird bei Tenant-Erstellung generiert und aendert sich nie. Er ist unabhaengig vom slug (der sich theoretisch aendern koennte).
Datenmodell
Abschnitt betitelt „Datenmodell“Neue Spalten auf traffino.site (Migration 000006)
Abschnitt betitelt „Neue Spalten auf traffino.site (Migration 000006)“| Spalte | Typ | Beschreibung |
|---|---|---|
| cf_zone_id | VARCHAR(50) | Cloudflare Zone ID |
| cf_pages_project | VARCHAR(100) | Pages-Projektname (= {resource_key}.pages.dev) |
| cf_deploy_hook_url | TEXT | Deploy Hook URL (manuell eingetragen) |
| cf_nameservers | TEXT[] | Zugewiesene Cloudflare-Nameserver |
| cf_ns_verified | BOOLEAN DEFAULT FALSE | NS-Delegation verifiziert |
| cf_ns_verified_at | TIMESTAMPTZ | Zeitpunkt der Verifikation |
| last_build_triggered_at | TIMESTAMPTZ | Letzter Build-Trigger |
| last_build_status | VARCHAR(20) | idle, building, success, failure |
| last_build_finished_at | TIMESTAMPTZ | Letzter Build abgeschlossen |
bucket_public existiert bereits auf site und wird fuer den R2-Bucket-Namen verwendet.
Neue Tabelle traffino.dns_record (Migration 000006)
Abschnitt betitelt „Neue Tabelle traffino.dns_record (Migration 000006)“| Spalte | Typ | Beschreibung |
|---|---|---|
| id | UUID PK | UUIDv7 |
| site_id | UUID FK → site | Zugehoerige Site |
| cf_record_id | VARCHAR(50) | Cloudflare Record ID |
| type | VARCHAR(10) NOT NULL | A, AAAA, CNAME, MX, TXT, etc. |
| name | VARCHAR(255) NOT NULL | Record-Name (z.B. @, www, mail) |
| content | TEXT NOT NULL | Record-Wert |
| ttl | INT DEFAULT 1 | TTL (1 = automatic) |
| proxied | BOOLEAN DEFAULT FALSE | Cloudflare Proxy aktiv |
| priority | INT | Nur fuer MX |
| managed | BOOLEAN NOT NULL DEFAULT FALSE | System-Record (nicht vom Kunden aenderbar) |
| Audit-Felder | created_at, created_by, row_version, row_period |
DNS Records werden sowohl lokal in der DB als auch bei Cloudflare gefuehrt. Die DB ist die Quelle der Wahrheit — Aenderungen gehen immer erst in die DB, dann per API zu Cloudflare.
DNS Record Ownership
Abschnitt betitelt „DNS Record Ownership“Records haben ein managed-Flag:
| managed | Erstellt von | Bearbeitbar durch | Loeschbar durch | Beispiele |
|---|---|---|---|---|
| true | System (Provisioning) | agency_owner | — (nie) | CNAME @/www → Pages |
| false | agency_owner oder tenant_admin | agency_owner, tenant_admin | agency_owner, tenant_admin | MX, TXT (SPF/DKIM), A fuer Subdomains |
Regeln:
managed=trueRecords werden bei Site-Erstellung automatisch angelegt und sind fuer Kunden unsichtbar in der DNS-Tabelle (nur agency_owner sieht sie mit einem “System”-Badge)- Kunden (tenant_admin) sehen und verwalten nur ihre eigenen Records (
managed=false) - agency_owner sieht alle Records, kann aber
managed=trueRecords nicht loeschen (nur Content aendern falls das Pages-Projekt umbenannt wird)
Konfiguration
Abschnitt betitelt „Konfiguration“Umgebungsvariablen (Backend)
Abschnitt betitelt „Umgebungsvariablen (Backend)“| Variable | Beschreibung |
|---|---|
CF_API_TOKEN | Cloudflare API Token (scoped: Zone Write, Pages Write, R2 Write, Access Write) |
CF_ACCOUNT_ID | Cloudflare Account ID |
CF_TUNNEL_ID | ID des bestehenden Cloudflare Tunnels |
Ein einzelner API-Token mit diesen Permissions reicht fuer alle Operationen. Kein Global API Key.
Nicht im CMS-Backend konfiguriert: Der Service Token fuer den Tunnel-Zugriff (Astro Build → CMS Service-API) wird im Cloudflare Dashboard erstellt und als Header in der Astro Build-Umgebung konfiguriert (CF-Access-Client-Id / CF-Access-Client-Secret). Das CMS-Backend kennt diesen Token nicht.
Feature-Package site
Abschnitt betitelt „Feature-Package site“UseCases
Abschnitt betitelt „UseCases“- CreateSite(ctx, adminID, tenantID, name, domain) — Site anlegen + Cloudflare-Provisioning (Zone, DNS, Pages, R2, Tunnel). Nur
agency_owner. - UpdateSite(ctx, adminID, siteID, name, deployHookURL?) — Site-Metadaten und Deploy-Hook-URL aktualisieren. Nur
agency_owner. - GetSite(ctx, accountID, siteID) — Site-Details inkl. Cloudflare-Status.
agency_ownersieht alle, Tenant-Rollen nur eigene Sites. - ListSites(ctx, accountID) — Sites auflisten die man sehen darf.
- CheckNSVerification(ctx, adminID, siteID) — NS-Verifikation bei Cloudflare pruefen und Status aktualisieren. Nur
agency_owner. - SetCustomDomain(ctx, adminID, siteID) — Pages Custom Domain setzen (nur moeglich wenn NS verifiziert). Nur
agency_owner. - TriggerBuild(ctx, accountID, siteID) — Deploy Hook aufrufen, Build-Status auf
buildingsetzen.agency_owner+tenant_admin. - CheckBuildStatus(ctx, accountID, siteID) — Build-Status bei Cloudflare abfragen und aktualisieren.
- DeactivateSite(ctx, adminID, siteID) — Site deaktivieren. Nur
agency_owner.
DNS-UseCases
Abschnitt betitelt „DNS-UseCases“- ListDNSRecords(ctx, accountID, siteID) — DNS Records der Site anzeigen.
- CreateDNSRecord(ctx, accountID, siteID, type, name, content, proxied, ttl, priority?) — Record anlegen (DB + Cloudflare API).
- UpdateDNSRecord(ctx, accountID, siteID, recordID, content, proxied, ttl, priority?) — Record aendern.
- DeleteDNSRecord(ctx, accountID, siteID, recordID) — Record loeschen.
Berechtigungen
Abschnitt betitelt „Berechtigungen“| Aktion | agency_owner | agency_employee | tenant_admin | tenant_member |
|---|---|---|---|---|
| Site erstellen | ja | — | — | — |
| Site bearbeiten | ja | — | — | — |
| Site-Details sehen | ja | ja | eigene | eigene |
| Sites auflisten | alle | alle | eigene | eigene |
| NS-Verifikation pruefen | ja | — | — | — |
| Custom Domain setzen | ja | — | — | — |
| Build ausloesen | ja | — | ja | — |
| Build-Status sehen | ja | ja | eigene | eigene |
| DNS Records sehen (alle) | ja | ja | — | — |
| DNS Records sehen (eigene) | — | — | ja (nur managed=false) | — |
| DNS Records aendern | ja | — | ja (nur managed=false) | — |
| Managed Records aendern | ja | — | — | — |
Repositories
Abschnitt betitelt „Repositories“SiteRepository— Create, Update, FindByID, ListByTenant, ListAll, UpdateBuildStatus, UpdateNSVerificationDNSRecordRepository— Create, Update, Delete, ListBySite, FindByID
Cloudflare Client
Abschnitt betitelt „Cloudflare Client“Eigener interner Client (internal/feature/site/cloudflare.go), kein externes SDK:
type CloudflareClient struct { httpClient *http.Client accountID string apiToken string tunnelID string}Methoden:
CreateZone(domain string) (zoneID, nameservers, error)CheckZoneActivation(zoneID string) (active bool, error)CreateDNSRecords(zoneID string, records []DNSRecord) error(Batch)CreateDNSRecord(zoneID string, record DNSRecord) (cfRecordID, error)UpdateDNSRecord(zoneID, cfRecordID string, record DNSRecord) errorDeleteDNSRecord(zoneID, cfRecordID string) errorListDNSRecords(zoneID string) ([]DNSRecord, error)CreatePagesProject(name string, productionBranch string) errorSetPagesCustomDomain(projectName, domain string) errorGetLatestDeployment(projectName string) (Deployment, error)CreateR2Bucket(name string) errorSetR2CORS(bucketName string, origins []string) errorGetTunnelConfig() (TunnelConfig, error)UpdateTunnelConfig(config TunnelConfig) error
Kein SDK, nur net/http + encoding/json. Cloudflare REST API v4 (https://api.cloudflare.com/client/v4/).
API-Routen
Abschnitt betitelt „API-Routen“Agency-Routen (agency_owner)
Abschnitt betitelt „Agency-Routen (agency_owner)“POST /api/agency/sites — Site erstellen (+ CF Provisioning)GET /api/agency/sites — Alle Sites auflistenGET /api/agency/sites/{id} — Site-DetailsPUT /api/agency/sites/{id} — Site bearbeitenPOST /api/agency/sites/{id}/check-ns — NS-Verifikation pruefenPOST /api/agency/sites/{id}/set-domain — Pages Custom Domain setzenPOST /api/agency/sites/{id}/build — Build ausloesenGET /api/agency/sites/{id}/build-status — Build-Status abfragenPUT /api/agency/sites/{id}/deactivate — Site deaktivierenGET /api/agency/sites/{id}/dns — DNS Records auflistenPOST /api/agency/sites/{id}/dns — DNS Record anlegenPUT /api/agency/sites/{id}/dns/{recordId} — DNS Record aendernDELETE /api/agency/sites/{id}/dns/{recordId} — DNS Record loeschenClient-Routen (tenant_admin, tenant_member)
Abschnitt betitelt „Client-Routen (tenant_admin, tenant_member)“GET /api/client/sites — Eigene Sites auflistenGET /api/client/sites/{id} — Site-DetailsPOST /api/client/sites/{id}/build — Build ausloesen (nur tenant_admin)GET /api/client/sites/{id}/build-status — Build-StatusGET /api/client/sites/{id}/dns — DNS Records auflisten (nur tenant_admin)POST /api/client/sites/{id}/dns — DNS Record anlegen (nur tenant_admin)PUT /api/client/sites/{id}/dns/{recordId} — DNS Record aendern (nur tenant_admin)DELETE /api/client/sites/{id}/dns/{recordId} — DNS Record loeschen (nur tenant_admin)Onboarding-Ablauf (neue Site)
Abschnitt betitelt „Onboarding-Ablauf (neue Site)“Schritt 1: Agency-Owner erstellt Site im CMS
Abschnitt betitelt „Schritt 1: Agency-Owner erstellt Site im CMS“POST /api/agency/sites mit name, domain, tenantID.
CMS fuehrt automatisch aus:
- Site in DB anlegen (UUIDv7)
- R2 Bucket erstellen (
bucket_public={resource_key}) - R2 CORS konfigurieren (erlaubte Origins:
*.pages.dev, Custom Domain) - Cloudflare Zone erstellen →
cf_zone_id+cf_nameserversspeichern - DNS Records seeden (Standard-Set: A/CNAME fuer
@undwww→ Pages) - Pages-Projekt erstellen (
cf_pages_project={resource_key}) - Tunnel Ingress Rule hinzufuegen (fuer Service-API bei Astro Build)
- Access Group + 2 Applications (Production + Preview) + 2 Policies erstellen (Zugangsschutz)
Response: Site-Daten inkl. Cloudflare-Nameserver die der Kunde bei seinem Registrar eintragen muss.
Schritt 2: Manuell (Agency-Owner, ~2 Minuten)
Abschnitt betitelt „Schritt 2: Manuell (Agency-Owner, ~2 Minuten)“- Im Cloudflare Dashboard: Git-Repository mit dem Pages-Projekt verbinden
- Repository:
traffino/sites(oder kundenspezifisches Repo) - Build Command:
npm run build - Output Directory:
dist/ - Root Directory:
sites/{site-name}/(Monorepo) - Environment Variables:
CMS_API_URL,CMS_SERVICE_TOKEN
- Repository:
- Deploy Hook erstellen (Dashboard → Settings → Builds → Deploy hooks)
- Deploy Hook URL im CMS eintragen (
PUT /api/agency/sites/{id})
Schritt 3: Domain-Verifikation
Abschnitt betitelt „Schritt 3: Domain-Verifikation“- Kunde aendert Nameserver beim Registrar → Cloudflare-NS eintragen
- Agency-Owner klickt “NS pruefen” im CMS (
POST .../check-ns) - Sobald verifiziert: CMS setzt Pages Custom Domain automatisch (
POST .../set-domain)
Schritt 4: Erster Build
Abschnitt betitelt „Schritt 4: Erster Build“- Agency-Owner klickt “Build ausloesen” im CMS
- CMS POST auf Deploy Hook URL
- Cloudflare klont Repo, baut Astro, deployt auf Pages
- CMS pollt Build-Status und zeigt Ergebnis an
Kunden-Onboarding-Funnel
Abschnitt betitelt „Kunden-Onboarding-Funnel“Wenn ein Kunde (tenant_admin/tenant_member) sich zum ersten Mal einloggt und seine Site noch nicht vollstaendig eingerichtet ist, zeigt das CMS einen gefuehrten Onboarding-Funnel statt des normalen Dashboards.
Onboarding-Status auf traffino.site
Abschnitt betitelt „Onboarding-Status auf traffino.site“Neue Spalte (Migration 000006):
| Spalte | Typ | Beschreibung |
|---|---|---|
| onboarding_completed_at | TIMESTAMPTZ | Null = Onboarding laeuft noch |
Onboarding-Schritte
Abschnitt betitelt „Onboarding-Schritte“Der Funnel zeigt dem Kunden eine Schritt-fuer-Schritt-Anleitung mit Fortschrittsanzeige:
Schritt 1: Willkommen (immer erledigt)
- Begruessung, kurze Erklaerung was Traffino ist
- “Weiter”-Button
Schritt 2: Nameserver aendern (wartet auf cf_ns_verified)
- Anzeige der Cloudflare-Nameserver (
cf_nameservers) - Anleitung: “Loggen Sie sich bei Ihrem Domain-Anbieter ein und aendern Sie die Nameserver”
- Kopier-Buttons fuer die NS-Eintraege
- “Ich habe die Nameserver geaendert”-Button → loest
CheckNSVerificationaus - Status: Haken wenn
cf_ns_verified = true - Hinweis: “Die Aenderung kann bis zu 24 Stunden dauern”
Schritt 3: Website ist live (wartet auf ersten erfolgreichen Build)
- Zeigt an dass die Website bereitsteht
- Link zur Live-Website (
https://{domain}) - Status: Haken wenn
last_build_status = 'success' - Dieser Schritt wird vom Agency-Owner erledigt (Git verbinden, Build ausloesen)
Schritt 4: Fertig
- “Onboarding abschliessen”-Button → setzt
onboarding_completed_at - Weiterleitung zum normalen Dashboard
Onboarding-Logik im Frontend
Abschnitt betitelt „Onboarding-Logik im Frontend“GET /api/client/sites → pruefen ob onboarding_completed_at IS NULL → ja: Redirect zu /_app/onboarding/{siteId} → nein: normales DashboardBei Tenants mit nur einer Site (Standard-Fall fuer Stufe 1) reicht die Pruefung auf die erste Site. Bei mehreren Sites: Onboarding pro Site einzeln.
Agency-Owner Sicht
Abschnitt betitelt „Agency-Owner Sicht“Der Agency-Owner sieht auf der Site-Detail-Seite den Onboarding-Fortschritt des Kunden:
- Welche Schritte sind erledigt
- NS-Verifikation Status
- Build Status
- Ob der Kunde das Onboarding abgeschlossen hat
DNS-Verwaltung
Abschnitt betitelt „DNS-Verwaltung“Standard-Records (Seed bei Site-Erstellung)
Abschnitt betitelt „Standard-Records (Seed bei Site-Erstellung)“| Typ | Name | Content | Proxied | Managed | Beschreibung |
|---|---|---|---|---|---|
| CNAME | @ | {resource_key}.pages.dev | ja | ja | Hauptdomain |
| CNAME | www | {resource_key}.pages.dev | ja | ja | www-Subdomain |
Diese Records sind managed=true und werden vom System verwaltet. Kunden sehen sie nicht.
Weitere Records (MX fuer Mail, TXT fuer SPF/DKIM, etc.) koennen Kunden (tenant_admin) ueber die DNS-Verwaltung selbst anlegen — diese haben managed=false.
Sichtbarkeit in der DNS-Tabelle
Abschnitt betitelt „Sichtbarkeit in der DNS-Tabelle“| Rolle | Sieht managed Records | Sieht eigene Records | Kann eigene aendern |
|---|---|---|---|
| agency_owner | ja (mit “System”-Badge) | ja | ja |
| agency_employee | ja (readonly) | ja (readonly) | — |
| tenant_admin | — | ja | ja |
| tenant_member | — | — | — |
Validierung
Abschnitt betitelt „Validierung“typemuss ein gueltiger DNS-Record-Typ sein (A, AAAA, CNAME, MX, TXT, SRV, CAA)namewird relativ zur Zone interpretiert (z.B.mailwird zumail.example.com)- Bei MX-Records ist
priorityPflicht - Keine doppelten CNAME-Records fuer denselben Namen
- Maximale Record-Anzahl pro Site: 100 (nur
managed=falsezaehlen) - Kunden koennen keine Records auf
@oderwwwanlegen (reserviert fuer managed Records)
Build-Trigger
Abschnitt betitelt „Build-Trigger“User aendert Content → Speichern → CMS fragt: "Jetzt deployen?" → Ja → POST auf cf_deploy_hook_url → last_build_triggered_at = now, last_build_status = 'building' → Frontend pollt /build-status alle 10s → CMS fragt Cloudflare Pages API nach letztem Deployment → Status aktualisieren: 'success' oder 'failure'Fehlerbehandlung
Abschnitt betitelt „Fehlerbehandlung“- Kein Deploy Hook hinterlegt →
ErrDeployHookNotConfigured - Deploy Hook antwortet nicht →
ErrDeployHookFailed - Build schlaegt fehl → Status
failure, Frontend zeigt Fehlermeldung - Concurrent Builds → Cloudflare queued automatisch, CMS zeigt
building
Deployments-Liste
Abschnitt betitelt „Deployments-Liste“Das CMS zeigt alle Deployments eines Pages-Projekts an — abgerufen ueber die Cloudflare Pages API (GET /accounts/{account_id}/pages/projects/{project_name}/deployments).
Datenquelle
Abschnitt betitelt „Datenquelle“Deployments werden nicht in der DB gespeichert, sondern live von Cloudflare abgefragt. Die Pages API liefert:
- Deployment ID
- Environment (
productionoderpreview) - Branch Name
- Commit Hash + Message
- Status (
idle,active,failure) - URL (
{resource_key}.pages.devfuer Production,{hash}.{project}.pages.devfuer Preview) - Alias-URLs (Branch-basiert:
{branch}.{project}.pages.dev) - Erstellt-am, Dauer
Anzeige im CMS
Abschnitt betitelt „Anzeige im CMS“Production Deployment:
- Zeigt die Live-URL:
https://{domain}(Custom Domain) undhttps://{project}.pages.dev - Status-Badge (active/failure)
- Commit-Info + Zeitpunkt
Preview Deployments (Feature-Branches):
- Zeigt die Preview-URL:
https://{hash}.{project}.pages.dev - Branch-Alias-URL:
https://{branch}.{project}.pages.dev - Branch-Name, Commit-Info, Zeitpunkt
- Status-Badge
UseCase
Abschnitt betitelt „UseCase“- ListDeployments(ctx, accountID, siteID) — Die letzten 30 Deployments von Cloudflare abrufen. Keine Paginierung noetig.
API-Routen
Abschnitt betitelt „API-Routen“GET /api/agency/sites/{id}/deployments — Alle DeploymentsGET /api/client/sites/{id}/deployments — Eigene Site Deployments (tenant_admin)Berechtigungen
Abschnitt betitelt „Berechtigungen“| Aktion | agency_owner | agency_employee | tenant_admin | tenant_member |
|---|---|---|---|---|
| Deployments sehen | ja | ja | eigene | — |
Cloudflare Client Methode
Abschnitt betitelt „Cloudflare Client Methode“ListDeployments(projectName string, limit int) ([]Deployment, error)
type Deployment struct { ID string Environment string // "production" oder "preview" URL string // Haupt-URL Aliases []string // Alias-URLs (Branch-basiert) Branch string CommitHash string CommitMsg string Status string CreatedAt time.Time Duration int // Build-Dauer in Sekunden}Zero Trust — Zugangsschutz in der Testphase
Abschnitt betitelt „Zero Trust — Zugangsschutz in der Testphase“Waehrend der Entwicklung/Testphase wird jede Site per Cloudflare Access geschuetzt. Nur freigegebene IP-Adressen koennen die Website aufrufen. Der Kunde kann sich im CMS selbst freischalten.
Architektur
Abschnitt betitelt „Architektur“Pro Site werden bei Erstellung automatisch angelegt:
- Access Group — enthaelt die freigegebenen IPs des Kunden (eine Group fuer beide Apps)
- Access Application “Production” (Typ
self_hosted) — scoped auf die Custom Domain (z.B.mueller-dach.de) - Access Application “Preview” (Typ
self_hosted) — scoped auf*.{resource_key}.pages.dev(Wildcard fuer alle Preview-Deployments) - Je eine Access Policy pro Application — Allow-Regel die auf die gemeinsame Access Group verweist
Besucher → Cloudflare Edge → Access Application prueft IP → IP in Access Group? → Durchlassen → Sonst → Blockiert (403)Zwei getrennte Applications, weil Production (Custom Domain) und Preview (.pages.dev) unabhaengig geoeffnet/geschlossen werden muessen. Die Access Group ist geteilt — eine IP-Freischaltung gilt fuer beide.
Datenmodell
Abschnitt betitelt „Datenmodell“Neue Spalten auf traffino.site (Migration 000006):
| Spalte | Typ | Beschreibung |
|---|---|---|
| cf_access_group_id | VARCHAR(50) | Cloudflare Access Group ID (geteilt) |
| cf_access_prod_app_id | VARCHAR(50) | Access Application ID fuer Custom Domain |
| cf_access_prod_policy_id | VARCHAR(50) | Access Policy ID fuer Production |
| cf_access_prod_enabled | BOOLEAN DEFAULT TRUE | Production-Zugangsschutz aktiv |
| cf_access_preview_app_id | VARCHAR(50) | Access Application ID fuer *.pages.dev |
| cf_access_preview_policy_id | VARCHAR(50) | Access Policy ID fuer Preview |
| cf_access_preview_enabled | BOOLEAN DEFAULT TRUE | Preview-Zugangsschutz aktiv |
Service Token (CMS-Backend, nicht Site-bezogen)
Abschnitt betitelt „Service Token (CMS-Backend, nicht Site-bezogen)“Ein einziges Service Token fuer die gesamte Agency — wird einmalig manuell im Cloudflare Dashboard erstellt. Es schuetzt den Zugriff auf das CMS-Backend ueber den Cloudflare Tunnel, damit Astro zur Build-Zeit Content abrufen kann (Service-API).
Das Service Token hat nichts mit den Kunden-Access-Policies zu tun. Die Kunden-Policies schuetzen die Kunden-Websites (Pages-Projekte), der Service Token schuetzt das CMS-Backend (Tunnel).
Ablauf bei Site-Erstellung
Abschnitt betitelt „Ablauf bei Site-Erstellung“Im Provisioning-Schritt (nach Zone + Pages + R2):
- Access Group erstellen:
POST /accounts/{account_id}/access/groups- Name:
{resource_key}-access - Initial leer (keine IPs)
- Name:
- Access Application “Production” erstellen:
POST /accounts/{account_id}/access/apps- Name:
{resource_key} - Domain:
{custom-domain} - Typ:
self_hosted
- Name:
- Access Application “Preview” erstellen:
POST /accounts/{account_id}/access/apps- Name:
{resource_key}-preview - Domain:
*.{resource_key}.pages.dev - Typ:
self_hosted
- Name:
- Je eine Access Policy pro Application erstellen:
- Allow-Regel mit Include-Bedingung: Access Group (IP-basiert)
“Meine IP freischalten”-Funktion
Abschnitt betitelt „“Meine IP freischalten”-Funktion“Der Kunde klickt im CMS einen Button. Das CMS:
- Liest die IP des Kunden aus dem
RequestContext(CloudflareCf-Connecting-Ip) - Ruft die aktuelle Access Group von Cloudflare ab
- Fuegt die IP hinzu (
PUT /accounts/{account_id}/access/groups/{group_id}) - Bestaetigt dem Kunden: “Ihre IP {x.x.x.x} wurde freigeschaltet”
IPs werden als /32 (einzelne Adresse) hinzugefuegt.
”IP entfernen”-Funktion
Abschnitt betitelt „”IP entfernen”-Funktion“Der Kunde sieht eine Liste seiner freigeschalteten IPs und kann einzelne entfernen. Der Agency-Owner sieht und verwaltet alle IPs.
Zugangsschutz oeffnen (Go-Live)
Abschnitt betitelt „Zugangsschutz oeffnen (Go-Live)“Wenn die Site live geht, oeffnet der Agency-Owner die Production-Policy — die Infrastruktur (Applications, Group, Policies) bleibt bestehen:
- Production-Policy aktualisieren:
include-Regel auf “Everyone” setzen cf_access_prod_enabled = falsesetzen
Die Preview-Policy bleibt typischerweise aktiv — Feature-Deployments sollen weiterhin geschuetzt sein.
Falls die Production-Site spaeter wieder geschuetzt werden soll (z.B. bei groesserem Umbau), wird die Policy einfach zurueck auf die IP-Group gesetzt und cf_access_prod_enabled = true.
Production und Preview koennen unabhaengig voneinander geoeffnet/geschlossen werden.
UseCases
Abschnitt betitelt „UseCases“- WhitelistIP(ctx, accountID, siteID) — Eigene IP zur Access Group hinzufuegen.
agency_owner,tenant_admin. - RemoveWhitelistedIP(ctx, accountID, siteID, ip) — IP entfernen.
agency_ownerentfernt beliebige,tenant_adminnur eigene (letzte hinzugefuegte). - ListWhitelistedIPs(ctx, accountID, siteID) — Freigeschaltete IPs anzeigen.
- DisableAccessProtection(ctx, adminID, siteID, environment) — Access Policy auf “Everyone” oeffnen (
environment=productionoderpreview). Nuragency_owner. - EnableAccessProtection(ctx, adminID, siteID, environment) — Access Policy zurueck auf IP-Group setzen. Nur
agency_owner.
API-Routen
Abschnitt betitelt „API-Routen“POST /api/agency/sites/{id}/access/whitelist — IP freischaltenDELETE /api/agency/sites/{id}/access/whitelist/{ip} — IP entfernenGET /api/agency/sites/{id}/access/whitelist — IPs auflistenDELETE /api/agency/sites/{id}/access — Zugangsschutz aufhebenPOST /api/agency/sites/{id}/access — Zugangsschutz aktivierenPOST /api/client/sites/{id}/access/whitelist — Eigene IP freischaltenDELETE /api/client/sites/{id}/access/whitelist/{ip} — Eigene IP entfernenGET /api/client/sites/{id}/access/whitelist — IPs auflistenBerechtigungen
Abschnitt betitelt „Berechtigungen“| Aktion | agency_owner | agency_employee | tenant_admin | tenant_member |
|---|---|---|---|---|
| Eigene IP freischalten | ja | ja | ja | — |
| Beliebige IP entfernen | ja | — | — | — |
| Eigene IP entfernen | ja | ja | ja | — |
| IPs auflisten | ja | ja | ja | — |
| Zugangsschutz aufheben | ja | — | — | — |
| Zugangsschutz aktivieren | ja | — | — | — |
Cloudflare Client Methoden
Abschnitt betitelt „Cloudflare Client Methoden“CreateAccessGroup(name string) (groupID, error)UpdateAccessGroupIPs(groupID string, ips []string) errorGetAccessGroupIPs(groupID string) ([]string, error)CreateAccessApp(name, domain string) (appID, error)CreateAccessPolicy(appID, groupID string) (policyID, error)OpenAccessPolicy(appID, policyID string) error— Policy auf “Everyone” setzenRestrictAccessPolicy(appID, policyID, groupID string) error— Policy zurueck auf IP-Group
Cloudflare Free Tier
Abschnitt betitelt „Cloudflare Free Tier“Zero Trust Free Plan: 50 User, unbegrenzte Access Applications. IP-basierte Regeln zaehlen nicht als User (kein Login noetig). Fuer eine Agentur mit 10-50 Kunden-Websites reicht der Free Plan.
Frontend
Abschnitt betitelt „Frontend“Neue Routen
Abschnitt betitelt „Neue Routen“/_app/sites — Site-Liste (Agency: alle, Tenant: eigene)/_app/sites/{id} — Site-Details (Status, Domain, Build)/_app/sites/{id}/deployments — Deployments-Liste/_app/sites/{id}/dns — DNS-Verwaltung/_app/sites/{id}/access — Zugangsschutz (IP-Whitelist)/_app/sites/create — Neue Site anlegen (nur agency_owner)/_app/onboarding/{siteId} — Kunden-Onboarding-FunnelUI-Komponenten
Abschnitt betitelt „UI-Komponenten“- SiteList — Tabelle mit Name, Domain, Status, letzter Build, Aktionen
- SiteDetail — Uebersicht mit Cloudflare-Status, NS-Verifikation, Build-Controls, Onboarding-Fortschritt
- SiteCreateForm — Tenant (Dropdown), Name, Domain
- DNSRecordTable — DNS Records mit Typ, Name, Content, Proxied-Badge, Aktionen. Managed Records nur fuer agency_owner sichtbar (mit “System”-Badge)
- DNSRecordForm — Modal fuer Anlegen/Bearbeiten (Typ-Dropdown, Name, Content, TTL, Proxied-Toggle)
- BuildStatusBadge — Farbiger Badge (idle=grau, building=blau, success=gruen, failure=rot)
- NSVerificationStatus — Nameserver-Anzeige mit Pruef-Button und Status
- DeploymentList — Tabelle mit Environment-Badge (Production/Preview), Branch, Commit, URL(s), Status, Zeitpunkt. Production-Deployment zeigt Custom Domain + pages.dev URL, Preview-Deployments zeigen Hash-URL + Branch-Alias
- AccessWhitelist — IP-Liste mit “Meine IP freischalten”-Button (prominent), Liste der freigeschalteten IPs mit Entfernen-Aktion, Schutz-Status-Badge
- OnboardingStepper — Mehrstufiger Funnel mit Fortschrittsbalken (Willkommen → Nameserver → Website live → Fertig)
- NameserverInstructions — Nameserver-Werte mit Copy-Buttons, Anbieter-spezifische Hinweise, Pruef-Button
Navigation
Abschnitt betitelt „Navigation“AppShell bekommt:
- “Sites” — fuer
agency_owner,agency_employee,tenant_admin,tenant_member - Angezeigt vor “Team” in der Sidebar
Keys unter site.* und dns.* in de.ts und en.ts.
ErrSiteNotFound— Site existiert nichtErrDomainAlreadyExists— Domain bereits in VerwendungErrDeployHookNotConfigured— Kein Deploy Hook hinterlegtErrDeployHookFailed— Deploy Hook konnte nicht aufgerufen werdenErrNSNotVerified— NS-Delegation noch nicht verifiziertErrDNSRecordNotFound— DNS Record existiert nichtErrDNSRecordProtected— Geschuetzter Record kann nicht geloescht werdenErrMaxDNSRecordsReached— Limit von 100 Records erreichtErrCloudflareAPI— Allgemeiner Cloudflare-API-Fehler (mit Detail-Message)ErrBuildAlreadyRunning— Build laeuft bereitsErrAccessNotEnabled— Zugangsschutz nicht aktivErrIPAlreadyWhitelisted— IP bereits freigeschaltetErrIPNotFound— IP nicht in der Whitelist
Abhaengigkeiten zu anderen Features
Abschnitt betitelt „Abhaengigkeiten zu anderen Features“| Feature | Abhaengigkeit |
|---|---|
| Media-Upload | Nutzt bucket_public von Site fuer R2-Zugriff |
| Build-Trigger (bestehend im Plan) | Wird durch dieses Feature implementiert |
| Service-API fuer Astro | Tunnel Ingress Rule wird hier provisioniert |
| Kontaktanfragen | Pages Function braucht die Site (separate Implementierung) |
| Analytics | Pages Function braucht die Site (separate Implementierung) |
Abgrenzung
Abschnitt betitelt „Abgrenzung“Nicht in diesem Feature:
- Pages Functions erstellen/deployen (wird manuell gemacht oder per CI)
- R2 Custom Domains (nicht noetig, Zugriff per S3-API)
- Tunnel-Erstellung (existiert bereits, nur Ingress Rules werden hinzugefuegt)
- SSL-Konfiguration (automatisch durch Cloudflare)
- Billing/Usage-Tracking pro Site
- Domain-Registrar-Integration (Kunde aendert NS selbst)