- Sep 6, 2026
- 8 min read
From Five Lead Silos to One Lead Hub: Before and After
When I audited my own sites, the honest answer to “who has contacted my business?” was: I don’t know. Leads were spread across an email inbox, three SaaS dashboards, a PocketBase database, a CSV file and Cloudflare KV. Each site had grown its own way of capturing people, and none of them talked to each other.
This post compares that starting point with the setup I run now, including the part that surprised me most: the PocketBase database I removed from the loop because the app only ever wrote to it.
Before: five mechanisms for five sites
| Site | How it captured leads | Where they ended up |
|---|---|---|
| multibodysystemsdynamics.com | Its own Pages Function → Workers KV → phone alert | KV (no site in the key, no consent record, no expiry) |
| www.jalcocertech.com | Footer form posting to formsubmit.co; the contact form sent nothing | An email inbox (and a dead form) |
| consulting.jalcocertech.com | A Formbricks qualifier survey, a Cal.com booking embed, a chatbot (off in production) | Formbricks and Cal.com dashboards |
| ebooks.jalcocertech.com | A link to another Formbricks survey | Formbricks |
| webaudit.jalcocertech.com | Audit modal → Express → PocketBase leads + audits, plus a CSV | PocketBase and a CSV file tracked in git |
| fossengineer.com | No forms at all | — |
Five mechanisms, two different contact addresses, three privacy policies in three different states, and analytics code for a tool I had already dropped. Most of it wasn’t wrong in isolation. Together it meant:
- No single list. Answering a deletion request meant remembering five places.
- No consistent consent. One site bundled marketing into a required
checkbox; another recorded
consent: YESfor everyone regardless. - Retention by hope. Nothing expired anything, except where I’d set it by hand.
- Vendors in the critical path. If a survey account lapsed, the main call-to-action on the consulting site would stop working.
After: one lead hub
6 sites ── form POST (same-origin /api/forms/*) ──► one Cloudflare Worker
webaudit backend ── server-to-server (keyed) ─────► ├─ Workers KV: the record of truth
Cal.com ── signed webhook ────────────────────────► ├─ phone alert (email domain only)
Mailjet ── bounce/complaint webhook ──────────────► ├─ double opt-in emails (Mailjet)
└─ do-not-send list (hashes)
homelab (pull-only) ── every 15 min ── SQLite copy · daily backups · drips · weekly digest
Every site posts to the same Worker on its own domain (so no CORS and no
third-party script), and every lead lands in one KV namespace with a key like
<site>:<kind>:<time>:<id>. The details of each piece are in earlier posts:
the forms Worker,
double opt-in with signed links,
the do-not-send list,
the homelab mirror,
the Cal.com webhook and
server-to-server audit leads.
The PocketBase question
Webaudit was the best-engineered site of the lot: rate limits, an SSRF guard, expiring report links, and PocketBase as its lead and audit store. After wiring it to the lead hub, every audit was saved twice — once in PocketBase, once in KV.
So I checked what the app actually read from PocketBase. The answer was nothing. Reports are served from files through time-limited tokens; the frontend never queries the database; the only code that touched PocketBase created records (and, later, a retention job I’d just written to delete them). PocketBase was a write-only copy with its own login, its own retention job, and a blind spot: deleting someone from the lead hub didn’t delete them there.
I turned it off by default rather than deleting the code
(PB_ENABLED=false), and the lead hub now receives the audit summary it was
missing: overall score, detected platform and the category scores. If I ever
build client features — logins, “my past audits”, shareable report pages —
PocketBase is one setting away.
Pros and cons, honestly
| Before (per-site tools + PocketBase) | After (one lead hub) | |
|---|---|---|
| One list of everyone who got in touch | ❌ Five places | ✅ One KV namespace, one SQLite copy |
| Right to erasure | ❌ Manual, easy to miss a store | ✅ One forget command (plus the provider’s dashboards for bookings and email) |
| Consent | ❌ Bundled in places, faked as YES in another | ✅ Recorded per lead with wording version and time; double opt-in for every subscription |
| Retention | ❌ Mostly none | ✅ Per kind, enforced by KV expiry (90 days for unbooked qualifiers, 24 months for enquiries, until unsubscribe for newsletters) |
| Third-party scripts on my pages | ❌ Survey widgets, an email relay | ✅ None for forms; works without JavaScript |
| Alerts | Some sites only | ✅ Every lead, privacy-friendly (email domain only) |
| Time to set up a new form | ✅ Minutes in a SaaS dashboard | ❌ Code: a registry entry, a form, a deploy |
| Survey analytics (drop-off per question) | ✅ Formbricks had it | ❌ Counts in a weekly digest, nothing per question |
| A ready app database for future features | ✅ PocketBase | ⚠️ Switched off, one setting to bring back |
| Who maintains compliance features | The vendors | ❌ Me: unsubscribe, bounces, suppression, retention are my code and my tests |
| Blast radius | A bug stays on one site | ❌ One Worker serves six sites — tests and a careful deploy matter |
| Platform dependency | Spread across vendors | ⚠️ Concentrated on Cloudflare (Workers, KV) plus a homelab |
The trade I made is clear in that table: I gave up convenience (dashboards, no-code edits, survey analytics) for control (one list, real consent, real retention, no vendor scripts). For a one-person business that wants to be able to answer “what do you hold about me?” in one command, that’s the right side to be on. For a team that edits surveys weekly, it might not be.
What I capture now
The complete picture after the migration — every form, what it stores, what happens with consent, how long it’s kept:
| Site | Form or source | Kind | What is stored | Email follow-up | Kept for |
|---|---|---|---|---|---|
| multibodysystemsdynamics.com | Contact form; ebook request | enquiry, ebook | Name, email, project, message (ebook: email); page, country, campaign tags | Reply only | 24 months |
| fossengineer.com | Question form | enquiry | Email, question, optional name; page, country | Reply only | 24 months |
| fossengineer.com | Quarterly-email box | newsletter | Email, consent wording + time | Double opt-in, then one email per quarter | Until unsubscribe (+30 days) |
| www.jalcocertech.com | Footer / contact form, optional “updates” tick | enquiry | Email, message, optional name; marketing consent if ticked | Reply; updates only after double opt-in | 24 months |
| www.jalcocertech.com | Mid-article newsletter box | newsletter | Email, consent wording + time | Double opt-in, then a 3-email welcome series | Until unsubscribe (+30 days) |
| consulting.jalcocertech.com | Qualifier form | qualifier | Offer, question, stage, budget, timeline, optional name/role, email | Reply; updates only after double opt-in | 90 days if no booking |
| consulting.jalcocertech.com | ”Notify me” box | newsletter | Email, consent | Double opt-in | Until unsubscribe (+30 days) |
| consulting.jalcocertech.com | Cal.com webhook | booking | Name, email, time, event, paid or not — no notes | ”PAID BOOKING” phone alert | 24 months |
| ebooks.jalcocertech.com | Footer “new ebooks” box | newsletter | Email, consent | Double opt-in | Until unsubscribe (+30 days) |
| webaudit.jalcocertech.com | Audit modal, sent by the backend | audit | Name, email, audited site, score, platform, category scores; optional tips consent | Tips only after double opt-in | 24 months |
| any site | Blocklisted sender | spam:* | The submission, for review | None, no alert | 30 days |
Across all of them: sign-ups never confirmed are deleted after 30 days; an unsubscribe leaves only a SHA-256 hash so the address is never emailed again; bounces and spam complaints from the email provider feed that same list; and every alert on my phone shows the email domain, never the person.
Takeaways
- Audit your own capture points before building anything; the problems were in the gaps between tools, not inside any one of them.
- Before keeping a second database “just in case”, check whether anything reads it. A write-only store is a liability with a login.
- Turn things off behind a setting instead of deleting them; the cost of keeping the option is one line.
- The final table is the real deliverable: if you can’t write it for your own business, you don’t know what you hold.