Catch the leads that leave your Astro forms without hitting submit.
When a visitor types into your form and walks away, the lead is saved. Around that capture sits the complete lead-ops platform: recovery emails, quote payments, and a self-hosted admin, with zero external services by default, just a SQLite file and your existing SMTP env vars. 1,102 unit tests and a Playwright e2e suite cover it.
Running in production on a live services business: versions 0.1.2 through 0.1.10 each shipped from a real-world finding there. The stories are in Hardened in production.
cool-astro-forms instruments the Astro forms you already have; it is not a form builder. It rebuilds the lead-capture layer WordPress sites assemble from premium form plugins and their paid addons as one MIT Astro integration for server-output sites, covering 5 stages:
- Capture: form-abandonment capture, per-visitor journey tracking, and IP geolocation on every saved entry
- Recover: a save-confirmed "progress saved" toast plus one automated follow-up email per abandoned lead
- Convert: quote-first Stripe and PayPal payments, plus shareable
/forms-paypayment links - Manage: the
/forms-adminUI (entries, abandoned leads, payments, analytics funnel, CSV +.dbexport) - Integrate: Google Drive uploads, signed outbound webhooks, and Cloudflare Turnstile spam control
We haven't found another open-source package that offers abandonment capture for Astro or static sites. The other 4 stages ship alongside it so a captured lead has somewhere to go.
Every step below is executed end-to-end by scripts/verify-quickstart.mjs: a real npm pack tarball installed into a scratch Astro project, built, served, and hit with a real abandon POST that lands a row in SQLite. If this section and that script ever disagree, the script is right.
npm install cool-astro-forms @astrojs/nodeRequires Astro 6 or 7, a server-output adapter, and Node 22.12+. The install compiles better-sqlite3 natively when no prebuilt binary matches your platform; if that build fails you need Python plus a C toolchain, or skip native modules entirely with the Turso/libSQL path (docs/serverless.md).
Optional scaffold first: init writes a full .env.example (every optional env var this package reads) and appends data/ to .gitignore:
npx cool-astro-forms initastro add cool-astro-forms also works, but it inserts a bare coolForms() call that fails validation on your next astro dev/astro build: siteId, siteUrl, and each form's notifyTo are required, with no defaults. Replace the bare call with the snippet below.
Every field shown here is required:
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
import coolForms from 'cool-astro-forms';
export default defineConfig({
output: 'server',
adapter: node({ mode: 'standalone' }),
integrations: [
coolForms({
siteId: 'my-site',
siteUrl: 'http://localhost:4321', // must match the origin you actually serve on — swap for your real domain once deployed
forms: {
contact: { notifyTo: 'owner@example.com' },
},
}),
],
});Deploying behind a proxy or CDN? Production hosts that terminate TLS need one extra security line; see the note after step 4.
Add a data-caf="<formId>" attribute; no other markup changes are needed:
<form data-caf="contact" method="post" action="/api/contact">
<input type="text" name="name" />
<input type="email" name="email" />
<button type="submit">Send</button>
</form>npm run build
node dist/server/entry.mjsThe capture route is auto-injected at /api/forms/abandon. A visitor who types into that form and leaves lands a row in data/forms.db and a notification email at notifyTo. That is the entire adoption contract: one coolForms() call, one attribute. Payments, Drive uploads, lead recovery, and the admin UI stay inert until you opt in.
Deploying behind a proxy? Most production Node hosts (Hostinger, Passenger/LiteSpeed, most PaaS) terminate TLS before your Node process, so Astro sees a plain-HTTP socket and only trusts the proxy's
X-Forwarded-Protoheader whensecurity.allowedDomainsis set. Without it, every urlencoded form POST to a server route fails with403 Cross-site POST form submissions are forbidden, and the first place you will meet that is the/forms-adminlogin form. Add your real domain once you deploy:security: { allowedDomains: [{ hostname: 'example.com', protocol: 'https' }], },Abandonment capture keeps working either way (it posts JSON, which Astro's CSRF check ignores); the break hits admin login and payment form posts.
-
The injected client script stages fields as the visitor types. Passwords,
data-caf-ignorefields, and card/CSRF-shaped names are never staged. -
Any of the 4 capture triggers POSTs the staged fields to
/api/forms/abandon. -
The server gates the save (origin check, rate limit, honeypot, email-or-phone requirement), dedupes repeat abandons within a 60-minute window, and writes one SQLite row with the journey trail and geolocation attached.
-
You get a notification email carrying the captured fields, the journey trail, the visitor's location, and the traffic source; the visitor sees a save-confirmed toast, and lead recovery (opt-in) sends one follow-up email with a link to finish.
-
A visitor who returns and submits converts:
recordSubmission()marks the entry converted and stitches the full journey onto it. -
/forms-admin/analyticsturnsform_startedpings and captures into a funnel with abandonment rate and top drop-off fields. -
Payments close the loop: create a quote from any entry, or share
/forms-pay?amount=200. The server recomputes every total, and only a verified provider webhook marks a payment paid. The pay page submits over fetch and hops to the provider as a plain GET, so edge bot-challenges (Cloudflare and friends) cannot break the checkout hop; seedocs/payments.mdsection 3b.
Every trigger, gate, and fallback in detail, with screenshots: docs/how-it-works.md.
| Feature | Detail |
|---|---|
| Abandonment capture | 4 triggers: exit-intent, external-link click, beforeunload, tab-hidden; dedupe window; per-form overrides |
| User-journey tracking | Per-visitor page trail, shown on each entry's timeline |
| IP geolocation | Every saved entry enriched (GEO_PROVIDER override supported); a failed lookup never blocks the save |
/forms-admin UI |
Entries, abandoned, payments, analytics funnel, CSV + .db export; server-rendered, password-protected |
| Payments | Quote-first Stripe Checkout + PayPal; shareable /forms-pay?amount= links; server-computed fees; fetch-first transport (edge-bot-challenge-proof checkout hop); inbound webhooks as the sole payment truth |
| Spam control | Cloudflare Turnstile (soft-fail), honeypot, rate limiting |
| File uploads | Google Drive via raw REST (no SDK); Drive failures fall back to email attachment, the entry always saves, and oversized files are flagged |
| Lead recovery | One automated follow-up email per visitor, one-click unsubscribe honored forever |
| Outbound webhooks | Signed (X-Caf-Signature): entry.submitted, entry.abandoned, payment.paid |
| GDPR mechanics | retentionDays purge, purgeVisitor() erasure, requireConsent gating |
| Storage | SQLite file by default; optional Turso/libSQL for serverless hosts (docs/serverless.md) |
WordPress sites pay for form-abandonment capture; Astro and static sites have had no open-source equivalent. That gap is why this package exists. It is an independent, clean-room implementation, not affiliated with or endorsed by any commercial form-plugin vendor (see NOTICE). The table maps the premium form plugin features you lose leaving WordPress to what this provides:
| Capability | WordPress forms stack (Pro + addons) | Hosted form SaaS | Self-hosted OSS form backends | cool-astro-forms |
|---|---|---|---|---|
| Abandonment / partial-entry capture | Paid addon | Not offered | Not offered | Built in |
| Automated lead-recovery emails | Via automation addons | Not offered | Not offered | Built in |
| Stripe / PayPal quote payments | Paid addons | Varies by plan | Not offered | Built in |
| Admin UI + analytics | WordPress admin | Their dashboard | Minimal or none | /forms-admin |
| Where your data lives | Your WP database | Their servers | Your infrastructure | Your SQLite file |
| Cost | Annual per-site license | Monthly plan | Free | Free, MIT |
One deliberate trade-off: this package captures and manages form data, but it doesn't generate form markup. Bring your own form; a drag-and-drop builder is the one WordPress feature it won't replace.
This package runs in production on a live services business. Versions 0.1.2 through 0.1.10 each shipped from a finding that surfaced there, and the fixes are in the defaults you install today:
-
Payments survive edge bot-challenges. A challenged navigation POST is structurally unrecoverable: the interstitial cannot replay the body. Only real browsers get challenged, so every curl and dev-environment test passes while production clicks die. The pay page submits over fetch (unchallengeable by design) and hops to checkout as a plain GET (docs/payments.md section 3b). When a Turnstile token expires mid-payment, the page recovers itself: inline explanation, refreshed widget, re-armed button.
-
Lead history survives redeploys. Git-deploy hosts rebuild the app directory on every release, wiping the default
data/dir.dbPathis env-configurable so the SQLite file can live outside the deploy dir; the package creates the directory and keeps the admin secret beside it. -
Turnstile in the real world. Test sitekeys mint short dummy tokens, so submit buttons arm on any non-empty token, never on token shape.
remoteipis not sent to siteverify because dual-stack visitors solve on one IP family and post on the other. Every rejection carries its Cloudflare error code, so failures diagnose themselves. -
Notification emails carry the whole picture. Journey trail, IP geolocation, and traffic source arrive next to the form fields, and hosts can brand every template through
templatesModule. A branded override looks like this (fictional demo brand):
docs/how-it-works.md— the full lifecycle walkthrough with screenshots: capture triggers, the server gate, admin views, analytics, recovery, payments, storagedocs/payments.md— Stripe/PayPal setup, the/forms-paycontract, fee caveats, webhook receiver recipesdocs/drive.md— Google Drive uploads: one-time consent, the 7-day token-expiry pitfall, the fallback contractdocs/recovery.md— lead-recovery emails: consent modes, per-form scoping, unsubscribe mechanicsdocs/serverless.md— Turso/libSQL storage, explicit secrets, cold-start-safe rate limitingdocs/gdpr.md— each retention, erasure, and consent mechanic mapped to the GDPR concept it servesdocs/faq.md— short self-contained answers: static sites, external services, payments, GDPR, production readinessdocs/comparison.md— the capability table as a standalone page, including when NOT to choose this package
Tag the form with data-caf and configure coolForms(). The injected client script stages typed values and POSTs them to /api/forms/abandon when any of the 4 capture triggers fires. The entry lands in SQLite with the visitor's journey trail and geolocation attached.
This package: it rebuilds the Pro-tier lead-capture layer of commercial WordPress form plugins (abandonment capture, payments, uploads, recovery emails, admin UI) as one MIT Astro integration with no per-site license and no hosted service.
Every abandoned entry stores the fields typed so far, the last-edited field, and a form_started ping. /forms-admin/analytics turns those into a captured→converted funnel with abandonment rate and top drop-off fields.
Create a pay-link from any entry in the admin, or share /forms-pay?amount=200 with no entry at all. The server recomputes every total, and payments are marked paid only by verified Stripe or PayPal webhooks (docs/payments.md).
Yes: /forms-admin ships in this package with entries, abandoned leads, payments, analytics, CSV and .db export, enabled by a single FORMS_ADMIN_PASSWORD env var. Server-rendered, no client framework.
Uploads land in your own Google Drive over raw REST (no SDK dependency). If Drive fails, files up to ~10MB fall back to email attachment; larger files are flagged and the entry still saves (docs/drive.md).
Test and build commands, the clean-room statement, and the blocking pre-publish checklist (including a required security audit) live in CONTRIBUTING.md.
MIT; see LICENSE.








