Skip to content
downpipes docs

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

RequirementWhy
A Cloudflare account on Workers PaidThe 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 accountYou 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 onceYou 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

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

  2. Replace the shipped sender address with your own

    EMAIL_FROM is 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_FROM is 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 to EMAIL_FROM, so the emailed first-Owner path needs only EMAIL_FROM.

    INVITE_EMAIL_FROM = "invites@your-domain.example"

    Any address you use must be on a domain you onboard in the next step.

  3. Onboard the sending domain in the Cloudflare dashboard

    This is the browser step, and it is the one people miss.

    1. Open the Cloudflare dashboard and select the account the engine is deployed into.
    2. Go to Compute, then Email Service, then Email Sending.
    3. Onboard the domain that EMAIL_FROM uses (and INVITE_EMAIL_FROM, if you set it).
    4. 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.

  4. Deploy the engine so the sender takes effect

    EMAIL_FROM is a deploy-time variable, so it only reaches the running engine on a deploy:

    cd engine
    npm run deploy

    Always use npm run deploy, never a bare wrangler 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 set DOWNPIPE_ALLOW_BINDING_RESET, which would let a failed reconcile drop them anyway. The reasoning is on deploy safety and bindings.

  5. 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 seeWhat it meansWhat to do
    SentThe 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 addressThere 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 boundThe [[send_email]] binding is missing.Restore the binding from step 1 and redeploy.
    The sending domain is not onboarded for Email SendingThe platform refused the sender.Finish step 3: onboard the EMAIL_FROM domain under Compute, Email Service, Email Sending.
    EMAIL_FROM is unset or not a custom-domain addressThe engine has no valid sender.Set EMAIL_FROM to an address on your onboarded domain and redeploy (steps 2 and 4).
    Email Service may not be enabled on this accountThe 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/preflight checks only that the EMAIL binding is bound and EMAIL_FROM is set. It does not attempt a send, so it will happily report email as configured while the shipped no-reply@downpipes.io default is still in place. The test send is the only thing that proves delivery.

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:

ConditionWhat to check
CONSOLE_ORIGIN is setThe 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 originA browser request from another origin is dropped.
BOOTSTRAP_OWNER_EMAIL is set to an address on a custom domainThis 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 boundStep 1 above.
A usable senderINVITE_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 oneOnce 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 .