Canonical Event Envelopes v1

10 min read•docs/MVP-EVENT-SCOPE.md•

MVP Event Scope

Source of truth: ../simiaroom-cx-v5.md Event Dictionary.

MVP does not require every dictionary event on day one. Prioritize events that power funnel, retention, risk, fit, and light actions.

Initial integration slice — first 8–12 authoritative events

Before expanding across the full P0 target set, prove collection, reconciliation, state derivation, and reporting with this backend-first slice:

Event Producer of record
authentication_succeeded / identity link Authentication backend
assessment_completed Assessment backend
therapist_selected Matching/selection backend
booking_confirmed Booking backend
booking_cancelled Booking backend
payment_succeeded Payment verification backend
payment_failed Payment verification backend
session_completed Session backend / approved staff workflow
session_no_show_recorded Session backend / approved staff workflow
therapy_pause_declared Approved product/staff workflow
treatment_completed Approved clinical/operational workflow
internal_case_resolved CX Orbit operational store

Expansion is allowed only after the slice meets the agreed completeness, duplicate, latency, and invalid-event targets.

Priority P0 target set — introduce incrementally

ID Event Why
AUTH-01 / AUTH-04 auth succeeded / identity link Identity continuity
ASM-02 / ASM-04 / ASM-09 assessment started / abandoned / completed Funnel friction
THR-01 / THR-02 / THR-05 options viewed / profile viewed / selected Matching funnel
BKG-01 / BKG-05 / BKG-06 / BKG-S03 / BKG-S04 booking start/submit/abandon/confirm/cancel Booking outcomes
PAY-04 / PAY-06 / PAY-07 payment started/succeeded/failed Revenue + friction
SES-04 / SES-06 / SES-08 / SES-S01 joined / completed / no-show / therapist attendance issue Session spine
CON-04 / CON-S01 pause declared / unplanned gap Retention risk
CHG-01 / CHG-02 therapist change requested / reason Fit quality
CHR-02 / CHR-06 / CHR-S01–S03 stop / churn reason / risk & churn states Churn analytics
RET-01 / CON-07 return / resumed therapy Reactivation
FDB-02 / FDB-04 / FDB-07 baseline / session feedback / dissatisfaction Quality signals
INT-01 / INT-03 / INT-09 case created / contact attempted / resolved Action loop
SYS-01 profile recomputed Derived freshness

Priority P1 — Month 2–3 if capacity

Area Events
Entry / GA ENT-01…ENT-10 (attribution + entry drop-off)
Assessment detail ASM-03, ASM-05–08, ASM-S*
Booking detail BKG-02–04, BKG-07–08, BKG-S01–02
Payment extras PAY-01–03, PAY-08–09, PAY-S*
Feedback extras FDB-01, FDB-03, FDB-05–06, FDB-S*
Continue CON-01–03, CON-05–06
Internal ops INT-02, INT-04–08, INT-S01

Priority P2 — post-MVP / nice-to-have

Fine-grained UI clicks that do not change state (many ENT/GA micro-events), optional insurance/refund deep paths, advanced quality review events — keep schema-ready but do not block MVP screens.

Event envelope (canonical) — locked v1

Status: locked for SDK + collector (T0.4). Delivery, retries, and validation live in SDK-AND-DATA-INGESTION.md and must not contradict this section.

Every ingested event maps to envelope v1 below. SDKs never write storage; they only send this envelope to the collector.

Required sender fields

Field Type Meaning
event_id UUID string Generated before first send; stable across all retries of the same payload
canonical_type string Dictionary event name (booking_confirmed, payment_succeeded, …). Wire alias: event_name (see compatibility)
source string Stable producer identifier (not a coarse channel enum)
source_event_id string Stable ID of this fact in the source system
occurred_at ISO-8601 UTC When the fact happened (…Z)
identity object Identity refs (anonymous_id, user_id, optional analytics session_id)
entity object Primary entity refs (type, id)
properties object Type-specific payload (allowlisted per event contract)

Also required on the wire (already in the SDK contract): event_version, project_id, environment.

Compatibility with the SDK wire shape

Do not treat these as different events:

