Set up outbound email and validate your sender domain
Outbound email is how your engine reaches a person: alert notifications, licence expiry warnings, the role-invite for a new member, and the first-run set-up link that claims the first Owner. It is optional. Nothing in the backup or recovery path depends on it, so you can configure it now, later, or never.
It is also one of the few parts of a deploy that cannot be finished from a terminal. Cloudflare makes you onboard and verify the sending domain in the dashboard, because sending mail from a domain is an assertion about that domain and it has to be proven with DNS. This page walks the whole thing, then shows you how to prove it works rather than assume it.
Email is fail-open, so a quiet inbox is not a fault
Until the sending domain is onboarded, every send is a clean, logged no-op rather than an error: notifications simply skip the email channel and the run carries on. So do not read silence after a first deploy as a broken deployment. It is the default until you complete the steps below.
What you need
| Requirement | Why |
|---|---|
| A Cloudflare account on Workers Paid | The send-email binding is a paid-plan feature, and the deploy already requires Workers Paid for its raised CPU limit. |
| A domain you control, on this account | You send from this domain, so Cloudflare makes you prove you hold it. The engine refuses a workers.dev sender at send time. |
| The engine deployed at least once | You are adding configuration to an engine that already exists. If you have not deployed yet, start with the first deploy. |
Configure and verify, step by step
Confirm the send-email binding is present
The engine ships with the binding already declared in
wrangler.toml, so in most cases there is nothing to add:[[send_email]] name = "EMAIL"This is the binding the engine sends through. If you have trimmed your
wrangler.toml, put it back: without it the engine has no way to send at all, and the test send in step 5 reports the binding as not configured.Replace the shipped sender address with your own
EMAIL_FROMis the address alerts and expiry warnings are sent from. The engine ships a placeholder you do not own:# engine/wrangler.toml ships this. It is OUR domain, not yours. EMAIL_FROM = "no-reply@downpipes.io"Edit that line in the existing
[vars]table to an address on the domain you are about to onboard. Do not paste a second[vars]table: TOML rejects a duplicate table, and the deploy will fail.EMAIL_FROM = "alerts@your-domain.example"Be aware that the shipped default is shaped like a valid sender, so the engine accepts it and preflight reports email as configured. It only fails at the platform, at send time, because you do not own that domain. That is why the test send in step 5 is the check to rely on.
INVITE_EMAIL_FROMis optional and separate. It is the sender for the role-invite, which the engine sends only when this is set, so leaving it unset lets you run alert and expiry email without emailing on every role grant. The first-run set-up link uses it when present and otherwise falls back toEMAIL_FROM, so the emailed first-Owner path needs onlyEMAIL_FROM.INVITE_EMAIL_FROM = "invites@your-domain.example"Any address you use must be on a domain you onboard in the next step.
Onboard the sending domain in the Cloudflare dashboard
This is the browser step, and it is the one people miss.
- Open the Cloudflare dashboard and select the account the engine is deployed into.
- Go to Compute, then Email Service, then Email Sending.
- Onboard the domain that
EMAIL_FROMuses (andINVITE_EMAIL_FROM, if you set it). - Cloudflare adds the required DNS records for you when the domain is on this account. Let it finish and wait for the domain to report as verified.
If Email Service itself is not yet enabled on the account, complete its set-up on that same screen first. Until the domain shows as verified, the platform refuses the sender and the engine records the refusal rather than pretending it sent.
Deploy the engine so the sender takes effect
EMAIL_FROMis a deploy-time variable, so it only reaches the running engine on a deploy:cd engine npm run deployAlways use
npm run deploy, never a barewrangler deploy: the npm script reconciles your live bindings first and refuses to ship a config that would drop a source you attached from the console. Do not setDOWNPIPE_ALLOW_BINDING_RESET, which would let a failed reconcile drop them anyway. The reasoning is on deploy safety and bindings.Prove it works with a test send
Do not assume. In the console, open Settings, expand Email delivery, and use Send a test email. The engine performs a real send to your own signed-in address and reports the result, including the platform’s own error code, and the console translates it:
What you see What it means What to do Sent The send was accepted. Check the inbox of your signed-in address, then its spam folder. If nothing arrives, re-check the domain’s onboarding state. You are signed in with the bare admin token, which has no email address There is nowhere to send to. This is the normal state right after a deploy that bootstrapped with the admin token. Claim an Owner identity first (a passkey, or Cloudflare Access), then sign in as that person and test again. No send_email binding is bound The [[send_email]]binding is missing.Restore the binding from step 1 and redeploy. The sending domain is not onboarded for Email Sending The platform refused the sender. Finish step 3: onboard the EMAIL_FROMdomain under Compute, Email Service, Email Sending.EMAIL_FROMis unset or not a custom-domain addressThe engine has no valid sender. Set EMAIL_FROMto an address on your onboarded domain and redeploy (steps 2 and 4).Email Service may not be enabled on this account The account has not completed Email Service set-up. Open Compute, then Email Service, complete set-up, then test again. Preflight proves presence, not delivery
The engine’s
GET /admin/preflightchecks only that theEMAILbinding is bound andEMAIL_FROMis set. It does not attempt a send, so it will happily report email as configured while the shippedno-reply@downpipes.iodefault is still in place. The test send is the only thing that proves delivery.
When the first-Owner link does not arrive
The console’s Email the owner a set-up link button always reports the same result. The engine gives every press the same answer, whatever the outcome, so a stranger who presses it cannot learn whether the engine has an Owner or what its settings are (sendBootstrapLink, engine/src/admin/router-auth-flow.ts). The console therefore cannot tell you why no email came. The test send above does not help yet, because it needs a person to sign in first.
The engine sends the email before it answers the press. If nothing arrives within a few minutes, one of these conditions was not met. The engine checks them in this order:
| Condition | What to check |
|---|---|
CONSOLE_ORIGIN is set | The engine needs it to build the link. It is set in the engine’s wrangler.toml before the first deploy. |
| The press came from the console’s own origin | A browser request from another origin is dropped. |
BOOTSTRAP_OWNER_EMAIL is set to an address on a custom domain | This is the only inbox the link goes to. It is a secret set per deployment, never committed; npm run deploy asks for it once if it is absent. |
The EMAIL binding is bound | Step 1 above. |
| A usable sender | INVITE_EMAIL_FROM if you set it, otherwise EMAIL_FROM. It must be an address on a domain you onboarded in step 3. The shipped no-reply@downpipes.io is on a domain you do not own, so the platform refuses it. |
| The engine has no Owner, and never had one | Once an Owner exists, the set-up path is closed and nothing is sent. |
The link works once, for 24 hours. Each press replaces the previous link, so only the newest link works. To see which condition failed, open a support pack: the engine records the reason as a sign-in signal, without the address or the link.
You do not have to wait for the email. The one-time admin token needs no email and reaches the same passkey set-up; see the admin token.
Where this fits
Email is configuration you add to a deployed engine, so it sits alongside the first deploy and the self-host runbook. Once sending works, the channels that use it are set up on the notifications page. The attributable first-run path that emails a set-up link is covered in identity and access. If you would rather not enable email at all, claim the first Owner with the one-time admin token instead, which the first deploy walks through.
Last updated .