Engineering Design
Architecture Decision Records

Architecture Decision Records

ADRs are append-only. Never delete or rewrite a decision. If a decision is reversed, add a new ADR that supersedes the old one and links to it.

Each ADR follows the format: Context (why this mattered) → Decision (what was chosen) → Consequences (trade-offs accepted).


ADR-001: Stripe Elements over Stripe Checkout

ContextCheckout UI must match Vespertene brand identity. Stripe Checkout redirects to a Stripe-hosted page.
DecisionUse Stripe Elements (embedded). Card data goes directly to Stripe — never touches our server. CSP configured to allow js.stripe.com.
ConsequencesWe own the full checkout UI and UX. Additional complexity: must implement our own validation, error states, and loading states. Trade-off accepted.

ADR-002: Cloudflare R2 over AWS S3

ContextDigital product ZIP files need reliable, fast delivery at low cost. S3 egress fees scale with downloads.
DecisionUse Cloudflare R2 (S3-compatible API). Free egress, CDN-backed, zero bandwidth fees.
ConsequencesVendor lock-in to Cloudflare ecosystem (already using Cloudflare DNS). Migration to S3 is a 1-day effort if needed — API is compatible. Trade-off accepted.

ADR-003: HttpOnly Cookies over localStorage for Auth Tokens

ContextJWT must be stored client-side. localStorage is accessible to JavaScript — XSS attack vector.
DecisionStore JWT in HttpOnly cookie (_medusa_jwt). Not accessible via JS. Sent automatically on every request.
ConsequencesRequires CSRF protection on state-mutating endpoints (Medusa handles this). Slightly more complex server setup. Significantly more secure. Trade-off accepted.

ADR-004: Integers in Cents for Prices

ContextFloating-point arithmetic produces rounding errors for currency. 0.1 + 0.2 !== 0.3 in IEEE 754.
DecisionStore all prices as integers in cents (AUD). 1999 = $19.99. Never use floats for money.
ConsequencesAll price display requires division by 100 before rendering. Formatters centralised in lib/currency.ts. No rounding bugs. Trade-off accepted.

ADR-005: UUID Download Tokens over Sequential IDs

ContextSequential download IDs are enumerable — an attacker could iterate /store/downloads/1, /2, /3.
DecisionGenerate tokens via crypto.randomUUID(). UUID v4 = 2^122 possible values. Ownership validated server-side on every request regardless.
ConsequencesUUIDs are longer in URLs. No enumeration attack surface. Defence in depth: even if a token is guessed, ownership check prevents access. Trade-off accepted.

ADR-006: Fly.io Free Tier for Phase 1 Backend

ContextPhase 1 is private (5 staff). Cost should be near-zero while validating the product.
DecisionDeploy Medusa backend to Fly.io free tier. Zero cost for Phase 1.
ConsequencesFree tier has cold starts after inactivity. Acceptable for staff-only use. Migrate to Railway ($5/mo) for Phase 2 public launch to eliminate cold starts. Trade-off accepted.

ADR-007: AP over CP (CAP Theorem)

ContextA distributed system must choose between Consistency and Availability when partitions occur.
DecisionChoose AP. Prioritise availability for customers. Enforce strong consistency only where money or file access is involved (token redemption, order idempotency).
ConsequencesEventual consistency on reads means users may briefly see stale product data. This is acceptable. Download token redemption and order creation remain strongly consistent. Trade-off accepted.

ADR-008: Exact Version Pinning for Medusa Packages

ContextMedusa minor releases have historically introduced breaking changes. ^ semver ranges would auto-upgrade on npm install.
DecisionPin all @medusajs/* packages to exact versions in package.json. Update deliberately and test in staging.
ConsequencesManual update process. Slower to get security patches. Significantly more stable builds. Phase 2: move to Renovatebot with staging auto-test on upgrade. Trade-off accepted.