Browser Telemetry & Consent

17 min read•docs/BROWSER-CONSENT-POLICY.md•

Browser consent, identity storage, and data-minimization (T1.14)

Status: ready for test (implement attestation 2026-08-12)
Purpose: Lock the client-browser policy for @cx-orbit/browser: consent gate, anonymous ID storage, attribution-window alignment with the event dictionary, and what the browser must not send.
Implementation: T2.13 (P1). This file is the spec T2.13 must implement. It does not ship the SDK.
Product contract: identity rules in ../simiaroom-cx-v5.md ID-01; envelope in MVP-EVENT-SCOPE.md / SDK-AND-DATA-INGESTION.md §7 (T0.4); staff UI access in PRIVACY-ACCESS-MATRIX.md (T1.11 — different audience).

This is a documentation/spec lock. It does not change collector ingest, @cx-orbit/node, or P0 backend instrumentation.


1. How to read this doc

1.1 What this task is / is not

This task (T1.14) Later — do not treat as done
Consent gate, ID storage, attribution windows, browser denylist T2.13 — @cx-orbit/browser (consent, persist, batch, pagehide). Status: ready. P1.
In-repo policy review (this file §10) T4.10 — SECURITY-REVIEW-T4.10.md reviewed this policy 2026-08-12 (SDK still not shipped). Named human sign-off residual.
Analytics/browser consent (may we store IDs and send behavioral events) T5.10 — outbound contact / notification consent UI (channel, caps, quiet hours). Blocked by this task + T3.8. Distinct flag.
What the browser may send T1.11 — what staff may see in Orbit UI. Different audience; do not merge.

Named security/product sign-off of this policy is residual (same class as T1.8 / T1.11). Do not invent a sign-off. T4.10 reviewed this file on 2026-08-12 even though @cx-orbit/browser is not shipped (SECURITY-REVIEW-T4.10.md §5).

1.2 Capacity guardrail

P0 is the backend decision loop (collector + @cx-orbit/node + authoritative outcomes). The P0 backend path is not consent-gated. This policy must not delay or add a consent check to server write keys or P0 backend events.

Browser SDK remains P1. Missing @cx-orbit/browser does not block T2.2 / T2.2a / T2.2b. Authoritative conversion spine stays backend-proved (ADR 0002).

1.3 Audiences (do not mix)

Audience Policy System
Simiaroom client in a web browser This file @cx-orbit/browser → collector (restricted browser write key)
Simiaroom backend services T0.4 envelope + ADR 0002; no browser consent gate @cx-orbit/node → collector (server write key)
Orbit staff T1.11 role × screen × data-class apps/web session; never a browser write key

T1.11 identifier class (user_id, anonymous_id, device_id) still applies after events land in Orbit. This file limits what the browser is allowed to emit; T1.11 limits which staff roles may view those identifiers.


Normative for T2.13. Collector server-key paths ignore this gate.

2.1 Default deny

State Meaning
Unset / unknown Treat as denied
analytics: false Denied
analytics: true Granted

MVP has one Orbit-relevant flag: analytics. Do not invent marketing, ads, or “necessary cookies” categories in the SDK. Simiaroom’s own cookie banner / legal copy is out of this repo; the product app calls cx.setConsent after its own flow.

setConsent is the only grant path. Loading the SDK script is not consent.

2.2 Required SDK behavior

Public API already sketched in SDK-AND-DATA-INGESTION.md §6: cx.setConsent(consent), cx.reset().

Consent Network Durable ID storage In-memory queue
Denied / unset No POSTs to collector. No sendBeacon. No keepalive flush. No cookie / localStorage / sessionStorage writes for Orbit IDs Drop; do not persist
Granted Batch + pagehide flush per ingestion §8.3 / §8.5 Persist anonymous_id + device_id per §3 Bounded memory queue allowed
Withdrawn (setConsent({ analytics: false }) or reset()) Stop immediately; drop queued events Delete Orbit ID keys; rotate next grant Drop

T2.13 Test Agent gate (preview, not this task): consent off → no events; consent on → flush works.

  • @cx-orbit/node / outbox / CDC / CSV / webhooks
  • Collector accept of server-key batches
  • Staff Orbit UI cookies (ADR 0001 Auth.js sessions)
  • T5.10 outbound contact consent (user_consent on INT contact attempts)
  • Identity merge (T2.3 job). The browser never merges accounts.

2.4 Product owns the banner

Orbit does not ship a CMP or cookie banner. Simiaroom web must:

  1. Show whatever notice legal requires
  2. Call cx.setConsent({ analytics: true }) only after a clear affirmative
  3. Call cx.setConsent({ analytics: false }) or cx.reset() on withdraw

If the product never calls setConsent, the SDK stays silent. That is correct.


3. Anonymous ID storage rules

