Zum Inhalt springen

Cloudflare-Integration — Design Spec

Datum: 2026-04-14 Status: Entwurf Scope: Stufe 1 — Cloudflare-Anbindung fuer Sites

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.

AufgabeMethodeWann
Zone (Domain) anlegenCloudflare APIBei Site-Erstellung
DNS Records seedenCloudflare API (Batch)Bei Site-Erstellung
Pages-Projekt erstellenCloudflare APIBei Site-Erstellung
Pages Custom Domain setzenCloudflare APINach NS-Verifikation
R2 Bucket erstellenCloudflare APIBei Site-Erstellung
R2 CORS konfigurierenCloudflare APIBei Site-Erstellung
Tunnel Ingress Rule hinzufuegenCloudflare APIBei Site-Erstellung (fuer Service-API)
Git-Repo verbindenManuell (Dashboard)Einmalig nach Erstellung
Deploy Hook eintragenManuell (in CMS)Einmalig nach Git-Verbindung
NS-Verifikation pruefenCloudflare API (Poll)Nach Domain-Registrar-Aenderung
Build ausloesenCMS → Deploy Hook POSTBei Content-Aenderung
Build-Status pruefenCloudflare Pages API (Poll)Nach Build-Trigger

Bei Tenant-Erstellung wird ein eindeutiger, sprechender Key generiert:

{firmenname-slug}-{nanoid5}

Beispiele: mueller-dach-x7k9p, pizzeria-roma-ab3mf, foto-schmidt-qw2nj

  • 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)

Der Tenant-Key wird als Praefix fuer alle Cloudflare-Ressourcen verwendet:

RessourceNamensschemaBeispiel
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}-accessmueller-dach-x7k9p-access
Tunnel Ingress{tenant-key}.internalmueller-dach-x7k9p.internal

Bei Tenants mit mehreren Sites (spaeter): {tenant-key}-{site-slug} (z.B. mueller-dach-x7k9p-blog).

Neue Spalte auf traffino.tenant (Migration 000006):

SpalteTypBeschreibung
resource_keyVARCHAR(50) NOT NULL UNIQUEEindeutiger 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).

SpalteTypBeschreibung
cf_zone_idVARCHAR(50)Cloudflare Zone ID
cf_pages_projectVARCHAR(100)Pages-Projektname (= {resource_key}.pages.dev)
cf_deploy_hook_urlTEXTDeploy Hook URL (manuell eingetragen)
cf_nameserversTEXT[]Zugewiesene Cloudflare-Nameserver
cf_ns_verifiedBOOLEAN DEFAULT FALSENS-Delegation verifiziert
cf_ns_verified_atTIMESTAMPTZZeitpunkt der Verifikation
last_build_triggered_atTIMESTAMPTZLetzter Build-Trigger
last_build_statusVARCHAR(20)idle, building, success, failure
last_build_finished_atTIMESTAMPTZLetzter 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)“
SpalteTypBeschreibung
idUUID PKUUIDv7
site_idUUID FK → siteZugehoerige Site
cf_record_idVARCHAR(50)Cloudflare Record ID
typeVARCHAR(10) NOT NULLA, AAAA, CNAME, MX, TXT, etc.
nameVARCHAR(255) NOT NULLRecord-Name (z.B. @, www, mail)
contentTEXT NOT NULLRecord-Wert
ttlINT DEFAULT 1TTL (1 = automatic)
proxiedBOOLEAN DEFAULT FALSECloudflare Proxy aktiv
priorityINTNur fuer MX
managedBOOLEAN NOT NULL DEFAULT FALSESystem-Record (nicht vom Kunden aenderbar)
Audit-Feldercreated_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.

Records haben ein managed-Flag:

managedErstellt vonBearbeitbar durchLoeschbar durchBeispiele
trueSystem (Provisioning)agency_owner— (nie)CNAME @/www → Pages
falseagency_owner oder tenant_adminagency_owner, tenant_adminagency_owner, tenant_adminMX, TXT (SPF/DKIM), A fuer Subdomains

Regeln:

  • managed=true Records 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=true Records nicht loeschen (nur Content aendern falls das Pages-Projekt umbenannt wird)
VariableBeschreibung
CF_API_TOKENCloudflare API Token (scoped: Zone Write, Pages Write, R2 Write, Access Write)
CF_ACCOUNT_IDCloudflare Account ID
CF_TUNNEL_IDID 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.

  • 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_owner sieht 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 building setzen. agency_owner + tenant_admin.
  • CheckBuildStatus(ctx, accountID, siteID) — Build-Status bei Cloudflare abfragen und aktualisieren.
  • DeactivateSite(ctx, adminID, siteID) — Site deaktivieren. Nur agency_owner.
  • 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.
