81% of people who start filling out a web form abandon it before submitting (The Manifest, 2018). For ecommerce, cart abandonment recovery emails convert about 10% of those back into customers. But for lead gen forms, contact forms, quote request forms — there's been no equivalent.
I built FormRecap over an Easter long weekend to fix this. It's a 2.7KB JavaScript snippet that detects form abandonment and sends recovery emails with magic links that restore the visitor's exact form state.
The entire product — API, database, session tracking, email dispatch, billing, and dashboard — runs on Cloudflare's developer platform. Here's how.
The architecture
Snippet (2.7KB IIFE)
↓ sendBeacon / fetch
Cloudflare Worker (Hono API)
↓
Durable Object (per-form session tracking)
↓ alarm fires after abandonment delay
Queue → Workflow → Resend (recovery email)
↓
Visitor clicks magic link → form restored
The stack:
| Service | Purpose |
|---|---|
| Workers | API + SPA serving |
| D1 | All relational data |
| Durable Objects | Per-form session state + abandonment timing |
| Queues + Workflows | Async recovery email pipeline |
| KV | Config cache, session tokens, rate limiting |
| Analytics Engine | High-volume event metrics |
The snippet
The client-side snippet is the most constrained part of the system. It runs on customer websites, so it has to be:
- Tiny — 2.7KB gzipped, zero dependencies
- Compatible — ES2015 target (no optional chaining, no nullish coalescing, no async/await)
- Privacy-first — excludes passwords, credit cards, SSNs, and 31 other sensitive field patterns by default, plus value-level regex for credit card and SSN formats
-
Non-blocking — uses
sendBeaconwithtext/plaincontent type to stay CORS-safelisted (no preflight requests)
It discovers forms via querySelectorAll with a body-level MutationObserver for SPAs, tracks field events (focus, blur, change, input), and detects abandonment through visibilitychange, pagehide, beforeunload, and SPA navigation (pushState/replaceState/popstate).
Per-customer encryption
Since the snippet captures form field data, security couldn't be an afterthought. Every customer's data is encrypted with its own key:
customerKey = HKDF(masterSecret, salt=siteId, info="formrecap-field-enc")
Field data is encrypted with AES-GCM-256 using that derived key. Email addresses use HMAC blind indexes for searchable lookups without storing plaintext. All signature and hash comparisons use timing-safe comparison to prevent timing attacks.
What surprised me
Durable Objects for session tracking turned out to be the perfect fit. Each form session gets its own DO instance that accumulates events, detects the email field, triggers the abandonment alarm, and persists encrypted snapshots to D1. The alarm API means abandonment detection is just "set alarm for N seconds after last activity" — no polling, no cron.
The CORS-safelisted sendBeacon trick saved a lot of complexity. By using text/plain as the content type, the browser treats it as a "simple request" and skips the preflight OPTIONS request entirely. The Worker parses the JSON body server-side regardless.
Smart Placement handles the latency trade-off automatically. API routes that hit D1 run near the database, while static assets serve from the nearest edge.
Infrastructure cost
On the Workers paid plan ($5/mo), the included limits cover roughly 1,000 free-tier customers. The main variable cost is Resend for recovery emails. At early scale, total cost is about a coffee per month.
Try it
- Live demo: formrecap.com/demo — fill out a form, leave the page, and watch the recovery flow
- Docs: docs.formrecap.com
- Free tier: 500 sessions/month on one site
I'm building this solo and in public. Feedback on the architecture, the snippet's privacy approach, or the product itself is very welcome.
Top comments (0)