Ember · architecture
v0.1.2 15 invariants
System architecture

Ember — direct-to-consumer coffee storefront

A containerized, HTTPS-only monorepo. A reverse proxy terminates TLS and routes to the Next.js storefront and the Express REST API; a background worker drains the email outbox; Postgres and Redis are the stateful backing services; Stripe and Resend are external integrations. Payment and subscription state mutate only via verified webhooks (webhook-as-source-of-truth).

Next.js 16Express 5Prisma 6 PostgreSQL 18Redis 7Caddy 2 StripeResendDocker ComposeTypeScript
Client

Browser

shopper · staff
  • Next.js storefront (SSR + hydrated React)
  • Stripe-hosted Checkout (card data never touches Ember → PCI SAQ A)
HTTPS · TLS 1.2/1.3 · HSTS
Edge

Caddy — reverse proxy

:443
  • Auto-HTTPS (Let's Encrypt) · HTTP→HTTPS 301 · HSTS preload
  • Security headers + CSP (Stripe allow-listed; SAQ A script hygiene)
  • /api/* → api  ·  / → web
routes by path
Application

web

Next.js 16 · :3000
  • App Router, RSC, next/image
  • 13 screens · token-driven design system
  • calls api via REST

api

Express 5 · :4000
  • REST: auth · catalog · cart · checkout · orders · subscriptions · admin
  • Stripe + Resend webhooks (raw-body verify)
  • middleware: session · CSRF · rate-limit · RFC 9457 · audit

worker

outbox drain
  • polls pending_emails
  • sends via Resend (idempotency key)
  • capped backoff → DLQ
Prisma 6 · ioredis
Data

PostgreSQL 18

Prisma 6
  • 15 tables · ULID keys · integer-cents money
  • UNIQUE(email), UNIQUE(event_id), CHECK(stock ≥ 0)
  • immutable order_items snapshots · transactional outbox

Redis 7

cache / limits
  • rate-limit store (multi-replica safe)
  • session cache
External integrations  ·  api & worker ⇄ providers (webhooks inbound)

Stripe

  • hosted Checkout + Billing
  • webhooks: checkout.session.completed, invoice, subscription.*
  • source-of-truth for payment state

Resend

  • transactional email (order confirmation)
  • Svix webhooks: bounce / complaint

HIBP

  • breached-password screen
  • k-anonymity (SHA-1 prefix only)
Edge / TLS Application services Stateful data External providers

Request flow — browse → buy

  1. Browse. Shopper opens /coffee/[slug]; web SSR-fetches GET /api/products/:slug.
  2. Add to cart. POST /api/cart/items — session-scoped cart in Postgres, optimistic UI.
  3. Checkout. POST /api/checkout/session reserves stock (atomic conditional decrement), creates a pending order, opens a Stripe Checkout Session.
  4. Pay. Card entered on Stripe-hosted page — no card data on Ember servers.
  5. Webhook. Stripe → /api/webhooks/stripe: verify signature → idempotency insert → on paid, flip order + enqueue confirmation email in the same transaction.
  6. Email. worker drains the outbox → Resend sends the confirmation.
  7. Confirm. /order/[id] reads Ember's own DB — never trusts the redirect as proof of payment.

Architectural invariants

15 rules in invariants.json (12 machine-checked + 3 manual), enforced by the build lint and the ship-time drift gate.

INV-1No raw card data anywhere — Stripe-hosted only (SAQ A).
INV-4/6Webhook signature verified before processing; idempotency via UNIQUE(event_id).
INV-8Rate limiter runs before the Argon2 password verify.
INV-9Design tokens only — no hard-coded colors outside the token layer.
INV-10Email sent only from the outbox/worker path (no inline sends).
INV-13/15Tenant isolation via scopedRepo; stock never negative (CHECK + atomic decrement).
webhook-as-source-of-truth transactional outbox RFC 9457 errors
← Gallery