Updating and running
Ship new versions, keep your instance's files safe, and run Sendoff locally.
Your copy of Sendoff is a git clone of the repo, made by bun create sendoff, and your settings live in its database, not in code. So updating is just moving to a newer version and redeploying.
Updating
From your Sendoff folder:
bun run update
It fetches Sendoff’s releases and shows the release notes for every version since yours. If you say yes, it moves your folder to the latest release, installs dependencies and runs bun run deploy. That applies any new database migrations first, then deploys the Worker with your instance’s config, and the Pages project in front of it if your DNS isn’t on Cloudflare. It doesn’t touch your secrets, DNS or SES setup.
Type the run: bun update on its own is Bun’s command for updating dependencies.
If your folder has uncommitted changes, or commits of its own, it stops without changing anything and says what to do. If your folder doesn’t have bun run update yet, run git pull once to get it.
Running the latest code
To run what’s on main, including changes that aren’t released yet:
git pull
bun install
bun run deploy
Unreleased changes don’t have release notes yet, so bun run update is the safer way to update.
What’s changed
Each version of the Worker has release notes on GitHub, under Releases (the ones tagged app@), and in apps/app/CHANGELOG.md in your folder. bun run update shows you the ones you’re about to get. If a version needs more than deploying, like running setup again, its notes say so.
To hear about new versions, open the Sendoff repo on GitHub and choose Watch › Custom › Releases.
Your instance’s files
Setup records your instance in apps/app/.sendoff/, which git ignores:
instance.json: your Cloudflare account, hostname, and the IDs of the database, KV namespace, queue, Turnstile widget, SES resources and Pages project (if you have one)wrangler.jsonc: the deploy config generated from itpages/: the Pages project’s code and config, generated from it, if you have one
bun run deploy needs them. If you lose them, or move to another machine, run bun run setup again. It finds the database, queue and SES resources that already exist instead of creating new ones, and keeps the Worker’s TOKEN_SECRET. It can’t look up the Turnstile widget or the Worker’s AWS key without these files, though, so it makes new ones. Delete the old widget in the Cloudflare dashboard afterwards. Setup lists the sendoff IAM user’s old key, with when it was last used, for you to delete: IAM allows a user only two keys, so setup stops if both are taken. It doesn’t delete the key itself, since another Sendoff instance in the same AWS account could be using it.
The repo’s own apps/app/wrangler.jsonc stays generic, so pulling updates never conflicts with your instance.
Running setup again
Setup is safe to re-run at any time. It checks every step and skips what’s already done. Run it to:
- Finish an interrupted setup, such as when DNS took too long
- Replace the MCP token, by choosing create a new one when it asks
- Recreate the Worker’s AWS key, if you deleted it
- Turn on delivery reports, when
doctorwarns that SES doesn’t send them (instances set up before Sendoff counted deliveries)
It never rotates TOKEN_SECRET, which signs the unsubscribe and tracked links in every email you’ve ever sent. Changing it would break all of them.
Running locally
cd apps/app
cp .dev.vars.example .dev.vars
bun run db:migrate
bun run dev
That serves the Worker at http://localhost:8787 with a local database and Turnstile’s test keys. Fill in real AWS keys in .dev.vars to send test emails. Open /preview/sample to see the layout with sample content.
Other commands, from the repo root:
| Command | What it does |
|---|---|
bun run test |
Runs the tests inside workerd, the Workers runtime |
bun run types |
Typechecks |
bun run check |
Lints and checks formatting |
cd apps/app && bun run bench |
Measures render CPU time against the Workers limit |