How Do Two Independent 3D Worlds Interoperate? Notes from Building Cross-World Transfer
federation3D world interoperabilitycross-world transferself-hosted 3D worldRS256 credentialnonce replay protectionreal client IP behind reverse proxy
Summary: When two 3D worlds are deployed on different people's servers, each with its own domain and account system, "moving a person from one world to another" is not a redirect link. These notes cover the four problems we hit in order while building federation transfer: how identity crosses a domain boundary, how credentials resist replay, what happens when two accounts share a name, and how a 200-line "over-engineered" module took every player down.
The most counter-intuitive thing about federation: the hardest part is not cryptography or protocol. It is the product question of what to do when two people have the same name.
1. First, separate "federation" from "one platform with many rooms"
These are routinely conflated, but architecturally they are different things:
| One platform, many rooms | Federation (independent worlds) | |
|---|---|---|
| Deployment | One service, one database | Separate deployments, domains and databases |
| Accounts | One user table | Each world has its own account system |
| Who decides | The platform | Each world's owner |
| Effect of an outage | Everything goes down | Only that world is affected |
| Interop difficulty | Almost none | Identity, assets, credentials, naming conflicts |
The reason to choose the second column is usually governance, not technology: no central party can switch your world off. The trade-off is everything in that right-hand column, all of which you now have to solve.
2. Problem one: how identity crosses a domain boundary
The laziest idea for moving a player from world A to world B is "let the two worlds share a user table". We did not take it, because once shared the two sides are no longer independent — A's administrator can see B's users, and if A's database goes down B cannot be entered either. Federation's entire point evaporates.
We went with one-time handover credentials:
- World A, after confirming the player's identity, signs a short-lived credential (we use RS256 asymmetric signing, so B only needs the public key to verify).
- The credential carries a nonce that can be consumed once — used and void, the same ticket cannot enter twice.
- The credential's lifetime is minutes, not days. Expired means void.
- Once B verifies the signature, it recognises (or creates) the person within its own system, then discards the ticket.
Three points deserve to be called out on their own:
Asymmetric signing rather than a shared secret. B needs only A's public key; A's private key never leaves its own server. That is what keeps the federation principle "the two worlds share no secret" intact.
The nonce must actually be checked. An awkward discovery along the way: our human login path had been generating a nonce that was never verified — the credential looked formal, but the anti-replay link was empty. Building federation is what forced us to close it. This class of "looks implemented, is not actually in effect" security code is very common, and it is worth checking your own login path for whether the nonce is really verified.
Keep credential lifetimes short. Crossing worlds is a "user clicks once and is gone" action; minutes are plenty. Setting it to hours leaves nothing but a long-lived forgery entry point.
3. Problem two: should "who I am" be persisted after crossing?
An easily overlooked design choice: for a session arriving by cross-world transfer, should the target world create a user and a character in its database?
Our answer is no — a transient session that exists only for the lifetime of that connection. Acceptance is a direct comparison of the users and characters row counts before and after the transfer: unchanged either way.
Why this matters:
- It does not pollute the target world's data. Otherwise every visitor leaves a "shadow account" behind, until the user table is full of records nobody can manage.
- It matches the semantics of federation. That person's identity still belongs to their original world. The target world is hosting, not absorbing.
- It keeps the privacy surface clean. A transient session leaves no trace; a target world's administrator should not receive your account details just because you passed through.
The cost: if the visitor wants to stay in the target world long-term, they have to create an account there explicitly. That friction is intentional.
4. Problem three: name collisions — the bug that demoted players to guests
This is where we cut ourselves deepest, and it is the archetype of a product problem being harder than a technical one.
The scenario: a player's nickname in world A is "Old Wang", and world B also has an "Old Wang" — B's own user, unrelated to A's visitor.
The old implementation's chain of events:
target world tries to create an account with the same nickname
-> unique key violation, server returns 500
-> front end sees the 500 and shows a login box
-> the player has no account in this unfamiliar world, login fails
-> the player becomes a guest
A naming collision ended with the player demoted to a guest. What the user experiences is "I transferred over, and I disappeared".
The new chain:
- Prefer reusing an existing account in the target world matched by email (the same person should be reused, not recreated).
- If the email does not match either, probe candidate nicknames one by one in the target world and pick a free one.
- If creation still hits a unique-key collision, catch it and retry with an automatically adjusted name, rather than surfacing the error to the front end.
Acceptance criterion: same nickname but different email, 4/4 PASS. The wording of the criterion is worth copying too — test the "same name" boundary specifically, because the normal path never covers it and it only ever fires on real users.
5. Problem four: the 200 lines that took every player down
This is the most instructive section of the whole exercise.
Symptom: after a federated transfer, the world's assets would not load. The cause turned out to be the browser's mixed-content block — the page is https and the world assets being fetched were http, so the browser refused outright.
How we fixed it the first time: we wrote roughly 200 lines of URL-normalisation, intending to rewrite every address form consistently.
Result: it broke path shapes, and every player's asset loading failed.
The lesson: a module that "normalises all input" is dangerous precisely because it acts on all traffic — one misjudged edge case does not break a feature, it breaks everyone. And what we actually had to solve was a very narrow problem.
The final solution: correct the protocol header only — one pure function, one line added at each of three load points. That is all. Acceptance: pure function 15/15, browser end-to-end 10/10, main-world smoke test 9/9.
Two lessons hardened out of this:
- Ask "what is the smallest correct change" before "how do we generalise this elegantly". Especially when the change acts on all traffic, generalisation is a risk, not a bonus.
- Be willing to fully revert a large change that went the wrong way. We deleted the 200 lines entirely, leaving no "maybe useful later" dead code — leave it there and the next person will wire it back in.
6. Two more traps that bite if you do not write them down
① Rate-limit identity behind a reverse proxy
Rate limiting is done per IP, but if you take the peer address of the connection directly, then behind a reverse proxy the entire world counts as a single IP — and your rate limiter will take out all users at once. The correct approach is to read the real client IP from X-Real-IP or the last segment of X-Forwarded-For. This bug does not appear under load testing (which usually connects directly) — only in production behind a proxy.
② Wide-open CORS is a design requirement, not a vulnerability
In a federation, the worlds are deployed on different domains, so cross-origin requests are ordinary business traffic. Opening CORS up is therefore an architectural requirement, not developer convenience.
The lesson is not "should we open it" but: it must be written into the architecture document with the reasoning. Otherwise the next person to take over — or the next security review — sees "production CORS is *", concludes "that is a vulnerability, fix it", and federation goes down. A design decision that is never explained in writing will eventually be fixed as a bug.
7. Who federation suits, and who it does not
| Scenario | Verdict |
|---|---|
| Several independent organisations each run a world but want users to visit each other | Fits — this is exactly what federation is for |
| You run everything yourself and always will | Skip federation for now. Make the single-world experience solid first |
| Using federation to solve "unified accounts" | Do not. That is an SSO problem, not a federation problem |
| Needing to migrate assets (inventory, models) across worlds | What transfers today is identity and session, not assets. Do not conflate them |
| Users flipping frequently between worlds | Be careful. Every crossing builds a new session, and frequent switching needs its own design |
8. Frequently asked questions
How are federation transfer credentials protected against forgery?
Asymmetric signing (RS256), so the verifying side needs only the public key; a credential lifetime measured in minutes; a nonce inside the credential that is consumed exactly once. All three are required — and the nonce check especially must actually run.
After transferring, does the other world get my user data?
Our implementation uses a transient session that creates no user and no character in the target world's database (verifiable by comparing table row counts before and after). Identity ownership stays with the original world.
What happens when two worlds have the same nickname?
Reuse an existing account by email first; if the email does not match, probe available candidate nicknames; if creation hits a unique-key violation, adjust the name and retry. The key is not to surface that error to the user — otherwise what they see is "transfer failed" or "I turned into a guest".
Why not share a single user table?
Because sharing ends the two worlds' independence: one side can see the other's users, either can take the other offline, and one administrator can manage the other's people. The value of federation is precisely that neither is subordinate to the other.
Can I bring things across worlds?
To be clear — what crosses is identity and session, not assets. Migrating inventories and models is an entirely separate problem. Do not ask the two in one sentence when evaluating options.
About Genesis
Genesis is a self-hosted 3D virtual world system built on Three.js + WebGL, helping individuals and businesses build their own 3D spaces. Accessible directly from a browser, compatible with both PC and mobile, it supports multiplayer online, federated teleportation, a shop system, and Agent integration—where an AI can enter your world as an embodied character. Your data runs on your own server, never passing through a third-party platform—so every world truly belongs to its owner.
If you are weighing options for multi-world interoperability, these notes come from building Genesis — a browser-side Three.js 3D virtual world foundation that runs on your own servers. The figures come from re-runnable acceptance scripts in the repository.
These notes come from our own development work. The figures come from in-repo acceptance scripts (same-name collision 4/4; protocol fix 15/15, end-to-end 10/10, smoke 9/9) and can be re-run.
Source Code and Repositories
- Gitee (faster from mainland China): https://gitee.com/miduoxinxijeji/miduo.git
- GitCode (mirror for mainland China): https://gitcode.com/qq_35054471/virtual-world
- GitHub: https://github.com/miduo100/3d-virtual-world
All three carry the same content; the first two are faster to reach from mainland China. The repositories contain the deployment guide and a demo entry point.