Architecture Decision Records
This page documents the key architectural decisions made during the design and implementation of OsuRender API.
ADR-001: Transactional Outbox Pattern
Status: Accepted
Context
The system needs to reliably dispatch render jobs from the API tier to GPU workers. Direct message broker enqueueing during the HTTP request creates a dual-write problem: if the database commit succeeds but the broker publish fails (or vice versa), the system enters an inconsistent state.
Decision
Implement the Transactional Outbox pattern. When a render job is created, both the Job row and an OutboxEvent row are inserted in the same PostgreSQL transaction. A dedicated OutboxDispatcher process polls/listens for pending events and dispatches them to Celery workers.
Consequences
Guaranteed consistency between job creation and dispatch — no lost jobs
Natural retry semantics via outbox event state machine
Dispatcher can be horizontally scaled using
FOR UPDATE SKIP LOCKEDAdds latency between job creation and actual dispatch (typically < 1s)
Requires a dedicated dispatcher process
ADR-002: PostgreSQL as Queue Backend
Status: Accepted
Context
Options considered: RabbitMQ, Amazon SQS, Redis Streams, PostgreSQL with SKIP LOCKED.
Decision
Use PostgreSQL with FOR UPDATE SKIP LOCKED as the queue backend via the Outbox pattern.
Consequences
No additional infrastructure dependency
Transactional consistency with job data
Sufficient throughput for 10K+ jobs/day
Not suitable for millions of messages/second (not our scale)
Queue operations add load to the primary database
ADR-003: Modal for GPU Compute
Status: Accepted
Context
Options considered: Self-managed Kubernetes with GPU nodes, AWS EC2 GPU instances, Modal serverless GPUs, RunPod.
Decision
Use Modal's serverless GPU infrastructure for render execution.
Consequences
Zero GPU infrastructure management
Pay-per-second billing eliminates idle costs
Automatic scaling to demand
Vendor dependency on Modal's platform
Cold start latency on first invocation
Mitigation: Local rendering fallback path exists via
USE_MODAL_GPU=0
ADR-004: Cloudflare R2 for Object Storage
Status: Accepted
Context
Options considered: AWS S3, Cloudflare R2, Self-hosted MinIO.
Decision
Use Cloudflare R2 for production, MinIO for local development. Both are S3-compatible.
Consequences
Zero egress fees (R2's primary advantage)
S3-compatible API means code works with MinIO locally
Fewer regions than AWS S3
Slightly less mature tooling ecosystem
ADR-005: Defense-in-Depth Security Model
Status: Accepted
Context
The API is publicly accessible via Cloudflare. Threat model includes: unauthenticated abuse, webhook spoofing, IP spoofing, zip bombs, and secret leakage.
Decision
Implement defense-in-depth with seven layers:
- HMAC-SHA256 webhook verification with replay protection (timestamp + nonce)
- Cloudflare-only ingress with
CF-Connecting-IPextraction - PostgreSQL advisory locks for per-IP concurrency limits
- ZIP structure validation on upload (ratio, nesting, corruption)
- Zip bomb protection during extraction (physical byte counting)
- Subprocess environment allowlisting — only whitelisted env vars passed to danser
- Global error masking in production mode
Consequences
Comprehensive protection against known attack vectors
Requires Cloudflare infrastructure lockdown at the perimeter level
ZIP validation adds upload latency
