Setup
Everything `bun run setup` creates in your Cloudflare and AWS accounts, step by step.
bun run setup connects Sendoff to your accounts and deploys it. bun create sendoff runs it for you after cloning; run it yourself from your Sendoff folder any time after. Every step checks what already exists first, so it’s safe to run again: after a failure, it skips what’s done and carries on. Your answers and resource IDs are saved in the gitignored apps/app/.sendoff/instance.json, and pre-fill the prompts next time.
Steps
Cloudflare account
Reads your account from wrangler whoami, opening a browser to log in if
needed, and asks which account if you have several. It asks you to confirm
the account is on Workers Paid, which Wrangler can’t check.
Your list
The name, sender name and address, an optional reply-to, an optional postal
address for the footer, and your website. Then the hostname for Sendoff (by
default news. plus your sender’s domain) and the SES region. Setup checks
whether the hostname’s DNS is on Cloudflare in this account. If it isn’t,
Sendoff reaches it through a small Pages project; see Domains outside
Cloudflare.
AWS access
If the AWS CLI is logged in, setup uses that login (including short-lived
SSO sessions) for its own AWS calls. It creates an IAM user called sendoff
with a send-only policy and gives the Worker that user’s key, so the Worker
can send email and read its own SES status but can’t change anything in your
account. Without the CLI, it writes the IAM policy it needs to
.sendoff/iam-policy.json and asks for keys.
Cloudflare resources
A D1 database (sendoff), a KV namespace for OAuth grants
(sendoff-oauth), a queue (sendoff-send), a Pages project
(sendoff-pages) if your DNS isn’t on Cloudflare, and a Turnstile widget
for your hostname. The Cloudflare API
token is only asked for here, and only
if the Turnstile widget or DNS records still need creating.
SES and SNS
Your domain as an SES identity with Easy DKIM, a custom MAIL FROM domain
(bounce. plus your domain), a configuration set (sendoff), an SNS topic
(sendoff-events) that only SES can publish to, an event destination for
bounces, complaints, rejections and deliveries, and SES’s account-level
suppression list.
DNS
Three DKIM CNAME records, an MX and SPF TXT record for the MAIL FROM
domain, and a DMARC record if your domain doesn’t have one. Records that
clash with existing ones are reported, not changed. Skipped once SES has
verified the domain and it has DMARC. If your DNS isn’t on Cloudflare,
there’s also a CNAME from your hostname to the Pages project, and setup
lists every record for you to add at your DNS provider.
Deploy
Generates .sendoff/wrangler.jsonc, applies the database migrations, and
deploys the Worker to your hostname with its secrets. If the Worker already
has an MCP token, setup asks whether to keep it or create a new one, and
shows any new token straight after the deploy. If your DNS isn’t on
Cloudflare, it then deploys the Pages project and adds your hostname to it
as a custom domain.
Wire up events
Waits for your hostname to answer (a new custom domain takes a few minutes
to get its certificate), subscribes /webhooks/ses to the SNS topic, and
waits for the Worker to confirm it. Then it waits for SES to verify your
domain.
Production access
Shows a description of what you send for AWS’s review and submits it only if you say yes. See Deliverability.
Finish
Domains outside Cloudflare
Sendoff normally runs on your hostname as a Workers Custom Domain, which only works when the domain’s DNS is on Cloudflare. If yours is somewhere else, you don’t have to move it. Setup creates a Cloudflare Pages project called sendoff-pages instead, because a Pages custom domain works with a single CNAME at any DNS provider. The project holds a few lines of code that pass every request, unchanged, to the Sendoff Worker over a service binding, which adds no cost or latency.
What changes for you:
- One more DNS record: a
CNAMEfrom your hostname (likenews.example.com) to the project’s*.pages.devaddress. Setup lists it with the SES records, which you add yourself, since setup can’t write to DNS outside Cloudflare. - Add the domain to the project in the dashboard, if setup can’t. Setup does it with your Wrangler login, and if that fails, it shows where in the Cloudflare dashboard to do it.
- Everything else is the same. Links in your emails still use your own hostname, and
bun run deployupdates the Pages project along with the Worker.
Your hostname has to be a subdomain, like news.example.com, not the domain itself. Most DNS providers don’t allow a CNAME on a bare domain.
Setup remembers the choice. If you move your DNS to Cloudflare later, Sendoff keeps using the Pages project, which carries on working.
What the Worker holds
Secrets, uploaded with the deploy:
| Secret | Purpose |
|---|---|
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY |
The sendoff IAM user’s send-only key |
MCP_TOKEN |
Authenticates Claude Code and Codex, and signs you in when connecting claude.ai |
TOKEN_SECRET |
Signs the confirmation, unsubscribe and tracked links in emails, plus preview links and sign-in sessions. Generated once and never rotated, because rotating it would break every unsubscribe link already sent. |
TURNSTILE_SECRET_KEY |
Verifies the signup form’s Turnstile challenge |
Variables, in the generated config: PUBLIC_URL, SES_REGION, SES_CONFIGURATION_SET, SES_EVENTS_TOPIC_ARN and TURNSTILE_SITE_KEY.
SES regions
Setup offers us-east-1, us-east-2, us-west-2, eu-west-1, eu-central-1 and ap-southeast-2. Production access, quotas and identities are per region, so pick one and stay in it.