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