Portals
Each audience has its own URL, OAuth client, redirect URIs, session cookie and permission policy. The user never picks a role inside the product.
Source: conversation export §14 and spec §7 / §14. Decision: ADR 0018.
Audiences
| Portal | Intended host (example) | Audience | Authentication |
|---|---|---|---|
| Player | my.playerpal.io | Players | Passkey, approved social, provisional PlayerPal ID/password (A0), recovery |
| Staff | admin.playerpal.io | PlayerPal workforce | Enterprise SSO, MFA, privileged-access controls |
| Operator | partners.playerpal.io/operator | Operator workforce | Enterprise OIDC/SAML |
| Provider | partners.playerpal.io/provider | Provider workforce | Enterprise OIDC/SAML |
| Affiliate | partners.playerpal.io/affiliate | Affiliate workforce | Enterprise SSO or approved passkey |
Opening the wrong portal returns "you don't have access to this workspace", not a role switcher. One person may hold several roles; they enter each through its URL and authorization context.
UI navigation is not an authorization boundary. APIs enforce tenant, audience, role and resource policies (see authorization.md).
What each audience may see
| Actor | Player identifier visible | Player email stored? |
|---|---|---|
| Player | Private pp_plr_... | No |
| Operator | pp_plr_opr_... only | No |
| Provider | pp_plr_prv_... only | No |
| Affiliate | Attributed or aggregate context | No |
| PlayerPal staff | Internal subject only where authorised | No player email |
Demo mapping
Local demos use host routing (ADR 0018), not path roles:
| Host | Audience |
|---|---|
player.localhost:8080 | Player (real Nest + ZITADEL auth) |
staff.localhost:8080 | Staff (simulated sign-in) |
operator.localhost:8080 | Operator |
provider.localhost:8080 | Provider |
affiliate.localhost:8080 | Affiliate |
portals.localhost:8080 / localhost:5173 | Launcher |
Path bypass (/player, …) remains for Vite direct mode. Presentation apps in demos/. HTML prototypes under spec/prototype/. Simulated sign-in outside the player portal is not production authentication.
Analytics (partner KPIs)
The operator, provider and staff portals have an Analytics screen, and the operator and provider Overviews show live tiles. The screen has three tabs:
- Your performance (Own tier)
- You vs PlayerPal network (anonymous comparison)
- Peer benchmarks
The rules are in kpis.md and ADR 0033. The partner portals still use simulated sign-in, so locally they read a seeded demo party through the API's /demo/kpis/* helper (DEMO_PARTNER_HELPER=1, demo parties only). They never use a partner credential in the browser. Running it locally needs Cube (task cube:db-role && task cube:up), CUBE_API_URL and CUBEJS_API_SECRET on the API. With only a handful of local parties, comparisons stay suppressed unless the API also sets BENCHMARK_ALLOW_LOW_THRESHOLDS=1 with lower BENCHMARK_* values.
Charts use Apache ECharts 5.6.0 (modular build, SVG renderer). The library is lazy-loaded, so the player portal never downloads it. One shared theme (components/kpi/echarts.ts) encodes the dataviz specs and the series colour #1b9bc8, which passes the validator on the portal surface. Every chart has a crosshair or item tooltip, arrow-key reading and a table view. Stat tiles and layout stay plain HTML.
Partner overview (Spec v0.1 §13)
The operator, studio and affiliate portals open on a registry-driven overview (components/kpi/overview/):
- two KPI rows
- activity over time
- partner mix
- campaigns (affiliates)
- geo mix and player segments
- rewards redeemed, with redemption and mission performance
- retention by cohort and missions
Every value comes from GET /v1/kpis/overview, and components never compute KPIs. A KPI whose feed hasn't delivered shows the API's requirement text (e.g. "Requires operator commercial feed"), never a zero. KPIs that don't apply to an audience aren't rendered. The affiliate portal shows PrimeWins (pp_aff_SIMPRIMEWINS) from the simulated network.
Multi-series charts use the validated 4-slot categorical palette in echarts.ts (dataviz reference dark slots, checked on the portal surface). Anything beyond four slices goes into a neutral "Other".