Aktionagency_owneragency_employeetenant_admintenant_member
Site erstellenja
Site bearbeitenja
Site-Details sehenjajaeigeneeigene
Sites auflistenallealleeigeneeigene
NS-Verifikation pruefenja
Custom Domain setzenja
Build ausloesenjaja
Build-Status sehenjajaeigeneeigene
DNS Records sehen (alle)jaja
DNS Records sehen (eigene)ja (nur managed=false)
DNS Records aendernjaja (nur managed=false)
Managed Records aendernja
  • SiteRepository — Create, Update, FindByID, ListByTenant, ListAll, UpdateBuildStatus, UpdateNSVerification
  • DNSRecordRepository — Create, Update, Delete, ListBySite, FindByID

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) error
  • DeleteDNSRecord(zoneID, cfRecordID string) error
  • ListDNSRecords(zoneID string) ([]DNSRecord, error)
  • CreatePagesProject(name string, productionBranch string) error
  • SetPagesCustomDomain(projectName, domain string) error
  • GetLatestDeployment(projectName string) (Deployment, error)
  • CreateR2Bucket(name string) error
  • SetR2CORS(bucketName string, origins []string) error
  • GetTunnelConfig() (TunnelConfig, error)
  • UpdateTunnelConfig(config TunnelConfig) error

Kein SDK, nur net/http + encoding/json. Cloudflare REST API v4 (https://api.cloudflare.com/client/v4/).

POST /api/agency/sites — Site erstellen (+ CF Provisioning)
GET /api/agency/sites — Alle Sites auflisten
GET /api/agency/sites/{id} — Site-Details
PUT /api/agency/sites/{id} — Site bearbeiten
POST /api/agency/sites/{id}/check-ns — NS-Verifikation pruefen
POST /api/agency/sites/{id}/set-domain — Pages Custom Domain setzen
POST /api/agency/sites/{id}/build — Build ausloesen
GET /api/agency/sites/{id}/build-status — Build-Status abfragen
PUT /api/agency/sites/{id}/deactivate — Site deaktivieren
GET /api/agency/sites/{id}/dns — DNS Records auflisten
POST /api/agency/sites/{id}/dns — DNS Record anlegen
PUT /api/agency/sites/{id}/dns/{recordId} — DNS Record aendern
DELETE /api/agency/sites/{id}/dns/{recordId} — DNS Record loeschen
GET /api/client/sites — Eigene Sites auflisten
GET /api/client/sites/{id} — Site-Details
POST /api/client/sites/{id}/build — Build ausloesen (nur tenant_admin)
GET /api/client/sites/{id}/build-status — Build-Status
GET /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)

POST /api/agency/sites mit name, domain, tenantID.

CMS fuehrt automatisch aus:

  1. Site in DB anlegen (UUIDv7)
  2. R2 Bucket erstellen (bucket_public = {resource_key})
  3. R2 CORS konfigurieren (erlaubte Origins: *.pages.dev, Custom Domain)
  4. Cloudflare Zone erstellen → cf_zone_id + cf_nameservers speichern
  5. DNS Records seeden (Standard-Set: A/CNAME fuer @ und www → Pages)
  6. Pages-Projekt erstellen (cf_pages_project = {resource_key})
  7. Tunnel Ingress Rule hinzufuegen (fuer Service-API bei Astro Build)
  8. Access Group + 2 Applications (Production + Preview) + 2 Policies erstellen (Zugangsschutz)

Response: Site-Daten inkl. Cloudflare-Nameserver die der Kunde bei seinem Registrar eintragen muss.

  1. 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
  2. Deploy Hook erstellen (Dashboard → Settings → Builds → Deploy hooks)
  3. Deploy Hook URL im CMS eintragen (PUT /api/agency/sites/{id})
  1. Kunde aendert Nameserver beim Registrar → Cloudflare-NS eintragen
  2. Agency-Owner klickt “NS pruefen” im CMS (POST .../check-ns)
  3. Sobald verifiziert: CMS setzt Pages Custom Domain automatisch (POST .../set-domain)
  1. Agency-Owner klickt “Build ausloesen” im CMS
  2. CMS POST auf Deploy Hook URL
  3. Cloudflare klont Repo, baut Astro, deployt auf Pages
  4. CMS pollt Build-Status und zeigt Ergebnis an

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.

Neue Spalte (Migration 000006):

SpalteTypBeschreibung
onboarding_completed_atTIMESTAMPTZNull = Onboarding laeuft noch

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 CheckNSVerification aus
  • 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
