Monorepo with three apps:
backend/: Elixir + Phoenix channels authoritative simulation server.frontend/: SvelteKit + Tailwind client.physics_engine/: Rust + wasm-pack + wgpu client renderer.
All clients join one fixed Phoenix topic: "global".
Screen.Recording.2026-02-28.at.01.03.30.mov
- Elixir 1.19+ and Erlang/OTP 28+
- Node 20+
pnpm- Rust stable
wasm-pack
- Backend
cd backend
mix setup && mix phx.server- Frontend
cd frontend
pnpm install && pnpm dev- WASM build (manual)
cd physics_engine
wasm-pack build --target web --release --out-dir ../frontend/static/wasm --out-name physics_engineWASM helper script:
./scripts/rebuild-wasm.shUse a single root env file:
.env(repo root)
Backend automatically loads this file in config/runtime.exs.
You can still override any value from the shell before starting.
cd backend
mix phx.serverFrontend is wired with envDir: '..', so it reads the same root .env.
Topic:
global
Client -> server:
brush:{ id, userId, x, y, add, radius, t }reset:{ id }
Server -> clients:
brush: immediate broadcast of received brush eventsreset:{ type: "reset", id, userId, tick }snapshot:%{type: "snapshot", w: 800, h: 600, bytesB64: "...", tick: tick_count}bytesB64isBase64(gzip(raw_grid_bytes))
- Grid:
800x600, bytes0or255 - Global state owner:
Backend.GlobalSimGenServer - Tick:
60Hz - Per tick:
- apply queued brush events
- vertical fall pass
- diagonal pass (
dx = +1 or -1) - opposite diagonal pass
- Diagonal direction order alternates each tick.
- Snapshot broadcast interval: every ~2 seconds.
- The server is authoritative; clients render locally for responsiveness.
- On snapshot receipt, frontend decodes + gunzips and calls
eng.import_state(bytes). - Reset clears authoritative state and broadcasts
reset+snapshot.