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

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

  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"
    remote = true

    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 exactly why the test send in step 5 is the thing to trust.

    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, then Support, and use Send a test email. The engine performs a real send to your own signed-in address and reports honestly what happened, 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.
    Email is not configured on this engine 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_FROM domain under Compute, Email Service, Email Sending.
    EMAIL_FROM is unset or not a custom-domain address The 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 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/preflight checks only that the EMAIL binding is bound and EMAIL_FROM is set, and it says so rather than claiming more. 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.

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, and 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 .