ADR 0145 — PostHog Topology, Taxonomy, and Privacy Posture for Raxx
Status: Proposed — pending operator review of the benefit matrix
Date: 2026-07-22 UTC
Deciders: software-architect (design), operator (review gate before sub-cards are filed)
Scope: getraxx (Vite), Antlers/raxx-next (Next.js App Router, CF Pages edge runtime), Raptor (Flask), iOS (Swift), console (out of scope for Phase 1)
Context
Three prior, independent efforts (#491/#482 demo funnel, #219/#2124 getraxx landing, #3899 Antlers session-replay+funnel) already wired partial PostHog instrumentation against no live PostHog project, using two different event-naming conventions, and with one flag (posthog_session_replay) bundling funnel-event capture together with full session replay. No topology, taxonomy, or privacy posture had been decided before code shipped. This ADR records the decisions needed to unify them: full rationale, matrix, and taxonomy live in posthog-raxx-adoption.md.
Decision
Adopt one PostHog project (raxx) in the existing MooseQuest LLC org, with environment and app as event properties rather than per-environment projects. Standardize on flat snake_case, <object>_<verb_past_tense> event names, rejecting the dot-namespaced convention used only in the unmerged #219 branch. Split the posthog_session_replay flag into an events-only flag (posthog_events_enabled, Phase 1) and a retained replay-only flag (posthog_session_replay, Phase 3, gated further on a masking inventory). Billing/trading/auth-adjacent events are server-captured from Raptor (posthog-python); UI-engagement events are client-captured from whichever surface renders them. distinct_id is the internal user UUID post-signup, never email — set via a client-side posthog.identify(userId) call at signup completion (§5 of the design doc), which is what merges the pre-signup anonymous session into that id; Raptor's server-side identify() sets person properties but cannot perform this merge (it never has access to the browser's anonymous distinct_id).
Language choice rationale
Not applicable — PostHog is consumed as hosted SaaS via SDKs (posthog-js, posthog-python, posthog-ios). No new service, process, or daemon is introduced.
Consequences
Positive
- One project means one
distinct_idspace — the waitlist→signup→passkey→paper-trade→structure funnel can actually be queried end-to-end. - Splitting the flag unblocks Phase-1 event capture without forcing a session-replay decision.
- A single naming convention keeps future taxonomy (experiments, cohorts) queryable without a cleanup pass.
- Server-side capture for state-changing events means PostHog data can't be spoofed or dropped by an ad-blocked/compromised client, and lines up with the existing
audit_log/workflow_idmodel.
Negative / risks
- Single project means a misconfigured event filter can leak staging/CI noise into a prod dashboard until the default filter is applied everywhere — mitigated by making
environment = prodthe default saved-insight filter, not an afterthought. - Renaming the six
landing.*events requires coordinating with whoever lands#219— a small one-time merge-conflict risk, not a design risk. - Edge-runtime constraint (§SDK placement in the design doc) means Antlers cannot use
posthog-nodeserver-side; any future Antlers-originated server-truth event must either round-trip through Raptor or use a rawfetch()capture call — a real, documented limitation, not swept under the rug.
Neutral
- No Raptor/Postgres schema changes; this is entirely an SDK/config/flag decision.
Alternatives considered
Alternative A — Separate raxx-prod / raxx-staging projects
Rejected because it doubles API-key rotation and taxonomy upkeep, and breaks the single-distinct_id funnel query for the sake of an isolation guarantee a saved filter already provides.
Alternative B — Keep dot-namespaced (category:action) event names org-wide
Rejected because the majority of already-shipped instrumentation (demo funnel, Antlers funnel, waitlist_signup) already uses flat snake_case; standardizing on the minority convention would mean renaming more code, not less.
Alternative C — Client-capture everything, including billing/trading state changes
Rejected — a client can be ad-blocked, offline, or compromised; billing/trading/auth state changes already have a server-side audited source of truth (audit_log), and PostHog mirroring should read from that source, not duplicate trust in the client.
Security / GDPR checklist
- PII collected: coarse UI-engagement signals + typed state-change mirrors; no email, no combined symbol+quantity+dollar, no broker names, no credentials.
- Retention period: PostHog org-level retention configured to match Raxx's GDPR retention policy (sub-card in Phase 1).
- Deletion on DSR: PostHog person-delete API call keyed on
internal_user_uuid, added to the existing DSR fulfillment path. Implemented inbackend_v2/api/services/posthog_dsr_service.py(card #4312 follow-up), wired intobilling_dsr_service.execute_dsr()alongside the idempotency_keys erasure sweep, raising (not swallowing) on any non-2xx per the risk note below. Implementation correction (2026-07-24): PostHog's personsbulk_deleteREST Data API endpoint requires a Personal API Key (person:writescope, bound to a PostHog user account) plus the numeric project ID — a genuinely different credential class from the single project/capture key described in "Secrets location + rotation" below, which cannot authenticate this endpoint. Seedocs/ops/vault-token-taxonomy.mdPostHog section (PH_PERSONAL_API_KEY_RAXX, not yet provisioned) for the vault path and provisioning status. - Audit trail: PostHog capture is additive telemetry only;
audit_logand Workflow-UUID tracing remain the system of record for state changes. - Stored credentials: none — WebAuthn credentials, challenges, and broker tokens are never DOM-renderable and never captured.
- Breach notification path: PostHog is a sub-processor; a breach triggers the existing vendor-breach notification process.
- Secrets location + rotation: Infisical vault (
/MooseQuest/posthog/,PH_PROJECT_API_KEY_RAXX). PostHog issues one project API key for event capture, used identically by client SDKs (posthog-js, CF Pages/iOS build env) and server SDKs (posthog-python,heroku config:set) — there is no separate secret-class server-only capture key in PostHog's key model. Rotatable without redeploy from the single vault source. - Kill-switch: both
posthog_events_enabledandposthog_session_replaydefault OFF and are single-flag kill switches with no redeploy required.
Revisit when
- The cookie-consent banner (#3215/epic #3126) ships — triggers the raxx.app legal-basis switch and unblocks the Phase-3 replay masking-inventory gate.
- Weekly-active volume crosses the experiments trigger threshold (proposed ≥500/surface, sustained 2 weeks — operator to confirm).
- A concrete BI need emerges that would justify the data-warehouse product.
- Console operator-usage analytics becomes a real ask (currently out of scope).