Skip to main content

Broadcast

3 min read

Cloudflare Worker scaffold — webhook + cron + Tier 2

The Cloudflare Worker is the long-lived home for the parts the Action can't run: the engagement collector cron, the Tier 2 15-minute approval window, and the cross-process StateStore / CounterStore / EngagementStore.


Scaffold

pnpm create broadcast-worker my-broadcaster
cd my-broadcaster
pnpm install
wrangler login

wrangler kv namespace create BROADCAST_STATE
wrangler kv namespace create BROADCAST_COUNTERS
wrangler kv namespace create BROADCAST_ENGAGEMENT
# Paste the IDs into wrangler.toml.

wrangler secret put GITHUB_WEBHOOK_SECRET
wrangler secret put APPROVAL_HMAC_SECRET
wrangler secret put BLUESKY_APP_PASSWORD       # if using Bluesky
wrangler secret put MASTODON_TOKEN             # if using Mastodon

wrangler deploy

The scaffolder copies template/ into your target, rewrites package.json, and prints the next steps. The CLI itself is path-traversal-safe — relative paths only, no .., no info flags as targets, project names constrained to [a-z0-9._-].

What ships in the scaffold

FileRole
wrangler.tomlKV namespace placeholders + */15 * * * * cron schedule.
src/index.tsfetch() for webhooks + scheduled() for the engagement collector.
src/config.tsVoice templates + buildAdapters(env) factory. The env-driven adapter closures let wrangler secret put rotate credentials without redeploy.
src/kv-stores.tsWorkers KV implementations of StateStore, CounterStore, EngagementStore. These ship inside your project so you own the code — no scaffolder import at runtime.
README.mdPer-project onboarding doc with the secrets checklist and production cutover notes.

Receipts and the signer

The scaffold defaults to createWorkersDevSigner from @sizl/broadcast. It uses crypto.subtle (no node:fs, no node:crypto) to generate an ed25519 keypair per isolate — every cold start mints a new key, so receipts are non-reproducible across isolates and the cassetteHash chain is dev-mode only. Every envelope is stamped DEV-KEY-DO-NOT-USE-FOR-PROD.

Swap for createPluckBureauSigner once the Bureau key is provisioned for your posting accounts (Bureau path lands in @sizl/broadcast B3+). Until then the scaffold's production checklist is explicit about the dev-key constraint.

CounterStore is best-effort

Workers KV does not expose a true compare-and-swap from the JS binding — no if-match etag. The KV CounterStore implementation reads, checks, writes — last-writer-wins. Under concurrent dispatch within KV's ~60-second eventual-consistency window, two requests can each observe dayUsd=X, both pass the cap check, and both write. The cap is breached by at most N-1 posts at concurrency N.

For typical release-webhook volume (~30 posts/week) this is acceptable; pin maxUsdPerDay below your real tolerance. For hard caps, migrate the counter to a Durable Object — the rest of the surface area is unchanged.

Tier 2 wiring is up to you

The scaffold's /approve and /kill HTTP handlers verify the HMAC-signed URL token from the email/web approval surface, but return 501 until you wire your Tier2Workflow.approve() / .kill() call. This is deliberate — clicking APPROVE on an email shouldn't appear to succeed when no dispatch happens. The 501 message points at the exact spot in handleApprovalCallback where the workflow call belongs.

Engagement collector is a stub

scheduled() walks the EngagementStore, finds posts whose next scheduled offset (1h / 6h / 24h / 72h / 7d via DEFAULT_ENGAGEMENT_OFFSETS_MS) has elapsed, marks the offset fetched, and logs a stub warning. Replace the warning with your platform-API fetch + appendHistory(signal) call once you have an API client. Until then, engagement is tracked-but-empty.

Previous
CLI

Stay in the loop. Sign up for our newsletter.

We care about your data. We'll never share your email.

Powered by Directive. This signup uses a Directive module with facts, derivations, constraints, and resolvers – zero useState, zero useEffect. Read how it works

Directive - Constraint-Driven Runtime for TypeScript | AI Guardrails & State Management