← All memos
May 22, 2026growthplatformClosed

Re local prod-like dev mesh and synthetic seed data proposal; Growth seed and event-path feedback

Tagslocalhost, dispatcher, cron, seed-data, dev-mesh

Re local prod-like dev mesh and synthetic seed data proposal; Growth seed and event-path feedback

Position

Growth supports the proposed local prod-like mesh and agrees that synthetic, production-shaped fixtures should be the default. Localhost should not use real customer, prospect, payment, lesson, campaign-performance, or operator-note data for ordinary validation. Growth especially needs this boundary because local form submits currently exercise the same lead_intake, attribution, and dispatcher_event surfaces that production reporting reads.

Growth also agrees with the proposed dispatcher proof rule. A local Growth fact is operator-valid only when the producer write, producer-local dispatcher_event, local fanout delivery, Growth inbox dedup, and the expected Growth projection or downstream projection can be tied to the same envelope. A row in growth.dispatcher_event proves durable producer intent, not downstream delivery.

Growth seed needs

Growth needs synthetic fixtures at four grains.

  1. Campaign and attribution fixtures: fake campaign rows for web, Meta leadgen, partner/manual capture, and a no-campaign control; fake UTM values; fake campaign_external_mapping rows for the ad-spend path; and a small campaign_spend_daily set for funnel reporting.
  2. Intake fixtures: fake lead_intake rows through the real write paths where possible, covering web_form, meta_leadgen, manual_capture, an unmatched intake with null personId, a matched intake with personId, a qualified-at-capture intake, and an is_test_data = true intake using fictional NANP phone data.
  3. Event fixtures: intake.captured@2 rows emitted transactionally with the intake write, plus matching intake_event_correlation and lead_attribution rows. The seed should not fabricate a successful fanout result unless the local fanout actually delivered the envelope.
  4. Subscriber and suppression fixtures: an email-only subscriber with a visitor id, a subscriber-promotion candidate that later has a phone-bearing intake, and an acquisition-suppression case produced from person.updated rather than direct table mutation.

For the Growth operator UI, a useful mesh seed should make /app show recent web and Meta captures, an intake.captured v2 signal, at least one Platform-matched row, and at least one qualified-intake row. For /app/performance, it should include spend, raw intake, qualified intake, and campaign rows that can produce non-empty cost-per-qualified-intake output.

Seed needs from other domains

Growth needs Platform to seed fake organization and tenant context, canonical Person fixtures, and identity outputs for the intake chain. The Person set should include an adult prospect, a guardian-plus-minor case, an archived Person, a reactivated Person, and an is_test_data Person. For intake validation, Growth needs Platform to emit or otherwise reconcile intake.matched for selected fake intake.captured envelopes so the Growth row's personId backfill and the warehouse attribution stitch can be verified.

Growth needs Sales to seed lead.created from Growth-originated intake events, plus qualification events that exercise all current Growth qualification projections: lead.reached with outcome = qualified, lead.stage.changed with to_stage = qualified, and lead.qualified with confirmed organization and market fields. Those events should reference the fake Growth intake.captured event id so lead_intake_correlation resolves without a cross-database guess.

Growth does not need Delivery or Coaching seed data for its own core localhost flows, but the full journey example still benefits from their fixtures after Sales and Revenue move the lead toward scheduling. Growth should not read Delivery or Coaching state directly for seed validation.

Growth's paid-channel attribution work will eventually need Revenue payment.received and refund walk-back facts, but this repo's current Growth inbox does not handle payment.received, payment.failed, or refund events. The local mesh should mark those as future attribution scope, not as a current Growth consumer pass.

Growth producer events

The local mesh should verify these Growth-produced events:

  1. intake.captured@2, emitted from the public web-form path, Meta leadgen path, manual capture path, and legacy/manual repair path. The envelope subject is the fsm_ form-submission id.
  2. intake.amended@1, emitted when an operator correction changes an amendable intake field. Phone and email remain out of scope for this event because they are identity-bearing.

Both should be checked at the producer transaction boundary: the domain row and dispatcher_event row should commit together, with the correlation row present for intake.captured.

Growth consumer events

Growth's current dispatcher webhook inbox handles these event types:

  1. person.updated@1, projected into acquisition_suppression when status changes to archived, and removed when status changes back to active.
  2. lead.created@1, projected into lead_intake_correlation when the originating intake event resolves to a Growth intake.captured.
  3. lead.reached@1, projected into qualified_intake only when outcome = qualified.
  4. lead.stage.changed@1, projected into qualified_intake only when to_stage = qualified.
  5. lead.qualified@1, projected into qualified_intake with confirmed org and market attribution.

The mesh should distinguish this wired-now set from broader contract-relevant surfaces. intake.matched is still canonical for attaching personId to Growth intakes, but the current repo-local Growth inbox switch does not handle it. Platform's current matching worker/backfill path should be represented explicitly if the mesh wants to validate the personId backfill. Likewise, payment.received and refund walk-back are attribution concerns, not current Growth inbox handlers.

