<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged webrx</title>
<link>https://coderlegion.com/tag/webrx</link>
<description>Powered by Question2Answer</description>
<item>
<title>How We Secured the MyZubster Metaverse: Session Ownership, Verified Explorers and the Road to Realti</title>
<link>https://coderlegion.com/31491/how-secured-the-myzubster-metaverse-session-ownership-verified-explorers-and-road-realti</link>
<description>&lt;p&gt;A technical update on the architecture, security controls, testing and remaining roadmap behind the open-source MyZubster Metaverse.&lt;br&gt;
The MyZubster Metaverse is moving from an experimental interface toward a secure, testable multiplayer environment.&lt;br&gt;
Our goal is not simply to display avatars in a browser. We want to create an open-source world connecting public profiles, verified contributors, Zorgax characters, community knowledge and software-development workflows.&lt;br&gt;
The public Metaverse is available at:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/metaverse&quot;&gt;https://www.myzubster.com/metaverse&lt;/a&gt;&lt;br&gt;
This article describes what we have implemented, the security problem we recently solved and what remains before the platform can support realtime interaction at scale.&lt;br&gt;
The current experience&lt;br&gt;
A user can enter the MyZubster world as a guest without providing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a cryptocurrency wallet;&lt;/li&gt;
&lt;li&gt;identity documents;&lt;/li&gt;
&lt;li&gt;a GitHub account;&lt;/li&gt;
&lt;li&gt;an installed application.&lt;br&gt;
The current interface supports guest onboarding through a public display name and also presents verified explorers associated with authorized public accounts.&lt;br&gt;
This creates two identity levels:&lt;ol&gt;
&lt;li&gt;Guest explorers use temporary world sessions.&lt;/li&gt;
&lt;li&gt;Verified explorers have characters linked to a MyZubster account or confirmed GitHub identity.&lt;br&gt;
“Verified” does not mean KYC or legal identity verification. It only confirms control of the connected public account.&lt;br&gt;
Building the presence layer&lt;br&gt;
The backend maintains a presence record for every active participant.&lt;br&gt;
A simplified record may contain:&lt;br&gt;
{&lt;br&gt;
sessionId,&lt;br&gt;
displayName,&lt;br&gt;
characterId,&lt;br&gt;
accountId,&lt;br&gt;
githubLogin,&lt;br&gt;
position,&lt;br&gt;
status,&lt;br&gt;
lastSeenAt&lt;br&gt;
}&lt;br&gt;
This model supports the main world operations:&lt;br&gt;
Join&lt;br&gt;
Synchronize&lt;br&gt;
Move&lt;br&gt;
Chat&lt;br&gt;
Emote&lt;br&gt;
Leave&lt;br&gt;
Retrieve events&lt;br&gt;
Public players must be discoverable by other clients, so each presence has a public session identifier.&lt;br&gt;
However, a public identifier must never be treated as proof that a request belongs to that player.&lt;br&gt;
That distinction became the focus of our latest security milestone.&lt;br&gt;
The original session-ownership problem&lt;br&gt;
A multiplayer client needs to identify which session it wants to update.&lt;br&gt;
For example:&lt;br&gt;
{&lt;br&gt;
&quot;sessionId&quot;: &quot;public-session-id&quot;,&lt;br&gt;
&quot;x&quot;: 120,&lt;br&gt;
&quot;y&quot;: 80&lt;br&gt;
}&lt;br&gt;
If the backend trusts only sessionId, a malicious user may be able to submit another player’s identifier and perform actions on their behalf.&lt;br&gt;
Depending on the exposed routes, this could allow an attacker to:&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;move another character;&lt;/li&gt;
&lt;li&gt;send chat messages under another identity;&lt;/li&gt;
&lt;li&gt;publish unauthorized emotes;&lt;/li&gt;
&lt;li&gt;retrieve session-specific events;&lt;/li&gt;
&lt;li&gt;terminate another player’s session.&lt;br&gt;
A public session ID is an identifier, not an authorization credential.&lt;br&gt;
Implementing possession-bound sessions&lt;br&gt;
We implemented a new security boundary in:&lt;br&gt;
Pull Request #34 — Bind world actions to session credentials&lt;br&gt;
The change was merged into main as commit:&lt;br&gt;
148a798&lt;br&gt;
Every new Metaverse session now receives two different values:&lt;br&gt;
Public session ID&lt;br&gt;
Private session token&lt;br&gt;
The public ID can appear in world-state responses. The private token proves that the client controls the session.&lt;br&gt;
Cryptographically random credentials&lt;br&gt;
The server generates a token using 32 random bytes:&lt;br&gt;
const sessionToken = crypto&lt;br&gt;
.randomBytes(32)&lt;br&gt;
.toString(&quot;base64url&quot;);&lt;br&gt;
This provides 256 bits of entropy.&lt;br&gt;
The raw token is returned only during session creation and is never included in public presence data.&lt;br&gt;
Storing only a digest&lt;br&gt;
The server does not persist the raw credential.&lt;br&gt;
Instead, it calculates a SHA-256 digest:&lt;br&gt;
const sessionTokenHash = crypto&lt;br&gt;
.createHash(&quot;sha256&quot;)&lt;br&gt;
.update(sessionToken)&lt;br&gt;
.digest(&quot;hex&quot;);&lt;br&gt;
Only sessionTokenHash is stored with the presence record.&lt;br&gt;
This prevents a database reader from immediately obtaining active bearer credentials.&lt;br&gt;
Constant-time comparison&lt;br&gt;
When a protected request arrives, the server hashes the submitted credential and compares it with the stored digest.&lt;br&gt;
The verification uses a constant-time comparison:&lt;br&gt;
crypto.timingSafeEqual(expected, received);&lt;br&gt;
A shared ownership middleware verifies that:&lt;/li&gt;
&lt;li&gt;the session exists;&lt;/li&gt;
&lt;li&gt;a credential was supplied;&lt;/li&gt;
&lt;li&gt;the credential has the expected format;&lt;/li&gt;
&lt;li&gt;its digest matches the stored value;&lt;/li&gt;
&lt;li&gt;the caller owns the requested session.&lt;br&gt;
Invalid or missing credentials return:&lt;br&gt;
HTTP 401 Unauthorized&lt;br&gt;
Protected world routes&lt;br&gt;
Session ownership is now required for mutable and session-specific operations such as:&lt;br&gt;
/sync&lt;br&gt;
/move&lt;br&gt;
/chat&lt;br&gt;
/emote&lt;br&gt;
/leave&lt;br&gt;
/events&lt;br&gt;
Knowing another participant’s sessionId is no longer sufficient to control their character.&lt;br&gt;
The authorization rule is now:&lt;br&gt;
Public session ID&lt;pre&gt;&lt;code&gt;  +
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Private possession token&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  =
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Authorized world action&lt;br&gt;
Keeping credentials out of public payloads&lt;br&gt;
The frontend sends the private token through a dedicated HTTP header:&lt;br&gt;
X-Metaverse-Session-Token: &amp;lt;private-token&amp;gt;&lt;br&gt;
Example:&lt;br&gt;
const createSessionHeaders = (sessionToken) =&amp;gt; ({&lt;br&gt;
&quot;X-Metaverse-Session-Token&quot;: sessionToken,&lt;br&gt;
});&lt;br&gt;
The world-state body remains limited to operational data:&lt;br&gt;
{&lt;br&gt;
&quot;sessionId&quot;: &quot;public-session-id&quot;,&lt;br&gt;
&quot;x&quot;: 120,&lt;br&gt;
&quot;y&quot;: 80&lt;br&gt;
}&lt;br&gt;
This separation reduces the probability that credentials are accidentally copied into:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;world events;&lt;/li&gt;
&lt;li&gt;analytics;&lt;/li&gt;
&lt;li&gt;public API responses;&lt;/li&gt;
&lt;li&gt;application logs;&lt;/li&gt;
&lt;li&gt;debugging output;&lt;/li&gt;
&lt;li&gt;cached payloads.&lt;br&gt;
The credential is also kept in React runtime memory instead of being written to localStorage or public profile objects.&lt;br&gt;
An in-memory token is not a complete defense against browser compromise, but it reduces persistent credential exposure.&lt;br&gt;
Identity boundaries&lt;br&gt;
The Metaverse now distinguishes between several layers:&lt;br&gt;
Account identity&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Public profile&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Character identity&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Temporary world session&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Private session credential&lt;br&gt;
These layers have different meanings.&lt;br&gt;
A verified GitHub account does not automatically prove legal identity, employment, payment eligibility or institutional affiliation.&lt;br&gt;
Similarly, a public session ID identifies a player’s presence but does not authorize requests for that player.&lt;br&gt;
Documenting these boundaries is essential for avoiding security and governance problems later.&lt;br&gt;
Testing the implementation&lt;br&gt;
The session-ownership change includes backend, frontend and architecture-level tests.&lt;br&gt;
Backend tests&lt;br&gt;
The targeted backend suites verify:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;secure token generation;&lt;/li&gt;
&lt;li&gt;SHA-256 digest storage;&lt;/li&gt;
&lt;li&gt;valid credential acceptance;&lt;/li&gt;
&lt;li&gt;missing credential rejection;&lt;/li&gt;
&lt;li&gt;invalid credential rejection;&lt;/li&gt;
&lt;li&gt;cross-session mutation prevention;&lt;/li&gt;
&lt;li&gt;protected movement and chat;&lt;/li&gt;
&lt;li&gt;protected emotes and leave operations;&lt;/li&gt;
&lt;li&gt;exclusion of secrets from public responses.&lt;br&gt;
Results:&lt;br&gt;
3 backend suites&lt;br&gt;
22 tests passed&lt;br&gt;
Frontend tests&lt;br&gt;
Frontend coverage verifies:&lt;/li&gt;
&lt;li&gt;token retention in application memory;&lt;/li&gt;
&lt;li&gt;use of the session-token header;&lt;/li&gt;
&lt;li&gt;authenticated world operations;&lt;/li&gt;
&lt;li&gt;removal of tokens from normal JSON bodies;&lt;/li&gt;
&lt;li&gt;correct handling of protected session requests.&lt;br&gt;
Results:&lt;br&gt;
1 frontend suite&lt;br&gt;
5 tests passed&lt;br&gt;
Integration and deployment&lt;br&gt;
The completed verification included:&lt;br&gt;
Production frontend build: successful&lt;br&gt;
git diff --check: clean&lt;br&gt;
Marketplace regression suite: passed&lt;br&gt;
Vercel preview deployments: successful&lt;br&gt;
Main CI workflow: successful&lt;br&gt;
Production deployment: successful&lt;br&gt;
The public Metaverse route was also loaded after deployment.&lt;br&gt;
Current architecture&lt;br&gt;
The current system can be summarized as:&lt;br&gt;
React Metaverse Client&lt;pre&gt;&lt;code&gt;  |
  | HTTPS
  | X-Metaverse-Session-Token
  v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Node.js / Express API&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  |
  | Session ownership middleware
  | Hash verification
  v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Metaverse Presence Storage&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  |
  v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Shared world synchronization&lt;br&gt;
