Skip to content

Self-hosting

Deploy Doota to your own Cloudflare account — prerequisites, the deploy path, and what each Worker needs.

Updated View as Markdown

Doota is meant to be run by you. This section takes a fresh Cloudflare account to a working instance that receives and sends real mail on your own domain.

Is self-hosting for you?

Be honest with yourself before you start — setting Doota up is a developer-level task, not a point-and-click install. There’s no hosted Doota to sign up for.

You’ll be comfortable here if you can:

  • Use a terminal (run commands, copy output).
  • Install tools like Node and pnpm, and clone a Git repository.
  • Edit a config file and paste in IDs.
  • Manage a domain’s DNS and a Cloudflare account.

Plan for 30–60 minutes the first time.

Still with us? Here’s what you’ll need.

What you’ll need

  • A Cloudflare account (the free tier is enough to start; Workers Paid is recommended for Queues throughput and higher limits).
  • A domain you can point at Cloudflare — Doota’s model is one deployment = one company = one mail domain. You can onboard several domains, each as its own organization.
  • Node 22.18+, pnpm, and a local clone of the repo.
  • A scoped Cloudflare API token for the running app — Doota calls the CF API to onboard domains (zone, Email Routing, DNS, DKIM). See Cloudflare API token. (Deploying itself needs no token locally — you sign in through the browser.)

The deploy path, end to end

Doota deploys as one stack (Alchemy): resources, Workers, bindings, migrations, and secrets all reconcile from a single declaration in infra/. No manual provisioning, no ID wiring, no secret-by-secret pushes.

Configure

Decide your instance’s settings — serving domain(s), the setup-wizard token, and the app’s Cloudflare API token. Start at Configuration; required secrets you don’t provide are minted automatically on the first deploy.

Deploy — pick your path

From your machine: pnpm alchemy login once, then pnpm infra:deploy — your instance comes up and prints its URL. From GitHub: set two repository secrets and every merge to main deploys itself. Both paths: Deploy.

First run

Create the super-admin via /setup, onboard your first domain so Email Routing and sending come online, send a test message. See First run.

What each Worker binds

Five Workers deploy; three carry state. A queue binds to exactly one consumer, so the web app only produces — the async handlers live in the mail Workers.

Worker D1 DB R2 MAIL_RAW KV AUTH_KV DO MAIL_EVENTS EMAIL_SENDER Queues
doota (web) produces inbound, outbound
doota-mail-inbound produces + consumes inbound
doota-mail-jobs consumes outbound + events, produces outbound; cron
doota-landing
docs

The Durable Object class MailEventHub lives in doota-mail-jobs; the other Workers bind to it by script_name. That’s why doota-mail-jobs deploys the DO migration.

In this section

Configuration

Step 1 — decide your domains, setup token, and every variable’s meaning.

Cloudflare API token

The app’s runtime token: exact scopes, and when you can drop Zone:Create.

Deploy

One command from your machine, or auto-deploy from GitHub on merge.

First run

Genesis, onboard a domain, create a mailbox, send a test message.

Operations

Backup and restore, migrations, and key management.

Troubleshooting

Mail not arriving, sending failing, and where to look.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close