# V-Series Platform Consolidation Plan — Shared Online Services & Cross-Game Identity

Created: 2026-06-12 (per `V1_V7_PLAN_SET_AUDIT_2026-06-12.md` §6.1 items 2–3)
Status: Proposed Companion: `V_SERIES.md` (portfolio map)

## Problem 1 — Five parallel online-services stacks

Every game version specs its own auth/matchmaking/leaderboards/anti-cheat/
telemetry layer, in a different language and framework:

| Ver | Stack                                   | Count                      | Where          |
| --- | --------------------------------------- | -------------------------- | -------------- |
| V2  | TypeScript Nx adapter libs              | 50                         | `V2/services/` |
| V4  | Rust + Axum microservices               | 25 specced (13 scaffolded) | `V4/services/` |
| V5  | NestJS services over one shared runtime | 16                         | `V5/services/` |
| V6  | Rust services (world/gateway/Ori)       | 9                          | `apps/v6/`     |
| V7  | Rust/TS services                        | 8                          | `apps/v7/`     |

All five doc sets simultaneously preach "layer on V1, don't fork." The
duplicated surface — session auth, parties/friends, matchmaking, leaderboards,
telemetry ingest, anti-cheat signal collection, replay storage — is the single
most repeated engineering plan in the portfolio, while the genuinely
game-specific parts (V2 rollback transport, V4 lockstep, V6 agent cognition, V7
realm meshing) are _not_ shareable and rightly live per-version.

### Decision

Extract a **shared game-platform service tier** ("GPS") with V7's hybrid trust
model as the design anchor (platform-inviolable identity/economy/safety;
game-specific logic stays per-title):

- **GPS-owned (one implementation, multi-tenant by `gameId`):** player session
  auth (on V1 identity), friends/parties/presence, matchmaking _orchestration_
  (per-game rule plugins), leaderboards/seasons, telemetry ingest + schema
  registry, anti-cheat signal bus + case management (per-game detectors plug
  in), replay blob storage + metadata, entitlements (see Problem 2), and
  creator-payout rails (Abundantia generalized).
