Cutout
SELF-HOST GUIDE

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:

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:

  1. Open Email › Email Routing and click Enable.
  2. Under Routes, edit the catch-all address, set the action to Send to a Worker, and pick the cutout worker.

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).

  1. Dashboard › Email › Email Sending › Onboard Domain.
  2. 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.

  1. Dashboard › Email › Email Routing › Destination addresses.
  2. Click Add destination address and enter the address (e.g. your real Gmail).
  3. 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

  1. Talk to @BotFather on Telegram and create a bot. Copy the HTTP API token.
  2. Set the token as a Worker secret:
    nix develop --command wrangler secret put TELEGRAM_BOT_TOKEN -c wrangler.production.toml
  3. 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
  4. Register the webhook with Telegram (replace <TOKEN> and <HOST>; the secret_token param is only needed if you set TELEGRAM_WEBHOOK_SECRET):
    curl -X POST "https://api.telegram.org/bot<TOKEN>/setWebhook" \
      -d "url=https://<HOST>/telegram/webhook" \
      -d "secret_token=<WEBHOOK_SECRET>"
  5. 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 visit https://api.telegram.org/bot<TOKEN>/getUpdates: the integer (negative for groups, positive for DMs) is what you put in a rule destination as telegram:<chat_id>.

Discord

  1. 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.
  2. 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
  3. 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.
  4. Invite the bot to your server via OAuth2 (scope bot, permission Send Messages for the target channels).
  5. 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.