← All projects
Session Handoff · LeadScout DFY

LeadScout β€” safety audit done + exact "make it sellable" fix plan (2026-07-11)

Saved 2026-07-11

Paste it into a fresh Claude Code session and say “resume from this handoff.” It's briefing only — Claude will wait for your next instruction.

LeadScout β€” safety audit done + exact "make it sellable" fix plan (2026-07-11)

ACTIVE MODES (re-invoke first in the fresh session)

The app (confirmed by name + id)

---

PART A β€” already SHIPPED + LIVE + verified this session (the 3 UX fixes). DO NOT redo.

Goal was "fix 3 broken things." All done, deployed to the master, verified by a real run.

  1. Campaign-detail page β€” new src/pages/CampaignDetail.jsx + route /campaigns/:id in App.jsx. Opens a campaign β†’ its leads (filtered), scan status, counts (Scanned/No-website/Social-only/Leads-saved), Launch-scan + Verify + Push-to-GHL + Export scoped to it.
  2. Dashboard clickable — Home.jsx stat tiles navigate (Campaigns/Leads/No-Website→/leads?site=no_website/Social-Only); recent-campaign rows open detail. StatCard.jsx gained optional onClick. Leads.jsx reads ?site=/?campaign= initial filters. Campaigns.jsx rows are clickable. AppLayout.jsx nav stays active on /campaigns/*.
  3. City / County / Zip choice β€” Campaigns.jsx New-Campaign form has a 3-way toggle. Entity Campaign changed: zip_codes now OPTIONAL (required is [name, niche]), added search_areas (array) + location_mode (enum zips/city/county) β€” back-compatible (old zip campaigns still open). scan-campaign builds the Outscraper query from search_areas β†’ else zip_codes β†’ else location_label; stamps zip only when the area is a real 5-digit zip.

---

PART B β€” THE JOB FOR THE FRESH SESSION: make LeadScout a SELLABLE, clone-ready template.

Sondra: "I have to be able to sell this... make sure everything is working." She chose HANDOFF β†’ fresh session for this because it's a security + live-data migration on her 154 real leads and this session crossed her 250K freshness line.

Verdict from the /app-safety-check (why it's NOT sellable yet) β€” πŸ”΄ DO NOT SELL YET

Two CRITICAL holes, both confirmed by actually running them:

  1. πŸ”΄ World-readable data, no login. public_settings = "public_without_login" + RLS is null on ALL 6 entities. Anonymous GET to .../entities/Campaign and .../entities/Lead returned 200 + full records (pulled a real business name/phone/address with zero auth). Every buyer's leads would be public.
  2. πŸ”΄ Paid functions open to anonymous callers. Anon POST to push-to-ghl + verify-leads got inside the function (400 "No leads selected" = past the door). Real IDs β†’ burns Outscraper/Firecrawl/GHL credits + mutates others' leads. All 3 functions run base44.asServiceRole with no auth.me() gate and no ownership check.
  3. 🟠 SHOULD-fix: caps are decorative. WorkspaceSettings (fields daily_outscraper_cap, daily_firecrawl_cap, max_results_per_scan) is only read/written by SettingsPage.jsx. UsageEvent (kind, units) is never written. No function checks a cap β†’ unlimited spend.
  4. 🟠 No Privacy/Terms page (collects business contact data + pushes to a CRM). Pages have no /privacy.
  5. is_remixable = False β€” must be turned ON to clone/sell (do LAST).

Load-bearing facts discovered (these shape the fix β€” don't re-derive):

THE FIX β€” ordered, safe sequence (checkpoint first!)

  1. npx base44 sandbox checkpoint --name "before sellable-security fixes".
  2. Require login β€” flip public_settings off public_without_login (try npx base44 auth …; if not CLI-exposed, Sondra toggles "require login" in the builder). Verify: anon GET /entities/Lead β†’ 401.
  3. Add owner_email (string) to Lead (sandbox edit base44/entities/Lead.jsonc, syncs live). Add it to ScrapeJob + UsageEvent too if you scope those.
  4. Fix the 3 functions (base44/functions/*/entry.ts, they're pulled in sandbox-work):
  1. Backfill the 157 existing leads' owner: exec loop β€” for each lead, Lead.update(id, { owner_email: <its campaign's created_by> }) (campaign owner = sondra for all current data). Custom field β†’ update works.
  2. Granular RLS (the ENFORCED form β€” rls, not row_level_security) on every data entity, sandbox-editing each .jsonc:
  1. Publish (base44-workflow Β§6): sandbox edits already synced entities β†’ eject master β†’ VITE_BASE44_APP_ID=6a46ad7a89bffa371e7637da npm run build β†’ verify bundle has master id + no copy id β†’ site deploy with base44/entities set aside β†’ functions deploy LAST (site deploy wipes the fn registry). Delete the eject "Copy".
  2. PROVE it (the DFY gate) with 2 real accounts (use Google login β€” email OTP was undeliverable + Turnstile blocks automated signup on this platform): (a) anon GET /entities/Lead β†’ 401; (b) anon POST to each function β†’ 401; (c) account B's dashboard empty + B cannot read A's campaign/lead by URL or API; (d) B cannot verify/push A's leads. Sondra (admin) still sees her 157.
  3. Only then: turn is_remixable ON + write NEW-OWNER-SHEET.md (buyer adds their own OUTSCRAPER/FIRECRAWL/GHL secrets; first signup = admin; publish once; functions re-register on their first builder publish).
  4. Add a /privacy page (pull from reference_canonical_legal_docs / context/legal/ β€” entity is a lead tool, not health data).

Definition of done (Gate 1 β€” verify, don't assert)

A stranger who clones LeadScout: logs in (becomes admin), sees ONLY their own campaigns/leads, the internet can't read the data API, anon can't fire the paid functions, caps are enforced, and the 3 UX fixes + full scan→website-check→score→push path all work — proven by running it with two accounts.

---

Housekeeping / uncommitted

Generated for the CEO Dashboard · source: PROJECTS/LeadScout DFY/handoffs/leadscout-safety-audit-and-sellable-fix-plan-2026-07-11.md 🀍