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.
2. Consent gate (browser SDK)
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.
2.3 What consent does **not** control
@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_consenton 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:
- Show whatever notice legal requires
- Call
cx.setConsent({ analytics: true })only after a clear affirmative - Call
cx.setConsent({ analytics: false })orcx.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_idanddevice_idare SDK-generated UUIDs (string). Suggested prefixesanon_/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()).
3.3 Cookie / storage attributes (when using cookies)
| 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.
9. T5.10 handoff (contact consent)
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
../simiaroom-cx-v5.md— ID-01 identity + attribution windowsMVP-EVENT-SCOPE.md— T0.4 envelope identity refsSDK-AND-DATA-INGESTION.md— §7 envelope, §9 identity, §12 privacy, §8.3/§8.5 browser deliveryPRIVACY-ACCESS-MATRIX.md— T1.11 staff access (do not conflict)SOURCE-EVENT-MAPPING.md/SOURCE-OWNERSHIP.md— browser vs backend producersadr/0001-stack-hosting-auth.md— browser keys; minimizationadr/0002-collector-sdk-outbox.md— collector ingress; backend proves outcomesworkflows/dod/month-1.md— T1.14 DoDworkflows/dod/month-2.md— T2.13 DoDworkflows/dod/month-4.md— T4.10 DoDworkflows/dod/phase-5.md— T5.10 DoD