Simple Inbox

Architecture

The single-Worker runtime, logical module boundaries, storage, and event flows.

Simple Inbox is deployed as one Cloudflare Worker named simple-inbox-cf.

                       +-------------------------------+
browser ---- fetch() ->|                               |----> D1
Email Routing email() ->      simple-inbox-cf          |----> private R2
Cloudflare Cron -------->                               |----> Email Sending
                       +-------------------------------+
                          web -> API -> mail modules
                               (in process)

The root entry exports fetch, email, and scheduled. Web, API, and mail remain separate source packages, but they run in one isolate and use in-process adapters rather than Service Bindings.

Logical responsibilities

ComponentOwns
Root/webPublic routing, UI/SSR, docs, security headers, setup redirects, /api/v1 bridge
APISetup, sessions, CSRF, scopes, mailbox authorization, contracts, orchestration
MailInbound capture, MIME parsing, threading, forwarding, outbound delivery, retention
D1Installation, users, mailboxes, projections, sessions, idempotency, workflows, tombstones
R2Canonical raw .eml objects under opaque keys

The root router is the only HTTP edge. The API calls mail through a narrow in-memory fetcher that accepts only the fixed https://mail.internal/internal/* surface. The public router never maps those mail routes, so /internal/* is not externally reachable.

Because all bindings share one physical Worker, source-package APIs, explicit binding construction, repository ownership, authorization checks, and boundary tests enforce the logical trust zones.

Installation flow

Before setup, only setup and public service metadata are usable. Protected APIs return unavailable, inbound messages are rejected, scheduled retention is idle, and UI navigation goes to /setup.

The setup request must be same-origin and HTTPS, is source-rate-limited, and requires the independent SETUP_TOKEN. One transactional D1 batch creates the normalized owner, primary mailbox, only owner membership, and singleton installation record. The record stores the verified public origin, mail domain, and retention values. Exact replay is idempotent; conflicting or incomplete state fails closed.

Browser request flow

The browser calls same-origin /api/v1/*. The root Worker forwards only reviewed headers into the API app and regenerates a request ID. The API authenticates the caller, enforces the required scope and mailbox membership, validates the contract, and invokes a repository or private mail adapter.

Every raw-message or attachment response authorizes against D1 metadata before reading R2. Private responses use no-store caching, and object keys never appear in browser DTOs.

Inbound and outbound mail

Inbound email() processing persists the canonical raw message in R2, projects bounded searchable content into D1, associates or creates a thread, and then optionally forwards to the owner. Durable capture precedes forwarding, so forwarding failure does not erase mail.

Outbound operations reserve idempotency before invoking Email Sending. Provider interruption or an ambiguous response remains unknown; the application does not blindly retry and risk a duplicate. R2 remains outside D1 transactions and has a separate repair/lifecycle path.

Retention

The daily scheduled() handler advances at most the configured batch size. It deletes raw R2 bytes first, then transactionally removes expired D1 message data and repairs/deletes threads. Durable content-free tombstones retain retry state. Retention never invokes Email Sending, and Worker rollback cannot restore deleted data.

Deployment boundary

The root Wrangler config declares one Worker, D1, R2, Email Sending, rate limiting, and cron. Wrangler provisions D1/R2 and the root deploy task builds, migrates, and uploads the generated Worker config. Email domain verification, R2 lifecycle, custom domains, and Email Routing remain manual Cloudflare Dashboard owner actions.

No repository command discovers, imports, switches, or deletes a legacy resource.

On this page