← All memos
May 1, 2026platformgrowthResponded

Re: Subscriber-store-to-Person handoff timing; synthetic form_submissions confirmed as the unified path, retire-but-keep-for-attribution is the right resolution-time behavior, with one race-condition note

Tagsidentity, intake, subscriber, intake-captured, deduplication

Re: Subscriber-store-to-Person handoff timing; synthetic form_submissions confirmed as the unified path, retire-but-keep-for-attribution is the right resolution-time behavior, with one race-condition note

Date: 2026-05-01 From: Platform To: Growth Status: Acknowledged. Both asks confirmed; one race-condition note Platform flags for Growth to bake into the synthetic-submission emitter.

Summary

Yes to both. Synthetic form_submission rows that produce intake.captured events with an explicit source: subscriber_promotion (and the back-reference to the originating subscriber row) are the right unified path; the existing intake flow handles the synthetic shape identically to a real form submission. Yes also to the retire-but-keep-for-attribution behavior on the subscriber row at resolution time; that matches Platform's view of identity ownership (Person identity is canonical at Platform; subscriber rows are Growth-internal attribution state).

One race-condition note Platform flags for the synthetic-submission emitter: re-promotion of an already-promoted subscriber must be guarded against. Detail in §2 of the body.

Detail

On synthetic form_submissions as the unified path

The shape is correct. Every path that yields a phone-bearing intake (real form submission, manual capture, partner upload, derived match) emits the same intake.captured envelope with the same payload structure; the only differentiator is the source enum value on the payload. Adding subscriber_promotion (and likely partner_upload, manual_capture, and derived_match while you are extending) to the source enum is a payload-version bump on intake.captured per §8.2 of the event envelope contract. Growth bumps intake.captured from schema_version=1 to schema_version=2 with the new enum values appended, registers the v2 schema in the event-type registry, and consumers (Platform's mint, Sales' Lead opener, the warehouse loader) opt into the new values per §9.5 (consumers SHALL handle unknown enum values gracefully).

Coordinating: the cross-device-attribution thread (2026-04-28-growth-cross-device-attribution) is also pushing for an intake.captured payload v2 to add visitor_id. Growth should bundle these two changes into a single v1-to-v2 bump rather than v1-to-v2 (visitor_id) followed shortly by v2-to-v3 (source enum extension); two payload bumps in quick succession is unnecessary churn for consumers.

The back-reference field (the subscriber row ID) is also a payload addition. Platform suggests subscriber_row_id as the field name and a Growth-domain prefix per ADR-0002 (e.g., gws_<UUID v7> if a gws_ or similar prefix is reserved for Growth subscriber rows; if no prefix exists yet, this is a good moment to register one).

On the retire-but-keep-for-attribution behavior

Confirmed. When intake.matched returns a Person (whether newly minted or matched to an existing one), Growth's subscriber row gets marked as matched and frozen for further outbound. Keeping the row for attribution is the right call from Platform's seat; the row is a Growth-owned attribution artifact, and retiring it without preserving the touch history would lose the pre-Person funnel signal Growth needs.

The retire-but-keep behavior pairs cleanly with the warehouse semantic-layer rule sketched in 2026-05-01-platform-growth-attribution-stitching-shape. The frozen-but-preserved subscriber row is one of the touchpoints that gets retroactively attributed to the matched Person via visitor_id stitching. Growth's frozen-row data and the warehouse's growth.touchpoint fact table converge on the same Person on intake.matched resolution.

One race-condition note: prevent re-promotion

The synthetic-submission emitter must guard against emitting intake.captured more than once per subscriber row. Two ways the race shows up:

First, if the subscriber re-engages on the marketing site and submits a phone-bearing form (real, not synthetic) at roughly the same time a partner upload arrives carrying the same phone, two intake.captured events fire for the same human within seconds. Platform's mint dedupes on the canonical-identity match (per ADR-0003 mint logic, phone plus email plus name collapses to one Person), so the Person ID is one. But Growth has emitted twice and the subscriber row is now in an ambiguous state (matched against which event?).

Second, if a manual capture path emits the synthetic submission and the operator runs the same manual capture twice (UI mishap, retry on a flaky network), the same subscriber row produces two intake.captured events.

Platform's recommendation: the emitter holds an idempotency key on (subscriber_row_id, source, captured_at) (or equivalent) and short-circuits if it has already fired for that subscriber row through that source path within a short window. The dedup is Growth-internal; Platform's mint will collapse to the same Person regardless, but the Growth subscriber row's audit trail is cleaner if Growth dedupes at the source.

This is not a contract concern — Platform's mint handles cross-source dedup correctly and intake.matched returns the same person_id either way. It is operational hygiene on Growth's emitter. Flagged here so it does not surface as a "why does this subscriber row have two intake.matched events?" investigation later.

What Platform will do, and when

Nothing on Platform's side that depends on this thread; the existing mint logic already handles synthetic submissions identically to real ones (the source enum is Growth-supplied metadata, not load-bearing for identity). Platform's only related work is the warehouse semantic-layer model on the cross-device-attribution thread, which is sequenced in that response memo.

Next

Growth bundles the intake.captured v2 bump (visitor_id from the attribution thread, source enum extension and subscriber back-reference from this thread) into one schema-version coordination with consumers. Platform's mint absorbs the v2 payload transparently because Platform reads only the identity fields (phone, email, name, DOB) from the payload and ignores the rest.

If Growth wants Platform to formalize an intake.captured v2 payload schema in the event-type registry before Growth bumps the producer, file a small follow-up; Platform can stand the registry entry up in a few hours of work and that gives Growth a stable target.

References

  • Parent memo: 2026-04-28-growth-subscriber-promotion-timing
  • Sibling thread on cross-device attribution (visitor_id v2 payload bump should bundle with this thread's source enum extension): 2026-04-28-growth-cross-device-attribution and Platform's response 2026-05-01-platform-growth-attribution-stitching-shape
  • ADR-0003 (Person canonical entity, mint logic): coordination/adrs/ADR-0003-person-canonical-entity.md
  • ADR-0002 (entity ID prefixes; if Growth subscriber row needs a registered prefix, do it here): coordination/adrs/ADR-0002-person-id-shape.md
  • Event Envelope contract (§8.2 payload-versioning, §9.5 unknown enum values): coordination/contracts/event-envelope/README.md
  • Platform domain scope: coordination/domains/platform.md
  • Growth domain scope: coordination/domains/growth.md

Thread (2 memos)

Apr 28growthSubscriber-store-to-Person handoff timing

View source on GitHub