Erie.pro directory flow (local → lead routing → billing)
All GTM plays (1–10) are mapped in GO-TO-MARKET-USE-CASES.md and in src/config/gtm-use-cases.ts.
What this does
- Intake —
POST /api/intakewithx-tenant-id: erie(or?tenant=erie) and body fields below. - Persist — Lead stored via existing
persistLead/ runtime store with tenant from resolved config. - Directory routing — When
LEAD_OS_DIRECTORY_TENANTSincludes the resolved tenant (defaulterie), andcategoryis set, the system resolves an activenodesrow bymetadata.category, checks billing ifLEAD_OS_BILLING_ENFORCE=true, writes `lead_os_directory_routes`, emits canonical events, and calls `sendLead()` (SuiteDash → Activepieces → generic webhook → simulated log). - Control plane — Operator actions unchanged; use
/dashboard/control-planewith operator cookie/JWT. - GTM tracking — Use `/dashboard/gtm` (or
npm run gtm:print -- --slug=erie-plumbing) to record Erie-first rollout status against the canonical GTM config; see GO-TO-MARKET-USE-CASES.md. Public route map: PRODUCT-SURFACES.md; deployed docs hub: `/docs`.
Completeness audit
Erie is now treated as the first complete city directory entry in the reusable directory coverage system.
Public and operator surfaces:
- City directory page:
/directory/city-erie-pa - Router surface:
/directory/lead-router - Runbook:
/docs/erie-pro - Expansion plan:
/docs/directory-expansion-plan - Source reference:
/docs/source/src/lib/erie/directory-lead-flow.ts
Verified implementation pieces:
- tenant seed:
erie - active buyer nodes:
plumber_erie_test_1,hvac_erie_test_1 - active categories:
plumbing,hvac - billing gate support through
LEAD_OS_BILLING_ENFORCE=true - route audit table:
lead_os_directory_routes - delivery handoff through
sendLead() - website-visible lead-router explanation
- website-visible city directory page
Operational work still required before live paid traffic:
- connect real buyer delivery destinations
- keep unsold category slots inactive or paused
- add real proof only after live traffic creates verified outcomes
Database seed
Migration `010_erie_directory_seed.sql` inserts:
- Tenant row `erie` in
lead_os_tenants - Nodes `plumber_erie_test_1` (plumbing) and `hvac_erie_test_1` (hvac)
- `billing_subscriptions` for
erieon plan `enterprise` (active) - Table `lead_os_directory_routes` for routing audit
Run npm run verify:migrations after applying SQL.
Environment
See `.env.erie.example`. Minimum for a full local demo:
LEAD_OS_TENANT_ID=erieLEAD_OS_DATABASE_URL/DATABASE_URLLEAD_OS_AUTH_SECRET,CRON_SECRETLEAD_OS_DIRECTORY_TENANTS=erie(optional; defaults toerie)LEAD_OS_BILLING_ENFORCE=trueto prove subscription gating on intake
Integration URLs (optional):
SUITEDASH_*— CRM contact createACTIVEPIECES_WEBHOOK_URL— automation JSON POSTPABBLY_WEBHOOK_URLorLEAD_OS_AUTOMATION_WEBHOOK_URL— secondary webhook
Detect configured keys (no secret values)
node scripts/detect-env-presence.mjsTest requests (PowerShell)
Submit a plumbing lead (replace origin if needed):
$body = @{
source = "contact_form"
tenantId = "erie"
category = "plumbing"
email = "test.plumber@example.com"
firstName = "Test"
lastName = "Lead"
message = "Need emergency drain service"
} | ConvertTo-Json
Invoke-RestMethod -Uri "http://localhost:3000/api/intake" -Method POST -Headers @{
"Content-Type" = "application/json"
"x-tenant-id" = "erie"
"Idempotency-Key" = "erie-demo-1"
} -Body $bodyWrong tenant (422):
Invoke-RestMethod -Uri "http://localhost:3000/api/intake?tenant=erie" -Method POST -Headers @{"Content-Type"="application/json"} -Body '{"source":"contact_form","tenantId":"other","category":"plumbing","email":"a@b.com"}'Billing blocked (402) — set LEAD_OS_BILLING_ENFORCE=true and remove or cancel erie row in billing_subscriptions, then retry.
Docker
docker compose up --buildSet LEAD_OS_TENANT_ID=erie and LEAD_OS_DIRECTORY_TENANTS=erie in .env before docker compose up. Worker service runs when REDIS_URL is set.
Operator smoke (curl)
Requires operator session cookie or documented operator auth — use the dashboard UI for pause/resume node and DLQ actions after signing in.
Limitations
- Idempotency cache for intake is in-memory (single instance); use a shared store in multi-replica production.
- Directory routing requires Postgres for node lookup and route row; without DB, fallback node map is minimal.
- SuiteDash throws if keys are partial; hub skips CRM when keys or email missing.