Technical Architecture — CX Orbit MVP
1. Product boundary
CX Orbit is an internal analytics + light action system for Simiaroom teams (product, ops, support, matching, leadership).
It must answer:
- Where do clients enter and drop?
- After which session does continuation improve / churn peak?
- Which clients are at risk, and why?
- How good is client–therapist fit?
- What insights and follow-ups are open, and what was their outcome?
It must not become: full ops CRM, messaging platform, form builder, clinical decision engine, or multi-tenant SaaS in v1.
2. Logical architecture
flowchart LR
subgraph sources [Data Sources]
Browser[Browser SDK]
Backend[Backend SDK]
Prod[(Simiaroom Product DB / CDC)]
GA[GA / Web Analytics]
Porsline[Porsline Forms]
OpsCSV[Periodic Ops Imports]
end
subgraph orbit [CX Orbit]
Collector[Collector API]
Validate[Auth + Schema + Privacy Validation]
Quarantine[(Invalid Event Quarantine)]
Raw[(Immutable Raw Events)]
Ingest[Canonical Mapping]
Identity[Identity Resolution]
Store[(PostgreSQL Operational Store)]
Analytics[(Analytical Store)]
Derive[Derived Profile + Metrics]
Risk[Rule-based Risk Engine]
Insights[Insight Generator]
API[Query API]
UI[Internal Dashboards]
Actions[Light Action Center]
end
Browser --> Collector
Backend --> Collector
Collector --> Validate
Validate -->|valid| Raw
Validate -->|invalid| Quarantine
Prod --> Raw
GA --> Raw
Porsline --> Raw
OpsCSV --> Raw
Raw --> Ingest --> Identity
Identity --> Store
Identity --> Analytics
Store --> Derive --> Risk
Analytics --> Derive
Derive --> Insights
Risk --> API
Insights --> API
Store --> API
API --> UI
API --> Actions
Actions --> Store
3. Layers
| Layer | Responsibility | MVP approach |
|---|---|---|
| SDKs | Convenient event production | Thin browser/backend clients around a storage-neutral protocol; no risk/state logic |
| Collector | Receive and protect the event boundary | Authenticate, rate-limit, validate schema/privacy, timestamp, batch, and route |
| Raw event stream | Replayable product/GA/system/internal events | Immutable accepted events + restricted invalid-event quarantine |
| Identity | Link anonymous → known user | Deterministic rules + attribution window from simiaroom-cx-v5 (ID-01). Browser storage/consent: BROWSER-CONSENT-POLICY.md (T1.14). Merge job: T2.3. |
| Operational store | Clients, therapists, sessions, payments, feedback, cases | PostgreSQL normalized model for current state, actions, permissions, and configs |
| Analytical store | Event history, funnels, cohorts, retention | Partitioned PostgreSQL initially or ClickHouse when volume/latency justifies it |
| Derived profile | Stage, cadence, pause/churn flags, risk signals | Recompute on meaningful events (SYS-01) |
| Metrics | Funnel, retention, fit, feedback, action KPIs | SQL/materialized views or scheduled jobs |
| Risk | Rule-based Healthy / Needs attention / High risk | Versioned rule set, explainable reasons |
| Insights | Pattern → actionable insight | Semi-manual; templates + frequency thresholds |
| Actions | Light follow-up tracking | Create/assign/status/result; no full ticketing |
| UI | Role-oriented pages | 4 P0 decision-loop screens; remaining role views are P1. As-built lock: P0-WIREFRAMES.md (T1.8). Staff how-to: USAGE-GUIDE.md (T4.6). Role × screen × data-class: PRIVACY-ACCESS-MATRIX.md (T1.11; enforce T5.6) |
4. Integration contracts
4.1 Product / status events
- Source of truth for booking, payment, session completion, cancel, no-show, therapist change
- Must attach:
user_id/anonymous_id,therapist_id,session_id/booking_id,payment_id, timestamps, journey stage refs - Must originate from the authoritative backend or a reconciled source table, not browser confirmation UI
- Prefer an outbox pattern for critical backend outcomes where the Simiaroom stack permits it
4.2 Behavioral (GA / product analytics)
- Entry, assessment progress, therapist browse, booking abandon, return
- Used for funnel friction; not sole source for status outcomes
- Explicit tracking is preferred over broad autocapture because the product handles sensitive mental-health journeys
4.3 Porsline (external forms)
- Forms live outside Orbit
- Orbit defines trigger points and import schema
- Responses join to client + session + therapist + journey stage
Catalog, join keys, and cadence: PORSLINE-FORMS.md (T1.5). Primary provider is Porsline; class is import until webhook (T1.9). T2.7 importer reuses T2.8 CSV audit. T2.6 live publish is still open.
Suggested trigger points (locked in the catalog; abandoned-payment send deferred to T2.13):
- After session
- After ~session 3 (sensitive continuation point)
- After therapist change
- After cancel
- After long gap
- After stop / non-continuation
- After failed/abandoned payment (if operable)
4.4 Periodic imports
CSV/API batch for ops/support extras that are not yet evented:
- Call logs, tickets, complaint status, manual follow-ups
Field-level class (evented / import / manual), latency per class, and feed owners: LIVE-VS-PERIODIC-FIELDS.md (T1.9). P0 booking / payment / session facts stay evented. Shipped P0 import feed: ops_support_ticket (CSV-IMPORT.md).
4.5 SDK and collector contract
- The event protocol and collector API are the stable integration contract
- Browser and backend SDKs are separate packages with separate credentials and guarantees
- SDKs never connect directly to PostgreSQL, ClickHouse, or another warehouse
- Delivery is at-least-once: stable
event_id, bounded batching/retries, and downstream deduplication - Invalid events are rejected or quarantined; schema-invalid payloads are not retried automatically
- HTTP success / partial / reject shapes, retry codes, quarantine fields, and resubmit dedup:
SDK-AND-DATA-INGESTION.md§11 (T1.13) - Full protocol:
SDK-AND-DATA-INGESTION.md
5. Non-functional constraints (MVP)
| Concern | MVP target |
|---|---|
| Delivery | Design → build → pilot in 4 months |
| Explainability | Every risk score shows reasons |
| Privacy | Internal users only; no clinical AI claims |
| Freshness | Near-daily acceptable for imports; product events preferably same-day |
| Complexity | Prefer rules + SQL over ML |
| Extensibility | Keep generic names in code (customer, provider, interaction) even if UI says client/therapist/session |
| Transaction isolation | Tracking failures never fail booking/payment/session workflows |
| Event delivery | At-least-once with idempotent event IDs; no exactly-once promise |
| Data minimization | No therapy notes, recordings, or raw sensitive answers in behavioral events |
| Observability | Accepted/rejected/dropped counts, queue depth, lag, freshness, and reconciliation are visible. Pilot numeric bar: DATA-QUALITY-BAR.md (T1.10), measured on T2.11 /dq. |
6. Suggested service split (implementation-agnostic)
- Collector API — auth, rate limits, schema/privacy validation, accepted/rejected responses
- Ingest workers — durable buffering, raw storage, external payload → canonical mapping
- Identity service/job — resolve links, emit
AUTH-04/ profile recompute - Domain API — clients, sessions, risk, insights, actions, reports
- UI app — internal authenticated dashboard
- Batch jobs — reconciliation, metrics refresh, import parsers, insight candidates
Exact stack (Next.js, Django, etc.) is an open implementation choice; this doc stays stack-neutral.
7. Open decisions to lock in Month 1
- Primary warehouse vs app DB for analytics
- Measured daily event volume, retention period, dashboard concurrency, and latency target
- PostgreSQL-only MVP vs managed ClickHouse analytical store
- Auth for internal Simiaroom users (SSO / shared IdP)
- Simiaroom implementation languages and first supported backend SDK
- Live vs periodic field class (evented / import / manual) —
LIVE-VS-PERIODIC-FIELDS.md(T1.9) - Data quality bar for pilot (completeness / lag / dup / quarantine / go-live) —
DATA-QUALITY-BAR.md(T1.10) - Event transport mix (SDK, webhook, CDC, batch dump) and producer of record per event
- Durable outbox/queue strategy for P0 authoritative events —
ops/OUTBOX-RELAY.md(T2.14). Simiaroom same-TX Postgres insert is the product-repo follow-on. - Porsline join key strategy —
PORSLINE-FORMS.md(T1.5). T2.7 importer on T2.8 audit path. T2.6 live send still open. - Gap thresholds for “dangerous pause” (days) — locked in
RISK-RULES-V1.md: expire onexpected_return_date, else 14 UTC days after pause - Risk rule weights / severity mapping — locked in
risk_v1as severity (attention/high) plus two-attention escalation; numeric weights deferred torisk_v1.1(T4.3) - Internal role × screen × data-class access matrix (support 360 minimum necessary, export rules, payment-secrets policy) —
PRIVACY-ACCESS-MATRIX.md(T1.11). Enforcement remains T5.6. - Browser consent, anonymous ID storage, and browser/collector minimization —
BROWSER-CONSENT-POLICY.md(T1.14). Implementation remains T2.13 (P1). - Security review of authz / PII / secrets / consent / audit —
SECURITY-REVIEW-T4.10.md(T4.10). Named human sign-off residual. Auth.js remains T5.5. - Reconciliation / event-loss review vs T1.10 completeness/dup —
RECONCILIATION-REVIEW-T4.12.md(T4.12). Fixture/staging window; live T4.1 still open. T2.14 outbox:ops/OUTBOX-RELAY.md. - Production data residency (parked ADR 0001; confirm before T4.1).