Building MyZubster World: The Architecture We Have Implemented and What Is Still Missing

Leader 1 2 41
calendar_today agoschedule10 min read

Building MyZubster World: The Architecture We Have Implemented and What Is Still Missing
MyZubster started as a marketplace, but our long-term vision is broader: we want to build a circular ecosystem connecting digital identity, virtual communities, knowledge sharing, real-world projects, and privacy-conscious transactions.
That vision is becoming MyZubster World, an experimental metaverse connected directly to the MyZubster marketplace.
This is not a post announcing a finished metaverse. It is a technical development log explaining what is already implemented, how the main components work, and what still needs to be built before the platform can offer a complete immersive experience.
The architecture
The current system is divided into several layers:
React frontend

Express API

Authentication and authorization

Metaverse services

MongoDB persistence

Vercel deployment
The frontend renders the world, room controls, character state, marketplace destinations, and user feedback.
The backend remains authoritative for identity, access, room lifecycle, invitations, capacity, and session state. The browser can request an action, but it cannot declare that the action succeeded.
This distinction is important. Client-side controls can improve usability, but they cannot provide real security.
Verified character identity
One of the first problems we addressed was identity.
A character name stored only in localStorage is not a verified identity. Anyone can open the browser’s developer tools and change it.
For authenticated users, the backend now retrieves the character linked to the account:
const character = await MetaverseCharacter.findOne({
accountUserId: userId,
worldId: WORLD.id,
identityStatus: 'account-linked'
});
The server returns the canonical character profile:
{
"characterName": "H4x0r",
"identityStatus": "account-linked",
"archetype": "explorer",
"worldId": "neon-plaza"
}
The frontend replaces any stale local profile with this server-provided identity.
Guest identities remain possible, but they are explicitly treated as unverified:
const verified =
player.identityStatus === 'account-linked';
This gives us two distinct modes:
Guest → temporary and unverified
Account-linked character → persistent and verified
We do not treat a client-supplied name, GitHub username, or MYZ-ID as proof of identity.
Authentication recovery
We also implemented expired-session handling.
A common failure occurred when the frontend still contained an old JWT. The interface appeared authenticated, but protected API calls returned 401 Unauthorized.
The frontend now recognizes this state:
if (profileError.status === 401) {
localStorage.removeItem('myzubster-token');
setAuthenticated(false);
setMessage('Session expired. Please sign in again.');
}
This prevents the application from remaining trapped between authenticated and unauthenticated states.
The GitHub OAuth callback and production environment configuration were also reviewed so authenticated users can return to the metaverse after login.
Persistent mission progress
The metaverse includes landmarks that users can discover.
For authenticated characters, visited landmarks are recorded server-side:
await MetaverseCharacter.findOneAndUpdate(
{

accountUserId: userId,
worldId: WORLD.id,
identityStatus: 'account-linked'

},
{

$addToSet: {
  'missionProgress.visitedLandmarks': landmarkId
}

}
);
Using $addToSet prevents duplicate landmark entries.
This allows progress to persist across:

  • Browser refreshes
  • New sessions
  • Different devices
  • Local-storage deletion
    Local state is still used for responsiveness, but MongoDB is the durable source of truth for verified accounts.
    Shared presence
    MyZubster World currently uses database-backed synchronization rather than a permanent WebSocket connection.
    The frontend periodically sends the active session identifier:
    const result = await syncMetaverse(
    sessionId,
    cursor
    );
    The server returns:
    {
    "players": [],
    "messages": [],
    "online": 0,
    "cursor": "2026-09-15T00:00:00.000Z",
    "transport": "shared-polling"
    }
    The current shared state supports:
  • Player presence
  • Position updates
  • Join and leave events
  • Chat messages
  • Emotes
  • Online-player counts
  • Reconnection states
    The synchronization cycle is intentionally predictable:
    const SYNC_INTERVAL_MS = 1800;
    If a request temporarily fails, the frontend enters a reconnecting state and tries again instead of immediately removing the player.
    Why we currently use polling
    MyZubster is deployed in a serverless environment.
    Long-lived connections can be unreliable when requests are distributed between stateless instances. An in-memory event stream on one server instance may not be visible to another instance.
    For this reason, the current architecture favors shared persistence:
    Client A ─┐
        ├── API instances ── MongoDB
    

    Client B ─┘
    This is less immediate than a dedicated realtime server, but it provides a coherent shared state during the experimental phase.
    The older event-stream endpoint remains available for compatibility, but the current web client uses shared polling.
    Presence expiration
    Player presence must not remain online forever after a browser closes unexpectedly.
    Presence records therefore include expiration timestamps:
    {
    lastSeenAt: Date,
    expiresAt: Date
    }
    Each synchronization refreshes the expiration time. MongoDB’s expiration strategy removes abandoned presence records after their retention window.
    This handles cases such as:

  • Closing the browser without pressing “Leave”
  • Losing the network connection
  • Browser crashes
  • Mobile operating systems suspending the tab
    Chat and emotes
    The current world includes temporary chat and emote actions.
    Chat input is cleaned and limited before persistence:
    const text = cleanText(req.body?.text, 280);

