axel.land
A browser-based remake of ActiveWorlds. Explore 3D worlds, meet other players, and build.
A browser-based remake of ActiveWorlds. Explore 3D worlds, meet other players, and build.
Virtual worlds from the late 1990s proved that persistent shared spaces could feel magical. You wandered into someone else’s build, read a sign they left years ago, bumped into a stranger in a plaza. Most of that infrastructure never made it to the modern web. Today’s browser games are usually isolated experiences or thin wrappers around native engines.
axel.land is the bet that a full multiplayer world can run in a tab with nothing installed. It is a modern remake of ActiveWorlds: explorable worlds connected through a universe server, rebuilt for the web with WebTransport, a custom game protocol, and a stack split across four services.
ActiveWorlds could assume a native client and a proprietary protocol. A browser remake has to earn every millisecond inside sandboxed tabs with strict memory limits. You need streaming terrain, physics, other players, and user content, all without freezing the page while a sector full of props loads.
I wanted to see whether that whole stack could live on the web: authentication, world routing, server simulation, and a client fast enough to keep up. Not a tech demo of a plaza. A real world you can enter, explore, and share.
The experience is deliberately simple to enter:
Three things define the product:
Worlds are made of sectors: 128-meter cubes of terrain and objects that stream in and out as you move. Props can carry surfaces such as text signs, picture panels, and custom materials. Chat is proximity-based within your current sector.
The auth API is a TypeScript service built on better-auth, with MongoDB for persistence and Redis for sessions.
It handles email/password login, verification, password reset, and anonymous tourist sessions for visitors who want to try a world without registering. Registered users pick a username and become citizens. Worlds are modeled as organizations, each with API keys that world servers use to authenticate back to the platform.
Cloudflare Turnstile protects sensitive endpoints. JWTs issued here are what the universe and world servers trust when a client connects.
The universe server is a Rust Axum service that sits between clients and world servers. Players connect over WebSocket with their auth token. World servers connect on a separate channel.
When a player requests entry into a world, the universe forwards that request to the matching world server. The world server responds with a WebTransport URI and a short-lived session token. The universe relays that back to the client, which then opens a direct connection to the world. After that handoff, the universe steps out of the hot path for gameplay.
Session state and world registry data live in Redis, so multiple universe instances can share presence and routing.
Each world runs as an independent Rust process built on Bevy ECS. This is where the game actually happens.
Models are shared resources. Many props can reference the same GLB, loaded once and reference-counted until nothing needs it. When a sector has no nearby players, it idles briefly and then unloads.
The web client is an Astro shell around a React application. Almost all of the game runs off the main thread in dedicated Web Workers:
The main thread owns a bitecs ECS world and SharedArrayBuffer views that workers read and write each frame. Input is sampled on the main thread, encoded as compact binary commands, and sent to the server. Remote players are interpolated between server snapshots.
This layout exists because browsers punish the main thread. Rendering, physics, and networking all compete for the same budget. Pushing them into workers keeps interaction responsive even when a busy sector is loading.
Control messages such as login, enter world, teleport, sector queries, and chat use Protocol Buffers. High-frequency data uses purpose-built binary formats:
The client requires WebTransport, SharedArrayBuffer, and OffscreenCanvas. An unsupported-browser gate explains those requirements instead of failing silently.
Browser (web) → auth API → accounts & sessions
↓
universe server → routes players to the right world
↓
world server → simulates physics, entities, and world dataSplitting the universe from the world server was the key architectural decision. It mirrors how ActiveWorlds separated universe login from world hosting, and it keeps simulation isolated so one busy world does not stall routing for everyone else.
Remaking ActiveWorlds for the web forced trade-offs the original never had to make. The hardest problems were not rendering. Three.js handles that well. The hard part was coordination: getting four services to agree on identity, routing a player to the right world server, and keeping entity state consistent when packets arrive out of order.
Sector streaming with reference counting turned out to be the right granularity for persistence and performance. Worlds can grow large without holding everything in memory, and SQLite per sector keeps load times predictable.
A multiplayer world in a browser is less about one clever library and more about drawing hard lines: what belongs on the main thread, what belongs in a worker, what belongs on the server, and what should never block a frame.
Client
Services