Lilith Metaverse · Architecture

V1 Integration & Identity Bridge

A focused page within the Lilith Metaverse Architecture documentation. The full map and every sibling page live in the Architecture hub.

6sections12 minread1diagram

On this page

V3 ("Lilith") is a metaverse, but it is not a separate product with its own login, its own wallet, and its own copy of every privacy obligation. It is a new embodiment surface bolted onto the existing V1 Oshun platform, and the defining architectural choice — the same one V2 makes about compliance — is that V3 forks none of the platform's spine. Identity, JWT issuance, OAuth client registration, the audit trail, the event bus, and the contract codegen are all shared @oshun/* packages that V1 already operates; V3 rides on them through a thin set of route surfaces and one purpose-built translation library. A V3 user is a V1 user — there is no V3-private account store — and the single seam that turns a V1 account into an embodied, avatar-bearing presence in a room is libs/v3/lilith-identity-bridge (@oshun/lilith-identity-bridge). That seam is the subject of this page, alongside the BFF / contracts / auth / audit integration points that surround it.

The grounding here is real and code-located. The bridge's binding, override, reputation, per-region, and DSAR logic live in libs/v3/lilith-identity-bridge/src/{index,per-region-rules,recording-consent}.ts; the V3 contract surface lives in libs/contracts/src/v3/; the shared identity foundation is libs/shared/identity (@oshun/identity) and libs/shared/auth-primitives (@oshun/auth-primitives), with audit and bus in libs/shared/audit-platform and libs/shared/event-bus. This page is the platform-integration companion in the "Authoring, Data, and Platform Integration" set; the section hub is ../V3_ARCHITECTURE.md.

What ships, honestly#

The identity bridge is real, substantive, server-authoritative code — not a descriptor shell. libs/v3/lilith-identity-bridge/src/index.ts (~1170 lines) implements account-avatar binding, realm display-name overrides, a reputation scoring model with graded bands, and an eight-category DSAR export, all as pure discriminated-union functions backed by the @oshun/lilith-identity-bridge automation suite (src/__tests__/index.spec.ts, recording-consent.spec.ts). per-region-rules.ts ships five real jurisdiction profiles (US-CA, DE, GB, IN, BR) with fan-token, age, content-rating, and residency evaluation; recording-consent.ts ships the class and concert recording-consent decision policy. The V3 contracts are real Zod under libs/contracts/src/v3/ and generate a real OpenAPI 3.1 document (openapi.tsbuildV3OpenApiDocument()). The shared identity, auth, audit, and event-bus packages are real V1 packages, each with their own test suites.

Three honest qualifications. First, the bridge is a pure decision kernel: it has no database, opens no socket, and does not itself verify a JWT signature. bindV3AvatarToV1Account() consumes an already-verified external OAuth account (carrying an accessTokenHash and verifiedAtIso) and returns a binding, a profile delta, an audit event, and an operator-console row — the actual JWT verification happens upstream in @oshun/auth-primitives / @oshun/identity JwtService, and the actual persistence + audit write happen at the call sites (the world server and tenant services). The library decides; the platform executes. Second, the capability descriptor's operationalMetric strings (account-avatar-link:8ms-budget, reputation-band:16ms-budget, realm-claim:24ms-budget) are descriptor labels, not measured latencies, and realm-claim has no dedicated function — it is covered by the per-region and bind paths. Third, the 24-hour avatar-swap cooldown is a contract field (AvatarBindingSchema.swapCooldownUntil) plus a server-side enforcement obligation; the bridge stamps the binding but does not run the cooldown clock. The sections below say which is which.

V1 integration: BFF, contracts, auth, audit#

V3 reaches into the V1 monorepo at four well-defined points. None of them duplicate logic V1 already owns.

BFF routes#

