The automation backbone
The lead engine running on this page
The capture system behind the free tools on this site — what it is made of, why it is ordered the way it is, and what happens when a piece of it goes down.
July 30, 2026 · 4 min read
The fastest way to show you what I build is to describe the thing you are currently standing on. Every free tool on this site runs through the system below. It is small, it is boring in the right places, and it is built so that the failures it will actually experience cost nothing.
The problem
A free tool is only worth running if the lead survives. That sounds obvious until you look at where most capture forms put the data: nowhere, for a few hundred milliseconds, while something more interesting happens.
Three things had to be true:
- A submission that reaches the server is never lost, whatever else is broken.
- The visitor never sees a failure they cannot act on. They typed their details; that is their whole job.
- The slow parts — enrichment, scoring, placing a callback — can fail and retry without charging me twice or texting me four times.
The shape
Two systems, and the split between them is the whole design.
InstantDB is the state layer. It knows things. Leads live there and it is the only queryable record.
Inngest is the work layer. It does things, reliably and later. Its event history is not a database and should never be treated as one.
Neither substitutes for the other. Drop the workflow engine and a failed callback vanishes silently. Drop the database and there is no lead list at all.
POST /api/lead
validate + honeypot + IP rate-limit
→ InstantDB write ← the lead is durable HERE
→ inngest.send({ leadId }) ← a pointer, not a payload
→ 200 { ok: true }
handleLeadCaptured [durable, 4 retries, each step checkpointed]
load → enrich → mark-enriched → notify → mark-notified
The decisions worth explaining
Persist before emitting. My first version emitted the event first and let the workflow do the storing. That meant the lead existed nowhere durable until the workflow ran, so any hiccup in delivery turned a real submission into a log line. Two lines moved. Now a workflow outage delays follow-up instead of destroying the lead, and every un-processed row is queryable by its status.
The event carries an id, not the lead. Once the row exists, the workflow can load what it needs. No names, no emails, no phone numbers on the event bus at all.
Every failure degrades to logging, never to an error page. A missing API key, a provider incident, a database timeout — the visitor still gets a success screen, because they did their part and telling them otherwise helps nobody. The failure is logged loudly, with the personal data stripped out:
[lead] failed to persist lead {
tool: 'callback', receivedAt: '…', ipHash: '952d0060…',
hasEmail: true, hasPhone: false
}
Enough to diagnose an incident. Not enough to leak anyone.
IPs are hashed, never stored. Rate limiting and duplicate detection need a stable identifier. Neither needs the actual address.
Bot traps that do not punish humans. An off-screen honeypot field — real people never fill it, naive bots fill everything they find. A filled honeypot gets the exact same success response a human would, so the bot has no signal to adapt to, and no work is done.
What it is made of
| Layer | Choice |
|---|---|
| Framework | Next.js 16, App Router |
| Store | InstantDB |
| Workflow | Inngest |
| Validation | Zod, at the trust boundary only |
| Hosting | Vercel, deployed from GitHub |
Validation happens exactly once, at the edge of the system. Validating the same data again downstream means maintaining two definitions of correct, and they drift.
How it was verified
Claims about reliability are worth nothing without the test that backs them, so:
a real submission on production wrote to InstantDB with every field intact and
the IP hashed, then Inngest executed all five steps and advanced the record to
status: "notified". The API was separately exercised for missing fields,
malformed JSON, per-tool required fields, a filled honeypot, and rate limiting.
The failure path was tested by running the system with no workflow key at all.
Result: the lead still landed, the response was still 200, and the error log
named the missing variable.
Where it goes next
The enrichment and notification steps are deliberately stubs with fixed interfaces — they get filled in per client, because "notify" means WhatsApp for one business and a Voice AI callback for another. The surrounding machinery does not change when they do.
That is the point of building it this way. The parts that are hard to get right — ordering, durability, what happens when something is down — are solved once. What varies between businesses is the last mile, and the last mile is the easy part.