Locked field Existing SDK / sketch alias Rule
canonical_type event_name Same value. If both are present they must be identical. Collector stores canonical_type. Senders may send only event_name; collector copies it to canonical_type.
identity.anonymous_id / identity.user_id top-level anonymous_id / user_id Nested identity is canonical. Flat aliases, if sent, must match.
actor.type top-level actor_type Nested actor is canonical.
entity + properties.*_id top-level booking_id / session_id / payment_id / therapist_id entity is the primary ref. Related IDs belong in properties and must not disagree with entity.

source is a producer ID (simiaroom_backend, simiaroom_booking, cx_orbit_browser, porsline, import, cx_orbit_system, …). The older sketch enum browser\|backend\|ga\|… is a channel hint only; do not use it as the uniqueness key.

Identity refs

"identity": {
  "anonymous_id": "string|null",
  "user_id": "string|null",
  "session_id": "string|null"
}
  • session_id here is the analytics/web session, not the therapy session. Therapy sessions use entity.type = "session" and/or properties.session_id.
  • Authoritative backend P0 outcomes require user_id.
  • Browser behavioral events may send anonymous_id with user_id: null until identify/link.
  • At least one of anonymous_id or user_id is required except for approved actor.type = system events that are not client-scoped (rare; must be in the event contract).
  • Browser consent, ID storage, and send-minimization: BROWSER-CONSENT-POLICY.md (T1.14). Backend P0 events are not consent-gated.

actor (required): { "type": "user|system|staff|internal", "id": "string" }.

Entity refs

"entity": { "type": "booking|session|payment|assessment|client|therapist|case|identity_link|…", "id": "string" }

Related entities go in properties (e.g. therapist_id on a booking event). properties must not repeat a different ID for the same entity.type.

Collector-owned fields (never trusted from sender)

received_at, ingestion / request ID, authentication result, validation status, collector version.

Idempotency (locked)

Canonical idempotency key = project_id + source + source_event_id.

This is the DoD key source + source_event_id, scoped by project_id (envelope already includes project_id; MVP project is simiaroom).

source_event_id must uniquely identify one fact within that source. Do not reuse a bare entity PK across different event types (a booking can be confirmed and later cancelled). Prefer the source outcome row ID. If the source has no outcome ID and the type can fire at most once per entity, use {canonical_type}:{entity.id}.

Transport dedup (SDK retries of the same envelope, unchanged from the ingestion doc) = project_id + event_id. This does not replace the canonical key. Same event_id retry → one raw row. New event_id with the same canonical key → no second canonical fact (see duplicate behavior).

Duplicate / late / out-of-order

Case Behavior
Duplicate (same canonical key, compatible payload) Accept; do not insert a second canonical fact. Response may flag duplicate. First accepted envelope remains the raw record.
Duplicate conflict (same key, different canonical_type or material identity/entity/occurred_at/required properties) Quarantine as IDEMPOTENCY_CONFLICT. Do not overwrite the canonical fact.
Retry / batch resubmit Same event_id and/or same canonical key must be safe. Replay of a batch must not double-count P0 outcomes.
Late (received_at well after occurred_at, or after later facts already applied) Store on the canonical log using occurred_at as event time. Recompute derived state from history. Flag for data-quality if later than the late threshold (P0 operational default 24h; imports/CDC default 7d; still stored if older).
Out-of-order Arrival order is not event order. Interpretation and derived state (Current Event + Current State + History) order by occurred_at then event_id. An earlier fact arriving after a later one is inserted into history and triggers recompute (SYS-01 path). Do not rewrite prior canonical payloads; correct via compensating events.

Exactly-once delivery is out of scope. At-least-once + these keys is the contract.

Collector HTTP success/partial/reject shapes, retryable vs permanent codes, quarantine fields, and resubmit item codes (DUPLICATE_EVENT_ID, DUPLICATE_CANONICAL_KEY, IDEMPOTENCY_CONFLICT, EVENT_ID_REUSED) are locked in SDK-AND-DATA-INGESTION.md §11 (T1.13).

Timezone (locked)

  • Store occurred_at, sent_at, and collector received_at as UTC (Z).
  • If the sender supplies offset/local time, convert to UTC; keep the sender zone in context.timezone when known.
  • properties.user_timezone / slot local times are scheduling display fields, not the storage clock.
  • Display in CX Orbit UI and calendar-day metrics: Asia/Tehran (IANA; DST handled by the zone), unless a report explicitly says otherwise.