V3 reuses V1's apps/oshun/bff rather than standing up a second gateway, and adds a /api/v3/lilith/* route family for sessions, classes, concerts, artists, venues, commerce, studio authoring, and the operator console. Two routes are specific to the tiered client stack: /api/v3/lilith/launch is the tier-router decision endpoint (it returns a LilithLaunchDecision), and /api/v3/pxstream/match is the Pixel Streaming session matchmaker that proxies to the lilith-pxstream-relay service. The critical isolation rule is that the world server and tenant services are never exposed directly to clients: all catalog, identity, billing, and scheduling calls go through the BFF, and all real-time traffic goes through the Lilith Realtime Gateway. That keeps the account root, the consent engine, and the audit pipeline on one trusted side of the boundary, with the embodied client on the other.

Contracts#

V3-specific contracts live under libs/contracts/src/v3/ and are plain Zod, generated into OpenAPI by the same V1 codegen path. primitives.ts defines the shared axes — V3TenantSchema (lilith-platform, lilith-commons, tara-studio, saraswati-stage), V3RealmSchema, V3VisibilityBandSchema, V3CitationRef, and a V3RoyaltySplit that superRefines to exactly 10000 basis points. lilith.ts carries the identity-bearing shapes: AvatarBinding (v1UserId, avatarId, avatarVersion, swapCooldownUntil, provenanceBundleId, consentRecordId), LilithSession (a sessionToken, the v1UserId, the embedded avatarBinding, and a gatewayPin), and Presence (which carries a visibilityBand). consent.ts defines EmbodiedConsent — keyed on v1UserId, scoped per tenant/realm/feature (camera, voice, recording, voice-clone-use, motion-tracking, spatial-transcript, …) with an evidenceRef and a revokedAt. openapi.ts turns all of this into a single V3OpenApiDocument via toJSONSchema, exposing /api/v3/lilith/launch plus per-descriptor CRUD paths tagged by tenant. Contracts V3 shares with V1 — identity, billing, residency — are reused unchanged; see ../../platform/contracts.html.

Auth#

Auth roots in the platform, not in V3. The access path uses the V1 JWT primitive: @oshun/identity defines OshunTokenClaims / AccessTokenClaims (sub, sid, role, permissions) under a DEFAULT_AUTH_CONFIG of 15-minute RS256 access tokens, and @oshun/auth-primitives provides the JwtService (createRsaJwtService / createHmacJwtService), the OAuthClientRegistry, and token-refresh / revocation. The bridge declares @oshun/auth-primitives as its sole upstream package, which is the honest statement of where verification lives. The world server validates the JWT on every connect; the Realtime Gateway re-validates on the transport handshake; and UE V3OnlineServices bridges the same JWT through the platform OAuth bridge for native console / headset sign-in. This is the same identity foundation documented in ../../platform/auth-identity.html; V3 composes it. Notably, @oshun/identity already ships a v2-account-binding module — the V3 bridge is the third product to bind an embodied or entitlement-bearing context onto the one account root, following an established pattern rather than inventing one.

Audit and the event bus#

Every operator action, persona-policy decision, generation job, ticket transaction, consent change, and royalty settlement is written to @oshun/audit-platform with V3-scoped event types — the bridge emits exactly these, e.g. v3.account_avatar.linked, v3.realm_display_name.override, and v3.session_dsar.export_packaged. Because V3 reuses the V1 audit platform, it inherits V1's audit-query, DSAR, and retention tooling instead of duplicating it. Cross-shard continuity rides @oshun/event-bus (Redis Streams): presence, takedown cascades, and continuity writes into V1 domains flow over the existing bus. Two libs/v3/* adapters make the "compose, don't fork" thesis concrete — @oshun/memory-iris-spatial writes scene/pose memories straight into the V1 Iris domain (buildIrisMemoryWritePlan, tagging pose memory as biometric sensitive-category with consent records), and @oshun/sophia-saraswati-grounding grounds artist backstories through the V1 @oshun/evidence-sophia evidence engine. Both are thin real adapters over V1 engines, not reimplementations.

The identity and account bridge#

@oshun/lilith-identity-bridge is the single translation layer between a V1 account and an embodied V3 session. It resolves who you are, which avatar you wear, what name and reputation you carry into a realm, and what jurisdiction rules apply — and it does so server-authoritatively, emitting typed decisions that the world server persists.

