Diagrams

ASCII diagrams of the paths that come up most in debugging.

Request lifecycle — POST /api/v1/emails

┌─────────┐     HTTPS             ┌────────────────────────────────┐
│ Client  │ ───────────────────→  │  Next.js route handler         │
└─────────┘                       │  src/app/api/v1/emails/route.ts│
     ▲                            └──────────────┬─────────────────┘
     │                                           │
     │ 200 {id, status}                          ▼
     │                            ┌──────────────────────────────┐
     │                            │  withApiAuth(handler)        │
     │                            │  ├─ parse Bearer token       │
     │                            │  ├─ validateApiKey()         │
     │                            │  │   └─ sha256 lookup        │
     │                            │  ├─ resolve tenant + domains │
     │                            │  └─ checkRateLimit() Upstash │
     │                            └──────────────┬───────────────┘
     │                                           │ ctx{orgId,tenantId,...}
     │                                           ▼
     │                            ┌──────────────────────────────┐
     │                            │  Handler                     │
     │                            │  ├─ check Idempotency-Key    │
     │                            │  │   ├─ hit → return cache   │
     │                            │  │   └─ miss → store lock    │
     │                            │  ├─ checkUsageLimit()        │
     │                            │  ├─ zod parse schema         │
     │                            │  ├─ filter suppressions      │
     │                            │  ├─ render template          │
     │                            │  └─ sendEmail() via SES v2 ──┼─→  AWS SES
     │                            └──────────────┬───────────────┘       │
     │                                           │                       │ async
     │                                           ▼                       │ (SNS topic)
     │                            ┌──────────────────────────────┐       ▼
     │                            │  INSERT messages             │    POST /api/webhooks/ses
     │                            │  incrementUsage()            │       │
     │                            │  storeIdempotency()          │       │
     │                            └──────────────┬───────────────┘       │
     │                                           │                       │
     │                                           ▼                       │
     │                            ┌──────────────────────────────┐       │
     │                            │  after() — non-blocking:     │       │
     │                            │  fanoutWebhookEvent(         │       │
     │                            │    "message.sent")           │       │
     │                            └──────────────┬───────────────┘       │
     │                                           │                       │
     └───────────────────────────────────────────┘                       │
                                                                         │
                            ┌────────────────────────────────────────────┘
                            │
                            ▼
        Later: message.delivered / bounced / failed
        UPDATE messages; fanoutWebhookEvent(same)

Webhook fan-out with retries

 source of event (send success, SES notification, SNS delivery)
                     │
                     ▼
  ┌──────────────────────────────────────────────┐
  │  fanoutWebhookEvent(orgId, event, data,      │
  │                      tenantId?)              │
  │                                              │
  │  SELECT webhook_endpoints                    │
  │    WHERE org_id = ?                          │
  │      AND enabled = true                      │
  │      AND event = ANY(events)                 │
  │      AND (tenant_id IS NULL OR = ?)          │
  └──────────────────────┬───────────────────────┘
                         │
                  for each endpoint
                         │
                         ▼
  ┌──────────────────────────────────────────────┐
  │  INSERT webhook_deliveries                   │
  │    {status: pending, attempts: 0}            │
  └──────────────────────┬───────────────────────┘
                         │
                         ▼
  ┌──────────────────────────────────────────────┐
  │  attemptDelivery(delivery)                   │
  │   ├─ safeFetch(url, 10s timeout,             │
  │   │    redirect: "error",                    │
  │   │    HMAC-sign body)                       │
  │   ├─ 2xx → UPDATE status=delivered           │
  │   └─ non-2xx / timeout / err:                │
  │        UPDATE attempts++, next_attempt_at    │
  │        = now + backoff[attempts]             │
  │        if attempts >= 5 → status=failed      │
  └──────────────────────────────────────────────┘

  backoff[] = [ 1s, 5s, 25s, 125s, 625s ]

                         │
                         │  (for pending, next_attempt_at <= now)
                         ▼
  ┌──────────────────────────────────────────────┐
  │  Cron /api/cron/retry-webhooks  (every 5m)   │
  │   SELECT pending where next_attempt_at<=now  │
  │   → attemptDelivery(each)                    │
  └──────────────────────────────────────────────┘