Locked envelope shape

{
  "event_id": "0198a1c2-3b4d-7e8f-9a0b-1c2d3e4f5a6b",
  "canonical_type": "booking_confirmed",
  "event_name": "booking_confirmed",
  "event_version": 1,
  "project_id": "simiaroom",
  "environment": "production",
  "source": "simiaroom_backend",
  "source_event_id": "bkg_evt_confirm_789",
  "source_version": "3.18.0",
  "occurred_at": "2026-08-03T10:20:30.123Z",
  "sent_at": "2026-08-03T10:20:31.004Z",
  "sdk": { "name": "@cx-orbit/node", "version": "0.2.0" },
  "actor": { "type": "system", "id": "booking-service" },
  "identity": {
    "anonymous_id": null,
    "user_id": "usr_123",
    "session_id": null
  },
  "entity": { "type": "booking", "id": "bkg_789" },
  "journey_ref": "J-04",
  "context": {
    "request_id": "req_123",
    "timezone": "Asia/Tehran",
    "locale": "fa"
  },
  "properties": {
    "therapist_id": "thr_456",
    "payment_id": "pay_456",
    "slot_id": "slot_1001",
    "session_number": 1,
    "user_timezone": "Asia/Tehran",
    "booking_context": "first_session",
    "scheduled_start_utc": "2026-08-10T14:00:00Z"
  }
}

P0 examples

Full JSON for three initial-slice types is in SDK-AND-DATA-INGESTION.md §7.6. Summary:

canonical_type Dict ID source (example) source_event_id (example) entity Identity
booking_confirmed BKG-S03 simiaroom_backend bkg_evt_confirm_789 booking / bkg_789 user_id required
payment_succeeded PAY-06 simiaroom_backend pay_ver_456 payment / pay_456 user_id required
session_completed SES-06 simiaroom_backend ses_complete_321 session / ses_321 user_id required

Producer-of-record rules

Event class Authoritative producer Notes
Page, view, click, form exposure Browser SDK / approved analytics source Behavior and intent only
Booking/payment/session/refund outcomes Simiaroom backend or reconciled source table Browser success screens are not proof
Pause/completion/change outcomes Backend or approved staff workflow Must preserve actor and policy version
Feedback Form/MyOutcomes source or backend receipt Sensitive fields follow restricted access policy
Internal case/action CX Orbit operational store Emit analytical events after committed state change

Every canonical event must have one producer of record. Duplicate observations from other sources may be retained as diagnostic events but must not create duplicate business facts.

State update rule

Current Event + Current State + Accumulated History + Global Flags → Interpretation → Next State / Action

Not every click changes state. Prefer PRODUCT/status events for stage transitions; use GA for conversion diagnostics.

Mapping checklist (engineering)

  • Publish mapping table: Simiaroom source field → canonical event + properties — SOURCE-EVENT-MAPPING.md (T1.3)
  • Define idempotency keys per source — locked: project_id + source + source_event_id (T0.4)
  • Define late/duplicate event handling — locked in envelope section above (T0.4)
  • Define timezone policy (store UTC, display Asia/Tehran) — locked (T0.4)
  • Define which events trigger SYS-01 recompute — initial list in SOURCE-EVENT-MAPPING.md § SYS-01 recompute triggers (T1.3)
  • Assign producer of record and owner to every P0 event — SOURCE-EVENT-MAPPING.md + SOURCE-OWNERSHIP.md (T1.3)
  • Add event version, privacy class, required fields, and allowed enums — initial 12-event slice in fixtures/events/contracts.yaml (T1.12)
  • Add schema fixtures to CI for backend and browser SDKs — backend 12-event slice: docs/fixtures/events/, packages/shared/schemas/events/, pnpm --filter @cx-orbit/shared test (T1.12; browser SDK deferred T2.13)
  • Define retryable/non-retryable collector responses — locked T1.13 in SDK-AND-DATA-INGESTION.md §11.5–§11.6
  • Define reconciliation query for every authoritative P0 event — per-type queries in SOURCE-EVENT-MAPPING.md § Initial 12-event slice (T1.3); remaining authoritative rows include CDC target table