flowchart LR v1["V1 Oshun account<br/><sub>@oshun/identity</sub>"] oauth["Headset OAuth issuers<br/>Quest · Vision Pro · PSVR 2"] jwt["JwtService verify<br/><sub>@oshun/auth-primitives</sub>"] bridge["<b>Lilith-Identity-Bridge</b><br/><sub>bindV3AvatarToV1Account()</sub>"] region["evaluateV3PerRegionRules()<br/><sub>US-CA · DE · GB · IN · BR</sub>"] out["Typed outputs"] profile["V1 profile delta<br/>v3.activeAvatarId"] audit["@oshun/audit-platform<br/>v3.account_avatar.linked"] world["World Server / BFF<br/>(persist + enforce)"] v1 --> jwt oauth --> jwt jwt --> bridge region --> bridge bridge --> out out --> profile out --> audit out --> world world --> profile

Account resolution and the OAuth bridge#

On session start a client presents a V1 JWT, which the platform verifies and resolves to a v1UserId and a residency zone. Native console / headset sign-in flows (Quest, Vision Pro, PSVR 2) arrive through the platform OAuth bridge: V3_SUPPORTED_OAUTH_BRIDGE_PLATFORMS enumerates exactly those three, and v3OAuthBridgeIssuerForPlatform() pins each to its canonical issuer (https://auth.meta.com/quest, https://appleid.apple.com/vision-pro, https://ca.account.sony.com/psvr2). bindV3AvatarToV1Account() is the entry point: it normalizes the external account, derives an externalAccountKey of the form ${platform}:${externalAccountId}, and rejects any account whose issuer does not match the bridge platform (oauth-account-not-verified) — the issuer-binding test ('rejects an OAuth account whose issuer does not match the bridge platform') guards exactly this. A successful bind upserts the linked external account and the avatar binding into the profile, returns the new externalAccountKey, and emits a v3.account_avatar.linked audit event. The same account can link Quest and Vision Pro and PSVR 2 against one V1 profile without losing the active avatar — the multi-platform test asserts that all three collapse to one account shape.

Avatar binding and the active avatar#

There is exactly one active avatar per session. bindV3AvatarToV1Account() stamps the binding with profileField: 'v3.activeAvatarId' and persistedToV1Profile: true, and v3AvatarIdVisibleInV1Profile() confirms read-back consistency: an avatar is "visible" only when it is the profile's activeAvatarId and appears as a persisted binding. The active avatar id lives on the immutable V1 profile, not a V3-private store — which is what lets a DSAR or a deletion reach it through the V1 pipeline. The anti-harassment 24-hour swap cooldown is represented in the contract (AvatarBinding.swapCooldownUntil) and enforced server-side; the bridge library produces the binding but, by design, does not itself run the cooldown timer (see "What ships, honestly"). The richer avatar mechanics — MetaHuman/VRM dual-authoring, costume rules, provenance bundles — belong to the avatar pipeline and the persona/rights layer, not to the bridge.

Display name, reputation, and presence privacy#

Three identity-adjacent decisions round out the bridge. Display name defaults to the V1 Oshun display name, but a per-realm override is permitted as an anti-stalking measure: applyV3RealmDisplayNameOverride() sanitizes the input (collapses whitespace, strips control characters, caps at 32 characters), writes a v3.realm_display_name.override entry with a synthesized v1AuditLogId, and emits a V3OperatorDisplayNameOverrideRow for the Lilith Operator Console; overriding the same realm again replaces the prior alias while other realms' aliases survive (both behaviours are tested). Reputation is never exposed as a numeric score to users: deriveV3ReputationSummary() computes a real weighted score from instructor ratings ((avg − 3) × 12 quality plus a log2 volume term), audience sentiment (positiveRatio × 24 − negativeRatio × 20 plus volume), completed safety reviews, and penalties (−18 per confirmed policy violation, −28 per confirmed harassment report), then collapses it into graded bands — new (< 45), known (≥ 45), trusted (≥ 75), and a hard flagged band whenever a confirmed harassment report exists, which also sets operatorEscalationRequired. buildV3OperatorReputationRows() sorts the moderation queue flagged-first. Presence privacy carries the user's "online in Lilith" visibility (the contract's Presence.visibilityBand, V1 default plus per-realm override) into the digest the world server broadcasts.

Per-region claims at bind time#

Jurisdiction is decided at the bridge, not improvised per feature. evaluateV3PerRegionRules() takes a profile id, tenant, age, requested content-rating, requested fan-token benefit, and storage/compute zones, and returns an allow / adapt / block decision. The five wave-1 profiles encode real policy: the US-CA profile permits a native-token fan economy and pins storage to us-west; DE/GB/IN/BR fall back to a v1-account-benefit (an adaptation, not a block) and pin to eu-de / uk / in / br respectively; age floors take the max of the account minimum and the per-tenant floor (Saraswati Stage's 16+ over a 13+ account); content above the regional rating is blocked (India caps at teen). buildV3PerRegionRulesJurisdictionReport() runs five fixtures and asserts all five profiles are covered with every dimension applied — the test confirms zero errors, and the block-path test shows underage Saraswati access, over-rating content, and residency drift each producing a block with the precise blockedReasons. The residency legality of moving data across these zones is decided upstream by @oshun/data-residency; the bridge decides which profile applies and emits the audit tags.

DSAR continuity#

A V3 user's data is part of the V1 subject record. buildV3SessionDsarExport() assembles eight categories — avatar history, voice transcripts, attendance, signed-edition holdings, recordings, Pixel Streaming sessions, consent decisions, and the ticket/refund/credit ledger — into a manifest, a V1 review action (generate_export_package), and a v3.session_dsar.export_packaged audit event, and fails loud if any category is missing (missing-v3-session-data) or an encrypted package lacks a key reference (missing-encryption-key). buildV3SessionDsarV1PipelineCoverageReport() derives a fullCoverage flag from the manifest, proving the export flowed into the V1 admin-dsar-review pipeline with every category present. Deletion cascades the same way — across Postgres rows, S3 media (recording re-render with the user redacted), and Redis hot state — because the identity binds through one root.

Edge cases and failure modes#

  • Issuer spoofing is rejected. A headset account whose issuer does not equal the canonical platform issuer returns oauth-account-not-verified; the bridge will not bind an avatar to an unverifiable external account.
  • The bridge decides, it never persists. Every public function returns a typed result (binding, profile delta, audit event, operator row); if the world server never writes it, nothing happened. There is no hidden side effect to audit around.
  • Reputation degrades to a band, never a number. A confirmed harassment report forces the flagged band and operator escalation regardless of an otherwise high score; the numeric score is internal and never surfaced to users.
  • Display names can't smuggle control characters. The override sanitizer strips control codes and caps length before anything reaches the audit log or the operator console.
  • Region adapts before it blocks. A fan-token request in a non-native-token market becomes a v1-account-benefit adaptation, not a hard failure; only age, over-rating, and residency drift produce a block.
  • DSAR refuses partial coverage. An export missing any of the eight categories is an error, not a smaller bundle — coverage is a gate, not a best-effort.

How it connects#

The bridge sits between the platform spine and the embodied runtime. The world server and gateway validates the V1 JWT on connect, persists the v3.activeAvatarId profile delta and the checkpoint/presence state the bridge stamps, and enforces the swap cooldown the bridge only records. Data architecture, tenancy, and residency consumes the residency zone and tenant context the bridge resolves, routing every durable row through @oshun/data-residency — the two pages are the identity half and the storage half of the same compliance story. The persona, tone, provenance, and rights decisions that act on a bound identity are owned by persona policy, provenance, and rights, which the bridge feeds the resolved account, reputation band, and consent context. Upstream, everything composes the platform foundations — the JWT and OAuth primitives in Auth & Identity, the shared schema codegen in Contracts, and the @oshun/* domain libraries catalogued in Oshun Domain Libraries.