Skip to content

Deployment Architecture

Subscribe Flow follows a modern cloud-native architecture with a clear separation of frontend (static SPA) and backend (API).

Deployment Diagram

graph TB
    subgraph "Client Layer"
        Browser[Browser]
        Mobile[Mobile App]
        SDK[SDK Client]
    end

    subgraph "CDN / Static Hosting"
        CF[Cloudflare Pages]
        PC[Preference Center SPA]
        AD[Admin Dashboard SPA]
        CF --> PC
        CF --> AD
    end

    subgraph "API Layer - Railway/Fly.io"
        LB[Load Balancer]
        API1[FastAPI Instance 1]
        API2[FastAPI Instance 2]
        Worker[Celery Worker]
        Beat[Celery-Beat Scheduler]
        LB --> API1
        LB --> API2
    end

    subgraph "Data Layer - Managed Services"
        PG[(PostgreSQL 15)]
        Redis[(Redis 7)]
    end

    subgraph "External Services"
        Resend[Resend API]
        Stripe[Stripe API]
        Webhooks[Customer Webhooks]
    end

    Browser --> CF
    Mobile --> CF
    SDK --> LB

    PC --> LB
    AD --> LB

    API1 --> PG
    API2 --> PG
    API1 --> Redis
    API2 --> Redis

    Worker --> PG
    Worker --> Redis
    Worker --> Resend

    Beat --> Redis

    API1 --> Resend
    API2 --> Resend
    API1 --> Stripe
    API2 --> Stripe

    API1 --> Webhooks
    API2 --> Webhooks

    Resend -.Webhook.-> LB
    Stripe -.Webhook.-> LB

    style CF fill:#f97316
    style LB fill:#3b82f6
    style PG fill:#8b5cf6
    style Redis fill:#ef4444
    style Resend fill:#10b981
    style Stripe fill:#6366f1
    style Worker fill:#f59e0b
    style Beat fill:#f59e0b

Components

Frontend (Cloudflare Pages / Netlify)

Technology: React 19 SPA with Vite 6+ (Bun runtime)

Deployment: - Static build (bun run build) - Automatic deployment on Git push (main branch) - Global CDN distribution - HTTPS by default - Custom domain support

Environment Variables:

Bash
VITE_API_URL=https://api.subscribeflow.com
VITE_SENTRY_DSN=https://...

Build Command:

Bash
cd apps/web && bun run build

Output Directory: apps/web/dist

Backend (Railway / Fly.io)

Technology: Python FastAPI with Uvicorn

Deployment: - Docker container - Auto-scaling (2-10 instances) - Health checks (/health) - Zero-downtime deployments

Dockerfile:

Docker
1
2
3
4
5
FROM python:3.13-slim
WORKDIR /app
COPY . .
RUN pip install uv && uv sync
CMD ["uv", "run", "uvicorn", "subscribeflow.main:app", "--host", "0.0.0.0", "--port", "8000"]

Environment Variables:

Bash
# Database & Cache
DATABASE_URL=postgresql://...
REDIS_URL=redis://...

# Authentication
JWT_SECRET_KEY=...
API_KEY_SALT=...
AUDIT_EMAIL_HMAC_KEY=...  # Keyed HMAC for subscriber-deletion audit entries (ADR-007 §4)

# Proxy & Network (Production Requirement)
TRUST_PROXY_HEADERS=true  # Erforderlich hinter dem Fly-Proxy für korrektes IP-Whitelisting und IP-Rate-Limiting.
                          # Ohne diese Einstellung ist request.client.host die Proxy-Adresse;
                          # gesetzte IP-Whitelists würden dann gegen diese prüfen und jeden aussperren.
                          # Lokal in .env.example auf false für schnelle Entwicklung.

# Resend (Email)
RESEND_API_KEY=re_...
DEFAULT_SEND_DOMAIN=mail.subscribeflow.net

# Session Cookie
SESSION_COOKIE_DOMAIN=.subscribeflow.net

# Stripe (Billing)
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_STARTER=price_...
STRIPE_PRICE_PROFESSIONAL=price_...

# Error Tracking — EU region required, see "Monitoring & Observability"
SENTRY_DSN=https://...@o0.ingest.de.sentry.io/0
SENTRY_TRACES_SAMPLE_RATE=0.0

Auf allen drei Backend-Prozessen setzen — API, Celery-Worker und Celery-Beat. Ein Prozess ohne SENTRY_DSN meldet nichts und sieht dabei genauso gesund aus wie einer, der meldet.

Celery Worker (Railway / Fly.io)

Technology: Celery with Redis backend

Tasks: - Resend API synchronization - Webhook delivery - Data retention cleanup - Email sending - Magic link email delivery

