Update Subscriptions Worker
This Worker stores subscriber state for the siteβs publication and blog update emails.
It deliberately does not use Cloudflare Email Sending or Queues. Cloudflareβs official pricing currently makes outbound Email Sending available only on Workers Paid, so real automatic delivery is handled by GitHub Actions through configured SMTP credentials.
Current deployment:
https://dz-update-subscriptions.qq893371906.workers.dev
updates@chemllm.org -> dz-update-subscriptions
What It Does
- Accepts web subscribe requests at
/subscribeand records confirmed subscribers. - Receives inbound Email Routing messages through
email(message, env, ctx). - Treats a subject containing
unsubscribeas an unsubscribe request. - Treats other inbound messages as a subscribe request.
- Stores subscriber state in D1.
- Exposes admin-only JSON export at
/admin/subscribers.json?token=.... - Exposes admin-only CSV export at
/admin/subscribers.csv?token=.... - Exposes admin-only digest generation at
/admin/digest?token=....
Turnstile Protection
Web subscriptions are protected by Cloudflare Turnstile. The browser renders the widget with the public site key from _config.yml, then posts the cf-turnstile-response token with the email form. The Worker validates that token against Cloudflare Siteverify before writing to D1.
Production Worker settings:
TURNSTILE_SECRET_KEY
TURNSTILE_REQUIRED=true
TURNSTILE_ACTION=update-subscribe
TURNSTILE_ALLOWED_HOSTNAMES=trotsky1997.github.io
TURNSTILE_SECRET_KEY is a Worker secret and must never be committed. If TURNSTILE_REQUIRED=true and the secret is missing, /subscribe fails closed with 503.
Sender
scripts/send-digest.mjs sends real digest email through SMTP.
Required configuration:
UPDATE_SUBSCRIPTIONS_WORKER_URL
UPDATE_SUBSCRIPTIONS_ADMIN_TOKEN
UPDATE_SMTP_HOST
UPDATE_SMTP_PORT
UPDATE_SMTP_USER
UPDATE_SMTP_PASS
UPDATE_SMTP_SECURE
UPDATE_MAIL_FROM
Optional:
UPDATE_MAIL_REPLY_TO
Run a dry-run pass:
node workers/update-subscriptions/scripts/send-digest.mjs --feed http://127.0.0.1:4000/updates.xml --worker-url http://127.0.0.1:8787 --admin-token dev-admin-token --dry-run true --force-send true
Run a real SMTP pass:
$env:UPDATE_SMTP_HOST="smtp.example.com"
$env:UPDATE_SMTP_PORT="587"
$env:UPDATE_SMTP_USER="user@example.com"
$env:UPDATE_SMTP_PASS="<app-password>"
$env:UPDATE_SMTP_SECURE="false"
$env:UPDATE_MAIL_FROM="Di Zhang Updates <updates@chemllm.org>"
node workers/update-subscriptions/scripts/send-digest.mjs --feed http://127.0.0.1:4000/updates.xml --worker-url http://127.0.0.1:8787 --admin-token dev-admin-token --force-send true
Local-Only Setup
Copy the example config locally:
Copy-Item workers/update-subscriptions/wrangler.example.toml workers/update-subscriptions/wrangler.toml
Apply the D1 migration locally:
npx wrangler d1 migrations apply dz-update-subscriptions --local --config workers/update-subscriptions/wrangler.toml
Start local Worker development:
npx wrangler dev --config workers/update-subscriptions/wrangler.toml
Simulate an inbound subscription email locally:
curl.exe --request POST "http://127.0.0.1:8787/cdn-cgi/handler/email?from=reader@example.com&to=updates@example.com" --data-raw "From: reader@example.com`nTo: updates@example.com`nMessage-ID: <local-test-1@example.com>`nSubject: subscribe`n`nsubscribe"
Export confirmed subscribers:
curl.exe "http://127.0.0.1:8787/admin/subscribers.json?token=dev-admin-token"
curl.exe "http://127.0.0.1:8787/admin/subscribers.csv?token=dev-admin-token"
Generate a digest from the Worker:
curl.exe "http://127.0.0.1:8787/admin/digest?token=dev-admin-token"
GitHub Actions
.github/workflows/update-digest.yml runs weekly and can also be triggered manually. It sends digest email through SMTP, one message per confirmed subscriber, and uploads audit artifacts.
The workflow never deploys Cloudflare resources and never calls Cloudflare Email Sending. If SMTP secrets are absent when there is work to send, the workflow fails instead of reporting a fake success.
Production Boundary
The production path requires Cloudflare DNS and Email Routing configured manually in the dashboard:
- route a custom address such as
updates@example.comto this Worker; - create a D1 database and bind it as
DB; - store
ADMIN_TOKEN,TOKEN_SECRET,TURNSTILE_SECRET_KEY, andTURNSTILE_REQUIREDas Worker secrets; - store the SMTP and Worker admin values as GitHub Actions repository secrets.