docs/DEV_NOTES.mdDeveloper Notes
Last updated: May 19, 2026 at 5:27 PM ET
Living document for engineering context that doesn't fit cleanly into the other docs. Update this as you find new things.
Related docs: README, ARCHITECTURE, INTEGRATIONS.
Current known issues
- EagleView sandbox is geographically restricted. Real customer
addresses outside the supported regions fall back to the manual
roof-area input. There's nothing to fix here other than flipping
EAGLEVIEW_API_BASEto production when ready. - Legacy
client_page_contentis still seeded by the new-client form even though the page builder owns content after creation. This is intentional (the legacy fallback synthesises sections from it for brand-new clients), but it means there's a hidden second source of truth for hero copy until the first page builder save. ADMIN_TOKENlives in URL query strings, which means it's in browser history, server logs, and anyRefererheaders when admins click off-site links. Long-term we should swap to a server-set cookie. Short-term, treat it like a password and rotate it regularly.- Migration 011 enables internal notes. Until you run
supabase/migrations/011_client_internal_notes.sqlthe notes textarea on the Overview tab silently drops what you type (the PATCH endpoint catches the "column does not exist" error and continues). Other settings still save. - Migration 012 enables Form V1 fields. Until you run
supabase/migrations/012_form_v1_fields.sql, submissions still succeed but four pieces of data are dropped silently with a log warning:leads.first_name,leads.last_name,quotes.timeline, andquotes.financing_interest. The two new columns onclient_pricing_rules(visible_materials,default_material) are read defensively — they fall back to defaults when missing. See FORM_FIELDS for the full schema. - Migration 013 enables submissions tracking. Until you run
supabase/migrations/013_submissions_tracking.sql, submissions still succeed but the status + tracking block (UTMs, gclid, fbclid, referrer, source_url, page_slug) are dropped on insert with a log warning. The Submissions dashboard renders fine — the tracking section just shows "—" for everything. The PATCH endpoint at/api/admin/submissions/:idreturns a 409 with a clear "run migration 013" message when the status column is missing. See ADMIN_GUIDE for the dashboard, list, and detail layout. - Migration 016 enables Zapier lead delivery. Adds
client_integrations.zapier_enabled/zapier_webhook_url/zapier_label, denormalisedquotes.zapier_status/zapier_last_attempt_at, and the newdelivery_attemptsaudit table. Until applied, every Zapier code path degrades gracefully: · admin form saves with a log warning, · /api/quotes still saves leads + returns success but no Zapier delivery happens and no audit row is written, · Submissions dashboard Zapier filter returns rows whosezapier_statusis null (i.e. all rows), · the detail page renders an empty Zapier section. No lead is ever lost — database remains the source of truth whether or not migration 016 has been applied. Seesupabase/migrations/016_zapier_delivery.sqland INTEGRATIONS for the full delivery model. - Test leads must never be sent to Zapier automatically. The
delivery layer in
lib/zapier.jschecksquote.is_testbefore posting and falls into askippedattempt withskip_reason = 'test_lead'instead. The retry endpoint setsisManualRetry=truewhich bypasses this skip — admins can force a test lead through end-to-end when validating a new Zap. Future delivery channels (CRM / SMS / email) MUST mirror this default. See INTEGRATIONS "Test-lead skip". - Migration 015 enables the thank-you page schedule CTA. Adds
client_branding.calendar_url. Until applied, the admin's "Calendar / booking URL" input saves nothing (the API tolerates the missing column with a log warning) and the thank-you page's "Schedule My Free Inspection" button falls back to the phone link. Other branding fields keep saving normally. Seesupabase/migrations/015_client_calendar_url.sql. - Migration 014 enables first-class test-lead tagging. Until
you run
supabase/migrations/014_test_leads.sql, the form's?test=truecapture and the admin's "Mark as test" toggle silently fall back tostatus='test'(the older marker) and log a warning. The Submissions list will still show those leads (with the old status filter) but the dedicated lead-type filter + badge won't light up until the migration runs. After the migration, existingstatus='test'rows are auto-converted tois_test=true, status='new'. See ADMIN_GUIDE "Submission status- test handling" for the full model.
- Future external delivery must skip test leads by default.
Any Zapier / CRM / SMS / email integration we add later must
check
quote.is_testbefore sending and provide an explicit opt-in for callers that want test traffic. Documented in the ADMIN_GUIDE; mirror the rule in the integration code. - Duplicate client doesn't copy assets. Storage-file copy is non-trivial; admins must re-upload logo / hero image for the new client. Documented in the duplicate modal so it shouldn't surprise anyone, but worth knowing.
- Inferred client status. The
Live / Draft / Needs setupbadge on the clients list is heuristic — there's no real status column. Adding one would let admins explicitly pause a client without having to delete it. - Asset uploads accumulate. Soft-deleted (or replaced) assets stay
in
client_assetswithis_active = false. No cleanup job exists. - EagleView prefetch timeouts can stall the form's "measuring" animation at the 25-second hard cap. The UX falls back to manual entry but the user sees a long delay first. The right fix is to abort earlier when the vendor explicitly says "still pending".
- No retry on Supabase insert failures. If the lead inserts but the quote insert fails, we return a 500 to the user and the lead is orphaned (no quote, no webhook fanout). A cleanup pass over orphan leads would be useful.
- Webhook fanout is fire-and-forget. A webhook that's silently down for hours will silently drop hundreds of leads. There's no retry, no dead-letter queue, no alerting beyond the Vercel logs.
Important things not to change
- Order of operations in
POST /api/quotes. Validate, calculate, insert lead, insert quote, then webhook fanout. Never let webhook failures fail the user-facing response. loadPublicQuotecolumn selection. Lead PII (full name, phone, email) is intentionally excluded from the public quote read. The thank-you URL is reachable by anyone with the token; don't leak PII back into the response.DEFAULT_PRICINGis the canonical template. New clients seed from it, andresolvePricingfalls back to its leaves at runtime. Changing a default changes behavior for every client who hasn't explicitly overridden that leaf — see PRICING_LOGIC for themin_job_floorexample.- The hero always renders the form on
instant-quote. All hero variants must keep the form visible. If you add a variant, include the<QuoteForm />injection point. runtime = "nodejs"on every API route. We use the service-role Supabase client and Node-only APIs (Buffer,setTimeout). Don't flip a route to the Edge runtime without checking that everything still works.force-dynamicon the public client page. Removing it would cache stale branding/pricing/sections and break the admin's expectation of "save and see it live."- RLS is on with no public policies. Adding a public policy to
leadsorquoteswould expose PII. Don't. - Section meta lives in two files for a reason.
lib/sections/meta.jsis the client-safe schema (no React imports).lib/sections/registry.jsre-exports it with the actual React Component. The admin imports only frommeta.jsto keep the editor bundle small.
Areas that are fragile
buildClientThemerole assignment. The scoring functions inlib/theme.jswork well empirically but they're heuristic. Adding a new "role" means tuning all three scoring functions in lockstep to avoid weird cross-effects.- EagleView response shape parsing.
extractRoofMetricstries multiple paths (structures[].roof.*.value, rootroof.*, legacy shapes, etc.) because the vendor's response varies by product / region / structure count. Touch with care; the test endpoint (/admin/eagleview-test) is your friend. - Section schema → editor rendering. The admin renders fields
from
SECTION_META[type].schemagenerically. A typo or misconfiguredtype(e.g."text"vs"textarea"vs"image") will silently fall through to a default text input. - The
synthesiseFromLegacyfallback inlib/page.js. Necessary for clients still living offclient_page_content. Adding a new section type that should auto-populate from legacy means extendingmergeLegacyIntoSection. Forgetting to do so means new clients get the new section, old clients don't. - Upsert-by-natural-key helper.
upsertByClientIdin the admin PATCH route works around the fact that some side tables don't have a syntheticidcolumn. If you add a new side table, mirror that pattern instead ofupsert(). - The
quote_number_seqsequence. Migration 009 sets it to start at 1001. If you restore from a backup that resets the sequence, the next quote number could collide with an existing one — the unique index will reject the insert. Bump the sequence after any restore.
Future feature ideas
- Per-client financing terms. Add columns to
client_pricing_rules(or a newclient_financingtable) for APR and term months and read them inapp/api/quotes/route.jsbefore caching the monthly payment. - Ask for pitch on the form. The pitch multiplier is already
plumbed; the public form just doesn't ask. Adding a question is a
~30-line change in
QuoteForm.jsx. - Email + SMS notifications. Use
notification_email/notification_phoneonclient_integrations. The plumbing exists, the sender does not. - Webhook retry / DLQ. Queue webhook deliveries (Supabase has a background-jobs-via-cron pattern, or use Trigger.dev / Inngest) so vendor outages don't silently drop leads.
- Auditable admin auth. Swap the shared
ADMIN_TOKENfor proper user accounts (Supabase Auth is right there). Add an audit log of who changed what. - Per-section preview library. The page builder preview currently renders the live page in an iframe. A dedicated "show me this section in isolation" view would speed up section authoring.
- PDF / email summary of the quote. Generate a PDF at submit time and attach it to the lead notification. Saves contractors a step.
- Pricing rules diff history. Capture every save to
client_pricing_rulesso contractors can see when (and why) numbers changed. - Bulk import of clients. A CSV-driven onboarding flow for agencies that manage many roofers.
- Theme builder polish. Theme dark hero HUD SVGs, the dark CTA panel, and footer accents off the resolved theme tokens instead of the hardcoded gradients they use today (the Phase 2 followup).
Cleanup opportunities
- Orphaned legacy
client_page_contentrows. Once a client's page builder has been saved, the legacy row is no longer read. We could add a migration that drops the table after every client has at least onepage_sectionsrow, or leave it as forever-fallback. - The root README still describes the original single-tenant shape. Either delete it or rewrite it as a one-page intro that points here.
- Hero image visual panel lives at
app/admin/clients/[clientId]/pages/[pageKey]/HeroImagePanel.jsx. It's the one custom field-type renderer in the page builder — every other section type still goes through the genericFieldEditorswitch. The pattern (add atype: "hero_image_panel"schema entry, special-case it in the schema map, write back throughonPatchContentwith a multi-key patch) is reusable if we want a similarly bespoke editor for another section later. The panel uses the four exported constants fromlib/sections/meta.js(HERO_IMAGE_POSITION_OPTIONS,HERO_IMAGE_ZOOM_MIN/MAX/STEP, etc.) as the single source of truth, so changing the zoom range or adding a new gradient option is a one-place edit. The readability warning is wired inline in the Overlay group. sharpis a runtime dependency now. Added when the New Client Wizard's brand detection grew image dominant-color extraction (logo / favicon / OG image). It's the same library Next.js's<Image>component uses, so Vercel builds it without issues. Local dev installs recompile native binaries on firstnpm install. Used only server-side fromlib/brandDetection.js— never imported into the browser bundle. See ONBOARDING § Brand detection.- Migration 017 enables saved notification templates. Adds the
templatestable for SMS / email lead-alert copy managed at/admin/templates. Until applied, the admin page falls back to the in-code defaults (lib/templates.js#DEFAULT_TEMPLATES), the Save button is disabled, and an amber banner names the migration to run. Read-only behaviour for the team — the template renderer is shared with future native delivery, but today Zapier is still the only sender. See NOTIFICATION_TEMPLATES. - Legacy webhooks are still active. The
forminit_webhook_url/ghl_webhook_url/hubspot_webhook_urlfields onclient_integrationsstill fire on every successful quote save alongside Zapier. The admin UI moved them into a collapsed "Legacy webhooks" section with a duplicate-delivery warning, but the wiring inapp/api/quotes/route.js#forwardToIntegrationsis unchanged. Don't remove the fields silently — clear them on the client row first (admins can do this from the Integrations tab) so any contractor still depending on them gets their data explicitly. See INTEGRATIONS "Legacy webhooks". - Notification email + phone are dead fields. Stored on
client_integrationsbut never read. Reserved for a future email/SMS notifier. The admin UI labels them "Stored but not sent anywhere today." FORMINIT_WEBHOOK_URLenv var. No longer used by the quote route (per-client URLs replaced it). Drop it from.env.exampleand any Vercel envs that still have it set.- Multiple
MATERIAL_LABELS/COMPLEXITY_LABELSdeclarations. These are duplicated inlib/quote.js,NewClientForm.jsx, andEditClientForm.jsx. Centralise into one module. - Unused
ResultScreen.jsx. The current submit flow redirects to the thank-you page; the in-form result screen is no longer the default path. Confirm it's truly unused before deleting. - Soft-deleted
client_assets. A cleanup job (or a "purge inactive" admin button) would let storage bills stop growing. - Inconsistent error response shapes. Quote API and admin API
routes both return
{ success, error, debug? }but the field names drift in places. Worth normalising.
Testing checklist before deployment
Run through this list every release. There's no automated test suite yet — see the cleanup section.
Local sanity
-
npm installclean. -
npm run lintpasses. -
npm run buildsucceeds. -
npm run devstarts without errors. - Visiting
/redirects to/admin/clients.
Public quote flow
-
/<knownSlug>/instant-quoterenders with the right branding (logo, colors, hero copy, trust strip). - Address autocomplete returns suggestions (requires
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY). - Picking a suggestion triggers the measuring animation; roof square footage prefills.
- Manual override of the roof area number works.
- Submitting the form returns a non-error response.
- The redirect to
/<slug>/thank-you?quote=<token>succeeds. - The thank-you page shows the saved estimate band and monthly payment.
-
leadsandquotesrows are present in Supabase with sensible values. - Configured webhooks received the lead+quote payload (check receiver dashboard or Vercel logs).
- GTM tag fires on both the landing page and the thank-you page (Tag Assistant).
Admin flow
-
/admin/clients?token=<ADMIN_TOKEN>lists clients. - Creating a new client succeeds and produces a working public URL.
- Editing pricing rules and saving reflects on the next quote submission.
- Theme color edits update the live page on next request.
- Page builder save (sections, variants, copy) reflects on the public page.
- Asset upload (logo) shows up in the site header on the public page.
Edge cases worth poking
- Submitting with a deliberately tiny roof (e.g. 400 sq ft 3-tab
simple gable) produces an estimate at or above the
min_job_floor. - EagleView prefetch failure (kill the env var, or use a sandbox address that returns null) still lets the user submit by typing the square footage.
- Visiting
/<slug>/thank-youwithout a?quote=token renders the empty state instead of 404ing. - Visiting
/<slug>/thank-you?quote=<garbage>renders the empty state. - Visiting
/<unknown-slug>/instant-quote404s.
After deployment to production
- Smoke-test one real quote on production (use a known contractor slug; submit yourself a real quote to your own email).
- Confirm Vercel logs show the lead insert, quote insert, and webhook responses for that submission.
- Confirm GTM is firing in production (Tag Assistant against the production URL).
- Confirm the EagleView env points to production (
EAGLEVIEW_API_BASE) if you've moved off sandbox.