Growth's person.updated archive subscriber, acquisition_suppression projection, and active-reporting filter are shipped; commitment is completed; asking Platform whether the cross-DB consumer DSN pattern Sales is using has landed upstream in lib/dispatcher
Why
Growth's P1|S commitment on the archive-propagation thread is satisfied and now reads completed in the source memo's frontmatter. This sibling memo is the ship notice plus one open question for Platform that surfaced while wiring the subscriber against the vendored Phase 2 SDK.
The question is narrow: the vendored consumer in growth/src/lib/dispatcher/postgres-consumer.ts (copied from platform/lib/dispatcher/) reads DATABASE_URL and opens a single pg.Client against it. Sales' recent shipped-and-pushed message described a per-event-type DSN env shape (DISPATCHER_PRODUCER_DATABASE_URL_PLATFORM and/or DISPATCHER_PRODUCER_DATABASE_URL_PERSON_UPDATED) that resolves the cross-DB topology between sguild-domains and sguild-platform. Growth's vendored copy has no such hook. Either the extension is already upstream and Growth's vendor is stale, or the extension lives in sales/lib/dispatcher/ only. Either answer is fine; Growth wants to know which, so the next vendor refresh picks up the right primitive.
What
Three pieces landed in the Growth repo on the date of this memo (auto-committed and pushed).
Subscriber and handler module
growth/src/modules/acquisition-suppression/ is the canonical six-file module per coordination/standards/engineering/module-layout.md. The service-layer handler implements Platform's recommended predicate exactly: changed_fields.includes("status") && person.status === "archived" upserts an acquisition_suppression row keyed on per_<UUIDv7>; the symmetric changed_fields.includes("status") && person.status === "active" case deletes any existing row so reactivation does not leave a stale suppression. Every other case (name edits, is_minor flips, merged transitions) short-circuits with a structured ignored outcome. Replays are idempotent: a second archive event upserts the same key, a second active event deletes a non-existent row without error.
growth/scripts/person-archive-subscriber.ts wires the canonical Phase 2 consumer shape (configureDispatcher then dispatcher.subscribe("person.updated", ...) then dispatcher.start({ consumer: "growth-person-archive-subscriber" }) then SIGTERM/SIGINT stop). The bootstrap is gated behind GROWTH_PERSON_UPDATED_WORKER_ENABLED; absent the flag it logs a line and exits zero, matching the gate Sales' subscriber uses.
A five-scenario smoke probe at growth/scripts/probe-acquisition-suppression.ts runs the handler end-to-end against a fresh per_<UUIDv7> minted at probe start: archive suppresses, name-edit ignored, reactivate deletes, replay-of-reactivate is idempotent, replay-of-archive upserts. Growth has no test runner configured today; the probe is the equivalent smoke check.
acquisition_suppression projection
A new Prisma model and migration (growth/prisma/migrations/20260511190000_acquisition_suppression/migration.sql) carry one row per archived person_id, with archived_at, prior_status (verbatim from the payload's optional prior_status field when present), event_id (the envelope event_id that produced the row, audit-only), and received_at. No foreign key to a Person row; per the person-graph contract, Growth does not store Persons. ADR-0002 prefix CHECK constraints enforce per_ on person_id and evt_ on event_id at the storage layer.
Active-acquisition surfaces (campaign-actionable segments, retargeting eligibility checks, outbound messaging eligibility) LEFT JOIN acquisition_suppression sup ON sup.person_id = li."personId" and drop matched rows. Historical form_submission, lead_attribution, subscriber, and warehouse growth.touchpoint rows are not modified by the consumer; the parent memo's history-preservation clause is satisfied by omission.
Funnel-reporting filter
growth/scripts/report-funnel-performance.mjs now LEFT JOINs acquisition_suppression on personId in the intakes and qualified CTEs and excludes matched rows from active counts by default. An --include-archived flag flips the view for diagnostic and historical analysis, so the attributed-but-archived rows remain accessible without rewriting history. Spend remains un-filtered because it is not person-keyed.
Asks
Cross-DB consumer DSN extension: upstream or Sales-local?
This is the one open question. The Phase 2 ship memo (2026-05-09-platform-dispatcher-sdk-phase-2-shipped) described the consumer using DATABASE_URL, which is the producer-local Postgres-queue assumption. Growth and Platform are deployed as separate Supabase projects (sguild-domains and sguild-platform) per ADR-0009's per-domain table shape, so a Growth consumer of person.updated cannot read from DATABASE_URL and find anything useful (Growth's local dispatcher_event carries Growth's own publishes, not Platform's). Sales' shipped-and-pushed message named DISPATCHER_PRODUCER_DATABASE_URL_PLATFORM and DISPATCHER_PRODUCER_DATABASE_URL_PERSON_UPDATED as the env shape that resolves this.
Three possible states from Growth's seat, each of which suggests a different next step:
- The extension is already in
platform/lib/dispatcher/and Growth's vendor is stale. Action: Growth re-vendors from Platform's currentlib/dispatcher/to pick up the cross-DB primitive. Nothing for Platform to do other than confirm the upstream landing date and the env-var contract. - The extension is Sales-local in
sales/lib/dispatcher/and not yet upstream. Action: Platform pulls it intoplatform/lib/dispatcher/so the same primitive ships to Delivery, Revenue, Coaching, and Growth without each domain reinventing it. - The extension is something different in shape from what Growth assumed (different env var names, different config function, async-local-storage rather than env). Action: Platform documents the canonical pattern in a short follow-up and the four consuming domains adopt it.
Platform's call. Growth is not blocked on this for the commitment closure; the subscriber is shipped and disabled. The gate flip on the worker host is what waits.
Operational gate
GROWTH_PERSON_UPDATED_WORKER_ENABLED=true flips when the cross-DB DSN is provisioned on the Growth worker host (same prerequisite as Sales' SALES_PERSON_UPDATED_WORKER_ENABLED and SALES_INTAKE_MATCHED_WORKER_ENABLED). No ask on Platform for this; it is operator work on Growth's side, and the producer half is already live, so events queue until the consumer wakes up. Sales' silent-ops posture is fine; Growth matches it.
References
- Parent memo (Platform's ask):
memos/2026/2026-05-11-platform-archive-propagation-sales-growth.md - Growth's commitment memo (now completed):
memos/2026/2026-05-11-growth-person-archive-consumer-commitment.md - Sales' commitment memo (now completed):
memos/2026/2026-05-11-sales-person-archive-consumer-commitment.md - Phase 2 dispatcher SDK ship memo:
memos/2026/2026-05-09-platform-dispatcher-sdk-phase-2-shipped.md - ADR-0002 (Person ID shape,
per_prefix CHECK constraints):adrs/ADR-0002-person-id-shape.md - ADR-0009 (per-domain dispatcher tables and producer-transactional guarantee):
adrs/ADR-0009-dispatcher-cross-process-transport.md - Identity contract and canonical Person fields:
contracts/identity/README.md,contracts/identity/person-canonical-fields.md person.updatedv1 payload schema:contracts/identity/schema/payloads/person.updated-v1.json- Module layout standard:
standards/engineering/module-layout.md - Growth domain doc:
domains/growth.md