---
title: Setup
description: Everything `bun run setup` creates in your Cloudflare and AWS accounts, step by step.
sidebar:
  icon: terminal
---

`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

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

2. **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](#domains-outside-cloudflare).

3. **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.

4. **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](/docs/quickstart#before-you-start) is only asked for here, and only
    if the Turnstile widget or DNS records still need creating.

5. **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.

6. **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.

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

8. **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.

9. **Production access**

    Shows a description of what you send for AWS's review and submits it only if
    you say yes. See
    [Deliverability](/docs/guides/deliverability#production-access).

10. **Finish**

    Saves your settings and runs the preflight checks.

## Domains outside Cloudflare

Sendoff normally runs on your hostname as a [Workers Custom Domain](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/), 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](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/), which adds no cost or latency.

What changes for you:

- **One more DNS record:** a `CNAME` from your hostname (like `news.example.com`) to the project's `*.pages.dev` address. 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 deploy` updates 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.
