Self-hosting Mailgrid
MIT-licensed. Self-host on Cloudflare + AWS for ~$8/mo plus SES at-cost. Full setup in 15 minutes.
Prerequisites
- Node 20+,
npm - Wrangler 4+ (
npm i -g wrangler && wrangler login) - AWS account with SES (or use the included Terraform module)
- A domain on Cloudflare (zone-level access)
Step 1 — Cloudflare resources
export CLOUDFLARE_ACCOUNT_ID=<your-account-id> wrangler d1 create inboxos # prints database_id wrangler kv namespace create KV # prints id wrangler r2 bucket create inboxos-emails wrangler queues create inboxos-webhooks wrangler queues create inboxos-webhooks-dlq
Paste the printed database_id and KV id into wrangler.toml. Set account_id at the top.
Step 2 — Secrets
openssl rand -hex 32 | wrangler secret put API_KEY_HMAC_SECRET openssl rand -hex 32 | wrangler secret put UNSUBSCRIBE_SECRET wrangler secret put AWS_ACCESS_KEY_ID wrangler secret put AWS_SECRET_ACCESS_KEY
Save the API_KEY_HMAC_SECRET value — you'll need it again to seed the first tenant.
Step 3 — Migrate & deploy
wrangler d1 migrations apply inboxos --remote wrangler deploy
Step 4 — Seed first tenant
node scripts/bootstrap-tenant.mjs \ --name "Your Company" \ --email "you@example.com" \ --hmac-secret "$(your HMAC secret)" > /tmp/seed.sql wrangler d1 execute inboxos --remote --file=/tmp/seed.sql
Save the printed mb_live_… token — it cannot be recovered.
Step 5 — AWS via Terraform (option A)
cd infra/aws cp terraform.tfvars.example terraform.tfvars $EDITOR terraform.tfvars ./bootstrap.sh # runs terraform + pipes IAM keys to wrangler secret
Step 5 — AWS manual (option B)
- SES → Verified identities → Create → Domain → enable Easy DKIM.
- Configuration sets → Create
inboxos-prod. - Event destinations → Add → SNS → create
mailgrid-eventstopic. - SNS → topic → Create subscription → HTTPS →
https://api.your-domain/api/webhooks/ses. - IAM → create user → policy scoped to
ses:SendEmail,ses:SendRawEmail,ses:SendBulkEmail. - Generate access keys → push to Wrangler.
Step 6 — Request SES production access
One-time AWS form. Until approved, you can only send to verified recipient addresses. SES → Account dashboard → Request production access.
Step 7 — Smoke test
curl -X POST https://api.your-domain/api/emails \ -H "Authorization: Bearer mb_live_..." \ -H "Content-Type: application/json" \ -d '{ "from":"hello@your-domain", "to":"you@example.com", "subject":"first", "text":"it lives" }'
Cost estimate
| Component | 10k emails/mo | 1M emails/mo |
|---|---|---|
| Cloudflare Workers Paid plan | $5.00 | $5.00 |
| D1 reads/writes | $0.40 | ~$3 |
| R2 storage + ops | $0.05 | ~$2 |
| KV reads | $0.01 | $0.50 |
| Durable Objects | $0.15 | $5 |
| Workers AI | $1.00 | $10 |
| AWS SES ($0.10/k) | $1.00 | $100 |
| Total | $7.61 | ~$125 |