FindLeads — Lead Generation with a Built-in CRM
A business with no website is a web developer's best prospect. FindLeads searches Google Places for them, flags the website-less as tier-1 leads, and wraps a tiny CRM around the results.
Overview
FindLeads is a personal lead-generation tool: it searches the official Google Places API (Text Search New) for businesses by category and location, flags businesses without a website as tier-1 prospects for web-design outreach, and layers a lightweight CRM on top — per-business notes, a contacted toggle, CSV export. Next.js 16 App Router with React 19, Neon Postgres via Drizzle ORM, Zod validation at every boundary, and a deliberately queue-less async job design: a job row in the database, background work via Next.js after(), and client-side polling.
The Problem
Prospecting for freelance web work means hours of manual map-scrolling to answer one question: which real, operating businesses near me have no website? The data exists in Google Places, but raw API results are unusable for follow-up — no memory of who was already contacted, no notes, no export. The interesting engineering constraint was scope discipline: one user, no auth, no queue infrastructure — how much durability can a plain database row and careful concurrency design deliver?
Questions Addressed
- 01
Can a resumable background job survive crashes and duplicate workers using nothing but Postgres and atomic updates?
- 02
How should durable CRM state coexist with re-runnable search snapshots so a re-scrape never wipes your notes?
- 03
How far does a test-first discipline go on a one-week, one-person MVP?
Methodology
Places Search & Tier-1 Flagging
Search jobs call Google Places Text Search (New) by category + free-text location. Every response is validated with Zod before anything touches the database, and businesses with no website field become tier-1 leads. Results are stored as per-job snapshots so each search run is reproducible.
A Queue-less, Resumable Job Worker
Instead of Redis or a queue service, jobs are database rows processed via Next.js after() with client polling over SWR. The worker checkpoints progress and reclaims work through single-UPDATE atomic claims, so a crashed or duplicated worker never double-processes — behavior pinned down by integration tests against a real Neon test database.
CRM Layer & the Durable/Snapshot Split
Durable CRM state (notes, contacted status) lives in a businesses table keyed by place_id, deliberately separated from per-job lead snapshots — re-scraping a city never resets what you know about a business. CSV export closes the loop for actual outreach. 123 tests (unit + real-DB integration) cover the pipeline; test code outweighs product code roughly 1.35:1.
Key Results
Key Findings
Postgres is a perfectly good job queue at single-user scale: an atomic single-UPDATE claim gives you crash-safety and duplicate-worker safety with zero new infrastructure.
Splitting durable identity (keyed by place_id) from run snapshots is what makes a scraper re-runnable — state you care about should never live in state you regenerate.
On AI-assisted builds, the test suite is the contract: 123 tests written alongside the implementation is what made "feature-complete" a verifiable claim instead of a feeling.
Conclusion
FindLeads is a complete, working MVP built in about a week — and honestly scoped: single-user by design, localhost-only so far, with its own 15-item severity-ordered gap audit committed to the repo. It's the clearest small example of the workflow behind the bigger projects: plan in phases, validate at boundaries, test against real infrastructure, and write down what's still weak.
Gallery