Audience send (10k recipients)

 POST /api/v1/audiences/aud_X/send
              │
              ▼
  ┌─────────────────────────────────┐
  │  Resolve audience + template    │
  │  Resolve sender domain          │
  │  checkUsageLimit() for the lot  │
  └──────────┬──────────────────────┘
             │
             ▼
  ┌─────────────────────────────────────────────┐
  │  Stream contacts from DB in pages of 1000   │
  │    exclude suppressed (tenant + org scoped) │
  └──────────┬──────────────────────────────────┘
             │
             ▼
  ┌─────────────────────────────────────────────┐
  │  Build message rows { id, to, html, ... }   │
  │  Dynamic chunking by byte count:            │
  │    accum += Buffer.byteLength(row)          │
  │    if accum >= 500KB → flush chunk          │
  └──────────┬──────────────────────────────────┘
             │
             ▼
  ┌─────────────────────────────────────────────┐
  │  db.insert(messages).values(chunk)          │
  │    status: "queued"                         │
  │  (≤1MB Neon HTTP statement cap respected)   │
  └──────────┬──────────────────────────────────┘
             │
             ▼
 Return 200  { queued: 23417, skipped_suppressed: 112 }
             │
             │  (caller's HTTP request is done)
             ▼
  ┌─────────────────────────────────────────────┐
  │  Cron /api/cron/drain-queue (every 1m)      │
  │    SELECT queued messages LIMIT 200         │
  │    → sendEmail() in parallel w/ p-limit     │
  │    UPDATE status, sentAt                    │
  │    fanoutWebhookEvent("message.sent")       │
  └──────────┬──────────────────────────────────┘
             │
             ▼
  ┌─────────────────────────────────────────────┐
  │  AWS SES async notifications                │
  │    → /api/webhooks/ses                      │
  │    UPDATE status (delivered/bounced)        │
  │    fanoutWebhookEvent(same)                 │
  └─────────────────────────────────────────────┘

Platform mode data model

                        ┌─────────────┐
                        │     org     │
                        │    (root)   │
                        └──────┬──────┘
                               │
               ┌───────────────┼───────────────┐
               │               │               │
       ┌───────▼──────┐ ┌──────▼─────┐ ┌──────▼──────┐
       │   tenant A   │ │  tenant B  │ │  tenant C   │
       │              │ │            │ │             │
       │  customer-a  │ │ customer-b │ │ customer-c  │
       └──────┬───────┘ └──────┬─────┘ └──────┬──────┘
              │                │               │
              │                │               │
   ┌──────────┴──────┐ ┌───────┴─────┐ ┌──────┴───────┐
   │ api_keys        │ │ api_keys    │ │ api_keys     │
   │ domains         │ │ domains     │ │ domains      │
   │ webhooks        │ │ webhooks    │ │ webhooks     │
   │ templates       │ │ templates   │ │ templates    │
   │ audiences       │ │ audiences   │ │ audiences    │
   │ suppressions    │ │ suppressions│ │ suppressions │
   │ messages        │ │ messages    │ │ messages     │
   │ usage_counters  │ │ usage_...   │ │ usage_...    │
   └─────────────────┘ └─────────────┘ └──────────────┘
   (tenantId = A)      (tenantId = B)  (tenantId = C)

   ─────────────── shared infrastructure ───────────────
   ┌─────────────────────────────────────────────────────┐
   │  AWS SES (one account)                              │
   │  AWS SNS (one account)                              │
   │  Stripe (org-level subscription)                    │
   │  Neon (one database)                                │
   │  Upstash Redis (one namespace, tenant-aware keys)   │
   └─────────────────────────────────────────────────────┘

Read/write rules:
  • platform-root key (tenantId = null on api_key) sees everything under org
  • tenant-bound key (tenantId = A) sees only tenant A + null-scoped shared data
  • suppressions with tenantId = null → shared across tenants (rare, admin use)

Auth flow (NextAuth v4 credentials)

  POST /api/auth/callback/credentials
                │
                ▼
  ┌──────────────────────────────┐
  │  authorize(credentials)       │
  │   ├─ lookup user by email     │
  │   ├─ bcrypt.compare(password) │
  │   ├─ if 2FA enrolled:         │
  │   │    verify TOTP code       │
  │   └─ return {id, email}       │
  └──────────┬───────────────────┘
             │
             ▼
  ┌──────────────────────────────┐
  │  jwt callback                │
  │   ├─ load membership          │
  │   ├─ set token.orgId          │
  │   │       token.role          │
  │   │       token.session_id    │
  │   │       token.session_version │
  │   └─ INSERT user_sessions     │
  └──────────┬───────────────────┘
             │
             ▼
  ┌──────────────────────────────┐
  │  session callback            │
  │   └─ expose {user, orgId,    │
  │       role, session_id,      │
  │       session_version}       │
  └──────────────────────────────┘

  Subsequent request:
  ┌──────────────────────────────┐
  │  requireSession()            │
  │   ├─ getServerSession()      │
  │   ├─ 30s in-memory cache     │
  │   │   for {session_id →      │
  │   │        user_sessions}    │
  │   ├─ check session_version   │
  │   │   matches stored         │
  │   └─ return active ctx       │
  └──────────────────────────────┘

  Revoke (e.g. password change):
  ┌──────────────────────────────┐
  │  sql.transaction([           │
  │    UPDATE users SET pw=...   │
  │    UPDATE users SET          │
  │      session_version++,      │
  │    DELETE user_sessions      │
  │      WHERE user_id = ?       │
  │  ])                          │
  └──────────────────────────────┘
  All in-flight JWTs now invalid.

Scheduled send lifecycle

 POST /api/v1/emails
 { scheduled_at: "2026-05-01T12:00:00Z" }
              │
              ▼
  INSERT messages
   status = "scheduled"
   scheduled_at = 2026-05-01 12:00:00Z

              │
              │  time passes
              ▼
  Cron /api/cron/send-scheduled (every 1m)
   SELECT messages
     WHERE status = "scheduled"
       AND scheduled_at <= now()
     LIMIT 100

              │
              ▼
  For each:
   sendEmail() or sendSms()
   UPDATE status = "sent", sentAt = now
   fanoutWebhookEvent("message.sent")
   incrementUsage()

              │
              ▼
  (message continues down the normal
   delivery/bounce/complaint flow)

  Cancel path:
  DELETE /api/v1/emails/msg_X
    if status = "scheduled" → UPDATE status = "canceled"
    else → 409 NOT_SCHEDULED

Idempotency replay window

 Request 1: {key, body} at t=0
              │
              ▼
   INSERT idempotency_keys
    { key, body_hash, response=null,
      locked_until = t+30s }
              │
              ▼
   run handler → response
              │
              ▼
   UPDATE idempotency_keys
    SET response = <json>,
        locked_until = null
              │
              ▼
   return response

 Request 2: {key, body} at t=2s (same body)
              │
              ▼
   SELECT idempotency_keys WHERE key=?
              │
              ▼
   locked_until > now → 409 IDEMPOTENCY_IN_FLIGHT
        (client should wait ~500ms and retry)
   else if body_hash matches → return cached response
   else → 409 IDEMPOTENCY_MISMATCH

 After 24h:
   DELETE row (cleanup cron)
   Subsequent replay → fresh send