Skip to content

lstep/PixelEruv

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

401 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Pixel Eruv

Pixel Eruv

The open-source virtual office platform

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 copying dist/ 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_IP unset β†’ defaults to 127.0.0.1 β†’ works because Docker maps ports to your host.
  • Production: set LIVEKIT_NODE_IP=<your-public-ip> (env var or .env file) β†’ 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:

  1. TLS cert SAN β€” frontend-entrypoint.sh appends PUBLIC_HOST to the self-signed cert's subjectAltName so browsers trust https://<PUBLIC_HOST>:4043.
  2. Email verification URL β€” APP_URL defaults to https://<PUBLIC_HOST>:4043, so verification and password-reset emails link to the correct public URL.
  3. LiveKit public URL β€” LIVEKIT_PUBLIC_URL becomes ws://<PUBLIC_HOST>:7880 so 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 traceparent header carries span context through NATS, so one trace spans pusher.nats.publish.input β†’ worldsim.apply_input and worldsim.tick β†’ worldsim.replicate β†’ pusher.ws.write_replication.
  • Browser β†’ backend link β€” the frontend stamps a W3C traceparent into each AuthFrame/InputFrame, so ws.send_input β†’ pusher.nats.publish.input β†’ worldsim.apply_input share one trace id.
  • Tick metrics as logs β€” worldsim.tick emits a structured log per tick with duration_ms, entity_count, replicated_clients, and snapshot_seq attributes (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:

🀝 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.

About

Remote Office facilitator

Resources

License

Stars

3 stars

Watchers

0 watching

Forks

Sponsor this project

Packages

 
 
 

Contributors