Mailbox and Email
Outbound mail uses branded HTML templates per category. Product mail sends from [email protected]; help and support replies use [email protected]. Transport is via Cloudflare Email Sending (Lorapok Labs domain).
Transport priority
ccm-mail-relayWorker (Pages service binding →send_email) — preferred; no REST API token- Cloudflare
EMAILbinding (Workers only) - Cloudflare Email REST API (
CLOUDFLARE_EMAIL_API_TOKEN) - Resend fallback (
RESEND_API_KEY)
Production setup
- Onboard
lorapok.techin Cloudflare Email → Email Sending - One-time sync cred vault → GitHub
admin-productionenvironment secrets:
gh secret set CLOUDFLARE_EMAIL_API_TOKEN --env admin-production \
--body "$(cred get cursor cloudflare_email_api_token)"
# CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID should already be set for admin-deploy
- One-time inbound routing (forward
@lorapok.techmail to ops inbox):
export CLOUDFLARE_API_TOKEN="$(cred get cursor cloudflare_api_token)"
export CLOUDFLARE_ACCOUNT_ID="$(cred get cursor cloudflare_account_id)"
node website/admin/scripts/setup-email-addresses.mjs
- CI/CD on push to
main—admin-deploydeploys Pages only when Mission Control paths change. It skipsenable-mail.mjsand stats-cron deploy to avoid slow Cloudflare mail API calls. Full mail repair runs via workflow_dispatch → deploy-infra or locally.
- Manual repair when mail breaks or you need an immediate fix:
node website/admin/scripts/repair-mail.mjs # same steps as CI, locally
- Use Mailbox → Send branded test email to verify outbound delivery
- Verify inbound routing (external →
@lorapok.tech):
node website/admin/scripts/verify-inbound-routing.mjs
# Manual: send from Gmail to [email protected] → should forward to opsForwardTo
For Mission Control Sync routing, sync Pages secret once: node website/admin/scripts/setup-routing-secret.mjs
Full blueprint (inbound + outbound + CI): Cloudflare Email and Routing.
Message categories
| Category | Template | Logo asset | Trigger |
|---|---|---|---|
subscribe | Branded welcome | logo-product.png | Website subscribe form |
invite | Admin invitation | logo-product.png | Team invite |
notice | Severity-styled notice | logo-notice.png | Dev notice broadcast |
compose | Mission Control message | logo-product.png | Admin compose |
test | Delivery confirmation | logo-help.png | Mailbox test button |
Storage (D1 + R2)
Outbound mail metadata is migrating off hot KV paths:
| Store | Contents |
|---|---|
ADMIN_D1 mail_messages | Per-send mailbox rows (replaces mailbox:messages KV blob) |
ADMIN_D1 mail_audit_resend | Masked Resend audit index |
ADMIN_D1 subscriber_index | Subscriber email hash index (Phase 2) |
STATS_R2 mail/audit/ | Raw masked audit JSON |
| KV scatter | subscriber:email:*, legacy subscribers array, mail aliases config |
Apply D1 schema after deploy:
cd website/admin
npx wrangler d1 execute ccm-admin-d1 --remote --file=./d1/schema.sql
Replay saved sends (vault redirect default):
node website/admin/scripts/resend-saved-mail.mjs # dry-run
node website/admin/scripts/resend-saved-mail.mjs --send # deliver (redirected)
node website/admin/scripts/resend-saved-mail.mjs --source=d1 # reconstruct from D1 logs
Subscribe validation
POST /api/subscribe rejects duplicate emails with 409 and { ok: false, error: "already_subscribed" }. The marketing site shows a friendly “already subscribed” message instead of an error.
All templates use the CCM / Lorapok Labs dark theme with animated gradient header bar, stat pills, and CTA buttons. Template logic lives in website/admin/functions/api/_shared/mail-branding.js and mail.js.
Mission Control mailbox UI
- Mail hub (
/dashboard/mail) — transport checklist, Cloudflare/Resend config, email identities (aliases), masked redirect target, link to Mailbox - Message log — filter by direction, category, status; search addresses and subjects
- Read modal — click any row to preview HTML (iframe) and plain text
- Compose — send branded HTML mail to any recipient
- Test — verify transport with a delivery confirmation email
Credential split (required)
| Variable | Purpose |
|---|---|
CLOUDFLARE_API_TOKEN | Pages/Workers deploy only (wrangler) |
CLOUDFLARE_EMAIL_API_TOKEN | Email Sending REST only (Pages secret) |
Never sync the deploy token as CLOUDFLARE_EMAIL_API_TOKEN. Load from secure cred vault:
export CLOUDFLARE_API_TOKEN="$(cred get cursor cloudflare_api_token)"
export CLOUDFLARE_EMAIL_API_TOKEN="$(cred get cursor cloudflare_email_api_token)"
export CLOUDFLARE_ACCOUNT_ID="$(cred get cursor cloudflare_account_id)"
Scripts
node website/admin/scripts/setup-email-addresses.mjs # inbound routing (one-time)
node website/admin/scripts/verify-mail-setup.mjs # probe email token
node website/admin/scripts/enable-mail.mjs # relay worker + Pages secret
node website/admin/scripts/repair-mail.mjs # full fix: enable + build + deploy + verify
node website/admin/scripts/deploy-pages-fast.mjs # fast deploy without mail API calls
node website/admin/scripts/probe-mail-token.mjs
node website/admin/scripts/setup-mail-secrets.mjs
node website/admin/scripts/resend-saved-mail.mjs # replay KV/D1 saved mail (dry-run default)
Troubleshooting
| Symptom | Fix |
|---|---|
| HTTP 401 on send | CLOUDFLARE_EMAIL_API_TOKEN needs Email Sending → Edit; redeploy Pages after wrangler pages secret put |
| Code 10203 | Enable Email Sending on account; onboard lorapok.tech in dashboard |
cloudflare-rest in health, 401 | MAIL_RELAY missing — run repair-mail.mjs |
| Mail works locally, fails in prod | Redeploy Pages after secret sync |