Applies only when analytics consent is granted. Keys are first-party to the Simiaroom site that loads the SDK.

3.1 Identifiers

ID Envelope field Storage Lifetime Purpose
Anonymous ID identity.anonymous_id First-party cookie or localStorage (one store; T2.13 picks and documents) 30 days from last write, sliding on activity Ghost journey; ID-01 same-device window
Device ID properties.device_id when the event contract allowlists it; not an envelope identity field Same store as anonymous ID 30 days, same sliding rule ID-01 same-device evidence (anonymous_id + device_id match)
Analytics session ID identity.session_id Memory + optional session cookie Tab / browser session; rotate on logout and on reset() Web session, not therapy session_id
User ID identity.user_id Memory only after identify(); do not persist user_id in a long-lived cookie Cleared on logout / reset() Known-user behavioral events. Authoritative login remains AUTH-01 (backend)

Envelope identity object is T0.4 / SDK §7.2:

"identity": {
  "anonymous_id": "string|null",
  "user_id": "string|null",
  "session_id": "string|null"
}

device_id is not inside identity. It is an allowlisted property (T1.12 identifier class), matching AUTH-01 fixtures.

3.2 Generation and format

  • anonymous_id and device_id are SDK-generated UUIDs (string). Suggested prefixes anon_ / dev_ for operability; not required for uniqueness.
  • Do not use canvas, WebGL, audio, font, or IP fingerprinting to build device_id.
  • Do not use email, phone, national ID, or user-agent as anonymous_id.
  • New IDs are created only after consent grant if none exist (or after reset()).
Attribute Value Why
Host First-party Simiaroom registrable domain only No third-party Orbit cookies
Secure Required on HTTPS (staging/prod) ADR 0001 threat model
SameSite Lax Cross-site leak reduction; landing attribution still works on top-level GET
HttpOnly Must be false if the JS SDK reads the cookie SDK cannot use HttpOnly; prefer localStorage if product security wants no JS-readable cookies — then T2.13 documents that choice
Max-Age / Expires 30 days Aligns with ID-01 same-device window (§4)
Path / on the product origin Available across assessment / booking paths

No __Host- requirement. No cross-subdomain cookie until security approves a short-lived, integrity-protected cross-domain identifier (SDK §9). Default: same origin only.

3.4 Logout, reset, shared device

Event Browser must
Product logout Clear identity.user_id; rotate identity.session_id; keep anonymous_id + device_id if analytics consent still granted (next visitor on a shared device gets a new user_id later; T2.3 treats multi-user device_id as ambiguous / no account merge)
cx.reset() or consent withdraw Delete all Orbit ID keys; drop queue; next grant creates new anonymous_id and device_id
Multiple user_ids on one device_id Browser still sends the stored device_id. Do not client-side merge or overwrite another account’s history

3.5 Queue persistence

Ingestion §8.3 allows “optional consent-compatible persistence” for the browser queue. Rule: persist the queue only while analytics consent is granted, and never persist payloads that violate §5. On withdraw, delete the persisted queue.


4. Attribution window alignment (event dictionary ID-01)

Source: simiaroom-cx-v5.md §5 ID-01. Runtime constants: packages/identity/src/constants.ts (T2.3). This policy must not invent different windows.

Scenario Evidence Confidence Window Browser role Merge?
Same device / same browser anonymous_id + device_id match + authentication_succeeded Deterministic 30 days Persist those IDs for 30 days (§3); send them on behavioral events when consented T2.3 job may link in-window history
Cross-device verified OTP / magic link / verified email or phone Deterministic 7 days before login Browser cannot verify contact. Do not send email/phone to invent this path Backend AUTH-03 / AUTH-01; T2.3
Assisted conversion Valid support/case identifier tied to user_id Deterministic 14 days Browser does not emit this Ops/product path; T2.3 constant exists (evaluate path may still be deferred)
IP only IP or coarse geo without a shared identifier Insufficient No window Browser must not send IP or geo as identity evidence No merge
Shared device Several user_ids on one device_id Ambiguous — Keep sending the device ID; do not collapse accounts in JS No account merge; separate links

Dictionary J-01 notes that also bind this policy:

  • IP alone never connects anonymous_id → user_id.
  • Ghost-to-login conversion counts only in-window, deterministic links. Out-of-window or low-confidence links are not conversion.

4.1 Why storage TTL is 30 days

The longest browser-relevant deterministic window is same-device 30 days. Cookie / localStorage TTL of 30 days (sliding) keeps ghost history eligible for that path and lets IDs expire when ID-01 would refuse the link anyway.

Cross-device 7-day and assisted 14-day paths do not require a longer browser cookie. They use backend-verified evidence.

4.2 Browser does not emit AUTH-04

identity_link_created (AUTH-04) is a backend / Orbit identity job event (simiaroom_auth / T2.3). The browser must not send AUTH-04, AUTH-01, or other authoritative P0 outcomes.

