Skip to content

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 LOCKED
  • Adds 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:

  1. HMAC-SHA256 webhook verification with replay protection (timestamp + nonce)
  2. Cloudflare-only ingress with CF-Connecting-IP extraction
  3. PostgreSQL advisory locks for per-IP concurrency limits
  4. ZIP structure validation on upload (ratio, nesting, corruption)
  5. Zip bomb protection during extraction (physical byte counting)
  6. Subprocess environment allowlisting — only whitelisted env vars passed to danser
  7. 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

Built with VitePress