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.
axel.land is a multiplayer virtual world that runs entirely in the browser. Walk through persistent 3D spaces, chat with other visitors, and see user-placed objects load around you as you move — no client download, no plugin.
It is a modern remake of ActiveWorlds: the same idea of explorable worlds connected through a universe server, but rebuilt for the web with WebTransport, a custom game protocol, and a stack split across four services.
Virtual worlds from the late 1990s and early 2000s proved that persistent shared spaces could feel magical — wandering into someone else’s build, reading a sign they left years ago, bumping into a stranger in a plaza. Most of that infrastructure never made it to the modern web. Today’s browser games tend to be isolated experiences or thin wrappers around native engines.
I wanted to see whether a full multiplayer world — streaming terrain, physics, other players, and user content — could run in a tab with nothing installed. That meant designing the whole stack from scratch: authentication, world routing, server simulation, and a client fast enough to keep up.
The experience is deliberately simple to enter:
Worlds are made of sectors — 128-meter cubes of terrain and objects that stream in and out as you move. Props can carry surfaces: text signs, picture panels, and custom materials. Chat is proximity-based within your current sector, so conversations feel local rather than global.
The system is split into four services that each do one job well:
Browser (web) → auth API → accounts & sessions
↓
universe server → routes players to the right world
↓
world server → simulates physics, entities, and world dataThe authentication API is a TypeScript service built on better-auth with MongoDB for persistence and Redis for session storage.
It handles email/password login with verification, password reset, and anonymous tourist sessions for visitors who want to try the 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 — 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.
SayRequest) is validated, rate-limited, and delivered to other clients in the same sectorModels are shared resources: many props can reference the same GLB, loaded once and reference-counted until no longer needed. When a sector has no nearby players, it idles for thirty seconds and then unloads.
The axel.land 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 sector full of props is loading.
Control messages — login, enter world, teleport, sector queries, chat — use Protocol Buffers. High-frequency data uses purpose-built binary formats:
The client requires WebTransport, SharedArrayBuffer, and OffscreenCanvas — features that only recently became viable across major browsers. An unsupported-browser gate explains the requirements instead of failing silently.
Remaking ActiveWorlds for the web forced trade-offs the original never had to make. ActiveWorlds could assume a native client and a proprietary protocol; axel.land has to earn every millisecond inside sandboxed tabs with strict memory limits.
The hardest problems were not rendering — Three.js handles that well — but 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. Splitting the universe from the world server was the key architectural decision. It mirrors how ActiveWorlds separated universe login from world hosting, and it keeps world simulation isolated so one busy world does not stall routing for everyone else.
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.
Client (web)
Services