Cloudflare Worker API for double opt-in subscriptions, list unsubscribe, and campaign delivery via Resend. Data lives in D1.
A self-hosted alternative to MailerLite/Substack/Buttondown.
- Node 20+
- Cloudflare account, Wrangler authenticated (
npx wrangler login) - Resend API key and verified sending domain
npm installCreate the database and record its id in wrangler.toml (database_id):
npx wrangler d1 create newsletterApply schema to the remote database:
npx wrangler d1 migrations apply newsletter --remoteFor local development:
npx wrangler d1 migrations apply newsletter --local
npm run dev| Name | Type | Purpose |
|---|---|---|
RESEND_API_KEY |
secret | Resend API authorization |
ADMIN_BEARER_TOKEN |
secret | Bearer for admin endpoints (campaign send, delete) |
RESEND_WEBHOOK_SECRET |
secret (optional) | Required to enable /api/webhooks/resend |
TURNSTILE_SECRET_KEY |
secret (optional) | If set, subscribe requires Turnstile |
FROM_EMAIL |
var | Resend From header |
BASE_URL |
var | Public Worker URL (no trailing slash); used for confirm/unsubscribe links |
SITE_URL |
var (optional) | Public website URL (no trailing slash). When set, GET / redirects here and email footers link here. Falls back to BASE_URL. |
CORS_ORIGIN |
var (optional) | Allowed browser Origin for /api/subscribe |
SITE_NAME |
var (optional) | Shown in confirmation subject; default “Newsletter” |
COMPANY_ADDRESS |
var (optional) | Postal line in footers. When unset/empty, the line is omitted. |
UNSUBSCRIBE_MAILTO |
var (optional) | Extra List-Unsubscribe mailto |
npx wrangler secret put RESEND_API_KEY
npx wrangler secret put ADMIN_BEARER_TOKENUse wrangler.toml [vars] or the Cloudflare dashboard for non-secret variables. See .env.example for a checklist.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/health |
— | Liveness |
OPTIONS, POST |
/api/subscribe |
— | JSON subscribe; CORS preflight supported |
GET |
/api/confirm |
— | Query token; double opt-in |
GET, POST |
/api/unsubscribe |
— | Query token; RFC 8058 POST body supported |
POST |
/api/campaigns/send |
Authorization: Bearer <ADMIN_BEARER_TOKEN> |
Create-and-send or send by campaign_id |
POST |
/api/webhooks/resend |
Svix signature | Marks bounced/complained from Resend events |
POST |
/api/admin/delete |
Authorization: Bearer <ADMIN_BEARER_TOKEN> |
Hard-delete a subscriber by email (GDPR) |
POST /api/subscribe with Content-Type: application/json:
{
"email": "user@example.com",
"source": "website"
}Optional: metadata (object), turnstile_token (if Turnstile is enabled server-side).
Honeypot: include a hidden field; it must be empty or omitted. Recognized keys are listed in worker/src/lib/validation.ts (website, url, company, hp, address). A non-empty value yields 200 { "ok": true } without subscribing.
Run from the repo root. Remote D1 requires a configured database_id and Cloudflare auth.
| Script | Purpose |
|---|---|
npx tsx scripts/import-csv.ts <file.csv> |
Import email[,status] (set NEWSLETTER_D1_NAME if not newsletter) |
npx tsx scripts/export-csv.ts |
Export subscriber rows as CSV |
npx tsx scripts/create-campaign.ts |
Create a campaign row (--slug, --subject, --kind, --html, --text) |
npx tsx scripts/send-campaign.ts |
Call deployed POST /api/campaigns/send (NEWSLETTER_API_URL, ADMIN_BEARER_TOKEN) |
npm run typecheck
npm run lint
npm test
npm run deployA daily cron (wrangler.toml [triggers]) calls runCleanup: prunes expired rate limit rows, used or expired confirmation tokens older than 7 days, and audit events older than 1 year.
GitHub Actions:
.github/workflows/ci.yml— runstypecheck,lint, andviteston every push and pull request..github/workflows/deploy.yml— manualworkflow_dispatchjob that applies remote D1 migrations and deploys the Worker. Requires repository secretsCLOUDFLARE_API_TOKENandCLOUDFLARE_ACCOUNT_ID.
npm install, thennpx wrangler login.npx wrangler d1 create newsletterand paste thedatabase_idintowrangler.toml.npx wrangler d1 migrations apply newsletter --remote.- Verify the sending domain in Resend (DKIM/SPF DNS records); create an API key.
- Set secrets:
npx wrangler secret put RESEND_API_KEY npx wrangler secret put ADMIN_BEARER_TOKEN # Recommended for deliverability hygiene: npx wrangler secret put RESEND_WEBHOOK_SECRET - Set vars (in
wrangler.toml[vars]or the dashboard):FROM_EMAIL,BASE_URL,SITE_URL,CORS_ORIGIN,SITE_NAME,COMPANY_ADDRESS, optionalUNSUBSCRIBE_MAILTO. npm run deployand checkGET {BASE_URL}/health.- In Resend, add a webhook → URL
{BASE_URL}/api/webhooks/resend, eventsemail.bounced,email.complained; copy the signing secret intoRESEND_WEBHOOK_SECRET. - Backfill any existing list with
scripts/import-csv.ts. - Wire your site’s signup form to
POST {BASE_URL}/api/subscribe.
| Path | Role |
|---|---|
worker/src/index.ts |
Router and scheduled handler |
worker/src/routes/ |
HTTP handlers |
worker/src/lib/ |
Email, validation, rate limits, signatures, cleanup |
migrations/ |
D1 SQL |
scripts/ |
CLI tools |
examples/ |
Reference signup-form integration |
.github/workflows/ |
CI and deploy |
- Contributions: see
CONTRIBUTING.md. - Security disclosures: see
SECURITY.md. Please do not file public issues for vulnerabilities. - License: MIT.