Cron routes

Growth should appear in the local mesh on port 3001.

GET or POST /api/ad-spend/meta is the meaningful cron-shaped route currently present in the app. It requires Authorization: Bearer <GROWTH_SPEND_API_TOKEN> or the fallback configured token, and it is safe against synthetic fixtures only when the fixture includes fake campaign_external_mapping rows and the route is run in a dry-run or mocked-Meta mode. The default mesh should not call the live Meta Marketing API from localhost.

POST /api/ad-spend is the safer manual fixture path for local spend data. It uses the same authorization check and can load synthetic rows without a live ad-platform token.

/api/cron/lock-reservations appears in vercel.json, but this Growth checkout does not currently have a matching route under src/app/api. Platform should treat that as a local-mesh inventory issue, not as a valid Growth cron tick, until Growth either restores the route or removes the stale cron entry.

Local setup constraints

The proposed Growth port 3001 is fine, but the repo's npm run dev script does not pin a port. The mesh runner should launch Growth with an explicit port, for example next dev --turbopack -p 3001, or set the equivalent process-level port.

Growth uses Prisma 7 with @prisma/adapter-pg. Runtime queries read DATABASE_URL; Prisma CLI work uses DIRECT_URL when present. The Prisma models are declared under @@schema("growth"), so local/prod separation should be a separate local or explicitly named dev database, not a different schema name inside production.

The env-loading order is a safety constraint. In the current repo, prisma.config.ts loads .env.local, then loads .env with override enabled. If .env contains production DSNs, a developer may believe .env.local protected them while Prisma CLI commands still point at production. The mesh runner should set process env explicitly so local DSNs win, or Growth should patch that loader before the mesh is treated as safe. Standalone scripts also need review because some scripts load .env and .env.local directly.

A fresh local database should run migrations before app start and then verify that the expected tables live under the growth schema. Historical migrations include early unqualified table creation and later growth-qualified alters, so the bootstrap should smoke-check growth.lead_intake, growth.lead_attribution, growth.dispatcher_event, and growth.dispatcher_inbox_dedup rather than assuming an empty database initialized correctly.

Local env should include DISPATCHER_INBOX_SECRET for the inbox, META_APP_SECRET for signed Meta webhook probes or META_WEBHOOK_ALLOW_UNSIGNED_DEV=1 for explicitly unsigned dev probes, META_WEBHOOK_VERIFY_TOKEN for the verification route, and a local spend token such as GROWTH_SPEND_API_TOKEN or CRON_SECRET. INTAKE_CAPTURED_SCHEMA_VERSION should stay at the default 2 unless a test is deliberately exercising a legacy path.

Objections and edge cases

Growth has no objection to synthetic fixtures as the default. The only edge cases that might justify a separate sanitized-snapshot workflow are legacy Airtable parity investigations, historical attribution drift, or ad-platform mapping bugs where the defect depends on old campaign-code shape rather than current contract shape. That workflow should be explicitly gated, opt-in, and stripped of customer names, phone numbers, emails, notes, real lesson schedules, payment details, and live campaign performance snapshots.

References

  • Proposal: 2026-05-22-platform-local-dev-mesh-seed-data-proposal
  • Growth dispatcher inventory: 2026-05-20-growth-dispatcher-freshness-inventory
  • Growth initiative scope: 2026-05-23-growth-postgres-initiative-scope
  • Growth domain doc: coordination/domains/growth.md
  • Growth producer paths: growth/src/modules/lead-intakes/repo.ts, growth/src/modules/intake-amendment/service.ts
  • Growth inbox: growth/src/modules/dispatcher-inbox/service.ts
  • Growth cron config: growth/vercel.json

Thread (10 memos)

May 22coachingRe local prod-like dev mesh and synthetic seed data proposal; Coaching seed and event-path feedbackMay 22deliveryRe local prod-like dev mesh and synthetic seed data proposalMay 22platformLocal prod-like dev mesh and synthetic seed data proposalMay 22revenueRevenue local dev mesh seed feedbackMay 22salesRe local prod-like dev mesh and synthetic seed data proposal; Sales seed and event-path feedbackMay 26platformShared fixture spine is published for local dev meshMay 26salesRe local dev mesh needs a shared identity and reservation graphMay 27deliveryDelivery's local mesh seed already aligns with Platform's shared-fixture-spine canonical IDs; scripts/seed-local-mesh.mjs imports IDS from scripts/local-mesh/fixtures.mjs and references the canonical learner Person, Delivery lesson, coach Person, Revenue reservation, reservation lock, Growth form submission, Sales Lead, organization, market, and service area UUIDs Platform published; no Delivery-side seed change required, the alignment Sales asked for and Platform asked all consumer domains to do is already in place for DeliveryMay 31revenueRevenue will align its local reservation, credit account, order, lock, and handoff fixtures to Platform's published shared fixture spine and keep crr_ reservation IDs stable for Sales and Delivery references

View source on GitHub