if (!text) {
return res.status(400).json({

success: false,
error: 'Message is empty'

});
}
Actions also use rate limits to reduce spam:
if (!allowAction(sessionId, 'chat', 700)) {
return res.status(429).json({

success: false,
error: 'Chat rate exceeded'

});
}
Chat history is intentionally temporary. It is not designed as permanent public content.
Production logging excludes message bodies, authorization headers, tokens, query strings, and session identifiers.
Server-authoritative virtual rooms
We have implemented persistent virtual rooms with an explicit lifecycle:
Draft

Published

Scheduled

Live

Ended

Archived
The permitted transitions are represented on the backend:
const ROOM_TRANSITIONS = {
draft: new Set(['published']),
published: new Set(['scheduled', 'live']),
scheduled: new Set(['live']),
live: new Set(['ended']),
ended: new Set(['archive']),
archive: new Set()
};
A request attempting an invalid transition is rejected:
if (!ROOM_TRANSITIONS[room.state]?.has(nextState)) {
return {

valid: false,
status: 409,
error: `Invalid room transition`

};
}
The frontend cannot move a room from ended back to live, even if someone manually modifies the request.
Host permissions
Room-management actions verify the account on the server:
function canManage(
actorUserId,
actorRole,
hostUserId
) {
return Boolean(

actorUserId &&
(
  actorRole === 'admin' ||
  String(actorUserId) === String(hostUserId)
)

);
}
Only the room host or an administrator can:

  • Update room settings
  • Publish a room
  • Create a session
  • Start a session
  • End a session
  • Generate an invitation
  • Inspect invitation status
  • Revoke an invitation
    Showing or hiding a button in React is not the authorization mechanism. Every operation is validated again by the backend.
    Room access policies
    Rooms currently support three policies:
    accessPolicy: {
    type: String,
    enum: [ 'public', 'authenticated', 'private' ]
    }
    Public rooms
    Public rooms can be discovered without authentication.
    Authenticated rooms
    These rooms require a valid MyZubster account.
    Private rooms
    Private rooms require explicit authorization.
    Private rooms do not appear in the public discovery response:
    function roomDiscoveryQuery(authenticated) {
    return {
    state: {
    $in: ['published', 'scheduled', 'live']
    

    },
    accessPolicy: authenticated

    ? { $in: ['public', 'authenticated'] }
    : 'public'
    

    };
    }
    Even authenticated users cannot discover private rooms unless they have received access through the invitation system.
    Unauthorized requests return a generic 404 response. This reduces information leakage about the existence of private spaces.
    Room capacity
    Hosts can configure room capacity between 1 and 500 participants.
    The value is validated server-side:
    const capacity = Number(input.capacity);

if (
!Number.isInteger(capacity) ||
capacity < 1 ||
capacity > 500
) {
return {

valid: false,
status: 400,
error: 'Capacity must be between 1 and 500'

};
}
When a session is full, new participants cannot join. An existing participant, however, can reconnect without being rejected as an additional person.
Room access and capacity become locked after session scheduling. This avoids changing fundamental rules while a session is already underway.
Session participation
The platform now supports:
Create session

Start session

Participant joins

Participant leaves

Host ends session
When joining, the backend checks:

  • Authentication
  • Session state
  • Room blocklist
  • Private-room authorization
  • Capacity
  • Existing participation
    A simplified version of the logic looks like this:
    if (session.state !== 'live') {
    return deny('Session is not live');
    }

if (room.blockedUserIds.includes(userId)) {
return deny('Access blocked');
}

if (
room.accessPolicy === 'private' &&
!room.allowedUserIds.includes(userId) &&
userId !== room.hostUserId
) {
return deny('Private room access denied');
}

