We built our own CRM because we ran into our own problem
A real, technical account of what we built, what broke, and what it does now — not "we built a CRM."
The problem
1Point runs a public site with several ways for a real prospect to get in touch — a contact form, a quote request, a trial signup, a meeting booker. All of it worked. None of it went anywhere. There was no admin account, no pipeline, no way for a human to see a lead, decide what to do with it, and track it through to a signed engagement. An audit found 9,061 tracked sessions and exactly 6 leads ever captured, all stuck at status "new" forever, lead score always 0 despite the column existing.
Before
The AdminDashboard that existed wasn't a CRM at all — it was a SaaS-billing panel for subscriptions and tickets, and it never queried the leads table. Two other admin/customer dashboards in the codebase were dead code: different auth context, mock data, unreachable routes.
What we built
A two-pipeline model, deliberately separate from a single "deal stage" field: a sales pipeline on the lead itself (new → discovery → proposal → won/lost), and a separate delivery pipeline on a new engagements table (onboarding → active → delivered → review → case study → upsell), linked through proposal_workstream_id.
That link is the part that mattered most. The obvious design — one engagement per problem a lead selects at intake — is wrong. A prospect might tick five problem categories during initial intake (funding, staffing, software, marketing, customer support) purely to explore options, then only two of those become real, scoped, priced work after discovery. Auto-creating five engagements from five checkboxes would have invented four pieces of "work" that were never sold. Engagements are created only from accepted proposal workstreams, after a proposal is actually built and won.
Architecture
Five migrations extended leads and added lead_activity, proposals + proposal_workstreams as structured rows (not JSON, so real revenue-by-division queries are possible), engagements, engagement_tasks, engagement_milestones, documents, messages, funnel_events, and review/testimonial/case-study fields on engagements — hooks for the exact authority flywheel this portfolio page is part of.
A new /get-started page replaced a forced service-category dropdown with a 5-step "tell us what you're trying to solve" intake, wired as the primary call-to-action sitewide. A submit-intake edge function handles it: honeypot + timing + rate-limit + bot-name-heuristic spam protection without a CAPTCHA, a transparent rule-based lead-scoring trigger, and a real acknowledgement email that names the prospect's actual selected problems back to them instead of a generic "thanks for contacting us."
Workflow
A relationship owner sees a new lead land in a 9-stage kanban with real neglect-obvious stat counters, works it through discovery, builds a proposal with priced workstreams inside the lead drawer, and — only on WON — a separate accept-proposal edge function creates or matches the client, provisions a real Supabase auth account and portal invite, and creates exactly one engagement per accepted workstream. The client then sees a simplified 7-step stage bar in their own portal, with per-engagement workstream cards, messages, documents, and tasks — all RLS-scoped so one client can never see another's data.
Challenges — what broke
The lead-scoring trigger overwrote manual work. It was originally BEFORE INSERT OR UPDATE — meaning every time an admin manually adjusted a lead's priority, the trigger silently recalculated and overwrote it on the next save. Caught before it shipped, changed to INSERT-only so the score is a fixed intake-time snapshot.
A one-time admin-bootstrap function was a real security exposure while it existed. No admin account existed on the project at all, so a temporary Edge Function was built to create the first one, used once, then genuinely deleted — confirmed by a 404 on direct call, absence from the remote function list, and zero remaining code references.
The site's own build script was quietly polluting real analytics. A Puppeteer-based prerender script runs on every deploy to bake real per-route SEO content into static HTML. It blocked Google Analytics and a handful of other third-party scripts from firing during that process — but not the site's own Supabase calls, so every deploy was writing thousands of fake "sessions" into the same table real visitors used. 91.5% of the site's all-time tracked sessions turned out to be the build process, not people.
What we fixed
Built a shared guard checked by all 8 tracking write paths in the app, gated on a flag set before any app code runs during prerender, plus a headless-automation signal that also catches other bots — stops the write from being attempted at all, not just blocked at the network layer after the fact.
Added a real classification column to the sessions table and backfilled every historical row from its stored user-agent — nothing deleted, but every report now separates human traffic from build noise automatically.
Ran a real end-to-end test under a fake lead named "TEST — CRM E2E" that selected 5 problem categories, moved through the full pipeline, got a 2-workstream proposal, both accepted — result: exactly 2 engagements created, proving the corrected model actually holds.
Verified cross-client RLS isolation directly between two real client accounts by querying the database as each — not by reading the policy definitions and assuming they work.
What the system does now
Every real lead lands in a working pipeline instead of a table nobody looks at. Attribution reporting shows revenue by acquisition channel, using granular per-source data rather than a generic "social" bucket. The public intake, the admin pipeline, and the client workspace are all live in production, running 1Point's own real pipeline today — not a demo of what a CRM could do.
Lessons
The most consequential decision in the whole build wasn't a line of code — it was refusing the obvious "one engagement per intake checkbox" shortcut and modelling the real gap between what a prospect explores and what actually gets sold. The second-most consequential thing was catching the analytics-pollution bug at all: it had been silently inflating a headline traffic number for months before anyone thought to check what a "session" actually was.
This is the same system running behind /get-started right now.