Phase 03 — Skalierungs-Infrastruktur
Historical phase-plan status: this README preserves the original Phase 03 dependency map and done definition. Current runtime/operator truth lives in
docs/improvement-plan/PROGRESS.md,docs/improvement-plan/OPERATOR-CHECKLIST.md,docs/ops/worker-queues.md,docs/architecture/outbox-pattern.md, anddocs/ops/asset-cdn.md. In particular, #51/A7/B2 CDN rollout is currently parked in backlog, not an active Phase 03 closeout item.
Dauer: 4-5 Wochen Tasks: 12 (dieses README dokumentiert alle 12; die State-/DB-Teilmenge 37-42 ist hier ausführlich, Worker/Backup/CDN 43-47/51 in separaten Detail-MDs vom Parallel-Subagent) Ziel: Boilerplate von „Single-Process-MVP" auf horizontal skalierbare Production-Architektur bringen. Alle In-Process-State-Stores raus, DB-Stack härten (PgBouncer + Read-Replica + Outbox), Worker-Queues sauber trennen, Backups + Tenant-Snapshots automatisieren, statische Assets über CDN ausliefern.
Warum diese Phase
Aktueller Zustand: Mehrere kritische Code-Pfade halten Zustand im Prozess-RAM (apps/api/src/auth/login-rate-limit.ts, apps/web/app/utils/auth/mfa/challenge-rate-limit.server.ts, apps/api/src/idempotency/store.ts, apps/web/app/utils/services/.server/subscriptionService.ts mit cachified-Wrapping, apps/api/src/auth/session/session-store.ts, apps/web/app/modules/mcp/mcp.server.ts). Sobald eine zweite Replica läuft, sind Rate-Limits umgehbar, Idempotency-Keys werden inkonsistent, Stripe-Webhook-Cache-Invalidations erreichen nur eine Instanz und Sessions ghosten.
DB-seitig hängen 100+ Prisma-Calls direkt am Primary, ohne Connection-Pooler. Bei 200 gleichzeitigen Web-Requests reicht ein Tab voll schwerer Dashboard-Loaders, um Postgres-max_connections auszuschöpfen. Außerdem laufen alle Reads gegen den Writer, und Stripe-Webhook-Side-Effects (Mail, Webhook, Audit-Log) sind nicht transaktional mit dem DB-Write gekoppelt — bei Crash zwischen Commit und Side-Effect verlieren wir Outbound-Events.
Phase 03 baut die fehlende Plumbing für Multi-Replica-Betrieb. Ohne diese Phase blockieren wir Phase 04 (Search braucht Outbox für async-Indexing) und Phase 05 (Feature-Flags brauchen einen Shared-Cache mit Pub/Sub-Invalidation).
Task-Reihenfolge
| Reihenfolge | Task-ID | Datei | Parallelisierbar mit | Subagent-Slot |
|---|---|---|---|---|
| 1 | #37 | shared-store-package.md |
— (Foundation für alles andere) | State-Subagent |
| 2 | #38 | shared-store-migration.md |
#40, #43, #51 | State-Subagent |
| 3 | #39 | pubsub-cache-invalidation.md |
#41, #44, #45 | State-Subagent |
| 4 | #40 | pgbouncer.md |
#38, #43, #45 | State-Subagent (DB-Teil) |
| 5 | #41 | read-write-split.md |
#39, #44, #46, #51 | State-Subagent (DB-Teil) |
| 6 | #42 | outbox-pattern.md |
#45, #47 | State-Subagent (DB-Teil) |
| 7 | #45 | worker-queues.md |
#39, #40, #41 | Worker-Subagent |
| 8 | #46 | streaming-exports.md |
#41 | Worker-Subagent |
| 9 | #47 | job-priority.md |
#42 | Worker-Subagent |
| 10 | #43 | backup-cli.md |
#38, #40 | Worker-Subagent |
| 11 | #44 | tenant-snapshot.md |
#39, #41, #43 | Worker-Subagent |
| 12 | #51 | asset-cdn.md |
#38, #41 | Worker-Subagent; currently backlog/operator-deferred |
Dependency-Graph (intern)
#37 shared-store-package ──► #38 shared-store-migration ──► #39 pubsub-cache-invalidation ─┐
│ │
└─────────────────► (nutzt Redis-Connection auch für BullMQ-Queues) │
│
#40 pgbouncer ──► #41 read-write-split ──► #42 outbox-pattern ──────────────────────────────┤
│ │
▼ │
#47 job-priority │
│
#45 worker-queues ──► #46 streaming-exports │
│
#43 backup-cli ──► #44 tenant-snapshot │
│
#51 asset-cdn (parked/backlog) ──────────────────────────────────────────────────────────────┤
│
▼
Phase 04 (Search/RAG)
Wichtige Reihenfolge-Regeln:
#37 shared-store-packageist hartes Fundament: liefert das Adapter-Interface, die Redis-/Memory-Implementations und die Higher-Level-Helpers (Rate-Limit, Cache, Session, Idempotency). Ohne dieses Package kann weder #38 noch #39 starten, und die Worker-Tasks (BullMQ teilt sich die Redis-Connection) werden auch davon abhängen.#38 shared-store-migrationmigriert die 6 In-Process-Stores. Muss nach #37, parallel zu #40 möglich.#39 pubsub-cache-invalidationist die logische Fortsetzung der Migration für Caches mit nicht-trivialer Konsistenz (Stripe-Subscription-Cache, Tenant-Cache, Feature-Flag-Cache). Setzt #38 voraus, weil dort der Pub/Sub-Adapter etabliert wird.#40 pgbouncerist unabhängig von Shared-Store, muss aber vor#41 read-write-splitstehen, weil R/W-Split dieDIRECT_URL-Konvention für Migrations braucht, die in #40 etabliert wird.#42 outbox-patternbraucht#41(publishOutboxEvent läuft innerhalb einer Transaction auf prismaWrite), und liefert die Foundation für#47 job-priority(priorisierte Dispatcher-Queue) sowie Phase 04 (async-Indexing).#45 worker-queuesund#43 backup-clisind weitgehend orthogonal und können parallel laufen, sobald #37 fertig ist.#51 asset-cdnwar ebenfalls orthogonal, ist aber seit der Operator-Entscheidung 2026-05-24 in A7/B2/#51 geparkt; nicht als aktiven Phase-03-Blocker zählen.
Sub-Phasen-Verteilung
State-Subagent (dieses README behandelt 37-42 ausführlich):
- #37 Shared-Store Package
- #38 Shared-Store Migration
- #39 Pub/Sub Cache-Invalidation
- #40 PgBouncer Setup
- #41 R/W-Split via Prisma $extends
- #42 Outbox-Pattern + Dispatcher
Worker-Subagent (separate Detail-MDs, eigene Verantwortung):
- #45 Worker-Queue-Trennung
- #46 Streaming-Exports + Row-Cap
- #47 BullMQ Job-Priority
- #43 Backup-CLI
- #44 Tenant-Snapshot-Tool
- #51 Asset-CDN (historisch geplant; aktueller Status: backlog/operator-deferred)
Beide Subagents synchronisieren auf der Redis-Connection-Konvention aus #37 (gleiche ioredis-Instanz, unterschiedliche logical-DB-Indizes; BullMQ-Queues auf Index 0, Shared-Store auf Index 1, Pub/Sub auf Index 2).
Phase Done-Definition
Diese Phase ist completed wenn:
- Alle 12 Tasks
completedinPROGRESS.md pnpm lint,pnpm typecheck,pnpm testglobal grün; testcontainers-basierte Integration-Tests laufen in CI gegen Redis + Postgres + PgBouncer- Staging deployed mit 2x Web-Replicas + 1x API + 2x Worker hinter Caddy-Reverse-Proxy; Last-Test (k6 mit 500 RPS auf 5min) zeigt: keine Login-Rate-Limit-Bypasses, keine Stripe-Cache-Inconsistencies, kein Connection-Exhaustion auf Postgres
- Stripe-Webhook → Subscription-Cache-Invalidation < 2s end-to-end (vorher: bis zu 1h stale)
- PgBouncer-
SHOW STATSzeigtcl_active < default_pool_sizeunter normaler Last; Connection-Reuse-Ratio > 90 % - Read-Replica führt > 60 % der Dashboard-Reads aus (gemessen via Prisma-Middleware-Counter)
- Outbox-Dispatcher hat 0 unverarbeitete Events > 60s alt im 24h-Monitor-Fenster
.env.example+.env.production.exampleenthalten alle neuen Keys (REDIS_URL,REDIS_SHARED_STORE_DB,REDIS_PUBSUB_DB,DATABASE_URL,DATABASE_READ_URL,DIRECT_URL) sauber dokumentiert- Doku-Files angelegt:
docs/observability/redis-stores.md,docs/database/connection-pooling.md,docs/database/read-replicas.md,docs/database/outbox-pattern.md gsd-code-reviewer-Subagent hat alle 12 Tasks-Commits reviewed; Findings adressiert- Phase-Summary in dieser README.md ergänzt (siehe unten)
- Rollback-Drill durchgeführt: PgBouncer-Container down → Web läuft via DIRECT_URL-Fallback weiter; Redis down → Memory-Adapter-Fallback aktiv (Single-Instance-Mode)
Phase Summary
(Wird nach Abschluss der Phase vom ausführenden Agent ergänzt.)
Lessons Learned
(Wird nach Abschluss ergänzt.)
Verweise
- Detail-MD-Template:
../DETAIL-MD-TEMPLATE.md - Agent-Workflow:
../AGENT-INSTRUCTIONS.md - Execution-Order:
../00-execution-order.md - Live-Status:
../PROGRESS.md - Vorgänger-Phase (Observability):
../02-observability/README.md - Nachfolge-Phase (Search + RAG):
../04-search-rag/README.md