Deploy your own Cutout
Cutout is self-host first. Fork the repo, point a few GitHub Secrets at your Cloudflare account, and pushes to main deploy to your own Worker via GitHub Actions.
01 Fork the repo
Fork ananthb/cutout on GitHub into your own account or org.
02 Create the Cloudflare resources
You need a KV namespace (rule storage), a D1 database (reverse-alias mappings + bot reply contexts), an R2 bucket (raw bytes for queued retries and Store-action persistence), and two queues (the retry pipeline and its dead-letter queue). All must exist before the first deploy so you can supply IDs and queue names.
Clone your fork locally and use wrangler from the dev shell:
nix develop # or install wrangler manually
wrangler kv namespace create KV
wrangler d1 create cutout-db
wrangler r2 bucket create cutout-emails
wrangler queues create cutout-retries
wrangler queues create cutout-retries-dlq
Save the returned id (KV) and database_id (D1): you'll paste them into GitHub Secrets next. The R2 bucket and queue names are referenced by name in wrangler.toml; no IDs to copy.
03 Configure GitHub Secrets
In your fork: Settings › Secrets and variables › Actions. Add:
CLOUDFLARE_API_TOKEN: token with Workers Scripts: Edit, Workers KV Storage: Edit, D1: Edit, Workers R2 Storage: Edit, Cloudflare Queues: Edit, and Email Routing: Edit permissions.CLOUDFLARE_ACCOUNT_ID: your Cloudflare Account ID (visible in the dashboard sidebar).KV_NAMESPACE_ID: theidfrom step 2.D1_DATABASE_ID: thedatabase_idfrom step 2.CF_ACCESS_TEAMandCF_ACCESS_AUD: your Cloudflare Access team domain and the AUD tag of the Access app guarding/manage(set this up in step 8).CF_API_TOKEN(optional): separate token with Account Analytics: Read scope. Unlocks the dashboard stats panels (forwarded/dropped totals, per-rule matches, top senders). Leave empty to ship without stats; the live event tail still works.
04 Push to main
The Deploy workflow generates wrangler.production.toml from your secrets, applies D1 migrations, runs wrangler deploy, and uploads CF_ACCESS_TEAM / CF_ACCESS_AUD / CF_API_TOKEN as Worker secrets. After it succeeds, your Worker is live at cutout.<your-workers-subdomain>.workers.dev (or a custom route you've added).
05 Enable Email Routing on each domain
For every domain you want Cutout to handle, in the Cloudflare dashboard:
- Open Email › Email Routing and click Enable.
- Under Routes, edit the catch-all address, set the action to Send to a Worker, and pick the
cutoutworker.
06 Onboard each domain to Email Service
This is what lets Cutout send outbound mail (replies, proxy-mode forwards, and fan-out beyond the first destination).
- Dashboard › Email › Email Sending › Onboard Domain.
- Accept the DNS records (MX / SPF / DKIM / DMARC) on the
cf-bounce.<yourdomain>subdomain.
07 Verify destination addresses
Cloudflare requires every email destination to be a verified address.
- Dashboard › Email › Email Routing › Destination addresses.
- Click Add destination address and enter the address (e.g. your real Gmail).
- Click the confirmation link sent to that address.
08 Protect /manage with Cloudflare Access
The management UI must not be public. Create an Access application whose policy covers cutout.<yourdomain>/manage/* (or whichever route you mapped to the worker). Copy the application's AUD tag and your team domain into the CF_ACCESS_AUD and CF_ACCESS_TEAM GitHub Secrets, then re-run the deploy workflow so the worker picks them up.
09 Optional: enable Telegram and Discord destinations
Skip this step if you only want to forward to email. The /manage editor only offers a chat channel as a destination kind once the relevant bot secrets are set on the Worker. Use wrangler secret put from your fork's checkout (the dev shell has wrangler):
Telegram
- Talk to @BotFather on Telegram and create a bot. Copy the HTTP API token.
- Set the token as a Worker secret:
nix develop --command wrangler secret put TELEGRAM_BOT_TOKEN -c wrangler.production.toml - Optional but recommended. Set a webhook secret so only Telegram can post to your worker:
nix develop --command wrangler secret put TELEGRAM_WEBHOOK_SECRET -c wrangler.production.toml - Register the webhook with Telegram (replace
<TOKEN>and<HOST>; thesecret_tokenparam is only needed if you setTELEGRAM_WEBHOOK_SECRET):curl -X POST "https://api.telegram.org/bot<TOKEN>/setWebhook" \ -d "url=https://<HOST>/telegram/webhook" \ -d "secret_token=<WEBHOOK_SECRET>" - Add the bot to each chat or channel you want messages forwarded to. To find a chat's
chat_id, post anything in the chat then visithttps://api.telegram.org/bot<TOKEN>/getUpdates: the integer (negative for groups, positive for DMs) is what you put in a rule destination astelegram:<chat_id>.
Discord
- Open the Discord Developer Portal and create an application. Under Bot, reset and copy the token. Under General Information, copy the Application ID and Public Key.
- Set all three as Worker secrets:
nix develop --command wrangler secret put DISCORD_BOT_TOKEN -c wrangler.production.toml nix develop --command wrangler secret put DISCORD_APP_ID -c wrangler.production.toml nix develop --command wrangler secret put DISCORD_PUBLIC_KEY -c wrangler.production.toml - Back in the developer portal, set the Interactions Endpoint URL (under General Information) to
https://<your-cutout-host>/discord/interactions. Discord pings this URL with a verification request: with the public key in place, the worker responds correctly and the URL saves. - Invite the bot to your server via OAuth2 (scope
bot, permission Send Messages for the target channels). - For each Discord channel you want forwarded messages in, copy its Channel ID (Discord Settings › Advanced › Developer Mode, then right-click channel › Copy Channel ID) and use it in a rule destination as
discord:<channel_id>.
Telegram replies (Telegram's native reply) and Discord replies (the Reply button on each forwarded message) route back through Cutout to the original email sender, so the chat client becomes a fully functional mailbox for that thread.
10 Add routing rules
Visit https://<your-cutout-host>/manage. Add Forward rules using glob patterns on the local part and domain. For email destinations, use the Proxy via rewrite mode toggle to ensure Reply-To works when replying via your custom domain (see How it works for the tradeoffs).
Updating
Pull from upstream into your fork's main branch (or merge a PR): the deploy workflow re-runs on every push to main.