This is a practical foundation, but it is not yet the final realtime architecture.&lt;br&gt;
What remains to be built&lt;br&gt;
Realtime communication&lt;br&gt;
The current synchronization model still depends on polling in important areas.&lt;br&gt;
We need a versioned realtime protocol supporting:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;authenticated WebSocket connections;&lt;/li&gt;
&lt;li&gt;session-to-connection binding;&lt;/li&gt;
&lt;li&gt;reconnect handling;&lt;/li&gt;
&lt;li&gt;ordered events;&lt;/li&gt;
&lt;li&gt;duplicate-event protection;&lt;/li&gt;
&lt;li&gt;presence expiration;&lt;/li&gt;
&lt;li&gt;backpressure;&lt;/li&gt;
&lt;li&gt;slow-client management.&lt;br&gt;
The protocol should remain usable by multiple clients, including future community-built and WebXR clients.&lt;br&gt;
Distributed rate limiting&lt;br&gt;
Per-process rate limiting is not enough when the backend runs across multiple instances.&lt;br&gt;
We need a distributed limiter for:&lt;br&gt;
Session creation&lt;br&gt;
Authentication attempts&lt;br&gt;
Movement&lt;br&gt;
Chat&lt;br&gt;
Emotes&lt;br&gt;
Reconnects&lt;br&gt;
Limits should be calculated using combinations of:&lt;/li&gt;
&lt;li&gt;IP address;&lt;/li&gt;
&lt;li&gt;account ID;&lt;/li&gt;
&lt;li&gt;session ID;&lt;/li&gt;
&lt;li&gt;route;&lt;/li&gt;
&lt;li&gt;world ID;&lt;/li&gt;
&lt;li&gt;time window.&lt;br&gt;
Chat and authentication need stricter limits than normal movement updates.&lt;br&gt;
Session expiration and revocation&lt;br&gt;
The possession credential is now implemented, but lifecycle controls still need improvement:&lt;/li&gt;
&lt;li&gt;explicit expiration;&lt;/li&gt;
&lt;li&gt;server-side revocation;&lt;/li&gt;
&lt;li&gt;abandoned-session cleanup;&lt;/li&gt;
&lt;li&gt;token rotation;&lt;/li&gt;
&lt;li&gt;reconnect-safe renewal;&lt;/li&gt;
&lt;li&gt;active-session limits;&lt;/li&gt;
&lt;li&gt;multi-device logout;&lt;/li&gt;
&lt;li&gt;suspicious-session invalidation.&lt;br&gt;
Canonical authentication&lt;br&gt;
Verified characters require a stable account-linking process.&lt;br&gt;
The remaining work includes:&lt;/li&gt;
&lt;li&gt;canonical GitHub OAuth;&lt;/li&gt;
&lt;li&gt;verified account-to-character relationships;&lt;/li&gt;
&lt;li&gt;account-link substitution protection;&lt;/li&gt;
&lt;li&gt;reauthorization;&lt;/li&gt;
&lt;li&gt;identity-change audit trails;&lt;/li&gt;
&lt;li&gt;account recovery procedures.&lt;br&gt;
The backend must never trust a GitHub username supplied directly by a browser.&lt;br&gt;
Privacy-safe analytics&lt;br&gt;
We need enough telemetry to maintain the platform without introducing unnecessary tracking.&lt;br&gt;
Useful operational metrics include:&lt;/li&gt;
&lt;li&gt;concurrent sessions;&lt;/li&gt;
&lt;li&gt;API latency;&lt;/li&gt;
&lt;li&gt;reconnect frequency;&lt;/li&gt;
&lt;li&gt;server errors;&lt;/li&gt;
&lt;li&gt;world capacity;&lt;/li&gt;
&lt;li&gt;rate-limit events;&lt;/li&gt;
&lt;li&gt;event throughput.&lt;br&gt;
Retention, aggregation and access rules must be documented before expanding analytics.&lt;br&gt;
Reliability and load testing&lt;br&gt;
Future tests must simulate:&lt;/li&gt;
&lt;li&gt;simultaneous joins;&lt;/li&gt;
&lt;li&gt;movement bursts;&lt;/li&gt;
&lt;li&gt;reconnect storms;&lt;/li&gt;
&lt;li&gt;malformed messages;&lt;/li&gt;
&lt;li&gt;database latency;&lt;/li&gt;
&lt;li&gt;backend restarts;&lt;/li&gt;
&lt;li&gt;multiple server instances;&lt;/li&gt;
&lt;li&gt;slow or disconnected clients.&lt;br&gt;
We also need measurable targets for latency, error rate, recovery time and maximum concurrent participants.&lt;br&gt;
Complete E2E acceptance testing&lt;br&gt;
The complete guest flow should be automated:&lt;br&gt;
Join&lt;br&gt;
→ receive credential&lt;br&gt;
→ become visible&lt;br&gt;
→ move&lt;br&gt;
→ chat&lt;br&gt;
→ publish an emote&lt;br&gt;
→ reject impersonation attempts&lt;br&gt;
→ reconnect safely&lt;br&gt;
→ leave or expire&lt;br&gt;
The same acceptance flow is required for verified characters.&lt;br&gt;
WebXR&lt;br&gt;
WebXR remains part of the roadmap, but it should follow security and realtime stabilization.&lt;br&gt;
The eventual world protocol should support:&lt;br&gt;
2D web client&lt;br&gt;
3D browser client&lt;br&gt;
WebXR client&lt;br&gt;
Mobile client&lt;br&gt;
Independent community clients&lt;br&gt;
All clients should use the same documented API and event protocol.&lt;br&gt;
Dependency-security debt&lt;br&gt;
The repository currently reports vulnerabilities during npm audit.&lt;br&gt;
These findings were already present and were not introduced by the Metaverse session-ownership pull request, which did not change package manifests or lockfiles.&lt;br&gt;
However, they remain technical debt.&lt;br&gt;
A separate dependency-hardening milestone should include:&lt;ol&gt;
&lt;li&gt;classifying production and development exposure;&lt;/li&gt;
&lt;li&gt;updating direct dependencies;&lt;/li&gt;
&lt;li&gt;reviewing transitive dependencies;&lt;/li&gt;
&lt;li&gt;replacing abandoned packages;&lt;/li&gt;
&lt;li&gt;regenerating lockfiles;&lt;/li&gt;
&lt;li&gt;running complete regression tests;&lt;/li&gt;
&lt;li&gt;updating SBOM and audit evidence.&lt;br&gt;
Disabling the audit to produce a green badge would hide the problem instead of solving it.&lt;br&gt;
Recommended roadmap&lt;br&gt;
The recommended implementation sequence is:&lt;/li&gt;
&lt;li&gt;Session expiration and revocation&lt;/li&gt;
&lt;li&gt;Distributed rate limiting&lt;/li&gt;
&lt;li&gt;Canonical account authentication&lt;/li&gt;
&lt;li&gt;Versioned realtime protocol&lt;/li&gt;
&lt;li&gt;Authenticated WebSocket transport&lt;/li&gt;
&lt;li&gt;Privacy-safe monitoring&lt;/li&gt;
&lt;li&gt;Reliability and load testing&lt;/li&gt;
&lt;li&gt;Complete E2E acceptance tests&lt;/li&gt;
&lt;li&gt;Dependency-security remediation&lt;/li&gt;
&lt;li&gt;WebXR client development&lt;br&gt;
Security, reliability and protocol stability must come before graphical complexity.&lt;br&gt;
Open-source participation&lt;br&gt;
We are looking for contributors interested in:&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;React;&lt;/li&gt;
&lt;li&gt;Node.js and Express;&lt;/li&gt;
&lt;li&gt;MongoDB;&lt;/li&gt;
&lt;li&gt;Redis;&lt;/li&gt;
&lt;li&gt;WebSockets;&lt;/li&gt;
&lt;li&gt;application security;&lt;/li&gt;
&lt;li&gt;privacy engineering;&lt;/li&gt;
&lt;li&gt;abuse prevention;&lt;/li&gt;
&lt;li&gt;load testing;&lt;/li&gt;
&lt;li&gt;WebXR;&lt;/li&gt;
&lt;li&gt;technical documentation.&lt;br&gt;
Repository:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/danieldirimini-myzubster/myzubster&quot;&gt;https://github.com/danieldirimini-myzubster/myzubster&lt;/a&gt;&lt;br&gt;
Public Metaverse:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/metaverse&quot;&gt;https://www.myzubster.com/metaverse&lt;/a&gt;&lt;br&gt;
Security implementation:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/danieldirimini-myzubster/myzubster/pull/34&quot;&gt;https://github.com/danieldirimini-myzubster/myzubster/pull/34&lt;/a&gt;&lt;br&gt;
Conclusion&lt;br&gt;
The MyZubster Metaverse now has a stronger session-security foundation.&lt;br&gt;
We moved from public identifier-based operations to possession-bound authorization using:&lt;/li&gt;
&lt;li&gt;256-bit random credentials;&lt;/li&gt;
&lt;li&gt;SHA-256 token digests;&lt;/li&gt;
&lt;li&gt;constant-time verification;&lt;/li&gt;
&lt;li&gt;dedicated authorization headers;&lt;/li&gt;
&lt;li&gt;memory-only client credentials;&lt;/li&gt;
&lt;li&gt;protected world routes;&lt;/li&gt;
&lt;li&gt;backend and frontend security tests.&lt;br&gt;
The next milestone is transforming this foundation into a distributed realtime system with strong authentication, rate limiting, privacy-safe observability and measurable reliability.&lt;br&gt;
The Metaverse is online, but the deeper work is still ahead: building an open world protocol that other developers can inspect, test and extend.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31491/how-secured-the-myzubster-metaverse-session-ownership-verified-explorers-and-road-realti</guid>
<pubDate>Sat, 17 Oct 2026 05:20:55 +0000</pubDate>
</item>
<item>
<title>Engineering a Browser-First Metaverse: WebXR Fallbacks, Server-Authoritative Rooms, and Versioned</title>
<link>https://coderlegion.com/30813/engineering-browser-first-metaverse-webxr-fallbacks-server-authoritative-rooms-versioned</link>
<description>&lt;p&gt;Engineering a Browser-First Metaverse: WebXR Fallbacks, Server-Authoritative Rooms, and Versioned Scene Manifests&lt;/p&gt;
&lt;p&gt;The MyZubster Metaverse is being developed as a browser-first platform rather than a VR-only application.&lt;br&gt;
Our baseline requirement is straightforward:&lt;br&gt;
A user must be able to access the experience from a normal desktop or mobile browser. WebXR should enhance the experience when available, but its absence must never block access.&lt;/p&gt;
&lt;p&gt;During the latest development cycle, we implemented two foundational components:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A browser and WebXR capability-detection layer with graceful fallback&lt;/li&gt;
&lt;li&gt;A versioned scene-manifest contract validated by the backend before publication&lt;br&gt;
This article explains the architecture, implementation details, validation rules, current production evidence, and the remaining roadmap.&lt;br&gt;
Project Links&lt;ul&gt;
&lt;li&gt;Repository: MyZubster-Ecosystem/myzubster&lt;/li&gt;
&lt;li&gt;Live Metaverse: myzubster.com/metaverse&lt;/li&gt;
&lt;li&gt;WebXR fallback: Pull Request #1597&lt;/li&gt;
&lt;li&gt;Scene-manifest validation: Pull Request #1601&lt;/li&gt;
&lt;li&gt;LIFE Pilot entry point: myzubster.com/life-pilot&lt;br&gt;
System Architecture&lt;br&gt;
The current metaverse architecture separates device capabilities, presentation, room authority, scene configuration, and evidence.&lt;br&gt;
┌─────────────────────────────────────────────────────────┐&lt;br&gt;
│                    React Browser UI                     │&lt;br&gt;
│                                                         │&lt;br&gt;
│ Capability checks → status UI → browser/WebXR fallback  │&lt;br&gt;
└────────────────────────────┬────────────────────────────┘&lt;pre&gt;&lt;code&gt;                    │
                    ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;┌─────────────────────────────────────────────────────────┐&lt;br&gt;
