Self-hosted newsletter

Run your own newsletter.

Kestrel is an open-source, self-hosted newsletter app. Write in Markdown, preview the exact email, schedule with a cancelable review window, and send to your subscribers. You keep the list, the consent record, the delivery record, and a permanent per-issue archive.

Runs on your own Cloudflare account. Your data never leaves it.

localhost:8787/dashboard
The Kestrel editor: the dashboard for the Windbreak demo publication, showing subscriber counts, a scheduled issue, two drafts, and the sent issues.

Ownership

You keep what matters

The pitch isn't another sending API. It's that the parts a newsletter is actually made of stay yours.

The list

Your subscribers

They live in your own D1 database, not a vendor's. Query or export them any time. Nobody rents you access to your own audience.

The consent

Proof of permission

Every subscription is double opt-in, with the confirmation and the unsubscribe recorded. The evidence you had consent stays with you.

The delivery

An auditable record

Each send records who received which issue, plus suppressions from hard bounces and spam complaints. You control the whole trail.

The archive

A permanent home

Every issue gets a lasting public page on your own domain. The links you send never rot behind someone else's platform.

How it works

Write, preview, schedule, send

One interface, an HTTP API, with two clients: a web editor and Claude. The steps are the same either way.

Write in Markdown

Draft an issue in the web editor or hand it to Claude. Same API underneath, same result. If a draft changed under you, a stale save is rejected rather than clobbered.

Preview the exact email

See precisely what lands in the inbox: the rendered email HTML and plain-text, not an approximation. Send yourself a test first.

Schedule with a review window

Queue a send behind a cancelable countdown. Change your mind and cancel before it fires; a Cron Trigger drives the sweep when it does.

Send to a double opt-in list

Only confirmed subscribers receive it. The send loop is idempotent, so it never double-sends, and bounces and complaints suppress on their own.

Archive it forever

Each sent issue becomes a permanent, indexable page on your domain. The same content stays readable long after the email is buried.

Under the hood

Built to be operated

Small, legible, and boring in the ways that matter. You run it on your own account and can read every line. It's open source under the MIT license.

TypeScript Cloudflare Workers D1 R2 Cron Triggers SES Resend
One API, two clients
A single HTTP API drives everything; the web editor and Claude are just clients of it. Nothing the editor can do is off-API.
Optimistic concurrency
Edits are revision-checked (If-Match / ETag), so a stale write is rejected with 409 instead of overwriting a newer draft.
Idempotent send loop
The scheduled sweep is safe to retry. It resumes a send without re-mailing anyone who already received the issue.
Three isolated environments
dev, staging, and production each get their own database, storage, and mail transport, so development can never reach a real inbox.
Swappable email provider
SES or Resend in production; a fake in-memory transport for local dev and tests. It's one variable.
Access-gated admin
Authoring sits behind Cloudflare Access. The reader pages are public, gated only by unguessable per-subscriber tokens.

Self-host

From clone to a running instance

Local dev needs Node and npm. No Cloudflare account is required until you deploy. The shortest honest path:

git clone https://github.com/kurtbruns/kestrel
cd kestrel && npm install
cp .dev.vars.example .dev.vars
npm run dev     # http://localhost:8787
npm run seed    # load the demo publication

Standing up a real, deployed instance takes more: a Cloudflare account, one Access application, an email provider (SES or Resend), and sending-domain DNS. The operator setup guide covers it.

Your audience. Your data. Your newsletter.

Kestrel is self-hosted and runs entirely on your own Cloudflare account.