After product login, the browser may identify(user_id) so subsequent behavioral events carry identity.user_id and the existing anonymous_id. Linkage of past anonymous events is T2.3, using ID-01 windows — not a client-side rewrite of raw events (SDK §9).

4.3 Envelope vs dictionary

T0.4 requires nested identity on every event. Browser events may send anonymous_id with user_id: null until identify (MVP-EVENT-SCOPE identity rules). Authoritative P0 backend outcomes still require identity.user_id. That split is unchanged.


5. Minimization — what the browser must NOT send

Allowlist per event contract (SDK §12, T1.12). If a field is not on the approved browser contract, do not send it.

5.1 Never — any browser event

Forbidden Why
Therapy notes, session recordings, chat/session content Architecture §5; SDK §12; T1.11 forbidden list
Raw assessment answers, intake free text, crisis narrative SDK §12; dictionary assessment is not therapy, but answers are still sensitive
ORS/SRS scores, restricted feedback free_text T1.12 / DATA-MODEL restricted; forms/backend (FDB-*), not browser
Email, phone, national ID, password, OTP code, magic-link token Direct PII / secrets. AUTH-03 is backend
Payment PAN, CVV, gateway tokens, IBAN, raw gateway payloads T1.11 payment-secrets; financial amount is not a browser field
Server write keys, AUTH_SECRET, DATABASE_URL, staff session cookies ADR 0001; browser key is a different, public, restricted key
Collector-owned fields: received_at, ingest request_id, auth result, quarantine IDs T0.4; never trusted from sender
Client IP, precise geolocation, Wi-Fi/cell identifiers ID-01 IP-only = no merge; SDK §12 remove/anonymize IP unless approved (not approved here)
Canvas / WebGL / audio / font fingerprints, full User-Agent strings used as identity Not ID-01 evidence; high entropy PII
Other users’ user_ids, therapist clinical notes, staff PII Wrong actor
write_key in event properties or context Header only; never in payload

5.2 Never — event **classes** from the browser

Browser is behavior and intent only (ADR 0002). Producer-of-record for outcomes is backend.

Do not emit from @cx-orbit/browser:

  • Authoritative P0 slice: authentication_succeeded, identity_link_created, assessment_completed, therapist_selected, booking_confirmed, booking_cancelled, payment_succeeded, payment_failed, session_completed, session_no_show_recorded, therapy_pause_declared, treatment_completed, internal_case_resolved
  • Other backend-owned P0 outcomes in SOURCE-OWNERSHIP.md / SOURCE-EVENT-MAPPING.md
  • Orbit-derived types (unplanned_gap_detected, SYS-01, churn state events)

T2.13 may instrument selected P1/behavioral types only, matching the mapping defer list (ASM-02, ASM-04, THR-01, THR-02, BKG-01, BKG-05, BKG-06, PAY-04, SES-04, RET-01) — still under this denylist for properties.

Collector (when T2.13 ships) rejects other classes on the browser write key (EVENT_CLASS_NOT_PERMITTED, ingestion §11 / ADR 0001 origin/rate/class limits). That check is defense in depth; the SDK must not send them.

5.3 Allowed (when consented and allowlisted)

May send Notes
Envelope v1 required fields event_id, canonical_type, event_version, project_id, environment, source = cx_orbit_browser, source_event_id, occurred_at, identity, entity, properties, actor
identity.anonymous_id Required for client-scoped browser events until identify
identity.user_id Only after identify(); product-supplied id, not email
identity.session_id Analytics session, not therapy session
properties.device_id If the event contract allowlists it
Coarse context.timezone, context.locale Scheduling/display metadata; UTC still on occurred_at
Allowlisted operational props e.g. therapist_id, assessment_id, page name, referral_id, option_rank — per contract, not a free bag
actor.type = user actor.id may be anonymous_id or user_id; never a staff id from the browser SDK

source must be cx_orbit_browser (or the locked producer ID in SOURCE-OWNERSHIP). Do not spoof simiaroom_booking / simiaroom_auth.

5.4 Privacy classes (T1.12) — browser default

Class Browser
operational Allowed if on the event allowlist
identifier Only anonymous_id / user_id / device_id as specified in §3; no email/phone
scheduling Coarse timezone / slot ids if allowlisted; no home address
financial Do not send (amount, currency, gateway)
internal Do not send (staff/case fields)
restricted Do not send

This is stricter than T1.11 staff grants: even ops may see financial in Orbit UI from backend payment events; the browser still must not originate those fields.


6. P0 backend path is not blocked

