Building Action Network on Cloudflare's free tier
How I built a production-hardened campaign action pages platform with 1,677 tests, 8 action types, 9 integrations, and AI-generated pages
How I built a production-hardened campaign action pages platform
The problem
Action Network starts at $59 a month and climbs past $1500 for anything that looks like a real campaign. It is closed source, US-centric, and the reporting lags real time by enough that it is never quite the source of truth you want during a launch. On top of the subscription, campaign engineers routinely burn thousands of dollars of billable time wiring up petition pages to the CRM, then wiring the CRM to the ad platforms, then wiring the ad platforms to the email vendor. Most of that glue work is the same job, done again, per client.
I wanted to see whether I could build the whole thing — the petitions, the letters, the event RSVPs, the email blasts, the platform integrations, the admin surface, the AI assist — for free, on Cloudflare's edge, end to end. Not a demo. A platform I would be willing to put a paying client on.
This is a writeup of how that went.
The architecture
The platform runs entirely on Cloudflare's free tier primitives: Workers for compute, D1 (SQLite) for submissions and page configs, KV for rate limiting and the edge cache, R2 for uploaded media and exported CSVs, and Turnstile for bot protection.
The frontend is Astro 6 with React 19 islands. Only the action forms themselves are interactive — the rest of the page is static HTML streamed from the edge. Templates and themes are composed in an emdash CMS plugin so a non-engineer can build a page without touching code.
The hot path has zero external dependencies. No Redis. No managed queue. No Postgres. No third-party API waits before the user sees a thank-you state. Everything happens either inside the Worker or inside waitUntil() after the response has already shipped.
The hot path
When a supporter signs a petition, the Worker runs a six-step pipeline before it sends the response: rate limit check (fixed-window KV counter hashed by IP + slug), Turnstile verification (fail-closed), geo filter (cf-ipcountry against per-page whitelist/blacklist), email deduplication (SHA-256 hash scoped to the slug), D1 write (single prepared-statement insert), and response sent. That is everything the user waits for. In production the p50 for that path is under 80ms from the edge city nearest the supporter.