GET /api/client/sites → pruefen ob onboarding_completed_at IS NULL
→ ja: Redirect zu /_app/onboarding/{siteId}
→ nein: normales Dashboard

Bei Tenants mit nur einer Site (Standard-Fall fuer Stufe 1) reicht die Pruefung auf die erste Site. Bei mehreren Sites: Onboarding pro Site einzeln.

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
TypNameContentProxiedManagedBeschreibung
CNAME@{resource_key}.pages.devjajaHauptdomain
CNAMEwww{resource_key}.pages.devjajawww-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.

RolleSieht managed RecordsSieht eigene RecordsKann eigene aendern
agency_ownerja (mit “System”-Badge)jaja
agency_employeeja (readonly)ja (readonly)
tenant_adminjaja
tenant_member
  • type muss ein gueltiger DNS-Record-Typ sein (A, AAAA, CNAME, MX, TXT, SRV, CAA)
  • name wird relativ zur Zone interpretiert (z.B. mail wird zu mail.example.com)
  • Bei MX-Records ist priority Pflicht
  • Keine doppelten CNAME-Records fuer denselben Namen
  • Maximale Record-Anzahl pro Site: 100 (nur managed=false zaehlen)
  • Kunden koennen keine Records auf @ oder www anlegen (reserviert fuer managed Records)
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'
  • 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

Das CMS zeigt alle Deployments eines Pages-Projekts an — abgerufen ueber die Cloudflare Pages API (GET /accounts/{account_id}/pages/projects/{project_name}/deployments).

Deployments werden nicht in der DB gespeichert, sondern live von Cloudflare abgefragt. Die Pages API liefert:

  • Deployment ID
  • Environment (production oder preview)
  • Branch Name
  • Commit Hash + Message
  • Status (idle, active, failure)
  • URL ({resource_key}.pages.dev fuer Production, {hash}.{project}.pages.dev fuer Preview)
  • Alias-URLs (Branch-basiert: {branch}.{project}.pages.dev)
  • Erstellt-am, Dauer

Production Deployment:

  • Zeigt die Live-URL: https://{domain} (Custom Domain) und https://{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
  • ListDeployments(ctx, accountID, siteID) — Die letzten 30 Deployments von Cloudflare abrufen. Keine Paginierung noetig.
GET /api/agency/sites/{id}/deployments — Alle Deployments
GET /api/client/sites/{id}/deployments — Eigene Site Deployments (tenant_admin)
Aktionagency_owneragency_employeetenant_admintenant_member
Deployments sehenjajaeigene
  • 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
}

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.

Pro Site werden bei Erstellung automatisch angelegt:

  1. Access Group — enthaelt die freigegebenen IPs des Kunden (eine Group fuer beide Apps)
  2. Access Application “Production” (Typ self_hosted) — scoped auf die Custom Domain (z.B. mueller-dach.de)
  3. Access Application “Preview” (Typ self_hosted) — scoped auf *.{resource_key}.pages.dev (Wildcard fuer alle Preview-Deployments)
  4. 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.

Neue Spalten auf traffino.site (Migration 000006):

SpalteTypBeschreibung
cf_access_group_idVARCHAR(50)Cloudflare Access Group ID (geteilt)
cf_access_prod_app_idVARCHAR(50)Access Application ID fuer Custom Domain
cf_access_prod_policy_idVARCHAR(50)Access Policy ID fuer Production
cf_access_prod_enabledBOOLEAN DEFAULT TRUEProduction-Zugangsschutz aktiv
cf_access_preview_app_idVARCHAR(50)Access Application ID fuer *.pages.dev
cf_access_preview_policy_idVARCHAR(50)Access Policy ID fuer Preview
cf_access_preview_enabledBOOLEAN DEFAULT TRUEPreview-Zugangsschutz aktiv

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).

Im Provisioning-Schritt (nach Zone + Pages + R2):

  1. Access Group erstellen: POST /accounts/{account_id}/access/groups
    • Name: {resource_key}-access
    • Initial leer (keine IPs)
  2. Access Application “Production” erstellen: POST /accounts/{account_id}/access/apps
    • Name: {resource_key}
    • Domain: {custom-domain}
    • Typ: self_hosted
  3. Access Application “Preview” erstellen: POST /accounts/{account_id}/access/apps
    • Name: {resource_key}-preview
    • Domain: *.{resource_key}.pages.dev
    • Typ: self_hosted
  4. Je eine Access Policy pro Application erstellen:
    • Allow-Regel mit Include-Bedingung: Access Group (IP-basiert)