Deployment: - Separate container (same codebase) - Auto-scaling based on queue length

Command:

Bash
uv run celery -A subscribeflow.worker worker --loglevel=info

Celery-Beat Scheduler (Railway / Fly.io)

Technology: Celery-Beat with Redis backend

Scheduled Tasks: (Quelle: workers/celery_app.py::beat_schedule)

Task Schedule Description
reset_monthly_email_counters 1st of month, 00:00 UTC Resets emails_sent_this_month to 0 for all organizations
send_daily_idea Täglich, IDEALAB_DAILY_SEND_HOUR:15 UTC idealab „Idee des Tages"-Broadcast. Selbst feature-geflaggt: ohne IDEALAB_DAILY_URL und IDEALAB_ORG_ID ein No-op (TF-447).

Command:

Bash
uv run celery -A subscribeflow.workers.celery_app:celery_app beat --loglevel=info

Deployment: - Eigene Fly-Prozessgruppe beat (siehe fly.toml), 256 MB - Genau eine Instanzfly scale count beat=1. Beat führt nichts aus, es enqueued nur; eine zweite Instanz stellt jeden Task doppelt in die Queue, ohne dass etwas fehlschlägt.

Database (Supabase / Railway Postgres)

Technology: PostgreSQL 15+

Configuration: - Connection pooling (PgBouncer) - Automated backups (daily) - Point-in-time recovery - Read replicas (post-MVP)

Scaling: - Vertical scaling (CPU/RAM) - Read replicas for analytics - Connection pooling (max 100 connections)

Cache (Upstash Redis / Railway Redis)

Technology: Redis 7+

Use Cases: - Session storage (Magic Link tokens, JWT sessions) - API response caching - Celery message queue + Beat schedule store - Per-organization rate limiting counters

Configuration: - Maxmemory policy: allkeys-lru - Persistence: RDB + AOF - Eviction: Automatic

Custom Domain DNS Configuration

Professional-tier organizations can configure custom email domains. The following DNS records must be set by the customer:

Record Type Name Value Purpose
TXT _resend.custom-domain.com (provided by Resend) Domain verification
MX custom-domain.com (provided by Resend) Inbound email routing
TXT custom-domain.com v=spf1 include:... SPF authentication
CNAME resend._domainkey.custom-domain.com (provided by Resend) DKIM signing

Domain verification is handled via the DomainService, which calls the Resend API to register and verify domains.

Deployment Pipeline

CI/CD (GitHub Actions)

YAML
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v5
      - run: uv sync
      - run: uv run pytest

  deploy-backend:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: superfly/flyctl-actions/setup-flyctl@master
      - run: flyctl deploy --remote-only

  deploy-frontend:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}

Documentation Deployment

Subscribe Flow betreibt zwei separate Dokumentations-Pipelines:

Dokumentation Ziel URL Workflow
Intern (vollständig) GitHub Pages talent-factory.github.io/subscribe-flow/ Standard MkDocs Deploy
Kunden (öffentlich) Cloudflare Pages docs.subscribeflow.net .github/workflows/docs-public.yml

Die Kunden-Dokumentation wird über Cloudflare Pages deployt und enthält nur die öffentlich relevanten Seiten (Guides, API-Referenz, SDK). Die interne Dokumentation auf GitHub Pages umfasst zusätzlich Architektur-Details, Entwickler-Guides und ADRs.

Monitoring & Observability

Sentry (Error Tracking)

Backend (implementiert): core/observability.py::init_sentry, aufgerufen in api/app.py::create_app() sowie im Celery-Worker (worker_process_init) und im Beat-Scheduler (beat_init). Jeder Prozess initialisiert für sich — der Worker forkt, und ein vor dem Fork erzeugter Client meldet nichts mehr.

Frontend (noch offen): JavaScript SDK, VITE_SENTRY_DSN.

Konfiguration

Variable Bedeutung
SENTRY_DSN Leer = Sentry vollständig aus. Es gibt bewusst kein zweites ENABLE_SENTRY-Flag, das man vergessen könnte zu setzen.
SENTRY_TRACES_SAMPLE_RATE Anteil der Requests mit Performance-Tracing (Default 0.0, also aus).

Das DSN muss in Staging und Produktion auf ein Sentry-Projekt in der EU-Region zeigen (ingest.de.sentry.io). Alert-Events transportieren Subscriber- und Organisations-UUIDs; ein US-gehostetes Projekt würde die Datenresidenz unterlaufen, für die der SES-Transport überhaupt existiert (ADR-005). send_default_pii ist im Code fest auf False — Sentry erfasst damit keine IP-Adressen, Cookies oder Request-Bodies.

