Install
$ agentstack add mcp-santigamo-reccado ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
The edge-native inbox — self-hosted email on Cloudflare, for your own domains
Receive, store, thread, search and send email from your own domains, running entirely on Cloudflare (Workers · Durable Objects · R2 · D1 · Queues) — with an MCP layer on the roadmap so agents can read, triage and draft your mail.
[](https://github.com/santigamo/reccado/actions/workflows/ci.yml) [](./LICENSE) [](https://workers.cloudflare.com) [](https://www.typescriptlang.org) [](https://hono.dev) [](CHANGELOG.md)
[](https://deploy.workers.cloudflare.com/?url=https://github.com/santigamo/reccado)
Reccado is a self-hosted, full-serverless email inbox that runs entirely on your own Cloudflare account — no third-party mail provider, no separate database to operate, no servers to patch.
Table of contents
- [Features](#features)
- [How it works](#how-it-works)
- [Quickstart (prove it locally in ~5 min)](#quickstart-prove-it-locally-in-5-min)
- [Deploy your own](#deploy-your-own)
- [1. Provision and deploy (pick one)](#1-provision-and-deploy-pick-one)
- [2. Wire your domain](#2-wire-your-domain)
- [3. Verify](#3-verify)
- [Configuration](#configuration)
- [Compatibility](#compatibility)
- [Troubleshooting](#troubleshooting)
- [Learn more](#learn-more)
Features
- Self-hosted, your Cloudflare account — your mail, your R2, your D1, your Durable Objects.
Nothing leaves your account.
- Full-serverless — Workers, Durable Objects, R2, D1 and Queues only. No VM, no container,
no third-party database to provision or back up.
- One Durable Object per mailbox — canonical mailbox state (messages, threads, labels, FTS
search, drafts, idempotency) lives in private per-mailbox SQLite, not a shared database.
- Idempotent inbound pipeline — Email Routing → R2 → Queue → Durable Object, with a DLQ for
poison messages and dedupe on Message-ID/raw hash so retries never double-store a message.
- Realtime UI — hibernatable WebSockets push new mail into the inbox without polling or
refreshing.
- Full-text search — SQLite FTS5 per mailbox over subject, sender, recipients and body.
- Human-confirmed sending — outbound mail always goes through an explicit draft →
request-send → confirm-send flow with an idempotency key; nothing sends silently.
- Multi-domain routing — store, forward or reject rules per domain/alias, with isolated
mailboxes per address.
- Agent-ready (optional) — an MCP layer is on the roadmap so agents can read, search and
draft mail, gated by the same human-confirmation invariant as the UI (see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)).
How it works
Inbound mail never touches a server you manage. Cloudflare Email Routing hands the raw message to a Worker, which writes it straight to R2 and enqueues a small metadata event; a Queue consumer hands that event to the one Durable Object that owns the target mailbox, which parses, indexes and pushes a realtime update to any open UI session.
flowchart LR
sender(("External sender")) -->|SMTP| ER["Email RoutingWorker: email handler"]
ER -->|raw MIME bytes| R2[("R2raw MIME + attachments")]
ER -->|metadata only, under 128 KiB| Q["Queueinbound-email"]
Q -->|terminal failures| DLQ[("Dead Letter Queue")]
Q --> DO["Mailbox Durable ObjectSQLite: messages · threads · FTS"]
R2 -. fetch raw MIME to parse .-> DO
DO -->|cross-mailbox index| D1[("D1domains · aliases · message_index")]
DO -->|realtime push| WS["Hibernatable WebSocket"]
API["Hono API Worker"] --> DO
UI["TanStack Start UI"] -->|HTTP| API
UI |live updates| WS
Queue messages carry metadata only (mailbox ID, R2 key, hashes, headers) — raw MIME and parsed bodies never leave R2 and the owning Durable Object. D1 is a rebuildable cross-mailbox index, not the source of truth: the mailbox Durable Object is the only component allowed to decide canonical mailbox state. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full accepted architecture and the outbound flow.
Quickstart (prove it locally in ~5 min)
This runs entirely on your machine via local Cloudflare Workers emulation (@cloudflare/vite-plugin) — no Cloudflare account or deployed resources required.
Starting fresh (no clone yet)? Scaffold a copy with npx degit santigamo/reccado my-inbox && cd my-inbox (or node scripts/create-reccado.mjs my-inbox, which also installs and points you at pnpm doctor). Already in the repo:
corepack enable # provides the repo-pinned pnpm; skip if you already have pnpm
pnpm install
pnpm dev
Node 24 is pinned in [.node-version](.node-version) (any >=22.15.0 works), and a [.devcontainer](.devcontainer/devcontainer.json) is provided for one-click GitHub Codespaces / VS Code Dev Containers — open it and run pnpm dev.
You don't run a setup step first: pnpm dev runs a predev hook that generates a local .dev.vars (if one is missing), applies the D1 migrations, and seeds a test@example.com mailbox into the same local D1 the dev server binds to. It's all idempotent, so re-running is safe (skip the .dev.vars generation with RECCADO_SKIP_DEV_VARS=1). Vite defaults to port 3000; if that's taken it prints the port it actually bound to — use that one in the commands below.
That generated .dev.vars also unlocks the /api/debug/phase0/* introspection endpoints the smoke script below uses, and intentionally leaves Cloudflare Access unset so local /api/* falls back to the dev bypass. See [.dev.vars.example](.dev.vars.example) for every supported variable.
In a second terminal, check the health endpoint and simulate an inbound email:
curl -sS http://localhost:3000/api/health
# {"ok":true,"readiness":{"ok":true,"status":"ready"},...}
pnpm smoke:email:local http://localhost:3000 fixtures/mime/simple-text.eml
Expected output (the script posts the fixture twice to prove duplicate delivery is idempotent):
first-delivery: Worker successfully processed email
r2-head: {"exists":true,"key":"raw/dev/mbx_.../2026/06/30/...-.eml","size":250,...}
duplicate-delivery: Worker successfully processed email
debug: {"messageCount":1,"messages":[{"id":"...","idempotency_key":"email:v1:mbx_...:message-id:...","subject":"..."}]}
queue-payload-sample: {"eventType":"email.received.v1","mailboxId":"mbx_...","rawR2Key":"raw/dev/...","rawSha256":"...","idempotencyKey":"email:v1:mbx_...:message-id:..."}
PASS: local email smoke completed with one DO message after duplicate delivery
The first delivery is the success signal that matters: r2-head.exists: true means the raw MIME landed in R2, and debug.messageCount: 1 after two deliveries of the same fixture proves the Durable Object deduplicated it. Open http://localhost:3000/mailboxes to see the seeded test@example.com mailbox in the UI.
Other local commands:
pnpm doctor # diagnose toolchain + local dev + config, with an exact fix per issue
pnpm test # vitest (Workers runtime via @cloudflare/vitest-pool-workers)
pnpm typecheck # tsc --noEmit
pnpm lint # biome lint .
pnpm check # typecheck + lint + test in one shot
pnpm run build # production build
pnpm smoke:ws ws://localhost:3000/api/mailboxes/mbx_test/ws # WebSocket hello/pong/echo smoke
Deploy your own
Three steps: provision + deploy the Worker and its resources, wire your domain (custom domain + Email Routing/Sending + Access), then verify. Step 1 is fully automated — pick one way below. Steps 2 and 3 are always required, whichever way you did step 1.
> Safe to fork. wrangler.jsonc ships placeholder resource names, a placeholder D1 id, and > MAIL_FROM_ADDRESS=noreply@mail.example.com. At any point, pnpm doctor --env dev (add > --cloud/--url) shows exactly what's still a placeholder or missing and the command to fix it. > Full command-level detail: [docs/IMPLEMENTATION.md](docs/IMPLEMENTATION.md).
1. Provision and deploy (pick one)
Fastest — one-click button. Forks the repo, provisions the R2 bucket / D1 / queues / Durable Object, prompts you for the secrets, and deploys the Worker — entirely in the browser, no local tooling.
[](https://deploy.workers.cloudflare.com/?url=https://github.com/santigamo/reccado)
One follow-up: the button doesn't seed your first mailbox, so run pnpm setup:mailbox once afterward (the scripted path below does it for you).
Preferred — scripted. pnpm setup:cloud provisions the core Cloudflare resources, resolves the real D1 id into a gitignored wrangler.generated..json, builds the TanStack Start app for the chosen env, patches the real bindings into dist/server/wrangler.json, then applies migrations and deploys from that built config. It never edits the tracked wrangler.jsonc. It is dry-run by default — the first command prints the plan and changes nothing:
pnpm wrangler login
pnpm setup:cloud --env dev --domain --address inbox@ # preview the plan
pnpm setup:cloud --env dev --domain --address inbox@ --apply # run it
If MAILBOX_ID_SECRET is absent, setup:cloud --apply generates it once and immediately uses that same value to seed the first mailbox in the same run. If the secret already exists, the script leaves it untouched and prints the exact MAILBOX_ID_SECRET= pnpm setup:mailbox ... --apply command to run yourself, because Cloudflare secrets are write-only and the CLI cannot derive the mailbox id without the original value.
If a prior --apply run set MAILBOX_ID_SECRET but failed before seeding the first mailbox (a now-orphaned secret — write-only, with no mailbox that uses it), setup:cloud detects this (no fingerprint recorded in .reccado/setup..json) and points you at --reset-secret:
pnpm setup:cloud --env dev --domain --address inbox@ --reset-secret --apply
This overwrites MAILBOX_ID_SECRET with a fresh value and reseeds the mailbox atomically in the same run. Never run --reset-secret after go-live — it changes the derived mailbox_id of every mailbox already seeded with the current secret.
Manual — run the commands yourself. This is the escape hatch when you do not want the scripted path. The main difference is that you must manage the D1 id and mailbox seed coupling yourself:
pnpm wrangler r2 bucket create
pnpm wrangler queues create
pnpm wrangler queues create
pnpm wrangler d1 create --location=weur # or maintain your own deploy config
pnpm d1:migrate:dev # D1_DB_NAME_DEV= to override the name
pnpm wrangler secret put MAILBOX_ID_SECRET --env dev # + ACCESS_JWT_AUDIENCE / ACCESS_TEAM_DOMAIN (step 2)
pnpm run deploy:dev # build + wrangler deploy --env dev --name reccado-dev
The Durable Object (MAILBOX_DO) needs no create step — Wrangler provisions it from the migrations block on first deploy. MAILBOX_ID_SECRET becomes write-only once set, so pair it with pnpm setup:mailbox while you still have the value. Drop --env dev (and use deploy / d1:migrate:prod) for production. Every secret is documented in [.dev.vars.example](.dev.vars.example) and [Configuration](#configuration); full detail in [docs/IMPLEMENTATION.md](docs/IMPLEMENTATION.md).
2. Wire your domain
DNS and identity live outside the Worker, so no button or script fully does them for you.
Custom domain — make the UI/API reachable on a hostname you control before treating it as an inbox. workers.dev is useful for smoke tests, but the supported protected path is custom domain + Cloudflare Access.
pnpm setup:domain --env dev --hostname inbox. # dry run
pnpm setup:domain --env dev --hostname inbox. --apply
Re-running against the same Worker is idempotent — safe to repeat. If the hostname is already attached to a different Worker (e.g. left over from a rename), the script refuses to steal it and prints how to detach it first instead of silently reassigning it. With CLOUDFLARE_API_TOKEN set (plus CLOUDFLARE_ACCOUNT_ID, or an account resolvable via wrangler whoami), this is checked up front through the Workers Custom Domains API; without a token, it falls back to an error-string check around the wrangler deploy call itself.
Email Routing — point inbound mail at the Worker. pnpm setup:routing --domain --env dev scripts the automatable pieces (enable routing + create the explicit-address "send to Worker" rule) — dry-run by default, --apply to run — and prints the required MX/SPF/DKIM records. Add --catch-all to configure *@ -> Worker; that path uses Cloudflare's Email Routing REST API because Wrangler currently rejects catch-all worker actions client-side even though the platform supports them. The DNS records are the one part you must add yourself (check status with pnpm wrangler email routing settings ).
Email Sending — configure outbound identity on a dedicated sending subdomain.
pnpm setup:sending --env dev --domain # dry run, hello@send.
pnpm setup:sending --env dev --domain --dmarc-rua you@ --apply # start the DMARC ramp with reports
Every run prints a loud Workers Paid preflight first: Cloudflare Email Sending on a free plan can only send to verified destination addresses — sending to arbitrary recipients requires a Workers Paid plan. The script can't detect your plan via the API, so this check is manual and does not block --apply.
--apply enables Email Sending for the subdomain, writes MAIL_FROM_ADDRESS + allowed_sender_addresses into wrangler.generated..json, and upserts SPF (always) and DMARC (per the ramp below) — the two records the script keeps under its own control. With CLOUDFLARE_API_TOKEN set, it also auto-adds the provider-generated DKIM TXT + MX records (parsed from wrangler email sending dns get , since that open-beta command has no --json mode) — pass --skip-provider-records to opt out and manage those two by hand instead. Cloudflare's own DKIM/MX output includes a suggested DMARC record too (typically p=reject); this script never applies it — DMARC stays exclusively owned by the ramp below so a fresh subdomain is never accidentally hard-enforced before it's been observed.
DMARC defaults to p=none (monitor mode) with relaxed alignment (adkim=r; aspf=r), the safe starting point for a brand-new sending subdomain:
p=none(default) — pass--dmarc-rua you@example.com, or the script warns that you'll get no
aggregate reports and no visibility into DKIM/SPF alignment before ramping up.
- Once reports show DKIM/SPF are aligned, re-run with
--dmarc-policy quarantine. - Once quarantine looks clean, re-run with
--dmarc-policy rejectto fully enforce.
Tighten alignment with --dmarc-alignment strict once you're confident (default is relaxed).
> Before you choose sender addresses, read [docs/EMAIL-DELIVERABILITY.md](docs/EMAIL-DELIVERABILITY.md): > keep inbound and outbound separated, isolate reputation per stream subdomain, and never run bulk > mail or experiments from your apex domain.
Cloudflare Access — Reccado has no built-in login; Access is the auth perimeter for the UI and /api/*. pnpm setup:access --hostname inbox. prints the dashboard steps to create a self-hosted Access application for the custom hostname, then sets ACCESS_JWT_AUDIENCE / ACCESS_TEAM_DOMAIN (+ optional ACCESS_ALLOWED_EMAILS) as secrets once you pass --aud / --team-domain (dry-run by default). See [SECURITY.md](SECURITY.md) for the model.
3. Verify
pnpm doctor --env dev --cloud --url https://inbox. # auth, D1, secrets, Access redirect
pnpm smoke:access https://inbox. # fails if unauthenticated /api/* returns 200
pnpm smoke:routing --domain --env dev # f
…
## Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [santigamo](https://github.com/santigamo)
- **Source:** [santigamo/reccado](https://github.com/santigamo/reccado)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.