Path Consent / this policy
Collector POST /v1/events:batch with server write key Not consent-gated. T1.13 / T2.2 unchanged.
@cx-orbit/node typed P0 methods Not consent-gated. Identify refs come from the backend session, not cookies.
Outbox / CDC / import Not consent-gated.
Browser write key Public (ADR 0001). Origin, rate, environment, and event-class limited. Used only after T2.13.
Identity job T2.3 Uses ID-01 windows in §4; does not wait on a browser consent field on historical backend events.

Staging collector and the 12-event backend slice can ship without a cookie banner, without @cx-orbit/browser, and without this policy being enforced in code.

When T2.13 exists, a leaked browser key must still be unable to emit P0 outcomes or read storage (ADR 0001).


7. Alignment with T0.4, ADR 0001, ADR 0002

Lock This policy
T0.4 envelope Browser sends envelope v1. Nested identity. UTC timestamps. Dual idempotency keys unchanged. Sender must not set collector-owned fields.
ADR 0001 Browser keys ≠ server keys; treated as public; no DATABASE_URL / AUTH_SECRET in the SDK. Mental-health minimization. Staff auth is a different cookie. Production data residency stays parked (ADR 0001); not re-decided here.
ADR 0002 Collector is the only write ingress. Browser SDK is transport-only. Backend proves outcomes. Browser = behavior/intent.

8. Alignment with T1.11 (do not conflict)

T1.11 is in flight / ready_for_test as a staff access matrix. This file does not change roles, screen grants, support 360 allowlists, or payment-secrets policy for the timeline.

Topic T1.11 T1.14
anonymous_id / device_id Staff: generally denied on 360 field catalog (keep clientId only) Client browser: may persist and send after consent, as ID-01 requires
Payment secrets / amount Support: no amounts on timeline Browser: never send financial fields
restricted ORS/SRS Staff gated; support denied Browser: never send
Consent Not a staff-UI concept Analytics consent for the client SDK
Enforcement T5.6 T2.13 (+ collector class limits)

If a T1.11 sentence still says “T1.14 not done”, treat this file as the T1.14 deliverable; do not reopen T1.11’s matrix to encode cookie rules.


T5.10 (SET-07) versions outbound contact policy: channel, user_consent, attempt caps, quiet hours. That is not cx.setConsent({ analytics }).

Handoff rule for T5.10: do not use analytics-cookie grant as permission to email/SMS a client. Do not use contact opt-out to silently disable backend P0 events. No silent bypass of either flag.


10. In-repo review (T1.14)

This is the security/product review evidence, not a fabricated stakeholder sign-off.

Item Record
Date 2026-08-12
Kind Implement-agent attestation against ID-01, T0.4 envelope, T2.3 windows, SDK §9/§12, ADR 0001/0002, T1.11 audience split
Named security lead Residual — no named sign-off in repo (same class as T1.8 / T1.11). Do not invent a name
Named product lead Residual — same

Cross-checked in this pass (pointers, not a Test Agent result):

Source Pointer Policy coverage
ID-01 simiaroom-cx-v5.md §5 §4 windows; IP-only no merge; shared device no merge
T0.4 envelope MVP-EVENT-SCOPE.md identity; SDK §7.2 §3 identity object; §5.3 allowed envelope
T2.3 constants packages/identity/src/constants.ts 30 / 7 / 14 day windows
SDK identity + privacy SDK-AND-DATA-INGESTION.md §9, §12 Consent APIs; no notes/recordings; browser keys
ADR 0001 docs/adr/0001-stack-hosting-auth.md Public browser keys; minimization; residency parked
ADR 0002 docs/adr/0002-collector-sdk-outbox.md Browser ≠ outcome authority; collector-only writes
T1.11 PRIVACY-ACCESS-MATRIX.md Staff vs browser; payment secrets; restricted
T1.3 defer list SOURCE-EVENT-MAPPING.md Behavioral types wait for T2.13

Approve Agent may still require named humans. This pack must not be treated as that sign-off. T4.10 reviewed this policy without a live SDK (SECURITY-REVIEW-T4.10.md).


11. Out of scope / residuals

Item Owner
@cx-orbit/browser code, tests, pagehide flush T2.13 (P1)
Collector EVENT_CLASS_NOT_PERMITTED + origin allowlist on browser keys T2.13 / T2.2 follow-on; catalogued in ingest contract
Signed security checklist, audit bar, production residency T4.10 SECURITY-REVIEW-T4.10.md. Residency still parked (ADR 0001; before T4.1).
Named security/product approval T4.10 / Approve Agent — do not invent
Cookie banner / CMP / legal notice copy Simiaroom product (not Orbit)
Outbound contact consent UI T5.10
Staff field enforcement T5.6 (after T1.11 + T5.5)
Cross-subdomain ID cookie Not approved; SDK §9 requires a later security approval
Assisted-conversion evaluate path in T2.3 Already a T2.3 residual; 14-day constant only
Changing P0 backend instrumentation Out of scope — must stay unblocked

12. References