send_default_pii deckt allerdings keine Breadcrumbs ab: Das SDK kopiert die extra-Felder eines Logrecords unverändert nach breadcrumb["data"]. Eine Logzeile mit einer Adresse — workers/tasks/feedback_email_tasks.py loggt to=<Adresse> auf ERROR — würde diese also beim nächsten Event mit ausliefern. Zwei Schichten verhindern das:

Schicht Wirkung
LoggingIntegration(level=WARNING) INFO-Records werden gar nicht erst zu Breadcrumbs. Das ist die volumenstarke Stufe, auf der beiläufige Personendaten in einer Form auftauchen, die kein Muster erkennt.
before_breadcrumb / before_send core/privacy.py::scrub_emails maskiert alles, was wie eine Adresse aussieht (a***@example.com), in Breadcrumbs und in den selbst befüllten Event-Feldern. Erkennung über die Form des Werts, nicht über den Feldnamen — eine Namensliste deckt nur die Aufrufstellen ab, die es beim Schreiben gab.

Beide Schichten sind Absicherung, kein Ersatz für die Regel in core/alerting.py: In report_alert gehören ausschliesslich Identifikatoren.

Tooling-Zugangsdaten (CLI, MCP, CI)

Die folgenden Variablen liest die Anwendung nicht. Sie werden von sentry-cli, vom Sentry-MCP-Server und von CI-Jobs (Releases, Source-Map-Upload) gebraucht und gehören deshalb in die lokale .env beziehungsweise in die Secrets der CI — nicht in die Runtime-Konfiguration der Backend-Prozesse.

Variable Woher Beispiel
SENTRY_ORG Organization Slug. Steht in der URL (https://<org-slug>.sentry.io/) oder unter Settings → General Settings. talent-factory
SENTRY_TEAM Team Slug. Settings → Teams, Name ohne führendes #. subscribeflow
SENTRY_AUTH_TOKEN Settings → Auth Tokens (Organization Token, für CI) oder https://sentry.io/settings/account/api/auth-tokens/ (User Token, für die lokale CLI). Scopes: org:read, project:read, project:releases, event:read. Wird nur einmal angezeigt. sntrys_…
SENTRY_URL Region-Endpunkt. Für unsere EU-Instanz zwingend — ohne diese Variable laufen CLI und MCP gegen die US-Region und finden die Organisation nicht. https://de.sentry.io

Der Auth-Token hat Schreibrechte auf die Organisation und darf nicht ins Repository gelangen. Wer die Werte nicht von Hand zusammensuchen will, lässt sie sich von der CLI schreiben:

Bash
1
2
3
sentry-cli login                # legt ~/.sentryclirc mit dem Token an
sentry-cli organizations list   # Org-Slugs
sentry-cli projects list        # Projekte samt Team-Zuordnung

Alert-Rules

Anwendungsfehler werden nicht automatisch aus Logzeilen erzeugt: Die LoggingIntegration läuft mit event_level=None, ERROR-Logs landen nur als Breadcrumbs. Events entstehen aus unbehandelten Exceptions und aus expliziten Aufrufen von core/alerting.py::report_alert. Jeder solche Aufruf trägt den Tag alert_event und einen stabilen Fingerprint, sodass gleichartige Vorfälle in einer Issue zusammenlaufen und eine Schwelle erreichen können.

alert_event Bedeutung Reaktion
retention.clock_miss touch_activity hat keine Zeile getroffen — ein Subscriber war aktiv, seine Retention-Uhr steht still. Ohne Eingriff wird der Datensatz nach 24 Monaten gelöscht, obwohl die Person gehandelt hat. Sofort: betroffene subscriber_id / organization_id aus dem Event-Kontext prüfen, last_activity_at manuell setzen, Ursache (Mandanten-Verwechslung oder Race mit einer Löschung) klären. Alert bereits ab dem ersten Event.

Metrics (Prometheus + Grafana)

  • API response time
  • Database query performance
  • Celery queue length
  • Resend API rate limits
  • Stripe webhook processing time

Logs (Structured Logging)

  • JSON format
  • Correlation IDs
  • Log levels (DEBUG, INFO, WARNING, ERROR)

Disaster Recovery

Backup Strategy

  • Database: Daily automated backups (7-day retention)
  • Redis: Not critical (can be rebuilt)
  • Code: Git repository (GitHub)

Recovery Time Objective (RTO)

  • Database: < 1 hour
  • Backend: < 15 minutes (redeploy)
  • Frontend: < 5 minutes (CDN cache invalidation)

Recovery Point Objective (RPO)

  • Database: < 24 hours (daily backup)
  • Redis: Acceptable data loss (cache)