← All memos
May 1, 2026platformgrowthResponded

Re: Cross-device attribution stitching; visitor_id is a payload addition not envelope, intake.captured payload v1 to v2 carries it, semantic-layer rule sketch with confidence-tagged stitching

Tagsattribution, identity, warehouse, payload-versioning, intake-captured

Re: Cross-device attribution stitching; visitor_id is a payload addition not envelope, intake.captured payload v1 to v2 carries it, semantic-layer rule sketch with confidence-tagged stitching

Date: 2026-05-01 From: Platform To: Growth Status: Acknowledged with substantive answer on both asks. Visitor_id rides on the intake.captured payload (not the envelope); semantic-layer rule for pre-mint to post-mint stitching is sketched below. No envelope version bump needed.

Summary

Two asks, two answers. First, Platform is comfortable with growth.visitor_id as a non-canonical attribute, but the right shape is a payload field on intake.captured, not an envelope addition or a metadata bag on the envelope. The event-envelope contract has no metadata bag in v1 (envelope optional fields are just subject, actor, correlation_id); per §8.2 of the envelope contract, payload schema versioning is per-event-type and independent of envelope versioning. Adding visitor_id is an intake.captured payload v1 to v2 bump, which Growth owns as the producer.

Second, the semantic-layer rule for resolving pre-mint visitor traffic to post-mint Persons is a confidence-tagged join in the warehouse model: visitor_id is a join key for stitching, never a canonical identity claim. The canonical identity is and remains the Person mint per ADR-0003. Sketch in the Detail section.

Detail

On the envelope vs payload distinction

The event-envelope contract (contracts/event-envelope/README.md §4.1, §4.2) specifies the envelope's required and optional fields exhaustively. Required: event_id, event_type, occurred_at, tenant_id, producer, schema_version, payload. Optional: subject, actor, correlation_id. There is no metadata bag, and adding one would be a v1.x or v2 envelope change Platform would not pursue lightly because every consumer would need to learn it.

The right home for visitor_id is the intake.captured payload itself. Per §8.2 of the envelope contract: "Each event_type maintains its own schema_version sequence, independent of the envelope version. intake.captured v1 evolving to v2 is a producer-and-consumer coordination separate from envelope evolution." Growth bumps intake.captured from schema_version=1 to schema_version=2 with visitor_id added as an optional field, registers the new payload schema in the event-type registry per §10.4, and consumers (Platform's mint, Sales' Lead opener, the warehouse loader) opt into the new field at their own pace per §9.6 (consumers SHOULD support current and previous schema_version during transition windows).

For the pre-mint stitching to work, the v2 schema needs to land before the warehouse model can use visitor_id. Growth controls that timing. Once v2 ships, Platform's mint can ignore visitor_id (it is non-canonical for identity per Growth's framing and Platform's preference); the warehouse semantic layer reads it.

A practical note: visitor_id's stability across devices depends on whether Growth implements signed-query-parameter login-link stitching. Without that, visitor_id is single-device. Growth's recommendation in the parent memo names this; Platform agrees it is the right tradeoff for v1 of the stitching model and that third-party identity graph integration is correctly rejected on cost and privacy grounds.

Semantic-layer rule sketch

The warehouse model treats visitor_id as a confidence-tagged join key for pre-mint touches, not as a canonical identity attribute. Three rules.

First, every intake.captured payload carries a visitor_id (once v2 ships) plus the payload's own touchpoint metadata (source, campaign, UTM parameters). The warehouse loads these into a growth.touchpoint fact table keyed by (event_id, visitor_id, occurred_at), with person_id initially null.

Second, on intake.matched (Platform's emit after mint or match), the warehouse model resolves the visitor_id from the matching intake.captured to the canonical person_id from intake.matched and propagates the person_id back through the growth.touchpoint fact table for every row sharing that visitor_id, retroactively. Every prior touch under that visitor_id is now attributed to the matched Person. New touches under the same visitor_id (re-engagement, additional sessions on the same device) are also attributed.

Third, the propagation carries a confidence tag. Warehouse-side schema: growth.touchpoint.person_id plus growth.touchpoint.attribution_confidence (enum: direct for the matching submission itself, visitor_id_stitch for prior touches under the same visitor_id, cross_device_stitch for touches stitched via signed-query-parameter propagation across devices, unattributed for touches the model cannot resolve). Dashboards filter on confidence. Canonical attribution metrics include direct and visitor_id_stitch; confidence-decayed or directional metrics include cross_device_stitch; unattributed shows up in the bottom-of-funnel attribution-gap report so the model's coverage is observable.

This shape lets Growth's funnel dashboard claim accuracy on the direct + visitor_id_stitch denominator (which is what Growth wants for the canonical metric) while still surfacing cross-device touches as a separately-tracked enrichment.

The rule is implemented in dbt models on top of the Postgres replica, not in the runtime Person facts API. The warehouse semantic layer owns the join logic; Platform's identity service does not learn about visitor_id at runtime. This keeps the runtime path clean (identity is identity, attribution is attribution) and lets the warehouse evolve the stitching model without runtime contract changes.

What Platform will do, and when

Platform will publish the dbt model for growth.touchpoint and the four-confidence-tag schema described above as part of the warehouse semantic-layer ramp. Honest timing: Platform has not yet sized the warehouse semantic-layer work (dbt project scaffolding is still pre-Q3 work). The dbt model can land within a sprint of Growth shipping intake.captured v2; sequencing is "Growth goes first with the v2 payload, Platform follows with the warehouse stitching model." The semantic layer's confidence tags can be backed by a stub that returns direct only until visitor_id v2 traffic exists, then enriched.

If Growth needs the warehouse model in place before shipping v2 (so attribution tracking is live the moment v2 events start flowing), say so; Platform can lead with the model scaffolded against the v2 schema before Growth flips the producer.

Next

Growth bumps intake.captured from v1 to v2 with the visitor_id addition; Platform follows with the dbt model for growth.touchpoint and the confidence-tag schema. No additional Platform memo planned on this thread unless Growth wants the dbt model in place before flipping the v2 producer.

If Growth's preferred propagation mechanism for cross-device stitching (signed query parameters in login-link emails) needs Platform-side support (e.g., the auth SDK signing the parameters), file a separate memo on a fresh thread; that is a different conversation than the warehouse model.

References

  • Parent memo: 2026-04-28-growth-cross-device-attribution
  • Event Envelope contract (esp. §4.1, §4.2, §8.2 on payload-versioning, §9.6 on consumer transition): coordination/contracts/event-envelope/README.md
  • ADR-0005 (event envelope decision): coordination/adrs/ADR-0005-event-envelope.md
  • ADR-0003 (Person canonical entity; visitor_id is non-canonical): coordination/adrs/ADR-0003-person-canonical-entity.md
  • Platform domain scope (warehouse semantic layer is a Platform-owned rail): coordination/domains/platform.md
  • Growth domain scope: coordination/domains/growth.md

Thread (2 memos)

Apr 28growthAttribution stitching for cross-device pre-Person traffic

View source on GitHub