Docs / Concepts / Architecture

Architecture

Mailgrid runs entirely on Cloudflare's edge with AWS SES as the outbound gateway. Here's the topology.

Overview

topology
                       Cloudflare Edge (Worker + bindings)
                    ┌─────────────────────────────────────┐
   HTTP / MCP / ───►│  Hono router                        │── SigV4 ──► AWS SES (ap-south-1)
   SMTP / Webhook   │     ├ middleware: auth, rate, valid │
                    │     └ 22 route modules              │            ▲
                    │       │  ▲                          │            │
                    │       ▼  │                          │            │
                    │  src/domain/* (15 modules)          │            │
                    │       │                             │            │
                    │  ┌────┴────┬──────┬─────┬──────┐    │            │
                    │  │   D1    │  R2  │ KV  │ AI   │    │            │
                    │  │ SQLite  │ EMLs │keys │Llama │    │            │
                    │  └─────────┴──────┴─────┴──────┘    │            │
                    │  ┌─────────┐  ┌──────────────┐      │            │
                    │  │ Queues  │  │ Durable Obj  │      │            │
                    │  │ webhook │  │ RateLimiter  │      │            │
                    │  │  + DLQ  │  │ (per-tenant) │      │            │
                    │  └─────────┘  └──────────────┘      │            │
                    └─────────────────────────────────────┘            │
                                  ▲                                    │
                                  └── SNS subscription ── topic ◄──────┘
                                      (bounce/complaint/open/click)

Storage

ServiceWhatWhy
D1Tenants, keys, events, suppressions, templates, contacts, streams, files, IPs, users, audit logsSQLite, relational, pay-per-request
R2Raw EML archive + File Cache binariesS3-compatible, no egress fees
KVAPI-key prefix → tenant cacheSub-ms reads on every auth check
Durable ObjectsPer-tenant rate-limit bucketStrong consistency for token-bucket math
QueuesSES webhook → event processingAsync + DLQ + retry
Workers AILlama 3.1 8B (templates, summarization, Q&A)Lives on the same edge

A request flow

For a single POST /api/emails:

  1. Cloudflare's edge accepts the TLS connection.
  2. Worker entry (src/index.ts) routes via Hono.
  3. Middleware: request-id → auth (KV lookup → D1 verify) → rate limit (DO) → Zod validate.
  4. Route handler in src/http/routes/emails.ts calls domain/email.ts:send().
  5. Domain checks suppressions, claims idempotency, renders template, resolves stream + attachments, signs SES request via SigV4.
  6. SES accepts the message and returns a messageId.
  7. waitUntil() schedules R2 archive write + event row, response returns.
  8. SES later publishes delivered / opened / bounced to SNS, which POSTs to /api/webhooks/ses, which enqueues into Cloudflare Queues, which consumes and writes more event rows.

No AWS SDK

The Workers bundle limit is 10 MB compressed. The AWS JavaScript SDK is ~2 MB just for SES. So Mailgrid does AWS API calls via raw fetch() + a hand-rolled SigV4 signer in src/lib/sigv4.ts (~150 lines). The total Worker bundle is currently 419 KB.

Why Cloudflare over AWS Lambda

DimensionWorkersLambda
Cold start~5 ms100–1000 ms
Global edgeDefault, 300+ POPsLambda@Edge / CloudFront extras
Stateful primitivesD1, R2, KV, DO, Queues bundledDynamoDB, S3, ElastiCache — all separate
Pricing (100k req/day)~$5/mo~$20/mo + egress
Outbound HTTPSFreePaid egress

Why AWS SES over Resend/Postmark