Legend
Color coding used across all diagrams. Every box that represents a file in this repo is clickable — it opens the file in the same tab.
1. Contractor acquisition funnel
How a fence contractor finds Next Estimate and reaches paid signup. Top-of-funnel sources flow into marketing pages, then into Stripe Checkout, then into the post-purchase confirmation lane.
pixel 967360269010748]:::ext ORG[Organic / Direct]:::ext REF[Referral]:::ext IDX[index.html
homepage]:::page PRO[for-fence-pros.html
product deep-dive]:::page ROI[calculadora-roi-cercas.html
Spanish ROI lead magnet]:::magnet SWITCH[switch-from-mysalesman.html
competitive LP]:::page REFUND[30-day-refund.html
risk-reversal LP]:::page DEMO[demo-estimate.html
public sandbox demo]:::page EX[example-company.html
example customer page]:::page PED[products-editor-demo.html
products editor demo]:::page START[start.html
7-day trial entry]:::page GS[get-started.html
plan picker]:::page STRIPE[Stripe Checkout
149/mo or 1490/yr]:::ext TYF[thank-you-fence.html
contractor confirm]:::confirm TRS[trial-success.html
trial start confirm]:::confirm TYS[thank-you-setup.html
setup complete]:::confirm ADS --> IDX ADS --> PRO ADS --> SWITCH ADS --> REFUND ORG --> IDX ORG --> ROI REF --> IDX IDX --> DEMO IDX --> EX IDX --> PED IDX --> START IDX --> GS PRO --> START SWITCH --> START ROI --> START REFUND --> START DEMO --> START START --> STRIPE GS --> STRIPE STRIPE --> TRS STRIPE --> TYF TRS --> TYS click IDX "../index.html" click PRO "../for-fence-pros.html" click ROI "../calculadora-roi-cercas.html" click SWITCH "../switch-from-mysalesman.html" click REFUND "../30-day-refund.html" click DEMO "../demo-estimate.html" click EX "../example-company.html" click PED "../products-editor-demo.html" click START "../start.html" click GS "../get-started.html" click TYF "../thank-you-fence.html" click TRS "../trial-success.html" click TYS "../thank-you-setup.html"
Tracking: Meta Pixel 967360269010748 on all marketing pages, separate contractor pixel 1710532113240325 on contractor-confirm pages. Microsoft Clarity vbr6bfp3p2 for session replay. Server-side Meta CAPI Lead + Subscribe events fire from stripe-webhook.js via _lib/meta-capi.js.
2. Contractor onboarding
Stripe payment fires the webhook, which creates the customer row + auth user + welcome email with magic link. The contractor fills out the intake form; Jake's runbook generates a SQL seed; pasting it into Supabase brings /e/<slug> live.
checkout.session.completed]:::ext WH[stripe-webhook.js
verify sig + branch]:::fn CUST[(public.customers
status=trial)]:::tbl AUTH[(auth.users
magic-link enabled)]:::tbl WELCOME[Welcome email
+ magic-link CTA]:::email CAPI[Meta CAPI Lead event]:::ext CSU[customer-setup.html
intake form]:::page GHL[GHL intake webhook
notifies Jake]:::ext RUNBOOK[estimator/RUNBOOK.md
Wave 2 procedure]:::manual PROMPT[estimator/_template/
CLAUDE-PROMPT.md]:::manual TPL[estimator/_template/
customer-seed.sql.txt]:::manual SEED[supabase/seed/
customer-SLUG.sql
generated by Claude]:::manual PASTE[Jake pastes seed in
Supabase SQL Editor]:::manual LINK[Manual user-customer
link SQL on first signin]:::manual LIVE["/e/SLUG live
served by estimate.html"]:::page MY[my-updates.html
contractor lands here]:::page STR --> WH WH --> CUST WH --> AUTH WH --> WELCOME WH --> CAPI WELCOME --> CSU CSU -->|optional photos| GHL GHL --> RUNBOOK RUNBOOK --> PROMPT PROMPT --> TPL TPL --> SEED SEED --> PASTE PASTE --> CUST CUST --> LIVE WELCOME -->|magic link| MY MY -.first signin.-> LINK click WH "../netlify/functions/stripe-webhook.js" click CSU "../customer-setup.html" click RUNBOOK "../estimator/RUNBOOK.md" click PROMPT "../estimator/_template/CLAUDE-PROMPT.md" click TPL "../estimator/_template/customer-seed.sql.txt" click SEED "../supabase/seed/customer-wr.sql" click LIVE "../estimate.html" click MY "../my-updates.html"
Legacy HTML flow (only for non-standard product lineups) generates estimator/<slug>/estimate.html instead of a DB row. See "Legacy HTML flow" in RUNBOOK.md. Wave 2 is now the default.
3. Email lifecycle (Resend)
Every transactional email the system sends, in time order. Welcome and trial-end emails are event-triggered by the Stripe webhook; the daily drip and post-conversion emails are cron-driven; lead-notification and update-request emails are user-action triggered.
invoice.payment_succeeded]:::ext LEAD[Homeowner submit]:::ext TICKET[Update-request created]:::ext CRON1[onboarding-drip.js
cron 14:00 UTC]:::fn CRON2[post-conversion-drip.js
cron 14:30 UTC]:::fn WH[stripe-webhook.js]:::fn SUB[submit-estimate.js]:::fn UR[update-request-email.js]:::fn E0[Day 0 Welcome
+ magic link]:::email E1[Day 1 drip]:::email E2[Day 2 drip]:::email E3[Day 3 drip]:::email E5[Day 5 drip]:::email E6[Day 6 drip]:::email E7[Day 7 drip]:::email TWE[trial-will-end
3-day warning]:::email POST[Day 30 check-in
leads + metrics]:::email LN[Lead notification
Resend fallback if no GHL]:::email TACK[Ticket ACK]:::email TRES[Ticket resolved]:::email IDEM[(onboarding_emails_sent
idempotency)]:::tbl HELPER[_lib/email.js
Resend wrapper]:::fn TRIAL --> WH --> E0 WH --> TWE CONV --> WH CRON1 --> E1 CRON1 --> E2 CRON1 --> E3 CRON1 --> E5 CRON1 --> E6 CRON1 --> E7 CRON1 --> IDEM CRON2 --> POST LEAD --> SUB --> LN TICKET --> UR --> TACK UR --> TRES E0 -. via .-> HELPER E1 -. via .-> HELPER POST -. via .-> HELPER LN -. via .-> HELPER TACK -. via .-> HELPER click WH "../netlify/functions/stripe-webhook.js" click CRON1 "../netlify/functions/onboarding-drip.js" click CRON2 "../netlify/functions/post-conversion-drip.js" click SUB "../netlify/functions/submit-estimate.js" click UR "../netlify/functions/update-request-email.js" click HELPER "../netlify/functions/_lib/email.js"
Idempotency: onboarding_emails_sent (customer_id, campaign) primary key prevents duplicate drip sends if a cron runs twice in a day. Welcome and trial-will-end emails fire from Stripe webhook events directly, not from cron.
4. Homeowner lead journey (per customer)
A homeowner lands on a contractor's branded estimator at /e/<slug>, walks the four-step flow, and submits. The submit handler writes to Supabase, fires the customer's GHL webhook (or Resend fallback during trial), generates a PDF, and shows the homeowner a thank-you page.
estimate.html canonical"]:::page S1[Step 1 — Address
Maps autocomplete]:::page S2[Step 2 — Fence type
tile grid]:::page S3[Step 3 — Review
map + breakdown]:::page S4[Step 4 — Contact + submit]:::page SUB[submit-estimate.js
Phase B3 lead router]:::fn RPC[(insert_estimate RPC
+ public.estimates row)]:::tbl GHL[Customer GHL webhook]:::ext EMAIL[Lead email via Resend
fallback if no GHL]:::email CAPI[Meta CAPI
Lead from /e/]:::ext PDF[get-estimate-pdf-url.js
1h signed URL]:::fn STG[Supabase Storage
lead-pdfs bucket]:::ext TY[thank-you.html
homeowner confirm]:::page LEG[flow/step1..step4.html
legacy estimator]:::legacy EST --> S1 --> S2 --> S3 --> S4 --> SUB SUB --> RPC SUB --> GHL SUB --> EMAIL SUB --> CAPI SUB --> PDF PDF --> STG SUB --> TY click EST "../estimate.html" click SUB "../netlify/functions/submit-estimate.js" click PDF "../netlify/functions/get-estimate-pdf-url.js" click TY "../thank-you.html" click LEG "../flow/step1-measure.html"
Local URL: http://localhost:8080/estimate.html?slug=<slug> (the /e/:slug rewrite is a Netlify rule, not a local-server feature). Legacy customers (kingdom, hicks-fencing pre-Wave-2) load estimator/<slug>/estimate.html directly.
5. Contractor self-service (/my/updates)
After magic-link signin, contractors land on a 4-tab dashboard where they can edit branding, pricing, view incoming leads, and adjust their service-area polygon. Self-service writes go straight into customers.branding_json / pricing_json / service_areas via RLS-scoped RPCs.
magic link]:::ext MU[my-updates.html
signed-in dashboard]:::page TB[Branding tab]:::page TP[Pricing tab]:::page TE[Estimates tab
map + lead list]:::page TS[Service Area tab
polygon editor]:::page CUST[(customers.branding_json)]:::tbl PRC[(customers.pricing_json)]:::tbl ESTT[(public.estimates)]:::tbl SA[(service_areas)]:::tbl PI[(product_images)]:::tbl UR[(update_requests)]:::tbl URH[update-request-email.js
ticket lifecycle]:::fn AUTH --> MU MU --> TB --> CUST TB --> PI MU --> TP --> PRC MU --> TE --> ESTT MU --> TS --> SA MU -. tickets .-> UR UR --> URH click MU "../my-updates.html" click URH "../netlify/functions/update-request-email.js"
6. Admin / ops surfaces
Two internal-only pages. /admin is the per-customer detail view (impersonation, health, ticket queue). /admin-dashboard is the founder scoreboard — CAC, payback, MRR, UTM channel attribution, action board.
customer list + detail
impersonate, MRR, churn]:::admin DASH[admin-dashboard.html
founder scoreboard]:::admin HERO[Hero: CAC · payback ·
net MRR · trial→paid %]:::admin FUN[Funnel: spend → trials
→ paid → subs 14d]:::admin CH[Channels: trials/conv
by utm_source 7d]:::admin IN[Inputs: creatives, posts,
DMs, spend 14d]:::admin HEA[Health: MRR line,
churn list, activity heatmap]:::admin ACT[Action board kanban
+ score heatmap 30d]:::admin SDS[sync-dashboard-snapshot.js
04:00 UTC]:::fn SMA[sync-meta-ads.js
04:30 UTC]:::fn REC[reconcile.js
03:00 UTC
Stripe ↔ Supabase drift]:::fn CUSTT[(customers)]:::tbl EST[(estimates)]:::tbl DSN[(daily_snapshots)]:::tbl DAC[(daily_actions)]:::tbl STR[(streaks)]:::tbl ACTT[(actions)]:::tbl CAPI[(meta_capi_events)]:::tbl STRIPE[Stripe API]:::ext META[Meta Ads API]:::ext DASH --> HERO DASH --> FUN DASH --> CH DASH --> IN DASH --> HEA DASH --> ACT ADM --> CUSTT ADM --> EST ADM --> CAPI HERO --> CUSTT FUN --> DSN CH --> CUSTT IN --> DSN IN --> DAC HEA --> CUSTT HEA --> EST ACT --> ACTT ACT --> STR SDS --> DSN SMA --> META SMA --> DSN REC --> STRIPE REC --> CUSTT click ADM "../admin.html" click DASH "../admin-dashboard.html" click SDS "../netlify/functions/sync-dashboard-snapshot.js" click SMA "../netlify/functions/sync-meta-ads.js" click REC "../netlify/functions/reconcile.js"
7. Background jobs (Netlify scheduled functions)
All scheduled jobs in cron-time order. Each runs once a day except reconcile, which is the safety net for Stripe ↔ Supabase drift.
| Cron | Function | Purpose |
|---|---|---|
03:00 UTC | reconcile.js | Detect drift between Stripe subscriptions and Supabase customer rows; auto-heal status mismatches and missing user links; email Jake a report. |
04:00 UTC | sync-dashboard-snapshot.js | Pre-create the day's daily_snapshots row; recompute streak via resolve_streak() RPC. |
04:30 UTC | sync-meta-ads.js | Pull last 7 days of Meta Ads insights (spend, impressions, clicks) into daily_snapshots. |
14:00 UTC | onboarding-drip.js | Fire trial drip emails on days 1, 2, 3, 5, 6, 7. Idempotent via onboarding_emails_sent. |
14:30 UTC | post-conversion-drip.js | Day-30 post-conversion check-in email with leads-last-30 metric. |
| event | stripe-webhook.js | Stripe webhook: trial signup, conversions, subscription status mapping, Meta CAPI Lead + Subscribe. |
| event | submit-estimate.js | Homeowner lead intake → Supabase + Resend + GHL fan-out. |
| event | update-request-email.js | Update-request ticket lifecycle (ACK on create, resolved on close). |
| event | get-estimate-pdf-url.js | Mint a 1-hour signed URL for a lead PDF in the private lead-pdfs bucket. |
8. Data layer (Supabase tables)
Core tables grouped by domain. Click any card to jump to the migration that defines it.
customers
One row per contractor. status ∈ {trial, active, paused, churned}; branding_json, pricing_json, stripe_customer_id, UTM/fbclid attribution.
users
App-side user profile linked to auth.users. Role ∈ {owner, staff, admin}. Manual link required on first signin (see runbook).
estimates
Homeowner lead submissions: contact, address (PostGIS lat/lng), fence type, addons, total, source slug, PDF path.
service_areas
Per-customer PostGIS polygons. One primary per customer; powers in_service_area flag on incoming leads.
product_images
Per-customer fence-type photos. Uploaded with pending_slug from customer-setup.html; manually claimed after seed.
update_requests
Contractor support tickets from /my/updates. RLS-scoped per customer; processed by Jake from /admin.
onboarding_emails_sent
(customer_id, campaign) primary key. Idempotency guard for the daily drip cron.
daily_snapshots
Per-day row: ad spend, impressions, clicks (Meta), IG/FB posts, DMs received/answered (manual entry).
daily_actions
5-checkbox daily commitment: ad creative, IG post, FB post, DMs zeroed, customer touch.
streaks
Single-row streak counter. Freezes reset on the 1st of the month.
actions
Action board: hypotheses, target metrics, status ∈ {idea, in_progress, shipped, measuring, killed}.
meta_capi_events
CAPI event audit + idempotency for Lead/Subscribe sends. Companion columns fbp, fbc on customers.
Full migration history: supabase/migrations/. Per-customer seed examples: customer-wr.sql, customer-cuesta-fence.sql, customer-trinity-fencing.sql.
9. External integrations
Every third-party system the codebase touches and which Netlify function owns the integration.
Stripe
Subscriptions ($149/mo, $1,490/yr). Webhook events: checkout.session.completed, customer.subscription.updated/deleted, customer.subscription.trial_will_end, invoice.payment_succeeded. Owned by stripe-webhook.js; reconciled nightly by reconcile.js.
Resend
All transactional email. Wrapper at _lib/email.js. Used by stripe-webhook (welcome, trial-end), drip crons, submit-estimate (lead fallback), update-request-email.
Meta CAPI
Server-side Lead (trial signup) + Subscribe (first paid invoice) events. Pixel 967360269010748. Helper at _lib/meta-capi.js. Audit table: meta_capi_events.
GoHighLevel (GHL)
Per-customer CRM webhook for lead delivery (ghl_webhook_url in customers row). One global Jake-side webhook for new-customer intake from customer-setup.html.
Supabase
Postgres + Auth (magic links) + Storage. Buckets: product-images (public), lead-pdfs (private, signed URLs only).
Google Maps
Address autocomplete on the homeowner estimator (client-side). Server-side geocoding fallback in submit-estimate.js.
Microsoft Clarity
Session replay + heatmaps on every public marketing page. Project vbr6bfp3p2.
Meta Pixel (browser)
Two pixels: main marketing 967360269010748, contractor-confirm 1710532113240325. Web events deduped against server CAPI by event_id.