Der Kunde klickt im CMS einen Button. Das CMS:

  1. Liest die IP des Kunden aus dem RequestContext (Cloudflare Cf-Connecting-Ip)
  2. Ruft die aktuelle Access Group von Cloudflare ab
  3. Fuegt die IP hinzu (PUT /accounts/{account_id}/access/groups/{group_id})
  4. Bestaetigt dem Kunden: “Ihre IP {x.x.x.x} wurde freigeschaltet”

IPs werden als /32 (einzelne Adresse) hinzugefuegt.

Der Kunde sieht eine Liste seiner freigeschalteten IPs und kann einzelne entfernen. Der Agency-Owner sieht und verwaltet alle IPs.

Wenn die Site live geht, oeffnet der Agency-Owner die Production-Policy — die Infrastruktur (Applications, Group, Policies) bleibt bestehen:

  1. Production-Policy aktualisieren: include-Regel auf “Everyone” setzen
  2. cf_access_prod_enabled = false setzen

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.

  • WhitelistIP(ctx, accountID, siteID) — Eigene IP zur Access Group hinzufuegen. agency_owner, tenant_admin.
  • RemoveWhitelistedIP(ctx, accountID, siteID, ip) — IP entfernen. agency_owner entfernt beliebige, tenant_admin nur eigene (letzte hinzugefuegte).
  • ListWhitelistedIPs(ctx, accountID, siteID) — Freigeschaltete IPs anzeigen.
  • DisableAccessProtection(ctx, adminID, siteID, environment) — Access Policy auf “Everyone” oeffnen (environment = production oder preview). Nur agency_owner.
  • EnableAccessProtection(ctx, adminID, siteID, environment) — Access Policy zurueck auf IP-Group setzen. Nur agency_owner.
POST /api/agency/sites/{id}/access/whitelist — IP freischalten
DELETE /api/agency/sites/{id}/access/whitelist/{ip} — IP entfernen
GET /api/agency/sites/{id}/access/whitelist — IPs auflisten
DELETE /api/agency/sites/{id}/access — Zugangsschutz aufheben
POST /api/agency/sites/{id}/access — Zugangsschutz aktivieren
POST /api/client/sites/{id}/access/whitelist — Eigene IP freischalten
DELETE /api/client/sites/{id}/access/whitelist/{ip} — Eigene IP entfernen
GET /api/client/sites/{id}/access/whitelist — IPs auflisten
Aktionagency_owneragency_employeetenant_admintenant_member
Eigene IP freischaltenjajaja
Beliebige IP entfernenja
Eigene IP entfernenjajaja
IPs auflistenjajaja
Zugangsschutz aufhebenja
Zugangsschutz aktivierenja
  • CreateAccessGroup(name string) (groupID, error)
  • UpdateAccessGroupIPs(groupID string, ips []string) error
  • GetAccessGroupIPs(groupID string) ([]string, error)
  • CreateAccessApp(name, domain string) (appID, error)
  • CreateAccessPolicy(appID, groupID string) (policyID, error)
  • OpenAccessPolicy(appID, policyID string) error — Policy auf “Everyone” setzen
  • RestrictAccessPolicy(appID, policyID, groupID string) error — Policy zurueck auf IP-Group

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.

/_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-Funnel
  • 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

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 nicht
  • ErrDomainAlreadyExists — Domain bereits in Verwendung
  • ErrDeployHookNotConfigured — Kein Deploy Hook hinterlegt
  • ErrDeployHookFailed — Deploy Hook konnte nicht aufgerufen werden
  • ErrNSNotVerified — NS-Delegation noch nicht verifiziert
  • ErrDNSRecordNotFound — DNS Record existiert nicht
  • ErrDNSRecordProtected — Geschuetzter Record kann nicht geloescht werden
  • ErrMaxDNSRecordsReached — Limit von 100 Records erreicht
  • ErrCloudflareAPI — Allgemeiner Cloudflare-API-Fehler (mit Detail-Message)
  • ErrBuildAlreadyRunning — Build laeuft bereits
  • ErrAccessNotEnabled — Zugangsschutz nicht aktiv
  • ErrIPAlreadyWhitelisted — IP bereits freigeschaltet
  • ErrIPNotFound — IP nicht in der Whitelist
FeatureAbhaengigkeit
Media-UploadNutzt bucket_public von Site fuer R2-Zugriff
Build-Trigger (bestehend im Plan)Wird durch dieses Feature implementiert
Service-API fuer AstroTunnel Ingress Rule wird hier provisioniert
KontaktanfragenPages Function braucht die Site (separate Implementierung)
AnalyticsPages Function braucht die Site (separate Implementierung)

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)