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
| Context | Checkout UI must match Vespertene brand identity. Stripe Checkout redirects to a Stripe-hosted page. |
| Decision | Use Stripe Elements (embedded). Card data goes directly to Stripe — never touches our server. CSP configured to allow js.stripe.com. |
| Consequences | We 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
| Context | Digital product ZIP files need reliable, fast delivery at low cost. S3 egress fees scale with downloads. |
| Decision | Use Cloudflare R2 (S3-compatible API). Free egress, CDN-backed, zero bandwidth fees. |
| Consequences | Vendor 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
| Context | JWT must be stored client-side. localStorage is accessible to JavaScript — XSS attack vector. |
| Decision | Store JWT in HttpOnly cookie (_medusa_jwt). Not accessible via JS. Sent automatically on every request. |
| Consequences | Requires 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
| Context | Floating-point arithmetic produces rounding errors for currency. 0.1 + 0.2 !== 0.3 in IEEE 754. |
| Decision | Store all prices as integers in cents (AUD). 1999 = $19.99. Never use floats for money. |
| Consequences | All 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
| Context | Sequential download IDs are enumerable — an attacker could iterate /store/downloads/1, /2, /3. |
| Decision | Generate tokens via crypto.randomUUID(). UUID v4 = 2^122 possible values. Ownership validated server-side on every request regardless. |
| Consequences | UUIDs 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
| Context | Phase 1 is private (5 staff). Cost should be near-zero while validating the product. |
| Decision | Deploy Medusa backend to Fly.io free tier. Zero cost for Phase 1. |
| Consequences | Free 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)
| Context | A distributed system must choose between Consistency and Availability when partitions occur. |
| Decision | Choose AP. Prioritise availability for customers. Enforce strong consistency only where money or file access is involved (token redemption, order idempotency). |
| Consequences | Eventual 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
| Context | Medusa minor releases have historically introduced breaking changes. ^ semver ranges would auto-upgrade on npm install. |
| Decision | Pin all @medusajs/* packages to exact versions in package.json. Update deliberately and test in staging. |
| Consequences | Manual 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. |