Re: intake.matched stopped flowing — cron gap, not fanout gap
Diagnosis
Sales' hypothesis 1 is correct: the issue is upstream of the fanout worker, not in the fanout worker's poll path.
The evidence that clears the fanout worker: person.updated events from Platform are reaching Sales at 72 events current within the hour. Platform and Sales share one fanout loop per (producer, consumer) pair — the loop that delivers intake.matched and person.updated from Platform to Sales is the same loop (Platform-produced events, fanout:sales consumer cursor in Platform's dispatcher_cursor table). If person.updated is flowing, the fanout loop is alive and its cursor is advancing. intake.matched missing from the same loop means Platform's dispatcher_event table has stopped accumulating new intake.matched rows, not that the fanout is failing to pick them up.
The component that produces intake.matched rows in Platform's dispatcher_event is the Vercel cron route at /api/cron/match-growth-intakes (schedule */5 * * * * per vercel.json). That route calls runGrowthIntakesMatchOnce from lib/growth-intakes-matcher.ts, which reads Growth's dispatcher_event table for intake.captured events with schema_version >= 2, mints or matches Persons, and emits intake.matched into Platform's dispatcher_event. The last migration-report JSON files in the Platform repo are timestamped 2026-05-12T20:32 and 2026-05-12T20:35 — manual runs from that day. The last intake.matched Sales received was 2026-05-12T23:22:45, roughly three hours after those manual runs, consistent with several more cron ticks firing before the job went silent.
Two candidate root causes, in probability order:
A. The cron route is failing with 5xx on every tick. Vercel surfaces cron failures in the Cron Jobs panel but does not alert by default; without alerting, consecutive failures accumulate silently. The most likely trigger: GROWTH_DATABASE_URL was rotated (Supabase sessions expire on project pause/restore or manual rotation) and the new value was not pushed to Vercel's environment. The route returns 503 { error: "GROWTH_DATABASE_URL not configured" } when the env var is absent, and a connection error from pg.Client when the DSN is stale. Either surfaces as a 5xx in the cron log.
B. The cron route is firing successfully but processing zero rows. This would happen if all Growth intake.captured rows now have personId already populated in growth.lead_intake. Unlikely given Sales' observation of 74 captured events with person_id=null on the Lead side, but worth ruling out.
Immediate diagnostic steps (Platform)
Open Vercel → Project → Cron Jobs →
match-growth-intakes. Check the status of the last 10–20 invocations. If they are all 5xx or "failed", candidate A is confirmed. If they are 2xx withcounts: {minted: 0, matched: 0, already_backfilled: N}, candidate B is confirmed.If candidate A: check whether
GROWTH_DATABASE_URLin Vercel's environment variables matches the current live Supabase pooler URL forsguild-domains. Update if stale, redeploy (or trigger a new cron invocation manually via the Vercel panel). The cron will pick up the backlog on the next tick.If candidate B: run
scripts/check-growth-intake-match-smoke.mjsto verify the cross-DB read path is healthy and that Growth'slead_intakerows for the affected intakes actually havepersonIdpopulated. If they do, the discrepancy between Sales'person_id=nulland Growth's backfilled state means Sales'intake-matcheshandler missed the backfill signal — that's a separate Sales-side investigation.
Recovery for the 74 Leads
Once the cron route is healthy, no manual intervention is needed. The matcher runs with unmatchedOnly: true, which means it processes Growth lead_intake rows where personId IS NULL. All 74 (or more) intakes that landed since 2026-05-12T23:22:45 will be matched on the next successful cron tick. For each, the matcher will:
- Mint or match a Person.
- Emit
intake.matchedinto Platform'sdispatcher_event. - Backfill
personIdinto Growth'slead_intake.
The fanout worker will deliver those intake.matched events to Sales. Sales' modules/intake-matches/ handler uses originatingIntakeEventId (which is stored on the Lead row as originatingIntakeEventId) to locate the existing Lead and write person_id. No re-import or manual Lead surgery is needed on Sales' side — the matched-arrives-first handler path covers the case where the Lead already exists.
The four Lead IDs Sales provided are usable for verification after the fix: once their person_id is non-null in Sales' database, the recovery is confirmed.
Alerting gap
Sales' ask 4 is valid. The current cron setup has no alerting on consecutive failures or throughput drop-to-zero. Adding that is Platform-owned work and is tracked as a commitment on this memo. The immediate fix is to check the Vercel Cron Jobs panel manually after any Platform deploy that touches env vars, but that is not a durable signal.
The right monitoring shape for the cron is: if the route returns 5xx on three or more consecutive invocations (15 minutes), or if the cron has not run successfully in 30 minutes, surface an alert. Vercel's cron does not have native alerting for this; the two options are (a) a lightweight external health-check that calls the route on a synthetic schedule with a test secret and alerts on failure, or (b) a Supabase function or scheduled script that checks Platform's dispatcher_event for intake.matched rows younger than 30 minutes and alerts if none exist. Platform will file the chosen approach once the immediate gap is closed.
References
- Sales memo:
2026-05-16-sales-intake-matched-stopped-flowing - Prior fanout gap (different event family):
2026-05-12-sales-intake-captured-fanout-gap - Platform's prior fix:
2026-05-12-platform-cross-db-consumer-dsn-upstream - Cron route:
platform/app/api/cron/match-growth-intakes/route.ts - Matcher module:
platform/lib/growth-intakes-matcher.ts - Fanout worker:
platform/lib/dispatcher/fanout.ts,platform/scripts/fanout-worker.ts