Building MyZubster World: What We Have Implemented and What Comes Next
MyZubster is evolving from a marketplace into a circular digital ecosystem connecting verified identities, virtual communities, knowledge sharing, real-world projects, and privacy-conscious services.
The virtual layer of this ecosystem is called MyZubster World.
MyZubster World is still experimental. We are not presenting it as a finished metaverse. However, its identity, access control, room lifecycle, privacy, moderation, scheduling, and communication foundations are already taking shape.
In this development update, I want to explain what we have implemented, how it works, and what we still want to build.
Try the project
The following links contain UTM parameters that can be measured through Vercel Analytics.
Enter MyZubster World:
https://www.myzubster.com/metaverse?utm_source=devto&utm_medium=article&utm_campaign=myzubster_metaverse_build&utm_content=enter_metaverse
Explore the MyZubster Marketplace:
https://www.myzubster.com/marketplace?utm_source=devto&utm_medium=article&utm_campaign=myzubster_metaverse_build&utm_content=explore_marketplace
Learn how MyZubster works:
https://www.myzubster.com/come-funziona?utm_source=devto&utm_medium=article&utm_campaign=myzubster_metaverse_build&utm_content=how_it_works
Explore the open-source repository:
https://github.com/MyZubster-Ecosystem/myzubster?utm_source=devto&utm_medium=article&utm_campaign=myzubster_metaverse_build&utm_content=github_repository
These URLs remain readable and clickable when copied into a DEV Community article. The utm_source, utm_medium, utm_campaign, and utm_content parameters help distinguish traffic generated by each link.
The current architecture
MyZubster World currently uses the following architecture:
React frontend
↓
Express API
↓
Authentication and authorization
↓
Metaverse services
↓
MongoDB persistence
↓
Vercel deployment and analytics
The frontend manages the interface, room controls, movement, forms, chat presentation, and user feedback.
The backend remains authoritative for:
- Character identity
- Room access
- Session lifecycle
- Private invitations
- Capacity
- Scheduling
- Participant moderation
- Stage permissions
- Room chat
- Message moderation The browser can request an operation, but the server decides whether it is permitted. Verified metaverse identities A character name stored only in the browser cannot be considered a verified identity. Anyone can open the browser developer tools and modify localStorage. For authenticated accounts, the backend loads the canonical character from MongoDB: const character = await MetaverseCharacter.findOne({ accountUserId: userId, worldId: WORLD.id, identityStatus: "account-linked" }); The frontend then replaces any outdated local profile with the server-provided character. The platform distinguishes between two identity modes: Guest character └── Temporary and unverified
Account-linked character
└── Persistent and verified
A client-supplied character name, GitHub username, or MYZ-ID is not treated as proof of ownership.
Authentication recovery
We implemented explicit recovery for expired JWT sessions.
if (error.status === 401) {
localStorage.removeItem("myzubster-token");
setAuthenticated(false);
setMessage(
"Session expired. Please sign in again."
);
}
This prevents the application from remaining trapped in an invalid authenticated state.
The login flow also preserves private-room invitation links. A user can open an invitation, authenticate, and return to the same invitation without losing its code.
Persistent mission progress
Verified characters can discover landmarks inside MyZubster World.
Visited landmarks are stored on the server:
await MetaverseCharacter.findOneAndUpdate(
{
accountUserId: userId,
worldId: WORLD.id,
identityStatus: "account-linked"
},
{
$addToSet: {
"missionProgress.visitedLandmarks":
landmarkId
}
}
);
Using $addToSet prevents duplicate landmark records.
Progress can survive:
- Browser refreshes
- New sessions
- Local-storage deletion
- Device changes Shared presence The current metaverse prototype supports shared presence through database-backed synchronization. Players can:
- Enter and leave the world
- See other online characters
- Move around the shared environment
- Send temporary chat messages
- Use emotes
- Reconnect after temporary network failures The client periodically requests the latest world state: const result = await syncMetaverse( sessionId, cursor ); The cursor allows the server to return only new information. Why we currently use polling MyZubster is deployed in a serverless environment. Long-lived connections can become unreliable when requests are distributed between stateless instances. An in-memory connection owned by one instance may not be visible to another. Our current architecture therefore uses shared persistence: Client A ─┐ ├── API instances ── MongoDB Client B ─┘ This is a pragmatic implementation for the experimental phase. A dedicated realtime transport remains part of the roadmap. Presence expiration Player-presence records contain an expiration timestamp. Every successful synchronization renews the player heartbeat. If the browser closes or loses its connection, the abandoned presence eventually expires. This prevents characters from remaining online forever after:
- Browser crashes
- Closed tabs
- Network failures
- Suspended mobile sessions Server-authoritative virtual rooms MyZubster World now supports persistent virtual rooms with a controlled lifecycle: Draft ↓ Published ↓ Scheduled ↓ Live ↓ Ended ↓ Archived Permitted transitions are defined 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() }; Changing React state or manually constructing an API request cannot move an ended room back into a live state. Host controls Room hosts can currently:
- Create a room draft
- Select the access policy
- Configure capacity
- Configure the stage policy
- Set an optional session date
- Publish a room
- Create a session
- Start a session
- Cancel a scheduled session
- End a live session
- Generate private invitations
- Revoke private invitations
- Remove participants
- Block participants
- Restore blocked participants
- Approve speaking requests
- Revoke stage access
- Delete room-chat messages Every operation is authorized again by the server: function canManage( actorUserId, actorRole, hostUserId ) { return Boolean( actorUserId && ( actorRole === "admin" || String(actorUserId) === String(hostUserId) ) ); } Hiding a button in React is not considered a security mechanism. Room access policies Rooms support three access policies: Public Authenticated Private Public rooms Public rooms can be discovered without authentication. Authenticated rooms Authenticated rooms require a valid MyZubster account. Private rooms Private rooms require explicit authorization. They are excluded from public discovery and from normal authenticated-room results. Unauthorized requests receive a generic 404 response. This reduces the amount of information revealed about the existence of private spaces. Room capacity Hosts can configure a capacity between 1 and 500 participants. The backend validates the value: if ( !Number.isInteger(capacity) || capacity < 1 || capacity > 500 ) { return { valid: false, status: 400, error: "Capacity must be between 1 and 500" }; } New participants cannot enter a full session. Existing participants can reconnect without being counted as additional users. Structural settings become locked after the session is scheduled. Secure private invitations Private rooms support invitation links generated using cryptographically secure randomness: const code = crypto .randomBytes(24) .toString("base64url"); The raw invitation code is returned to the host only when it is generated. The database stores a SHA-256 hash instead: const hash = crypto .createHash("sha256") .update(code) .digest("hex"); An invitation:
- Expires after 24 hours
- Can be redeemed once
- Can be revoked immediately
- Is replaced when a new invitation is generated
- Cannot override the room blocklist
- Survives the login redirect
- Is never stored in plain text Atomic invitation redemption One-time invitations must remain one-time when multiple requests arrive simultaneously. We implemented redemption as a conditional atomic MongoDB update: const claimedRoom = await VirtualRoom.findOneAndUpdate( { roomId, accessPolicy: "private", inviteTokenHash: suppliedHash, inviteExpiresAt: { $gt: new Date() }, blockedUserIds: { $ne: actorUserId } }, { $addToSet: { allowedUserIds: actorUserId }, $set: { inviteTokenHash: null, inviteExpiresAt: null } }, { new: true } ); The same database operation:
- Verifies the invitation hash.
- Verifies the expiration date.
- Checks that the account is not blocked.
- Adds the account to the room allowlist.
- Deletes the invitation. If two people submit the same link at the same time, only one request can consume it. Invitation status and revocation Hosts can inspect safe invitation metadata: { "active": true, "expiresAt": "2026-09-16T10:00:00.000Z" } The status API never returns:
- The invitation code
- The stored hash
- Account identifiers
- The room allowlist
Hosts can revoke an active invitation at any time. The invitation state is restored correctly after reloading the page.
Participant moderation
Hosts can see a privacy-conscious participant list.
The frontend receives data similar to:
{
"ref": "8af39a3d42b781b821003c14",
"characterName": "ExampleCharacter",
"archetype": "explorer"
}
The internal account ID is never sent to the frontend.
The opaque reference is specific to the current session:
function moderationParticipantRef(
sessionId,
userId
) {
return sha256(
${sessionId}:${userId}).slice(0, 24); } The host can: - Remove a participant
- Remove and block a participant
- Prevent immediate re-entry
- Remove private-room authorization
- Restore access later
The host cannot remove itself through the participant-moderation controls.
Privacy-safe blocklist controls
Blocked participants are represented using room-specific opaque references:
function blockedParticipantRef(
roomId,
userId
) {
return sha256(
blocklist:${roomId}:${userId}).slice(0, 24); } The host sees only the character name and archetype. Removing a participant from the blocklist does not automatically restore private-room membership. The account must receive and redeem another invitation. Moderated stage Rooms support two stage policies: Host only Host-approved speakers In a host-approved room, a joined participant can request permission to speak. Participant requests access ↓ Host receives the request ↓ Host approves or rejects ↓ Participant status updates The host reviews requests using opaque session references rather than account IDs. Complete stage lifecycle The moderated stage now supports: - Requesting permission to speak
- Cancelling a pending request
- Approving a request
- Rejecting a request
- Leaving the stage voluntarily
- Listing active speakers
- Revoking speaker permission Stage status is synchronized automatically. When a participant leaves, is removed, or is blocked, pending requests and speaker permissions are also removed. Repeated stage-exit calls are idempotent and do not generate duplicate lifecycle events. Session scheduling Hosts can assign an optional date and time to a room. The browser converts the local date to an ISO timestamp: const scheduledFor = new Date(localValue).toISOString(); The backend rejects:
- Invalid dates
- Dates in the past
- Attempts to modify the schedule after session creation A session cannot start before its scheduled time, even if someone bypasses the interface and calls the API directly. Rooms without a configured date can start immediately. Safe session cancellation A scheduled session can be cancelled before it starts. Cancellation:
- Archives the scheduled session
- Returns the room to its published state
- Restores the room settings
- Allows the host to correct and reschedule
- Generates a session_cancelled event Starting and cancelling both use conditional database updates. Only one operation can claim a scheduled session. If start and cancel arrive simultaneously, the second operation receives a conflict response. Automatic room synchronization Room pages automatically retrieve their authoritative state every five seconds. Users can see, without reloading:
- Session start
- Session cancellation
- Participant-count changes
- Their join status
- Session completion
- Timeline changes Cancelled and archived sessions are excluded from current-session lookup. The latest completed session remains visible so its history can still be inspected. Privacy-safe session history Sessions generate operational events such as: session_created session_started participant_joined participant_left participant_removed participant_blocked stage_approved stage_rejected stage_left stage_revoked session_cancelled session_ended The public timeline contains aggregate information: { "type": "participant_joined", "participantCount": 4, "sequence": 12 } Participant account IDs are not exposed. Private room-scoped chat Every virtual session now has its own isolated chat. Chat access requires:
- Authentication
- Membership in the session or host capability
- Absence from the blocklist
Writing is permitted only while the session is live.
Messages are stored under both room and session namespaces:
{
worldId:
virtual-room:${roomId}, sessionId, senderUserId, characterName, text, createdAt, expiresAt } The public response contains only: { "id": "message-id", "characterName": "H4x0r", "text": "Hello room", "createdAt": "2026-09-15T10:00:00.000Z" } Account, room, and internal session identifiers remain hidden. Chat safety and retention Room messages are: - Trimmed and sanitized
- Limited to 280 characters
- Rate-controlled
- Retrieved incrementally
- Deduplicated by the client
- Rendered as text rather than HTML
- Automatically deleted after 24 hours The client keeps only the most recent 100 messages in local state. Chat moderation Hosts and administrators can remove individual room-chat messages. Deletion is scoped to: Message ID
- Session ID
- Room namespace Knowing the ID of a message from another room is not enough to delete it. The frontend does not need the sender’s account ID to perform moderation. What we still want to build A dedicated realtime transport Movement, presence, rooms, stage status, and chat currently rely on polling. A future realtime layer should support:
- WebSockets or managed realtime channels
- Room-specific broadcasts
- Faster movement synchronization
- Immediate moderation events
- Reliable reconnection
- Horizontal scaling
- Regional routing Immersive room scenes The room-authorization and lifecycle systems exist, but joining a session does not yet launch a complete dedicated immersive scene. The planned connection is: Authorized session ↓ Short-lived room token ↓ Room-specific realtime channel ↓ Immersive scene Audio and video The stage currently manages speaking permission as server-side state. It does not yet activate a production audio or video transport. A WebRTC implementation will require:
- Media permissions
- Device selection
- Speaker enforcement
- Media-server architecture
- Network recovery
- Abuse protection
- Privacy documentation Participant message reporting Hosts can delete chat messages, but participants cannot yet report inappropriate content. A responsible reporting workflow needs:
- Report categories
- Minimal evidence retention
- Abuse prevention
- Human review
- Appeals
- Privacy boundaries Stronger distributed rate limiting The current chat includes a basic recent-message check. A production version should use atomic distributed rate limiting to prevent concurrent requests from bypassing the limit. Marketplace destinations The long-term objective is to connect virtual locations to real marketplace areas: Neon Plaza ├── Underground Culture ├── Kefir and Cultivation ├── University Collaboration ├── Creators └── Privacy Technology Each destination should load real marketplace information rather than functioning as a decorative link. Monero research We are evaluating Monero for suitable marketplace transactions. A production integration still requires work on:
- Wallet architecture
- Payment verification
- Confirmations
- Refunds and disputes
- Exchange-rate handling
- Accounting
- Secret management
- Operational security
- Regulatory responsibilities Monero remains a development objective, not a completed payment feature. Measuring this DEV article with Vercel Analytics The links in this post use the following campaign: utm_source=devto utm_medium=article utm_campaign=myzubster_metaverse_build Each destination also has a different utm_content value: enter_metaverse explore_marketplace how_it_works github_repository This makes it possible to distinguish which call to action generated the visit. You can inspect the resulting traffic in the MyZubster Vercel Analytics dashboard: https://vercel.com/myzubster/my-zubster-app/analytics The Vercel dashboard is intended for authorized project members. Visitors should use the public MyZubster links instead. Follow the development Enter the metaverse: https://www.myzubster.com/metaverse?utm_source=devto&utm_medium=article&utm_campaign=myzubster_metaverse_build&utm_content=final_metaverse_cta Explore the marketplace: https://www.myzubster.com/marketplace?utm_source=devto&utm_medium=article&utm_campaign=myzubster_metaverse_build&utm_content=final_marketplace_cta Read how the ecosystem works: https://www.myzubster.com/come-funziona?utm_source=devto&utm_medium=article&utm_campaign=myzubster_metaverse_build&utm_content=final_how_it_works_cta View the source code: https://github.com/MyZubster-Ecosystem/myzubster?utm_source=devto&utm_medium=article&utm_campaign=myzubster_metaverse_build&utm_content=final_github_cta MyZubster World remains experimental, but it is becoming a real server-authoritative platform rather than a purely visual demonstration. We are building it publicly, one verifiable layer at a time. What should we implement next: immersive rooms, dedicated realtime infrastructure, participant reporting, or marketplace destinations? Suggested DEV tags: #opensource #webdev #javascript #privacy
Top comments (0)