Instant Quote · Admin

Docs

Project documentation rendered straight from /docs.

docs/DEV_NOTES.md

Developer 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_BASE to production when ready.
  • Legacy client_page_content is 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_TOKEN lives in URL query strings, which means it's in browser history, server logs, and any Referer headers 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.sql the 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, and quotes.financing_interest. The two new columns on client_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/:id returns 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, denormalised quotes.zapier_status / zapier_last_attempt_at, and the new delivery_attempts audit 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 whose zapier_status is 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. See supabase/migrations/016_zapier_delivery.sql and INTEGRATIONS for the full delivery model.
  • Test leads must never be sent to Zapier automatically. The delivery layer in lib/zapier.js checks quote.is_test before posting and falls into a skipped attempt with skip_reason = 'test_lead' instead. The retry endpoint sets isManualRetry=true which 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. See supabase/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=true capture and the admin's "Mark as test" toggle silently fall back to status='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, existing status='test' rows are auto-converted to is_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_test before 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 setup badge 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_assets with is_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.
  • loadPublicQuote column 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_PRICING is the canonical template. New clients seed from it, and resolvePricing falls 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 the min_job_floor example.
  • 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-dynamic on 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 leads or quotes would expose PII. Don't.
  • Section meta lives in two files for a reason. lib/sections/meta.js is the client-safe schema (no React imports). lib/sections/registry.js re-exports it with the actual React Component. The admin imports only from meta.js to keep the editor bundle small.

Areas that are fragile

  • buildClientTheme role assignment. The scoring functions in lib/theme.js work 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. extractRoofMetrics tries multiple paths (structures[].roof.*.value, root roof.*, 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].schema generically. A typo or misconfigured type (e.g. "text" vs "textarea" vs "image") will silently fall through to a default text input.
  • The synthesiseFromLegacy fallback in lib/page.js. Necessary for clients still living off client_page_content. Adding a new section type that should auto-populate from legacy means extending mergeLegacyIntoSection. Forgetting to do so means new clients get the new section, old clients don't.
  • Upsert-by-natural-key helper. upsertByClientId in the admin PATCH route works around the fact that some side tables don't have a synthetic id column. If you add a new side table, mirror that pattern instead of upsert().
  • The quote_number_seq sequence. 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 new client_financing table) for APR and term months and read them in app/api/quotes/route.js before 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_phone on client_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_TOKEN for 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_rules so 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_content rows. 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 one page_sections row, 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 generic FieldEditor switch. The pattern (add a type: "hero_image_panel" schema entry, special-case it in the schema map, write back through onPatchContent with a multi-key patch) is reusable if we want a similarly bespoke editor for another section later. The panel uses the four exported constants from lib/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.
  • sharp is 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 first npm install. Used only server-side from lib/brandDetection.js — never imported into the browser bundle. See ONBOARDING § Brand detection.
  • Migration 017 enables saved notification templates. Adds the templates table 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_url fields on client_integrations still 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 in app/api/quotes/route.js#forwardToIntegrations is 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_integrations but never read. Reserved for a future email/SMS notifier. The admin UI labels them "Stored but not sent anywhere today."
  • FORMINIT_WEBHOOK_URL env var. No longer used by the quote route (per-client URLs replaced it). Drop it from .env.example and any Vercel envs that still have it set.
  • Multiple MATERIAL_LABELS / COMPLEXITY_LABELS declarations. These are duplicated in lib/quote.js, NewClientForm.jsx, and EditClientForm.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 install clean.
  • npm run lint passes.
  • npm run build succeeds.
  • npm run dev starts without errors.
  • Visiting / redirects to /admin/clients.

Public quote flow

  • /<knownSlug>/instant-quote renders 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.
  • leads and quotes rows 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-you without a ?quote= token renders the empty state instead of 404ing.
  • Visiting /<slug>/thank-you?quote=<garbage> renders the empty state.
  • Visiting /<unknown-slug>/instant-quote 404s.

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.