- **Per-game (never GPS):** netcode/simulation, game servers, world/realm state
  (Nephthys stays V7's), gameplay services (V6 Ori cognition tiers), and any
  latency-critical in-match path.
- **Language ruling:** GPS services follow the repo's stack policy — Rust for
  the hot paths (telemetry ingest, presence fan-out), TypeScript/Nx for
  control-plane CRUD. V5's NestJS tier and V2's adapter libs migrate by
  consumption, not rewrite: new GPS endpoints are stood up first, then each
  game's existing service spec is re-pointed and its local implementation
  retired section by section.

### Sequencing (relative milestones, no calendar commitment)

1. **M1 — contracts:** define `libs/contracts/gps/` (OpenAPI + event schemas)
   for the nine GPS domains above; each game version maps its existing spec to
   the contract and records deltas as ADRs in its own `docs/decisions/`.
2. **M2 — first consumer:** V7 (earliest-stage implementation, strongest
   trust-model fit) builds against GPS natively instead of its own §-tier
   service stubs; V6's gateway adopts GPS presence/telemetry.
3. **M3 — brownfield adoption:** V5 re-points its 14 wrapper services (only
   anti-cheat + compliance-dsar hold real logic today — those migrate as GPS
   plugins); V4's 12 `/health`-only routers are implemented once, as GPS, not
   re-scaffolded per game; V2's adapter libs consume GPS.
4. **Exit criterion:** no new per-game implementation of any GPS-owned domain is
   accepted; per-game TODOS sections for those domains point at GPS contract
   versions.

### Risks

- Multi-tenant blast radius (one GPS outage hits every title) → per-game cell
  isolation + per-tenant rate isolation are M1 contract requirements.
- Premature abstraction (only V3 is near-operational today) → GPS extracts only
  domains already specced ≥3 times (the table above), nothing speculative.

## Problem 2 — No cross-game account/entitlement story

V4 claims Iris-based identity reuse; V6's Ori carries _agents_ across realms; V7
extends the Ori to carry _player characters_; V2 specs cross-product
entitlements ("Lilith meditation-streak unlocks a V2 card border"). No document
defines the player's single account, wallet, and entitlement graph across V2–V7.

### Decision

One **Oshun player account** (V1 identity, already the auth root everywhere)
with three attached graphs:

1. **Profile graph** — display identity, age/region attributes (single source
   for every game's age gate), platform links (PSN/Xbox/Steam/Apple/Google),
   block/mute lists shared portfolio-wide (a block in one title blocks in all —
   safety floor, per V7's platform-inviolable rule).
2. **Entitlement graph** — purchases, unlocks, cross-product grants. Schema:
   `entitlement(id, ownerAccount, grantingProduct, consumingProduct, kind, sourceRef, platformConstraints, revocation)`.
   Platform-store constraints (an iOS-purchased item's visibility on PSN,
   cert-ban interactions per `V2/legal/platform-cert-bans.json`) are first-class
   fields, not afterthoughts. Wallet: Aje rails for crypto where certified;
   platform stores elsewhere; **one ledger** regardless of rail.
3. **Character/being graph** — the Ori event store, with V6 agents and V7 player
   characters as distinct subject types (schema-evolution rules in
   `V6/docs/ori-schema-evolution.md`). Game saves stay per-title; only portable
   identity state crosses titles.

### Rules

- A game may **read** cross-game attributes only through scoped, consented
  queries (V1 Iris consent model governs); no bulk profile export to game
  services.
- Cross-product grants (the V2↔Lilith example) are declared in the granting
  product's catalog and validated against the consuming product's cert-ban
  registry at publish time, not at runtime.
- Deletion: account erasure fans out to every attached graph with per-product
  attestation (extends V1's DSAR pipeline; V3 already routes through it — the
  same cascade covers entitlements and Ori subjects, with V7 realm-side deletion
  handled per `V7/docs/operator-data-protection.md`).

### Open items (tracked, not silently deferred)

Rewritten on 2026-09-18: the first and third lines were statements, not tasks.
The contracts library keeps each domain under `libs/contracts/src/<domain>/`, so
the GPS contracts go to `libs/contracts/src/gps/`, not the `libs/contracts/gps/`
path M1 names.

- [ ] **GPS.M1** Start milestone M1: create `libs/contracts/src/gps/` with zod
      schemas and the OpenAPI document for the nine GPS-owned domains listed
      under "Decision", exported as `@oshun/contracts/gps`, each schema carrying
      `gameId` as the tenant key and the per-game cell and rate-isolation fields
      the "Risks" section requires. One child per domain so each can land alone.
  - [ ] **GPS.M1.a** Player session auth on V1 identity (session, platform
        links, portfolio-wide block and mute lists). **Verify:**
        `gps/session.spec.ts` round-trips valid and invalid fixtures; a block
        recorded for one `gameId` is visible to a query for another.
  - [ ] **GPS.M1.b** Friends, parties and presence. **Verify:**
        `gps/social.spec.ts`: a friend edge is symmetric and scoped to the
        account, not the `gameId`; a party carries one `gameId` and refuses a
        member blocked by any other member; a presence record without an expiry
        is refused.
  - [ ] **GPS.M1.c** Matchmaking orchestration with a per-game rule-plugin
        contract. **Verify:** spec with two plugins that disagree.
  - [ ] **GPS.M1.d** Leaderboards and seasons. **Verify:**
        `gps/leaderboards.spec.ts`: a score is keyed by `gameId`, board, season
        and account; a submission to a closed season is refused; two games'
        boards with the same name do not collide.
  - [ ] **GPS.M1.e** Telemetry ingest and its schema registry. **Verify:** spec
        that an unregistered event type is refused.
  - [ ] **GPS.M1.f** Anti-cheat signal bus and case management, with the
        per-game detector plug-in contract. **Verify:** `gps/anticheat.spec.ts`:
        a signal names its detector, its version and its `gameId`; a case cannot
        close without an actor and a reason; a detector plug-in that declares no
        signal types is refused.
  - [ ] **GPS.M1.g** Replay blob metadata. **Verify:** `gps/replays.spec.ts`: a
        record carries the content hash, the byte length, the game build it was
        recorded on and its retention class; a record with no hash is refused.
  - [ ] **GPS.M1.h** Entitlements:
        `entitlement(id, ownerAccount,     grantingProduct, consumingProduct, kind, sourceRef, platformConstraints,     revocation)`
        with platform-store constraints as first-class fields, validated against
        `V2/legal/platform-cert-bans.json` at publish time. **Verify:** spec
        that a grant violating a cert ban is refused when it is declared, not at
        runtime.
  - [ ] **GPS.M1.i** Creator-payout rails generalised from Abundantia.
        **Verify:** `gps/payouts.spec.ts`: amounts are integer minor units with
        a currency; a payout names its rail, its run and the ledger entries it
        settles; the creator floor Abundantia enforces (70%) is a field the
        schema requires, not a default.
  - [ ] **GPS.M1.j** Each game version maps its existing service spec to these
        contracts and records the deltas as ADRs in its own `docs/decisions/`
        (V2, V4, V5, V6, V7). **Verify:** one ADR per version; no domain left
        unmapped.
- [ ] **GPS.CERT** Review the entitlement schema against each platform's
      cross-buy policy before any cross-product grant ships. _Board tag
      2026-09-18: a review of store policies by a person with access to the
      partner agreements; an agent prepares the policy-by-field table from the
      public documents._ `blocked:governance`
- [ ] **GPS.ORI** Add the Ori subject-type discriminator (V6 agent versus V7
      player character) to V6's event schema under the evolution rules of
      `V6/docs/ori-schema-evolution.md`, before V7 writes player characters into
      the store. **Verify:** a schema-evolution spec reads events written before
      the change, and an event without a subject type is refused after it.