│                  Metaverse Client Runtime               │&lt;br&gt;
│                                                         │&lt;br&gt;
│ Scene loading → controls → chat → emotes → interactions │&lt;br&gt;
└────────────────────────────┬────────────────────────────┘&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                    │
                    ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;┌─────────────────────────────────────────────────────────┐&lt;br&gt;
│                 Server-Authoritative API                │&lt;br&gt;
│                                                         │&lt;br&gt;
│ Rooms → sessions → access → capacity → stage → tokens   │&lt;br&gt;
└────────────────────────────┬────────────────────────────┘&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                    │
                    ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;┌─────────────────────────────────────────────────────────┐&lt;br&gt;
│                  Versioned Scene Contract               │&lt;br&gt;
│                                                         │&lt;br&gt;
│ Assets → spawn points → zones → portals → capabilities  │&lt;br&gt;
└────────────────────────────┬────────────────────────────┘&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                    │
                    ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;┌─────────────────────────────────────────────────────────┐&lt;br&gt;
│                    LIFE Evidence Layer                  │&lt;br&gt;
│                                                         │&lt;br&gt;
│ Pilot → provenance → evidence → KPI/MRV → visualization │&lt;br&gt;
└─────────────────────────────────────────────────────────┘&lt;br&gt;
The separation creates several important boundaries:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;The browser reports its capabilities.&lt;/li&gt;
&lt;li&gt;The server decides whether a room transition is valid.&lt;/li&gt;
&lt;li&gt;The manifest describes what the client may load.&lt;/li&gt;
&lt;li&gt;The evidence layer determines what real-world claims can be displayed.&lt;/li&gt;
&lt;li&gt;The 3D client is never the authority for authentication or environmental evidence.&lt;br&gt;
What Currently Works&lt;br&gt;
Neon Plaza can be entered through a normal browser without installing a native application or connecting a VR headset.&lt;br&gt;
In a production desktop smoke test, we verified:&lt;/li&gt;
&lt;li&gt;Authenticated access&lt;/li&gt;
&lt;li&gt;Character-associated entry&lt;/li&gt;
&lt;li&gt;Neon Plaza loading&lt;/li&gt;
&lt;li&gt;An active online session&lt;/li&gt;
&lt;li&gt;Movement controls&lt;/li&gt;
&lt;li&gt;Emotes&lt;/li&gt;
&lt;li&gt;Room chat&lt;/li&gt;
&lt;li&gt;Browser capability detection&lt;/li&gt;
&lt;li&gt;Non-blocking fallback when immersive WebXR is unavailable&lt;br&gt;
This was a desktop production verification. Real mobile devices and physical WebXR headsets still require a structured validation matrix.&lt;br&gt;
Part 1: Browser and WebXR Capability Detection&lt;br&gt;
The first milestone was implemented in PR #1597.&lt;br&gt;
Explicit WebXR States&lt;br&gt;
WebXR availability is not represented as a simple boolean.&lt;br&gt;
export const WEBXR_STATUS = Object.freeze({&lt;br&gt;
CHECKING: 'checking',&lt;br&gt;
SUPPORTED: 'supported',&lt;br&gt;
UNSUPPORTED: 'unsupported',&lt;br&gt;
ERROR: 'error'&lt;br&gt;
});&lt;br&gt;
This distinguishes between four different conditions:&lt;br&gt;
checking&lt;br&gt;
│&lt;br&gt;
├── supported&lt;br&gt;
├── unsupported&lt;br&gt;
└── error&lt;br&gt;
A browser that does not expose navigator.xr is different from a browser that exposes the API but fails during capability detection.&lt;br&gt;
That distinction is useful for both debugging and user communication.&lt;br&gt;
Browser Baseline&lt;br&gt;
The client checks a small browser baseline independently of WebXR:&lt;br&gt;
export function browserCapabilityBaseline(browser = window) {&lt;br&gt;
return [
 {
 label: 'JavaScript',
 ok: true
 },
 {
 label: 'Fetch API',
 ok: typeof browser.fetch === 'function'
 },
 {
 label: 'Local storage',
 ok: Boolean(browser.localStorage)
 },
 {
 label: 'CSS Grid',
 ok: Boolean(
   browser.CSS?.supports?.('display', 'grid')
 )
 },
 {
 label: 'Connection',
 ok: browser.navigator?.onLine !== false
 }
];&lt;br&gt;
}&lt;br&gt;
WebXR is checked separately because it is an enhancement rather than a baseline dependency.&lt;br&gt;
Detecting Immersive VR&lt;br&gt;
The detection function accepts a navigator-like dependency:&lt;br&gt;
export async function detectImmersiveVr(&lt;br&gt;
navigatorLike = navigator&lt;br&gt;
) {&lt;br&gt;
if (!navigatorLike?.xr?.isSessionSupported) {&lt;br&gt;
 return {&lt;br&gt;
 status: WEBXR_STATUS.UNSUPPORTED,&lt;br&gt;
 reason: 'api-unavailable'&lt;br&gt;
 };&lt;br&gt;
}&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;  try {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const supported =
  await navigatorLike.xr.isSessionSupported(
    'immersive-vr'
  );

return {
  status: supported
    ? WEBXR_STATUS.SUPPORTED
    : WEBXR_STATUS.UNSUPPORTED,

  reason: supported
    ? null
    : 'session-unsupported'
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  } catch (_error) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return {
  status: WEBXR_STATUS.ERROR,
  reason: 'capability-check-failed'
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }&lt;br&gt;
}&lt;br&gt;
Passing navigatorLike as an argument makes the function easier to test.&lt;br&gt;
For example:&lt;br&gt;
const fakeNavigator = {&lt;br&gt;
  xr: {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;isSessionSupported: async (mode) =&amp;gt;
  mode === 'immersive-vr'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }&lt;br&gt;
};&lt;/p&gt;
&lt;p&gt;const result =&lt;br&gt;
  await detectImmersiveVr(fakeNavigator);&lt;/p&gt;
&lt;p&gt;expect(result.status).toBe('supported');&lt;br&gt;
The unsupported case can also be tested without a real headset:&lt;br&gt;
const result =&lt;br&gt;
  await detectImmersiveVr({});&lt;/p&gt;
&lt;p&gt;expect(result).toEqual({&lt;br&gt;
  status: 'unsupported',&lt;br&gt;
  reason: 'api-unavailable'&lt;br&gt;
});&lt;br&gt;
Non-Blocking Fallback&lt;br&gt;
The UI maps the capability state to a user-facing message:&lt;br&gt;
const XR_COPY = {&lt;br&gt;
  [WEBXR_STATUS.CHECKING]:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;'Checking WebXR support…',
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  [WEBXR_STATUS.SUPPORTED]:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;'Immersive WebXR is available',
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  [WEBXR_STATUS.UNSUPPORTED]:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;'Browser mode active: a headset is not required',
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  [WEBXR_STATUS.ERROR]:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;'WebXR could not be verified: continue in browser mode'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;};&lt;br&gt;
The important behavior is that an unsupported or failed WebXR check does not produce a fatal application state.&lt;br&gt;
{xr.status !== WEBXR_STATUS.SUPPORTED &amp;amp;&amp;amp; (&lt;br&gt;
  &lt;small&gt;&lt;/small&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Neon Plaza remains accessible from desktop and mobile.
The WebXR check never blocks entry.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  &lt;br&gt;
)}&lt;br&gt;
The capability model therefore follows progressive enhancement:&lt;br&gt;
Standard browser&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Browser-based Neon Plaza&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Optional capabilities&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ├── WebGL enhancements
  ├── spatial audio
  ├── immersive WebXR
  └── headset controllers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Part 2: Server-Authoritative Room State&lt;br&gt;
The metaverse room lifecycle is enforced by the backend rather than trusted to client-side state.&lt;br&gt;
Forward-Only State Machine&lt;br&gt;
The backend defines valid transitions:&lt;br&gt;
const ROOM_TRANSITIONS = Object.freeze({&lt;br&gt;
  draft: new Set(['published']),&lt;br&gt;
  published: new Set(['scheduled', 'live']),&lt;br&gt;
  scheduled: new Set(['live']),&lt;br&gt;
  live: new Set(['ended']),&lt;br&gt;
  ended: new Set(['archive']),&lt;br&gt;
  archive: new Set([])&lt;br&gt;
});&lt;br&gt;
The resulting lifecycle is:&lt;br&gt;
draft&lt;br&gt;
  │&lt;br&gt;
  ▼&lt;br&gt;
published&lt;br&gt;
  │&lt;br&gt;
  ├──────────────┐&lt;br&gt;
  ▼              ▼&lt;br&gt;
scheduled       live&lt;br&gt;
  │              │&lt;br&gt;
  └──────►───────┘&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;             ▼
           ended
             │
             ▼
          archive
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The backend rejects invalid transitions:&lt;br&gt;
if (&lt;br&gt;
  patch.state &amp;amp;&amp;amp;&lt;br&gt;
  patch.state !== room.state&lt;br&gt;
) {&lt;br&gt;
  const allowed =&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ROOM_TRANSITIONS[room.state]?.has(patch.state);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  if (!allowed) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return {
  valid: false,
  status: 409,
  error:
    `Invalid room transition ` +
    `${room.state} -&amp;gt; ${patch.state}`
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }&lt;/p&gt;
&lt;p&gt;  room.state = patch.state;&lt;br&gt;
}&lt;br&gt;
A modified browser therefore cannot return an ended room to the live state simply by changing its local representation.&lt;br&gt;
Access Policies&lt;br&gt;
Rooms support three access policies:&lt;br&gt;
accessPolicy: {&lt;br&gt;
  type: String,&lt;br&gt;
  enum: [&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;'public',
'authenticated',
'private'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  ],&lt;br&gt;
  default: 'authenticated'&lt;br&gt;
}&lt;br&gt;
The room model also includes bounded capacity:&lt;br&gt;
capacity: {&lt;br&gt;
  type: Number,&lt;br&gt;
  min: 1,&lt;br&gt;
  max: 500,&lt;br&gt;
  default: 100&lt;br&gt;
}&lt;br&gt;
Join Validation&lt;br&gt;
Before joining, the backend checks session state, blocklists, private-room access, and capacity:&lt;br&gt;
async function validateJoin({&lt;br&gt;
  session,&lt;br&gt;
  room,&lt;br&gt;
  actorUserId&lt;br&gt;
}) {&lt;br&gt;
  if (session.state !== 'live') {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return {
  valid: false,
  status: 409,
  error: 'Session is not live'
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }&lt;/p&gt;
&lt;p&gt;  const actor = String(actorUserId);&lt;/p&gt;
&lt;p&gt;  if ((room.blockedUserIds || []).includes(actor)) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return {
  valid: false,
  status: 403,
  error: 'Access blocked'
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }&lt;/p&gt;
&lt;p&gt;  const privateAccessDenied =&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;room.accessPolicy === 'private' &amp;amp;&amp;amp;
!(room.allowedUserIds || []).includes(actor) &amp;amp;&amp;amp;
actor !== String(room.hostUserId);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  if (privateAccessDenied) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return {
  valid: false,
  status: 403,
  error: 'Private room access denied'
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }&lt;/p&gt;
&lt;p&gt;  const current =&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;new Set(session.participantUserIds || []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  const capacityReached =&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;!current.has(actor) &amp;amp;&amp;amp;
current.size &amp;gt;= session.capacity;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  if (capacityReached) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return {
  valid: false,
  status: 409,
  error: 'Session is at capacity'
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }&lt;/p&gt;
&lt;p&gt;  return { valid: true };&lt;br&gt;
}&lt;br&gt;
This is important because client-side room membership cannot be considered authoritative.&lt;br&gt;
Part 3: Realtime Session Tokens&lt;br&gt;
After a successful join, the backend issues a short-lived token scoped to the metaverse session.&lt;br&gt;
function mintRealtimeToken(&lt;br&gt;
  session,&lt;br&gt;
  actorUserId&lt;br&gt;
) {&lt;br&gt;
  const secret =&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;process.env.REALTIME_TOKEN_SECRET ||
process.env.JWT_SECRET;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  if (!secret) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;throw new Error(
  'Realtime token secret is not configured'
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }&lt;/p&gt;
&lt;p&gt;  return jwt.sign({&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sub: String(actorUserId),
purpose: 'metaverse-realtime',
sessionId: session.sessionId,
roomId: session.roomId,
sceneManifestVersion:
  session.sceneManifestVersion
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }, secret, {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;expiresIn: '5m'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  });&lt;br&gt;
}&lt;br&gt;
The token binds the connection to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A user&lt;/li&gt;
&lt;li&gt;A specific purpose&lt;/li&gt;
&lt;li&gt;A session&lt;/li&gt;
&lt;li&gt;A room&lt;/li&gt;
&lt;li&gt;A scene-manifest version&lt;br&gt;
Using a dedicated purpose value reduces the risk of using a generic application token in the wrong context.&lt;br&gt;
The short expiration also limits the useful lifetime of a captured realtime credential.&lt;br&gt;
Future work will include:&lt;/li&gt;
&lt;li&gt;Token refresh&lt;/li&gt;
&lt;li&gt;Explicit revocation&lt;/li&gt;
&lt;li&gt;Reconnect behavior&lt;/li&gt;
&lt;li&gt;Redis-backed realtime coordination&lt;/li&gt;
&lt;li&gt;Horizontal scaling&lt;/li&gt;
&lt;li&gt;Multi-region latency handling&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Replay-resistance analysis&lt;br&gt;
Part 4: Privacy-Aware Moderation References&lt;br&gt;
Moderation interfaces do not need to expose raw account IDs.&lt;br&gt;
The backend generates opaque session-scoped participant references:&lt;br&gt;
function moderationParticipantRef(&lt;br&gt;
sessionId,&lt;br&gt;
actorUserId&lt;br&gt;
) {&lt;br&gt;
return hashRoomInviteCode(&lt;br&gt;
  &lt;code&gt;${sessionId}:${actorUserId}&lt;/code&gt;&lt;br&gt;
).slice(0, 24);&lt;br&gt;
}&lt;br&gt;
Room blocklist references are scoped separately:&lt;br&gt;
function blockedParticipantRef(&lt;br&gt;
roomId,&lt;br&gt;
actorUserId&lt;br&gt;
) {&lt;br&gt;
return hashRoomInviteCode(&lt;br&gt;
  &lt;code&gt;blocklist:${roomId}:${actorUserId}&lt;/code&gt;&lt;br&gt;
).slice(0, 24);&lt;br&gt;
}&lt;br&gt;
The public session response returns a count rather than the participant-ID array:&lt;br&gt;
function publicSession(session) {&lt;br&gt;
return {&lt;br&gt;
  id: session.sessionId,&lt;br&gt;
  roomId: session.roomId,&lt;br&gt;
  state: session.state,&lt;br&gt;
  capacity: session.capacity,&lt;/p&gt;
&lt;p&gt;  participantCount:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(session.participantUserIds || []).length,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  sceneManifestVersion:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;session.sceneManifestVersion,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  lifecycleVersion:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;session.lifecycleVersion,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  startedAt: session.startedAt || null,&lt;br&gt;
  endedAt: session.endedAt || null&lt;br&gt;
};&lt;br&gt;
}&lt;br&gt;
This does not provide anonymity by itself, but it limits unnecessary identifier exposure.&lt;br&gt;
Part 5: Versioned Scene Manifest v1&lt;br&gt;
The next milestone was delivered in PR #1601.&lt;br&gt;
The manifest is the contract between:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;Room configuration&lt;/li&gt;
&lt;li&gt;Backend validation&lt;/li&gt;
&lt;li&gt;Client runtime&lt;/li&gt;
&lt;li&gt;Asset delivery&lt;/li&gt;
&lt;li&gt;Capability requirements&lt;/li&gt;
&lt;li&gt;Future rollback behavior&lt;br&gt;
Supported Contract Values&lt;br&gt;
const SCENE_MANIFEST_SCHEMA_VERSION = '1';&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;const ASSET_TYPES = new Set([
  'gltf',
  'glb',
  'image',
  'audio',
  'video'
]);&lt;/p&gt;
&lt;p&gt;const ZONE_TYPES = new Set([
  'portal',
  'stage',
  'link'
]);&lt;/p&gt;
&lt;p&gt;const LIGHTING_PRESETS = new Set([
  'day',
  'sunset',
  'night',
  'studio'
]);&lt;/p&gt;
&lt;p&gt;const CAPABILITIES = new Set([
  'webgl2',
  'webxr-immersive-vr',
  'audio',
  'video'
]);&lt;br&gt;
The schema is deliberately constrained.&lt;br&gt;
An unrestricted JSON document would make compatibility, validation, migration, and rollback increasingly difficult.&lt;br&gt;
Safe Default Manifest&lt;br&gt;
New rooms receive a minimal valid scene definition:&lt;br&gt;
function defaultSceneManifest() {&lt;br&gt;
  return {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;schemaVersion:
  SCENE_MANIFEST_SCHEMA_VERSION,

assetBundleVersion: '1',

assets: [],

spawnPoints: [
  {
    id: 'default',
    position: [0, 0, 0]
  }
],

interactionZones: [],

stage: {
  position: [0, 0, 0],
  capacity: 1
},

lighting: {
  preset: 'day'
},

capabilities: []
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  };&lt;br&gt;
}&lt;br&gt;
The room creation service applies it automatically:&lt;br&gt;
const room = await VirtualRoom.create({&lt;br&gt;
  roomId: crypto.randomUUID(),&lt;br&gt;
  slug: safeSlug,&lt;br&gt;
  name: safeName,&lt;br&gt;
  hostUserId: String(actorUserId),&lt;br&gt;
  accessPolicy,&lt;br&gt;
  capacity: numericCapacity,&lt;br&gt;
  stagePolicy,&lt;br&gt;
  sceneManifest: defaultSceneManifest()&lt;br&gt;
});&lt;br&gt;
This avoids creating rooms with an undefined scene state.&lt;br&gt;
Example Scene Manifest&lt;br&gt;
A more complete manifest could look like this:&lt;br&gt;
{&lt;br&gt;
  &quot;schemaVersion&quot;: &quot;1&quot;,&lt;br&gt;
  &quot;assetBundleVersion&quot;: &quot;42&quot;,&lt;br&gt;
  &quot;assets&quot;: [&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;id&quot;: &quot;neon-plaza&quot;,
  &quot;type&quot;: &quot;glb&quot;,
  &quot;url&quot;: &quot;https://cdn.myzubster.com/scenes/neon-plaza.42.glb&quot;
},
{
  &quot;id&quot;: &quot;ambient-audio&quot;,
  &quot;type&quot;: &quot;audio&quot;,
  &quot;url&quot;: &quot;https://cdn.myzubster.com/audio/neon-plaza.7.ogg&quot;
},
{
  &quot;id&quot;: &quot;life-dashboard&quot;,
  &quot;type&quot;: &quot;image&quot;,
  &quot;url&quot;: &quot;https://cdn.myzubster.com/life/dashboard.12.webp&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  ],&lt;br&gt;
  &quot;spawnPoints&quot;: [&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;id&quot;: &quot;main-entrance&quot;,
  &quot;position&quot;: [0, 1.6, 4]
},
{
  &quot;id&quot;: &quot;life-hub&quot;,
  &quot;position&quot;: [12, 1.6, -3]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  ],&lt;br&gt;
  &quot;interactionZones&quot;: [&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;id&quot;: &quot;life-portal&quot;,
  &quot;type&quot;: &quot;portal&quot;,
  &quot;position&quot;: [10, 0, -3],
  &quot;radius&quot;: 1.5,
  &quot;destination&quot;: &quot;life-hub&quot;
},
{
  &quot;id&quot;: &quot;pilot-details&quot;,
  &quot;type&quot;: &quot;link&quot;,
  &quot;position&quot;: [14, 1, -4],
  &quot;radius&quot;: 1,
  &quot;destination&quot;: &quot;https://www.myzubster.com/life-pilot&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  ],&lt;br&gt;
  &quot;stage&quot;: {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;position&quot;: [0, 0, -8],
&quot;capacity&quot;: 30
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  },&lt;br&gt;
  &quot;lighting&quot;: {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;preset&quot;: &quot;sunset&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  },&lt;br&gt;
  &quot;capabilities&quot;: [&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;webgl2&quot;,
&quot;audio&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  ]&lt;br&gt;
}&lt;br&gt;
The manifest contains only declarative scene information. It does not grant permissions or override room authority.&lt;br&gt;
Part 6: Manifest Validation&lt;br&gt;
Identifier Validation&lt;br&gt;
Identifiers use a restricted character set:&lt;br&gt;
function validId(value) {&lt;br&gt;
  return (&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typeof value === 'string' &amp;amp;&amp;amp;
/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(value)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  );&lt;br&gt;
}&lt;br&gt;
This avoids IDs containing markup, control characters, or unexpected path syntax.&lt;br&gt;
Vector Validation&lt;br&gt;
Positions must contain exactly three finite values:&lt;br&gt;
function validVector(value) {&lt;br&gt;
  return (&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Array.isArray(value) &amp;amp;&amp;amp;
value.length === 3 &amp;amp;&amp;amp;
value.every(Number.isFinite)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  );&lt;br&gt;
}&lt;br&gt;
These values are invalid:&lt;br&gt;
[0, 1]
[0, 1, null]&lt;br&gt;
[0, 1, NaN]
[0, 1, Infinity]&lt;br&gt;
HTTPS-Only URLs&lt;br&gt;
External resources must use HTTPS:&lt;br&gt;
function validHttpsUrl(value) {&lt;br&gt;
  if (&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typeof value !== 'string' ||
value.length &amp;gt; 2048
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  ) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return false;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }&lt;/p&gt;
&lt;p&gt;  try {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return new URL(value).protocol === 'https:';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  } catch {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return false;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }&lt;br&gt;
}&lt;br&gt;
This rejects examples such as:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;http://example.com/scene.glb&quot;&gt;http://example.com/scene.glb&lt;/a&gt;&lt;br&gt;
javascript:alert(1)&lt;br&gt;
invalid-url&lt;br&gt;
HTTPS is only the first boundary. Future validation should also enforce:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Approved asset origins&lt;/li&gt;
&lt;li&gt;Content-Type checks&lt;/li&gt;
&lt;li&gt;Download timeouts&lt;/li&gt;
&lt;li&gt;File-size limits&lt;/li&gt;
&lt;li&gt;Redirect restrictions&lt;/li&gt;
&lt;li&gt;Content hashes&lt;/li&gt;
&lt;li&gt;glTF structural validation&lt;/li&gt;
&lt;li&gt;Geometry budgets&lt;/li&gt;
&lt;li&gt;Texture budgets&lt;/li&gt;
&lt;li&gt;CORS requirements&lt;br&gt;
Bounded Collections&lt;br&gt;
Manifest collections are explicitly bounded:&lt;br&gt;
if (&lt;br&gt;
!Array.isArray(input.assets) ||&lt;br&gt;
input.assets.length &amp;gt; 100&lt;br&gt;
) {&lt;br&gt;
errors.push(&lt;br&gt;
  'assets must be an array with at most 100 entries'&lt;br&gt;
);&lt;br&gt;
}&lt;br&gt;
Spawn points require at least one entry and allow a maximum of 32:&lt;br&gt;
if (&lt;br&gt;
!Array.isArray(input.spawnPoints) ||&lt;br&gt;
input.spawnPoints.length &amp;lt; 1 ||&lt;br&gt;
input.spawnPoints.length &amp;gt; 32&lt;br&gt;
) {&lt;br&gt;
errors.push(&lt;br&gt;
  'spawnPoints must contain between 1 and 32 entries'&lt;br&gt;
);&lt;br&gt;
}&lt;br&gt;
These limits reduce accidental or malicious resource amplification before the runtime even begins downloading scene assets.&lt;br&gt;
Part 7: Publication Gate&lt;br&gt;
A scene is validated when its manifest is updated and again when the room is published.&lt;br&gt;
const manifestCandidate =&lt;br&gt;
patch.sceneManifest !== undefined&lt;br&gt;
  ? patch.sceneManifest&lt;br&gt;
  : room.sceneManifest;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;if (&lt;br&gt;
  patch.sceneManifest !== undefined ||&lt;br&gt;
  patch.state === 'published'&lt;br&gt;
) {&lt;br&gt;
  const validation =&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;validateSceneManifest(manifestCandidate);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  if (!validation.valid) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return {
  valid: false,
  status: 400,
  error: 'Invalid scene manifest',
  details: validation.errors
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }&lt;/p&gt;
&lt;p&gt;  room.sceneManifest =&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;validation.manifest;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;br&gt;
This establishes an important invariant:&lt;br&gt;
Invalid scene manifest&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      │
      ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Backend validation failure&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      │
      ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Publication rejected&lt;br&gt;
The frontend may eventually provide immediate validation feedback, but frontend checks are not authoritative.&lt;br&gt;
An API consumer could bypass the official interface. The backend must enforce the contract independently.&lt;br&gt;
Part 8: Persistence Model&lt;br&gt;
The room currently stores:&lt;br&gt;
sceneManifestVersion: {&lt;br&gt;
  type: String,&lt;br&gt;
  default: '1'&lt;br&gt;
},&lt;/p&gt;
&lt;p&gt;sceneManifest: {&lt;br&gt;
  type: mongoose.Schema.Types.Mixed,&lt;br&gt;
  default: null&lt;br&gt;
}&lt;br&gt;
The public room response includes both fields:&lt;br&gt;
function publicRoom(room) {&lt;br&gt;
  return {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;id: room.roomId,
slug: room.slug,
name: room.name,
state: room.state,
accessPolicy: room.accessPolicy,
capacity: room.capacity,
stagePolicy: room.stagePolicy,
sceneManifestVersion:
  room.sceneManifestVersion,
sceneManifest:
  room.sceneManifest || null,
scheduledFor:
  room.scheduledFor || null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  };&lt;br&gt;
}&lt;br&gt;
This is sufficient for the current validation milestone, but the next persistence model should introduce immutable historical versions.&lt;br&gt;
Part 9: Automated Tests&lt;br&gt;
The manifest tests cover:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The safe default manifest&lt;/li&gt;
&lt;li&gt;Versioned asset bundles&lt;/li&gt;
&lt;li&gt;HTTPS asset references&lt;/li&gt;
&lt;li&gt;Valid spawn points&lt;/li&gt;
&lt;li&gt;Valid interaction zones&lt;/li&gt;
&lt;li&gt;Portal destinations&lt;/li&gt;
&lt;li&gt;Unsupported schema versions&lt;/li&gt;
&lt;li&gt;Insecure URLs&lt;/li&gt;
&lt;li&gt;Malformed vectors&lt;/li&gt;
&lt;li&gt;Duplicate IDs&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Unsupported capabilities&lt;br&gt;
Example:&lt;br&gt;
test(&lt;br&gt;
'rejects unsupported schema versions and insecure asset URLs',&lt;br&gt;
() =&amp;gt; {&lt;br&gt;
  const manifest = {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...defaultSceneManifest(),

schemaVersion: '2',

assets: [{
  id: 'plaza',
  type: 'glb',
  url: 'http://cdn.example.com/plaza.glb'
}]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  };&lt;/p&gt;
&lt;p&gt;  const result =&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;validateSceneManifest(manifest);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  expect(result.valid).toBe(false);&lt;/p&gt;
&lt;p&gt;  expect(result.errors).toEqual(&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;expect.arrayContaining([
  'schemaVersion must be 1',
  'assets[0].url must use https'
])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  );&lt;br&gt;
}&lt;br&gt;
);&lt;br&gt;
The implementation passed:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;Backend lint&lt;/li&gt;
&lt;li&gt;Backend unit tests&lt;/li&gt;
&lt;li&gt;Frontend unit tests&lt;/li&gt;
&lt;li&gt;Backend build&lt;/li&gt;
&lt;li&gt;Frontend build&lt;/li&gt;
&lt;li&gt;Security audit&lt;/li&gt;
&lt;li&gt;Continuous evidence checks&lt;br&gt;
Production Evidence and Current Limits&lt;br&gt;
The following behavior was verified against the production desktop experience:&lt;/li&gt;
&lt;li&gt;Authentication was active&lt;/li&gt;
&lt;li&gt;Neon Plaza loaded&lt;/li&gt;
&lt;li&gt;The session appeared online&lt;/li&gt;
&lt;li&gt;Movement controls were visible&lt;/li&gt;
&lt;li&gt;Emotes were available&lt;/li&gt;
&lt;li&gt;Chat was available&lt;/li&gt;
&lt;li&gt;The capability panel rendered&lt;/li&gt;
&lt;li&gt;Browser baseline checks passed&lt;/li&gt;
&lt;li&gt;WebXR fallback messaging appeared&lt;/li&gt;
&lt;li&gt;The browser experience remained available without immersive WebXR&lt;br&gt;
The following items remain unverified or incomplete:&lt;/li&gt;
&lt;li&gt;Real-device Android testing&lt;/li&gt;
&lt;li&gt;Real-device iOS testing&lt;/li&gt;
&lt;li&gt;Physical WebXR headset entry and exit&lt;/li&gt;
&lt;li&gt;Controller compatibility&lt;/li&gt;
&lt;li&gt;Long-running realtime reliability&lt;/li&gt;
&lt;li&gt;High-concurrency room testing&lt;/li&gt;
&lt;li&gt;Versioned CDN asset loading&lt;/li&gt;
&lt;li&gt;Manifest rollback&lt;/li&gt;
&lt;li&gt;Full accessibility verification&lt;/li&gt;
&lt;li&gt;Complete LIFE evidence integration&lt;br&gt;
This separation is intentional. A roadmap entry is not evidence that a capability is complete.&lt;br&gt;
Connecting the Metaverse to LIFE&lt;br&gt;
MyZubster is also exploring LIFE-oriented environmental and circular-economy workflows.&lt;br&gt;
The metaverse can provide a public and educational interface for those projects, but it must not replace the underlying evidence system.&lt;br&gt;
The proposed flow is:&lt;br&gt;
Real-world pilot&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Measurement or observation&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Data provenance&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Evidence classification&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;KPI / MRV calculation&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MyZubster project record&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Metaverse representation&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Educational mission&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Community feedback&lt;br&gt;
A scene might contain:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;A portal to a pilot&lt;/li&gt;
&lt;li&gt;A visualization of measurements&lt;/li&gt;
&lt;li&gt;A description of the methodology&lt;/li&gt;
&lt;li&gt;A list of missing evidence&lt;/li&gt;
&lt;li&gt;A contribution mission&lt;/li&gt;
&lt;li&gt;Replication instructions&lt;/li&gt;
&lt;li&gt;KPI and MRV status&lt;br&gt;
However:&lt;br&gt;
3D visualization ≠ verified measurement&lt;br&gt;
simulation ≠ field result&lt;br&gt;
self-declaration ≠ independent validation&lt;br&gt;
project goal ≠ achieved environmental impact&lt;br&gt;
These distinctions should be visible in the interface.&lt;br&gt;
Possible Evidence Contract&lt;br&gt;
A future scene object could include an evidence reference:&lt;br&gt;
{&lt;br&gt;
&quot;id&quot;: &quot;water-savings-display&quot;,&lt;br&gt;
&quot;type&quot;: &quot;evidence-panel&quot;,&lt;br&gt;
&quot;position&quot;: [4, 1.5, -2],&lt;br&gt;
&quot;evidence&quot;: {&lt;br&gt;
  &quot;status&quot;: &quot;measured&quot;,&lt;br&gt;
  &quot;source&quot;: &quot;pilot-observation-2026-10&quot;,&lt;br&gt;
  &quot;methodologyVersion&quot;: &quot;0.2&quot;,&lt;br&gt;
  &quot;independentVerification&quot;: false&lt;br&gt;
}&lt;br&gt;
}&lt;br&gt;
Possible evidence states might include:&lt;br&gt;
planned&lt;br&gt;
self-declared&lt;br&gt;
observed&lt;br&gt;
measured&lt;br&gt;
methodology-reviewed&lt;br&gt;
independently-verified&lt;br&gt;
disputed&lt;br&gt;
superseded&lt;br&gt;
The client could render these states differently without misrepresenting their confidence level.&lt;br&gt;
Remaining Engineering Work&lt;br&gt;
Immutable Manifest History&lt;br&gt;
The current active manifest should eventually become a pointer to an immutable history.&lt;br&gt;
{&lt;br&gt;
roomId: 'room-123',&lt;br&gt;
version: '7',&lt;br&gt;
schemaVersion: '1',&lt;br&gt;
assetBundleVersion: '42',&lt;br&gt;
checksum: 'sha256:...',&lt;br&gt;
manifest: {},&lt;br&gt;
createdBy: 'account-reference',&lt;br&gt;
createdAt: '2026-10-12T00:00:00Z'&lt;br&gt;
}&lt;br&gt;
The structure should become:&lt;br&gt;
VirtualRoom&lt;br&gt;
  │&lt;br&gt;
  └── activeManifestVersion: 7&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SceneManifestVersion&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;├── version 5
├── version 6
└── version 7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Safe Rollback&lt;br&gt;
Rollback should create a new version based on a previous snapshot:&lt;br&gt;
v5 → v6 → v7&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      │
      └── rollback to v6
                ↓
               v8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Version 7 should not be deleted or overwritten. This preserves auditability.&lt;br&gt;
CDN Asset Pipeline&lt;br&gt;
A future pipeline should follow:&lt;br&gt;
upload&lt;br&gt;
  ↓&lt;br&gt;
validate&lt;br&gt;
  ↓&lt;br&gt;
scan&lt;br&gt;
  ↓&lt;br&gt;
optimize&lt;br&gt;
  ↓&lt;br&gt;
calculate hash&lt;br&gt;
  ↓&lt;br&gt;
assign immutable version&lt;br&gt;
  ↓&lt;br&gt;
publish to CDN&lt;br&gt;
  ↓&lt;br&gt;
reference from manifest&lt;br&gt;
Asset URLs should be immutable:&lt;br&gt;
/scenes/neon-plaza.42.glb&lt;br&gt;
/textures/neon-floor.18.webp&lt;br&gt;
/audio/neon-ambient.7.ogg&lt;br&gt;
Client-Side Scene Loader&lt;br&gt;
The client will eventually need a structured loading pipeline:&lt;br&gt;
fetch room&lt;br&gt;
   ↓&lt;br&gt;
fetch active manifest&lt;br&gt;
   ↓&lt;br&gt;
validate schema version&lt;br&gt;
   ↓&lt;br&gt;
compare required capabilities&lt;br&gt;
   ↓&lt;br&gt;
load critical assets&lt;br&gt;
   ↓&lt;br&gt;
load optional assets&lt;br&gt;
   ↓&lt;br&gt;
report failures&lt;br&gt;
   ↓&lt;br&gt;
enter scene&lt;br&gt;
Graceful Asset Failure&lt;br&gt;
Not every asset failure should destroy the whole scene.&lt;br&gt;
critical asset missing&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;→ load fallback scene
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;important asset missing&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;→ continue with visible warning
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;optional asset missing&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;→ omit it and continue
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A future asset entry could declare this:&lt;br&gt;
{&lt;br&gt;
  &quot;id&quot;: &quot;neon-plaza&quot;,&lt;br&gt;
  &quot;type&quot;: &quot;glb&quot;,&lt;br&gt;
  &quot;url&quot;: &quot;&lt;a rel=&quot;nofollow&quot; href=&quot;https://cdn.myzubster.com/scenes/neon-plaza.42.glb&quot;&gt;https://cdn.myzubster.com/scenes/neon-plaza.42.glb&lt;/a&gt;&quot;,&lt;br&gt;
  &quot;criticality&quot;: &quot;critical&quot;,&lt;br&gt;
  &quot;fallbackAssetId&quot;: &quot;basic-plaza&quot;&lt;br&gt;
}&lt;br&gt;
Runtime Resource Budgets&lt;br&gt;
Valid JSON does not guarantee safe runtime performance.&lt;br&gt;
We still need budgets for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;File size&lt;/li&gt;
&lt;li&gt;Polygon count&lt;/li&gt;
&lt;li&gt;Texture dimensions&lt;/li&gt;
&lt;li&gt;Texture memory&lt;/li&gt;
&lt;li&gt;Draw calls&lt;/li&gt;
&lt;li&gt;Animation count&lt;/li&gt;
&lt;li&gt;Shader complexity&lt;/li&gt;
&lt;li&gt;Audio duration&lt;/li&gt;
&lt;li&gt;Video resolution&lt;/li&gt;
&lt;li&gt;Concurrent requests&lt;/li&gt;
&lt;li&gt;Scene initialization time&lt;br&gt;
Mobile Validation&lt;br&gt;
A real-device test matrix should include:&lt;/li&gt;
&lt;li&gt;Android Chrome&lt;/li&gt;
&lt;li&gt;Android Firefox&lt;/li&gt;
&lt;li&gt;iOS Safari&lt;/li&gt;
&lt;li&gt;iPadOS Safari&lt;/li&gt;
&lt;li&gt;Low-memory devices&lt;/li&gt;
&lt;li&gt;Slow networks&lt;/li&gt;
&lt;li&gt;Small screens&lt;/li&gt;
&lt;li&gt;Touch-only interaction&lt;/li&gt;
&lt;li&gt;Screen rotation&lt;/li&gt;
&lt;li&gt;Background and resume behavior&lt;br&gt;
WebXR Device Validation&lt;br&gt;
Physical-headset testing should cover:&lt;/li&gt;
&lt;li&gt;Entering immersive mode&lt;/li&gt;
&lt;li&gt;Exiting immersive mode&lt;/li&gt;
&lt;li&gt;Session interruption&lt;/li&gt;
&lt;li&gt;Controller input&lt;/li&gt;
&lt;li&gt;Focus loss&lt;/li&gt;
&lt;li&gt;Boundary changes&lt;/li&gt;
&lt;li&gt;Sleep and resume&lt;/li&gt;
&lt;li&gt;Spatial audio&lt;/li&gt;
&lt;li&gt;Motion comfort&lt;/li&gt;
&lt;li&gt;Performance degradation&lt;br&gt;
Realtime Reliability&lt;br&gt;
Load tests should cover:&lt;/li&gt;
&lt;li&gt;Concurrent joins&lt;/li&gt;
&lt;li&gt;Concurrent leaves&lt;/li&gt;
&lt;li&gt;Chat bursts&lt;/li&gt;
&lt;li&gt;Stage requests&lt;/li&gt;
&lt;li&gt;Moderation operations&lt;/li&gt;
&lt;li&gt;Reconnect storms&lt;/li&gt;
&lt;li&gt;Token refresh&lt;/li&gt;
&lt;li&gt;Database latency&lt;/li&gt;
&lt;li&gt;Redis degradation&lt;/li&gt;
&lt;li&gt;Server restart&lt;/li&gt;
&lt;li&gt;Network partitions&lt;br&gt;
Accessibility&lt;br&gt;
Browser-first also means accessible-first.&lt;br&gt;
The remaining accessibility work includes:&lt;/li&gt;
&lt;li&gt;Keyboard navigation&lt;/li&gt;
&lt;li&gt;Screen-reader semantics&lt;/li&gt;
&lt;li&gt;Focus management&lt;/li&gt;
&lt;li&gt;Reduced motion&lt;/li&gt;
&lt;li&gt;Colour contrast&lt;/li&gt;
&lt;li&gt;Text scaling&lt;/li&gt;
&lt;li&gt;Captions&lt;/li&gt;
&lt;li&gt;Touch-target sizing&lt;/li&gt;
&lt;li&gt;Non-spatial alternatives&lt;/li&gt;
&lt;li&gt;Cognitive-load review&lt;br&gt;
Important project information must never be available only through an immersive 3D interaction.&lt;br&gt;
Proposed Roadmap&lt;br&gt;
C2.1 — Immutable Manifest Versions&lt;br&gt;
create version model&lt;br&gt;
→ validate manifest&lt;br&gt;
→ calculate checksum&lt;br&gt;
→ store immutable snapshot&lt;br&gt;
→ update active pointer&lt;br&gt;
C2.2 — Rollback&lt;br&gt;
select previous version&lt;br&gt;
→ validate snapshot&lt;br&gt;
→ create a new version&lt;br&gt;
→ update active pointer&lt;br&gt;
→ preserve audit history&lt;br&gt;
C2.3 — Asset Pipeline&lt;br&gt;
upload&lt;br&gt;
→ validate&lt;br&gt;
→ optimize&lt;br&gt;
→ hash&lt;br&gt;
→ version&lt;br&gt;
→ publish&lt;br&gt;
→ reference&lt;br&gt;
C2.4 — Client Scene Loader&lt;br&gt;
fetch&lt;br&gt;
→ validate&lt;br&gt;
→ negotiate capabilities&lt;br&gt;
→ load&lt;br&gt;
→ apply fallbacks&lt;br&gt;
→ expose status&lt;br&gt;
C2.5 — First LIFE Scene&lt;br&gt;
select pilot&lt;br&gt;
→ define evidence contract&lt;br&gt;
→ define KPI/MRV boundaries&lt;br&gt;
→ generate manifest&lt;br&gt;
→ implement scene&lt;br&gt;
→ run public test&lt;br&gt;
How to Contribute&lt;br&gt;
The most useful contributions currently include:&lt;/li&gt;
&lt;li&gt;Mobile browser test reports&lt;/li&gt;
&lt;li&gt;Real-headset WebXR test reports&lt;/li&gt;
&lt;li&gt;Scene-manifest reviews&lt;/li&gt;
&lt;li&gt;glTF and GLB validation&lt;/li&gt;
&lt;li&gt;Asset optimization&lt;/li&gt;
&lt;li&gt;Accessibility testing&lt;/li&gt;
&lt;li&gt;Realtime load tests&lt;/li&gt;
&lt;li&gt;Security reviews&lt;/li&gt;
&lt;li&gt;CDN design&lt;/li&gt;
&lt;li&gt;Rollback implementation&lt;/li&gt;
&lt;li&gt;LIFE evidence modelling&lt;/li&gt;
&lt;li&gt;KPI/MRV data contracts&lt;/li&gt;
&lt;li&gt;Technical documentation&lt;br&gt;
A useful test report should contain:&lt;br&gt;
device&lt;br&gt;
operating system&lt;br&gt;
browser&lt;br&gt;
exact version&lt;br&gt;
network conditions&lt;br&gt;
steps performed&lt;br&gt;
expected result&lt;br&gt;
observed result&lt;br&gt;
logs or screenshots when safe&lt;br&gt;
related issue or pull request&lt;br&gt;
Do not include:&lt;/li&gt;
&lt;li&gt;Passwords&lt;/li&gt;
&lt;li&gt;Authentication tokens&lt;/li&gt;
&lt;li&gt;Private keys&lt;/li&gt;
&lt;li&gt;Wallet seed phrases&lt;/li&gt;
&lt;li&gt;Unredacted personal information&lt;/li&gt;
&lt;li&gt;Sensitive participant details&lt;/li&gt;
&lt;li&gt;Precise private locations&lt;br&gt;
Conclusion&lt;br&gt;
The MyZubster Metaverse now has two important engineering foundations:&lt;ol&gt;
&lt;li&gt;A browser-first capability layer where WebXR is progressive enhancement&lt;/li&gt;
&lt;li&gt;A server-validated scene contract that blocks invalid manifests from publication&lt;br&gt;
The completed work is publicly traceable:&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;PR #1597 — Browser and WebXR fallback&lt;/li&gt;
&lt;li&gt;PR #1601 — Versioned scene-manifest validation&lt;br&gt;
The next phase is not simply about adding more 3D content.&lt;br&gt;
It is about creating a reliable technical chain:&lt;br&gt;
code&lt;br&gt;
→ validated contract&lt;br&gt;
→ immutable version&lt;br&gt;
→ versioned asset&lt;br&gt;
→ reliable session&lt;br&gt;
→ real-world pilot&lt;br&gt;
→ evidence&lt;br&gt;
→ understandable visualization&lt;br&gt;
→ community contribution&lt;br&gt;
That is the direction of the MyZubster Metaverse: an open, browser-first environment connecting virtual experiences to public code, documented projects, explicit evidence boundaries, and real-world collaboration.&lt;br&gt;
Status verified: October 12, 2026.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;15:21&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30813/engineering-browser-first-metaverse-webxr-fallbacks-server-authoritative-rooms-versioned</guid>
<pubDate>Mon, 12 Oct 2026 13:23:15 +0000</pubDate>
</item>
</channel>
</rss>