Pixel Eruv is an open-source platform for building persistent, pixel-art virtual workspaces where distributed teams can meet, collaborate, and communicate naturally.
Inspired by platforms like Gather, ZEP and WorkAdventure, but designed from the ground up as a modern, extensible platform, Pixel Eruv combines the familiarity of a top-down multiplayer world with enterprise collaboration tools.
Instead of switching between chat applications, video meetings and shared documents, teams interact inside a shared virtual office where communication is driven by proximity and collaboration happens naturally.
β¨ Features (MVP)
- πΊοΈ Persistent multiplayer pixel-art worlds (single world per deployment)
- ποΈ Proximity-based audio and video powered by LiveKit
- π¬ Real-time chat and presence (PocketBase-backed)
- π’ Offices and maps within a world
- π Authentication via PocketBase (email/password, email verification, OAuth2 social login)
- π¨ Tiled map support
- π Extension system β add NPCs, custom behaviors, and objects as separate programs in any language
- β‘ High-performance Go backend
- π Self-hostable via Docker Compose (no Kubernetes required)
- π Audit log + observability β searchable audit event history (HTMX UI) + OpenTelemetry traces (motel dev / OpenObserve optional)
- π Admin portal β unified email/password login via PocketBase, protects PB admin + audit UI behind a single auth gate
π§ Roadmap (post-MVP)
- π€ AI assistants and NPC agents (built on the extension system)
- π’ Multi-organization support (organizations β worlds β maps)
- π¬ Matrix Synapse chat (federation, rich clients, E2EE)
- π± Plant growth, user inventories, owned workplaces, whiteboard objects
ποΈ Architecture
Pixel Eruv is built around a server-authoritative architecture inspired by modern multiplayer games.
The backend is responsible for world simulation, entity replication, permissions and persistence, while the frontend combines a Phaser 4 renderer for the virtual world.
Audio, video and screen sharing are delegated to LiveKit, allowing the simulation engine to remain focused on the virtual environment.
Core technologies include:
- Go for backend services
- Phaser 4 for world rendering
- LiveKit for media
- Protocol Buffers for networking
- PocketBase for durable data (users, world config, audit logs)
- NATS for internal event distribution and reactive state
- SeaweedFS / RustFS for asset storage
π Philosophy
Pixel Eruv is not just another Gather clone.
It is a platform for building spatial collaboration applications.
The virtual office is only one possible client. The same backend can power desktop applications, mobile clients, AI agents, accessibility-focused interfaces, or entirely different visualizations of the same shared world.
By separating simulation, communication and presentation, Pixel Eruv aims to become the open foundation for the next generation of collaborative software.
π οΈ Getting Started
The lite MVP runs a single world: NATS for the bus, a Pusher (WebSocket proxy), a World Simulator (authoritative tick loop + replication), and a Phaser frontend served by nginx. Two ways to run it:
Deploying to a server? See
docs/quick-start.mdβ a step-by-step admin guide covering copyingdist/to a remote host, TLS via a host nginx proxy, login credentials, and map design/upload.
Prerequisites
- Docker and Docker Compose (for the bundled path), or
- Go 1.26+, Node 22+, and a NATS server (for the native/dev path)
Bundled (Docker Compose)
make up
This builds the Pusher and World Sim images, starts NATS, and serves the frontend with nginx. Open http://localhost:4080 β you should see a 20Γ20 tile world. Move with the arrow keys; each browser tab is a player.
For remote access the same compose file also exposes HTTPS on port 4043
with a self-signed cert generated at startup. Set PUBLIC_HOST to the host's
LAN IP or hostname and rebuild β one variable drives the TLS cert SAN, the
email verification URL (APP_URL), and the LiveKit public URL:
PUBLIC_HOST=192.168.1.10 make up
Then open https://192.168.1.10:4043 and accept the self-signed cert warning
once. See the Quick Start for Admins for putting a real
TLS certificate and a host nginx proxy in front.
To stop: make down. To tail logs: make logs.
Self-contained dist/ (no source needed on the host)
make dist-x86 # linux/amd64 β for Docker deployment on a server
make dist-macos # darwin/arm64 β for native macOS execution
This stages pre-built binaries, web assets, Dockerfiles, compose,
nginx.conf, livekit.yaml, and PocketBase migrations into
dist/. Copy the whole dist/ directory to another machine and run:
docker compose -f dist/docker-compose.yml up --build
The frontend is served on http://<host>:4080 (HTTP) and https://<host>:4043
(HTTPS, self-signed). For remote browsers, set PUBLIC_HOST=<host-ip> and use
the HTTPS endpoint β the auth flow needs a secure context. See the
Quick Start for putting a real TLS certificate in front.
Native binaries + dev server
Build the backend and frontend:
make build # β dist/bin/pusher, dist/bin/worldsim
make web # β dist/web/
Start a NATS server (e.g. docker run -p 4222:4222 nats:2.10-alpine -js),
then run the two binaries against it:
NATS_URL=nats://localhost:4222 ./dist/bin/worldsim &
NATS_URL=nats://localhost:4222 WS_ADDR=:8081 ./dist/bin/pusher &
For frontend development with hot reload, use the Vite dev server (it proxies
/ws to the Pusher on :8081):
cd frontend && npx vite
Open http://localhost:5173.
To serve the built frontend natively instead, point an nginx instance at
dist/web/ with docker/nginx.conf (change the proxy_pass upstream
from pusher:8081 to 127.0.0.1:8081 for a non-Docker host).
ποΈ LiveKit node IP
- Dev (now):
LIVEKIT_NODE_IPunset β defaults to127.0.0.1β works because Docker maps ports to your host. - Production: set
LIVEKIT_NODE_IP=<your-public-ip>(env var or.envfile) β LiveKit advertises that IP in ICE candidates β browsers on the internet can route media back to it.
π Remote access (PUBLIC_HOST)
A single env var drives everything remote browsers need:
| Var | Default | Purpose |
|---|---|---|
PUBLIC_HOST |
localhost |
Host IP/hostname remote browsers use to reach the stack |
Setting it auto-configures three things:
- TLS cert SAN β
frontend-entrypoint.shappendsPUBLIC_HOSTto the self-signed cert'ssubjectAltNameso browsers trusthttps://<PUBLIC_HOST>:4043. - Email verification URL β
APP_URLdefaults tohttps://<PUBLIC_HOST>:4043, so verification and password-reset emails link to the correct public URL. - LiveKit public URL β
LIVEKIT_PUBLIC_URLbecomesws://<PUBLIC_HOST>:7880so the browser's LiveKit SDK can reach the SFU.
For LAN testing:
PUBLIC_HOST=192.168.1.10 docker compose -f docker/docker-compose.yml up --build
Then open https://192.168.1.10:4043 and accept the self-signed cert warning
once. For a real domain + Let's Encrypt cert, put a host nginx proxy in front
(see docs/quick-start.md).
οΏ½ LiveKit API secret
LiveKit rejects secrets shorter than 32 characters. The dev config ships with
a 40-char placeholder (devsecretdevsecretdevsecretdevsecret123) set in both
docker/livekit.yaml (under keys:) and docker/docker-compose.yml
(LIVEKIT_API_SECRET on ext-av). The two must match β ext-av signs
join tokens with this secret and LiveKit verifies them.
For anything beyond local dev, generate a fresh secret and replace it in both files:
openssl rand -hex 32
After rotating the secret, restart livekit and ext-av, and have any
connected browser rejoin the room (old tokens are signed with the old secret
and will fail with token signature is invalid until refreshed).
οΏ½π Debugging with motel
The backend (pusher, worldsim, all four extensions) and frontend are instrumented with OpenTelemetry traces and logs. Telemetry is off by default; flip it on when you want runtime evidence while debugging.
Dev uses motel (a local TUI collector). For production, OpenObserve can be added to the Docker stack on CPUs with AES-NI support β see the FAQ for details.
motel is a local OpenTelemetry collector with a query API and TUI. Install it from github.com/kitlangton/motel.
Quick start (one command):
make build # one-time: build the Go binaries
make debug # starts NATS + motel + pusher + worldsim with OTel on
This starts a standalone NATS container, ensures motel is running, then
launches both Go services with OTEL_ENABLED=true. Ctrl-C stops the
services and cleans up NATS. For the frontend with browser traces:
make debug-frontend # in a second terminal: Vite dev server + OTel
Open http://127.0.0.1:27686 for the motel TUI, or http://localhost:5173 for the game.
Or run them manually β start motel once (it runs as a machine-global daemon, shared across projects):
motel start
Then run the services with OTEL_ENABLED=true, pointing at motel's default
OTLP/HTTP endpoint:
OTEL_ENABLED=true \
OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:27686 \
NATS_URL=nats://localhost:4222 ./dist/bin/worldsim &
OTEL_ENABLED=true \
OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:27686 \
NATS_URL=nats://localhost:4222 WS_ADDR=:8081 ./dist/bin/pusher &
For the frontend, set the Vite env vars at build/dev time:
cd frontend
VITE_OTEL_ENABLED=true \
VITE_OTEL_ENDPOINT=http://127.0.0.1:27686/v1/traces \
npx vite
Open http://127.0.0.1:27686 for the motel TUI, or query the API directly:
curl "http://127.0.0.1:27686/api/traces/search?service=worldsim&operation=apply_input"
curl "http://127.0.0.1:27686/api/logs/search?service=worldsim&body=tick"
What you get:
- Cross-service traces β a
traceparentheader carries span context through NATS, so one trace spanspusher.nats.publish.input β worldsim.apply_inputandworldsim.tick β worldsim.replicate β pusher.ws.write_replication. - Browser β backend link β the frontend stamps a W3C
traceparentinto eachAuthFrame/InputFrame, sows.send_input β pusher.nats.publish.input β worldsim.apply_inputshare one trace id. - Tick metrics as logs β
worldsim.tickemits a structured log per tick withduration_ms,entity_count,replicated_clients, andsnapshot_seqattributes (motel has no metrics endpoint, so these ride on logs/spans).
Env vars:
| Var | Default | Purpose |
|---|---|---|
OTEL_ENABLED |
false |
Arm the Go exporters (true/1/yes) |
OTEL_EXPORTER_OTLP_ENDPOINT |
http://127.0.0.1:27686 |
OTLP base URL |
OTEL_SERVICE_NAME |
caller-provided | Override service.name |
OTEL_TRACES_SAMPLE_RATIO |
1.0 |
TraceIDRatio sampler (0.0β1.0) |
VITE_OTEL_ENABLED |
false |
Arm the frontend exporter |
VITE_OTEL_ENDPOINT |
http://127.0.0.1:27686/v1/traces |
Frontend OTLP traces URL |
VITE_OTEL_SERVICE |
pixeleruv-frontend |
Frontend service.name |
When telemetry is disabled, the Go services use a no-op tracer/logger and the frontend registers no provider β zero overhead.
π Audit log
A standalone audit service records lifecycle and interaction events (player
connections, bans, chat messages, zone transitions, map reloads, extension
registrations, A/V tokens) to its own SQLite database and serves a searchable
web UI at /audit/. The /audit/world page shows live world state: per-map
overview, connected players (linked to their events), zone occupancy, and
extension status. Each audit event carries an optional trace ID linking to
the corresponding OpenObserve trace β audit tells you what happened, OTel
tells you why. See the
audit & observability design
for the full event catalog and storage upgrade path.
π Admin portal
A standalone Go service (backend/cmd/admin) provides email/password login
via PocketBase and a signed cookie session. nginx uses auth_request to
protect PB admin (/_/) and the audit UI (/audit/) β log in once at
/admin/ and access all admin services without re-authenticating. Only users
with is_admin=true in PocketBase can log in. A public welcome page at
/welcome provides community-customizable landing content.
Project layout
proto/ Protobuf definitions (frames, replication, components)
backend/cmd/pusher WebSocket β NATS proxy (no game logic)
backend/cmd/worldsim Authoritative simulation: tick loop, ECS, movement, replication
backend/cmd/audit Audit log service (NATS sub + SQLite + HTMX UI)
backend/cmd/admin Admin portal (email/password login + auth_request endpoint)
backend/internal/audit Audit event types + Emit helper (shared by all services)
frontend/ Phaser 4 client + generated TS protobuf
docker/ Dockerfiles for pusher, worldsim, frontend
dist/
bin/ Native binaries (build output)
web/ Frontend build output + map assets
sprites/ Character spritesheets (worldsim auto-seeds sprite_bases)
maps/ Default Tiled map + tileset PNGs (worldsim auto-seeds the maps record)
docker/ Dockerfiles, nginx.conf, livekit.yaml
docker-compose.yml Self-contained compose (run from dist/)
pb_migrations/ PocketBase collection schemas
π± Project Status
Pixel Eruv is currently in active design and early development.
Contributions, ideas, discussions and architectural feedback are welcome as the project evolves.
π Documentation
The project documentation lives in documentation/. Start
with the system overview for the big
picture, then dive into the detailed specs:
- 00 β System overview β high-level architecture, start here
- 01 β Vision and goals β project purpose and MVP scope
- 02 β Functional requirements β what the system must do
- 03 β Non-functional requirements β scale, latency, availability targets
- 04 β Tech stack β technology choices and rationale
- 05 β Architecture β detailed component wiring and data flows
- 06 β Data model and persistence β where data lives and why
- 07 β Network protocol β WebSocket frames and NATS subjects
- 08 β Authentication and identity β PocketBase auth, tokens, identity mapping
- 09 β Pusher β WebSocket proxy service
- 10 β World Simulator β authoritative simulation service
- 11 β Replication β how game state reaches clients
- 12 β Netcode β prediction, reconciliation, interpolation (skeleton)
- 13 β ECS design β entity-component-system rationale and decisions
- 14 β Zones and interactions β zones, knock-to-join, AOI (partial)
- 15 β Maps and Tiled β map authoring and layering (skeleton)
- 16 β Avatars β avatar composition and bubbles (skeleton)
- 17 β Chat β text chat (PocketBase-backed)
- 18 β Extensions β peer-simulator extension system
- 19 β LiveKit β audio/video integration
- 20 β Roadmap β MVP and post-MVP phases
- A1 β Why Phaser 4 β engine choice rationale (appendix)
- A2 β Existing applications β prior-art survey (appendix)
π€ Contributing
Pixel Eruv is an open-source community project.
Whether youβre interested in Go, Phaser, networking, distributed systems, game development, UI/UX or documentation, contributions are welcome.
Together, letβs build the open infrastructure for spatial collaboration.