if (
!alreadyJoined &&
participantCount >= session.capacity
) {
return deny('Session is at capacity');
}
Short-lived realtime tokens
After authorization, the backend can generate a short-lived token for future room-scoped realtime connections:
jwt.sign(
{

sub: userId,
purpose: 'metaverse-realtime',
sessionId: session.sessionId,
roomId: session.roomId,
sceneManifestVersion:
  session.sceneManifestVersion

},
secret,
{

expiresIn: '5m'

}
);
These tokens expire after five minutes.
The current room page does not pretend this token launches a complete immersive realtime client. That integration is still pending.
Privacy-safe session history
Room sessions produce technical events:

  • session_created
  • session_started
  • participant_joined
  • participant_left
  • session_ended
    The frontend polls for new events using an incremental cursor.
    Public event representations contain aggregate information, such as participant count, but do not expose participant account IDs.
    {
    "type": "participant_joined",
    "participantCount": 4,
    "sequence": 12
    }
    Events are retained temporarily and are presented as operational history, not permanent user tracking.
    Private invitation links
    The latest implementation makes private rooms practically accessible.
    A host can generate an invitation code:
    const code = crypto
    .randomBytes(24)
    .toString('base64url');
    The raw code is returned once to the host. It is not stored in the database.
    Instead, the backend stores a SHA-256 hash:
    const hash = crypto
    .createHash('sha256')
    .update(code)
    .digest('hex');
    During redemption, the submitted code is hashed and compared using timingSafeEqual:
    const valid =
    expected.length === supplied.length &&
    crypto.timingSafeEqual(expected, supplied);
    The invitation:
  • Expires after 24 hours
  • Can be redeemed once
  • Can be revoked immediately
  • Is replaced when a new invitation is generated
  • Cannot override a blocklist
  • Survives the login redirect
  • Never exposes its stored hash through the status API
    After successful redemption, the authenticated account is added to the room allowlist and the invitation is cleared.
    Invitation management
    An host can inspect only safe invitation metadata:
    {
    "active": true,
    "expiresAt": "2026-09-16T00:00:00.000Z"
    }
    The response never includes:
  • The invitation code
  • Its hash
  • The invited user ID
  • The room blocklist
    The host can revoke the invitation through an authenticated DELETE request:
    DELETE /api/metaverse/rooms/:room/invitations
    After revocation, the old link can no longer authorize a new account.
    The connection with the marketplace
    MyZubster World is not intended to remain separate from the marketplace.
    The marketplace explores areas such as:
  • Underground and alternative subcultures
  • Kefir communities
  • Plant cultivation and documentation
  • University assistance
  • Creative and technical collaboration
  • Privacy-conscious services
    Our proposed cycle is:
    Discover

    Learn

    Collaborate

    Exchange

    Document

    Share

    Discover again
    A user may discover a cultivation project inside the metaverse, join its community room, contribute knowledge, request assistance through the marketplace, and document the result for other members.
    This is what we mean by a circular ecosystem: the virtual environment should lead to useful activity rather than functioning only as decoration.
    What is still missing
    The foundations are growing, but several important systems are not complete.
    1. Immersive room connection
      The authorization and session layers exist, but entering a room does not yet launch a dedicated immersive scene.
      We still need to connect:
      Authorized room session

      Short-lived realtime token

      Room-specific transport

      Immersive scene
      The scene must preserve the same access, capacity, and moderation rules enforced by the API.

    2. Dedicated realtime transport
      Database polling is appropriate for the current prototype, but larger rooms will require a dedicated realtime service.
      We need to evaluate:
  • WebSockets
  • Managed realtime infrastructure
  • Horizontal scaling
  • Reconnection strategies
  • Presence consistency
  • Regional latency
  • Abuse protection
    The migration should be driven by measurements, not by the assumption that WebSockets are automatically better.
    1. Participant moderation
      Hosts need privacy-conscious moderation tools.
      The roadmap includes:
  • Removing a participant
  • Blocking future access
  • Restoring access
  • Revoking private-room membership
  • Recording moderation events
  • Preventing immediate unauthorized re-entry
    We must provide these controls without publishing internal account identifiers.
    1. Stage requests
      Rooms already contain a stage policy:
      stagePolicy: {
      type: String,
      enum: [ 'host-only', 'host-approved' ]
      }
      However, the complete interaction is not yet implemented.
      The intended flow is:
      Participant requests to speak

      Host reviews the request

      Host approves or rejects

      Server assigns stage capability

      Capability expires or is revoked

    2. Room-specific chat
      The global prototype supports chat, but room-scoped communication still needs stronger boundaries.
      Messages should be associated with:
  • A room
  • A session
  • An authorized participant
  • A defined retention policy
  • Moderation state
    1. Marketplace destinations
      Marketplace categories currently need a deeper connection with the virtual world.
      Our plan is to turn categories into interactive destinations:
      Neon Plaza
      ├── Underground Culture
      ├── Kefir and Cultivation
      ├── University Collaboration
      ├── Creators
      └── Privacy Technology
      Each destination should load real marketplace information rather than static decorative content.
    2. Collaborative activities
      The long-term experience should support useful interaction:
  • Workshops
  • Study sessions
  • Community presentations
  • Cultivation journals
  • Project showcases
  • Mentoring
  • Marketplace demonstrations
    These activities still require data models, permissions, interfaces, and moderation rules.
    1. Automated testing
      We have policy and interface-wiring tests, but the project still needs broader coverage:
  • Integration tests with MongoDB
  • Authentication tests
  • Invitation race-condition tests
  • Capacity concurrency tests
  • OAuth callback tests
  • Browser end-to-end tests
  • Accessibility tests
  • Load testing
    One particularly important case is simultaneous redemption of a monouso invitation. The final production implementation should use an atomic database update so two requests cannot consume the same code at nearly the same time.
    1. Monitoring
      The application uses Vercel Analytics and deployment observability, but product-specific monitoring needs to improve.
      We want to measure:
  • Metaverse entry funnel
  • Successful authenticated joins
  • Room creation
  • Session starts
  • Invitation redemption
  • Marketplace transitions
  • Reconnection frequency
  • API errors and latency
    Analytics events must remain privacy-conscious and avoid collecting unnecessary identity or message data.
    1. Accessibility and mobile interaction
      The metaverse must remain usable outside a desktop keyboard environment.
      We still need:
  • Touch controls
  • Keyboard navigation
  • Reduced-motion support
  • Screen-reader labels
  • High-contrast states
  • Responsive room controls
  • Accessible chat
  • Performance testing on low-end devices
    1. Monero research
      We are evaluating Monero as a possible payment option for appropriate marketplace transactions.
      A production integration still needs research and implementation for:
  • Wallet architecture
  • Payment verification
  • Unique payment identification
  • Confirmation policies
  • Refunds
  • Disputes
  • Exchange-rate handling
  • Accounting
  • Regulatory responsibilities
  • Secret management
  • Operational security
    Monero payment support should therefore be considered a roadmap item, not a finished production feature.
    How we are building
    We are developing MyZubster through small, reviewable increments.
    Recent pull requests introduced:
  • Persistent mission progress
  • Canonical character identity
  • Expired-JWT recovery
  • Filtered room discovery
  • Authoritative room details
  • Room lifecycle management
  • Join and leave controls
  • Private-room authorization
  • Privacy-safe session events
  • Host room settings
  • Secure invitation links
  • Invitation status and revocation
    This approach lets us review each security and privacy boundary before adding more visual complexity.
    The infrastructure underneath the world may be less visible than a 3D animation, but it determines whether the final experience can be trusted.
    Explore MyZubster
  • Enter MyZubster World
  • Explore the Marketplace
  • Learn how MyZubster works
  • View the source code
    MyZubster World is still experimental, but its identity, access, lifecycle, invitation, persistence, and privacy foundations are now taking shape.
    We are building it publicly, one verifiable layer at a time.
    What would you implement next: immersive rooms, realtime transport, marketplace destinations, or host moderation?
🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

Building MyZubster World: The Metaverse Infrastructure We Have Implemented

Myzubster - Sep 15

7 Best Tools for Founders Building in Public (2026 Guide)

Udit060 - Jul 15

We’re Taking Zorgax From GitHub Into the Real World — And N4K48 Is One of the First Experiments

Myzubster - Sep 1

SEO-Friendly Web Design Checklist: Architecture Before Aesthetics

stepan-nikonov - Aug 30

What Is SARIF and How Does It Help Security Tools Work Together?

Ganesh Kumar - Jul 4
chevron_left
2.1k Points44 Badges
Rimini
48Posts
3Comments
15Connections

Related Jobs

View all jobs →

Commenters (This Week)

1 comment
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!