V5 is an open-world narrative universe split into cells — an Urban heist city, a
three-era Period crime saga, a Frontier outlaw trail, a Hunter's
monster-contract world, a Sci-Fi galactic theatre, and the cross-cutting Mind
Palace deduction layer. What makes that a universe rather than six unrelated
games is identity-shaped, and the shape is the Bureau: one canonical
account, one Bureau XP ledger that every cell feeds, one Mind Palace graph, one
cosmetic inventory — the same player record on PC, PlayStation, Xbox, Switch,
and iOS, surviving a platform switch without losing a clue or a cosmetic. This
page is about the machinery that keeps that promise: persistence (the
V5Core encrypted save substrate and the V5Persistence ledgers, replays, and
New Game+ migration that ride on it), the online backbone (the 16-service
V5OnlineServices client catalog, its real HTTP transport, and the
@v5/service-shared NestJS tier under apps/v5 that backs it), and
cross-play / cross-progression (the policy that decides who can match whom
and what travels with an account). The deterministic combat the session services
wrap lives one layer down in
Netcode, Authority & Determinism; the
seasonal cadence layered on top is in
Live Service & DLC. The section hub is
../V5_ARCHITECTURE.md.
What ships, honestly#
The split is clean, and stating it up front keeps the rest of the page honest.
The client tier is real Unreal C++. V5/ue/Source/V5OnlineServices ships a
16-endpoint service catalog (UV5_Online_ServiceCatalog::BuildEndpointCatalog)
fronted by a family of UBlueprintFunctionLibrary request builders
(UV5_Online_AuthService, …MatchmakingService, …AntiCheatClient, and the
rest) over a 1,200-line typed surface (V5OnlineServicesTypes.h). Crucially,
the transport underneath is genuine network I/O: V5OnlineServices.Build.cs
declares the HTTP and Json modules, and FV5OnlineHttp turns a request
value into a real IHttpRequest from FHttpModule — the comment in
V5OnlineHttpClient.cpp records this as the module's "first real network I/O,
replacing the prior 'build a POD struct, never transmit' path that the
2026-05-31 audit flagged." Authentication is real cryptography, not a token
template: FV5JwtCrypto is a self-contained FIPS 180-4 SHA-256, RFC 2104
HMAC-SHA256, RFC 7519 HS256 JWT, and RFC 7636 PKCE S256 implementation, unit
-tested against published vectors.
The service tier is real TypeScript + NestJS. apps/v5 holds 16 service
packages (each with a contract.json, Dockerfile, and k8s/ manifests), and
every one delegates its handle() to handleRealRequest in
@v5/service-shared, which resolves live pg.Pool + ioredis clients and
dispatches to a domain that is not CRUD: a Glicko-2 rating pipeline, an OAuth
refresh-rotation flow with reuse detection, a hash-chained balance ledger, a
CRDT Mind Palace merge. Each domain carries #[cfg(test)]-equivalent Vitest
cases asserting computed answers (Glickman's worked Glicko-2 example, a revoked
token family, a verified ledger chain).
Three honest labels travel with the rest of the page:
- The save cipher is real but not the prose's cipher.
UV5_SaveGameSubsystemgenuinely encrypts every save with a per-account, per-platform-derived key and validates integrity on load — but it is a SHA-1 keystream XOR, not the AES the design notes imply, and the "Oodle Kraken compression" the architecture names is not in the shipped code. Encryption and the integrity check are real; compression is spec. - There are two backend code paths, and the live one is real.
evaluateServiceSpecificRulestill returns the oldjwt.v5.<account>.oauthfixture string for contract tests — but the deployedservice.handle()callshandleRealRequest→executeRealServiceRequest, the Postgres/Redis-backed path. The fixture path is for golden contracts; the real path is what runs. - Kernel anti-cheat is a planned client tier. The client builds EAC signal requests and the server keeps a real strike journal, but the EAC kernel driver itself is named, not in-tree — exactly as V4 and V2 label it.
The online backbone#
The service catalog and the HTTP seam#
Gameplay code never hand-rolls a URL. BuildEndpointCatalog enumerates all 16
services — auth, friends, parties, matchmaking, leaderboards, replays,
telemetry, anti-cheat, crash-reporting, companion-app-bridge,
mindpalace-cloud-sync, workshop, compliance-dsar, live-service-calendar,
balance-ledger, faction-rep — each as an FV5OnlineEndpointDefinition carrying
its verb, path, bRequiresJwt, and bSupportsOfflineQueue flags. A
UV5_Online_* builder fills an FV5OnlineServiceRequest (account, JWT, region,
JSON body, per-cell path slug), and FV5OnlineHttp::BuildHttpRequest constructs
the real IHttpRequest: it sets the verb, an Authorization: Bearer header
when a JWT is present, X-V5-Account and X-V5-Region routing headers, and the
JSON body for non-GET methods. Base URL resolution is fail-soft and never
fabricated — V5_ONLINE_BASE_URL is treated as an API gateway, else
V5_ONLINE_HOST plus the service's catalog port (4210 + service index).
The latent node UV5_Online_HttpRequest::Activate is where the fail-loud
discipline shows: a request whose endpoint bRequiresJwt but whose token is
empty short-circuits to a 401 before touching the network
(auth.jwt.required), a transport failure yields TransportFailureResponse
(service.unreachable, status 0), and a 503 sets bQueuedOffline from the
endpoint's offline-queue flag. No path fabricates a success it did not receive.
Identity, JWT, and PKCE#
UV5_Online_AuthService::IssueOAuthJwt mints a genuine HS256 token. It
assembles RFC 7519 registered claims (iss, sub, aud, provider, iat,
nbf, exp, jti) with a 3,600-second TTL, derives the jti from
SHA-256(account|provider| iat) so each issuance is unique yet deterministic,
signs with FV5JwtCrypto::IssueHs256, and derives an opaque refresh token
as HMAC(SHA-256(key.refresh), SHA-256(accessJwt)) — revocable server-side and
not user-forgeable. VerifyAccessJwt does the real inverse: a constant-time
signature comparison plus exp/nbf enforcement. The tests prove behaviour,
not shape: an access token issued at t=1000 is a 3-part compact JWT, verifies
at t=1000, is rejected at t=4600 (past its exp), and a tampered
signature fails. VerifyPkceChallenge enforces RFC 7636 §4.1 length bounds
(43–128) and checks BASE64URL(SHA256(verifier)) == challenge.
Server-side, AuthDomain (domain/auth.ts) is Postgres-backed and stricter
still. resolveCanonicalAccount enforces oldest-verified-credential-wins:
the first verified (platform, externalSubject) pair binds a canonical
v5acct.… id, and later exchanges for the same pair return that id — one human
cannot fork into multiple V5 accounts per platform. refreshAccessToken
implements RFC 6749 §6 rotation with RFC 6819 reuse detection: presenting an
already-rotated token sets every row in the family to revoked = TRUE and
returns reuse_detected. Refresh tokens are stored only as SHA-256 hashes,
never cleartext. The Vitest suite asserts a stable canonical id across
exchanges, a pkce_failed on a bad verifier, and a family-wide revocation on
replay.
Matchmaking and the skill model#
The client builders are deliberately thin — BuildQueueRequest and
BuildCrossplayQueueRequest carry mode, region, integer SkillRating, ping,
and (for cross-play) Platform, ModeId, and bCrossplayOptOut to a per-cell
/v5/matchmaking/{cell}/queue path. The authority is server-side and genuine.
MatchmakingDomain (domain/matchmaking.ts) runs a real Glicko-2 update
(Glickman 2013): glicko2Update computes the g(φ) and expected-score terms,
then solves the volatility step with an Illinois (regula falsi) root finder
— and the test pins it to the canonical worked example,
toBeCloseTo(1464.06, 1) / toBeCloseTo(151.52, 1) / toBeCloseTo(0.05999, 4)
for a 1500/200/0.06 player against three opponents. The queue is a Redis
ZSET banded by cell:region:mode:skillBand:pingBand (200-wide skill bands,
three ping bands), ordered by enqueue time; formMatch uses an atomic
ZPOPMIN so two formation passes never claim the same player, and re-enqueues
what it took if a rare race leaves it short.
Anti-cheat and integrity#
Anti-cheat is defended in depth and labelled by tier. The client
UV5_Online_AntiCheatClient builds eight distinct, dedicated-endpoint requests
— EAC session signal (requiresEAC: true), aim plausibility, sub-tick aimbot
snap, line-of-sight wall-hack history, position-delta speed-hack, auto-fire
interval pattern, a weighted classifier carrying modelId: V5_AntiCheat_ML, and
an appeal routed to /companion/support/anti-cheat-appeal. Server-side,
AntiCheatStore (domain/anti-cheat.ts) makes this stateful: recordSignal
journals each flagged detector to Postgres and recomputes the account's running
strike count over verdicts ≥ strike, driving the three-strike ladder
(warning → ranked-suspension → ban-review); fileAppeal queues a
queued-human-review row on the companion-app path. The governing rule matches
the platform posture: model output prioritises a review queue, it never
silently disciplines.
The diagram's last edge is the most important honesty seam.
resolveBackingClients hands a service live clients only when its catalog-
declared V5_POSTGRES_URL / V5_REDIS_URL resolve to non-empty values;
otherwise it returns configured: false, and executeRealServiceRequest serves
the 503 outage contract. buildHealth reports not_configured in that case
rather than the old hardcoded serviceHealthy: true that made the outage branch
dead code. Absence is reported loudly, never faked.
Persistence and save#
The save model and the encrypted envelope#
UV5_SaveGame (in V5Core) is the model behind every cell's save: a
USaveGame at CurrentSaveVersion = 2 (down to
MinimumSupportedSaveVersion = 1) whose FV5SaveDocument namespaces an
FV5CellSaveSlot per cell — Urban, Period, Frontier, Hunter, Sci-Fi, Steampunk,
and Mind Palace — alongside the account id, active cell, completed-cells set,
and the shared Bureau XP ledger. ImportFromJsonString runs
MigrateLoadedDocument, so a schema bump is a versioned migration, not an
ad-hoc rewrite.
Durability is real. UV5_SaveGameSubsystem::SaveProfile serialises to JSON,
then EncryptJsonPayload builds an FV5EncryptedSaveEnvelope: a GUID nonce, a
plaintext SHA-1 for integrity, a KeyId, and a hex cipher produced by
ApplyKeyStream XOR-ing the bytes against a counter-mode SHA-1 keystream. The
key material is per-account and per-platform — BuildPlatformKeyMaterial
derives it from the IOnlineSubsystem identity (the unique net id, falling back
to a hashed auth token, falling back to a local device id) prefixed by the
account. On load, DecryptEnvelopeBytes rejects an envelope whose account/slot,
KeyId, or recomputed plaintext SHA-1 does not match — a save encrypted under a
different platform key fails loudly rather than loading garbage.
RotateGenerations keeps the triple-redundant
Current → Previous → Fallback chain the architecture specifies, and
LoadProfile walks the three in order, auto-promoting a recovered older
generation back to current. Cloud sync is a real
IOnlineUserCloud::WriteUserFile, and it fails loud — "platform cloud
unavailable: no online subsystem" — when no platform cloud is registered.
The currency ledger and Bureau XP#
UV5_Persist_CurrencyLedger is an audit-trailed, per-cell ledger.
BuildStartingLedger seeds each cell with its native currency — Urban/Period
Cash, Frontier Gold, Hunter Crowns, Sci-Fi Credits, Mind Palace Bureau Scrip —
and ApplyTransaction mints a deterministic TransactionId, rejects any
transaction that would drive a balance negative, updates lifetime
earned/spent, and appends every movement (accepted or not) to a Journal so the
balance is reconstructable. The connective progression record is
FV5BureauXPLedger: a TotalXP, a per-cell XP map, and a tier computed
monotonically as CurrentTier = max(1, TotalXP / 2500 + 1) — which is exactly
why New Game+ gates on tier 20 (47,500 XP). This is the single object
cross-progression syncs as the account's shared spine.
Replays, New Game+, and save archaeology#
UV5_Persist_ReplaySerialization builds a versioned deterministic replay packet
whose exact binary canonical form binds replay/account/key identifiers, issuance
and expiry, a base64url nonce, every frame field, IEEE float bits, event kind,
and ordered tags. The packet carries a SHA-256 payload digest and HMAC-SHA256
signature; verification uses the shared V5IntegrityCore and constant-time
digest comparison. UV5ReplayIntegritySubsystem fails closed unless an external
key-custody resolver and an atomic durable nonce consumer are configured, clears
transient key bytes after use, rejects future/expired packets and consumed
nonces, and retains bounded receipt-linked audit events. This makes the
in-memory nonce window defense-in-depth rather than the durable source of truth.
UV5_Persist_NewGamePlus encodes the Year-1 NG+ plan as data and validates it
in code: five launch cells, ≥ 2 story branches and ≥ 3 difficulty modifiers each
(≥ 10 and ≥ 15 totals), a strict carry-over set (Bureau XP, cosmetics, codex,
photo mode, Mind Palace archive) against a reset set (critical-path state,
economy, wanted heat, bounty debt), and bNoPaidPowerCarryover so purchased
power never crosses a reseed. UV5_Persist_SaveSlotArchaeology imports an old
v1/v2 save, reads its ending.* flags, and triggers the matching NG+ branch —
ten authored scenarios, each verified to read an ending flag and target an
ngplus.* branch — so a finished campaign measurably reshapes the replay.
Cross-play & cross-progression#
Cross-play decides who matches whom; cross-progression decides what travels.
Both are policy-rich, and the policy is a checked-in artifact:
V5/crossplay/crossplay-progression-policy.json. It names a single canonical
account (idPrefix: v5acct, singleAccountIdRequired: true,
conflictPolicy: oldest_verified_credential_wins) with nine platform
credentials — Steam on Windows/Linux, Apple on Mac/iOS, PSN on PS5/PS5 Pro, Xbox
Live on XSX/XSS, Nintendo on Switch 2 — each with its credential type and
required OAuth scopes. That policy is not prose:
AuthDomain.resolveCanonicalAccount enforces the oldest-credential rule
server-side, and the client mirror BuildPlatformCredentialLinkRequest binds an
external subject to the canonical v5acct.… id.
Four progression domains carry explicit, service-owned merge policies:
profile (auth, last-verified-write-wins), cosmetics (companion-app-bridge,
entitlement union), bureauXp (companion-app-bridge, monotonic ledger sum), and
mindPalace (mindpalace-cloud-sync, node last-writer-wins with accusations
append-only). The last is backed by real CRDT code: mindpalace-cloud-sync.ts
merges evidence nodes by version-vector causal dominance with a deterministic
(updatedUnix, lastWriter) tiebreak, and unions accusations — commutative and
idempotent, so re-syncing the same diff is a no-op. Cross-play is on by
default across all nine platforms; the single per-cell opt-out is Sci-Fi
spaceship PvP (mode.scifi.spaceship-pvp), and the runtime honours it precisely
— an opted-out ticket routes to a platform-only pool, every other mode stays
crossplay, gated for input parity rather than blanket console segregation.
Companion save transfer is a signed, encrypted-at-rest bundle
(v5-save-transfer-bundle-v1) with a
newer_slot_version_wins_with_manual_review import policy, exercised by five
named QA scenarios.
Where this connects#
- Down to the match: Netcode, Authority & Determinism — the deterministic sim the replay-determinism digest and server-validated score writes depend on, and the dedicated-server authority the cross-cell world runs under.
- Sideways to live service: Live Service & DLC — the seasonal calendar, persistent world events, workshop marketplace, and faction-rep decay that ride this same backbone and ledgers.
- Platform foundations: Persistence & Data — the Postgres / Redis / object-storage substrate these services are specified to deploy onto, and the account-root and data-residency posture they inherit.