ADR 0033. Partner KPI Platform: Visibility Tiers, Anonymous Comparison, Cube Behind Nest
Status
Accepted.
Extends ADR 0026 (distinct Account counts and the Account floor) from studio analytics to every partner-facing KPI. Adds HMAC request signing alongside the machine OAuth in ADR 0016, for KPI reads only. Authorization stays application-native (ADR 0029). The event log stays Phase 1 under ADR 0005.
Context
Operators, providers and affiliates each want KPIs about the players linked to them. They also want to know how those players compare with the wider PlayerPal network and with their competitors. Every portal dashboard number is mock data today, and no table records funnels, logins or data access.
Four constraints shape the design:
- Scope. A party may only see players linked to it. The existing studio
endpoints trust a caller-asserted
x-party-idbehind one shared machine key, so any key holder can claim any party. - Anonymity. Comparison figures must never let a party identify a competitor. This applies directly (names, ranks) and by inference: if a market has two operators, "network minus you" reveals the other one, and if one operator holds most of a market, a "market average" is in effect that operator's figure.
- Cost. No paid BI and no heavy new stateful platform. Metabase was rejected because it needs a JVM pod, a metadata database and, ideally, a read replica.
- Market dimension. Country is needed for comparisons, but PlayerPal never stores player PII (ADR 0012).
Decision
-
Four visibility tiers. Every KPI value belongs to exactly one tier:
- Own: the party's own players (operator: active links; provider: Accounts reached through its integrations).
- Network comparison: your players (Y) against the rest of the PlayerPal network (R = all active Accounts minus Y), expressed as per-player rates, an index and a percentile band.
- Benchmark: anonymous peers of the same party type.
- Network: staff only.
Definitions are in docs/kpis.md.
-
Scope comes from the authenticated principal, never from the request. Partner routes ignore
party_idin query strings and inx-party-idheaders. -
Anonymity rules, enforced in Nest guards at write time (suppressed segments store no values) and again at read time:
- Account floor: at least 50 distinct Accounts per cell (ADR 0026).
- Peer floor: at least 5 other parties of the requester's type in the comparison population.
- Dominance rule: suppress the segment if any single other party contributes more than 50% of the measure, or the top two more than 75%.
- Statistics only: p25, p50, p75, mean, and the requester's percentile band. Never min/max, exact rank, per-peer values or names.
- No absolute network totals for partners. Network size is shown only as a band.
- Fixed segments only: closed weeks and months, and markets from the catalogue. No arbitrary ranges or ad-hoc filters, so a party cannot difference two segments.
- A segment that fails a rule falls back from market to global, or is suppressed with a reason.
- The thresholds are configuration (
BENCHMARK_MIN_PEERS,BENCHMARK_MIN_ACCOUNTS,BENCHMARK_DOMINANCE_P,BENCHMARK_DOMINANCE_P2). Production must not set them below the values above.
-
Market comes from activity, not the player. Providers and operators report an ISO 3166-1 alpha-2
market(the licensed jurisdiction) per activity event or batch, and it is checked against the integration's declared markets. A player's market is derived per day from their activity and is never stored on the identity. -
Domain event log. Funnels, logins, linking, credential changes and KPI data access are recorded in
domain_events, an append-only table written in the same transaction as the state change. This is Phase 1 of ADR 0005. It records no PII, and Delete Account nullsplayer_idand keeps the event. -
Cube Core as an internal semantic layer behind Nest.
- Cube is an app service (
services/cube): self-hosted, Apache 2.0, a single container, with pre-aggregations stored in Postgres. - It holds the metric definitions and does the caching.
- Cube is never reachable by partners. Nest maps each public catalogue request to a fixed Cube query and signs a short-lived internal JWT that carries the principal's scope.
- As defence in depth, Cube's
queryRewritealso enforces the party filter. - Staff ad-hoc work uses Cube's SQL API over a port-forward.
- Cube is an app service (
-
Partner KPI authentication.
- OAuth 2.0 client credentials (ADR 0016) is the default.
- HMAC-SHA256 request signing is an additional scheme for partners
who cannot run OAuth.
- Per-party keys, with two active at once for rotation.
- Secrets are encrypted at rest.
- Each request carries a timestamp and a nonce, and is rejected if the timestamp is more than 5 minutes off or the nonce was seen before.
- Portal sessions cover the browser.
- All three resolve to one principal (party, party type, client, scopes) checked by the same guard.
Consequences
- The studio analytics endpoints take the provider from the partner
principal. The legacy shared key with
x-party-idstays behindSTUDIO_LEGACY_PARTY_HEADER(default on, so local demos keep working) and must be set tofalsein production. The response shape is unchanged. - Comparisons can be suppressed in thin markets. Early on, with only a handful of operators per market, partners will mostly see global figures. This is intended.
- KPI definitions live in
services/cube/modeland are tested against hand-written SQL.docs/kpis.mdmust be kept in sync with them. - Network comparisons and peer benchmarks are computed by a job in the API
process, directly over the same
analyticsviews the Cube model uses, because per-player distributions and per-requester populations are not a good fit for Cube. The anonymity rules live in one pure module (analytics/compare/comparison.math.ts) and are applied before anything is stored. - One more service to run (Cube). If volume grows, Cube Store or ClickHouse can sit behind Cube without changing the public API.
- HMAC adds a secret type that must be decrypted to verify, so a master-key rotation procedure is needed before production.
- Glossary additions: Visibility tier, Network comparison, Market. Day-to-day reference: docs/kpis.md.