diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..d6f23d5
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,31 @@
+name: CI
+
+# Typecheck, tests, and build on every PR and every push to main.
+# Mirrors the CI in mateuseap/chesskernel so both apps gate the same way.
+
+on:
+ pull_request:
+ push:
+ branches: [main]
+ paths-ignore:
+ - '**.md'
+ - 'docs/**'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ build-test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: pnpm
+ - run: pnpm install --frozen-lockfile
+ - run: pnpm build # shared builds first so typecheck and tests resolve @pixelhub/shared
+ - run: pnpm typecheck
+ - run: pnpm test
diff --git a/.github/workflows/publish-images.yml b/.github/workflows/publish-images.yml
index 27c5108..0dad51c 100644
--- a/.github/workflows/publish-images.yml
+++ b/.github/workflows/publish-images.yml
@@ -1,4 +1,4 @@
-name: Publish Images
+name: Publish Images to GHCR
on:
push:
diff --git a/README.md b/README.md
index 105e3ba..3333cba 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,9 @@
**A virtual space you can actually walk around in.**
2D pixel world · Proximity chat · Self-hosted
-[](https://github.com/mateuseap/pixelhub/actions)
+[](https://github.com/mateuseap/pixelhub/actions)
+[](https://github.com/mateuseap/pixelhub/actions)
+[](https://github.com/mateuseap/pixelhub/releases)
[](LICENSE)
[](https://github.com/mateuseap/pixelhub/stargazers)
[](https://github.com/mateuseap/pixelhub)
@@ -37,6 +39,29 @@ Video calls put everyone in a grid; real rooms let you drift between conversatio
| 🎙 **Proximity Voice** | Self-hosted LiveKit SFU, audio-only Opus, volume falls off with distance |
| 🐳 **Docker-first** | Multi-stage images, nginx WebSocket proxy, GHCR publishing |
+## Architecture
+
+Three packages: a Phaser 3 client, an authoritative Colyseus server, and a shared pure-TypeScript core (map, collision, proximity, validation) that both sides import. The browser reaches the server through the client's nginx proxy; voice runs peer-to-SFU through self-hosted LiveKit.
+
+```mermaid
+flowchart LR
+ browser(["browser: Phaser 3 client"])
+ nginx["nginx client pod
static assets + /colyseus proxy"]
+ server["Colyseus server :2567
authoritative state, 20 ticks/s"]
+ shared["@pixelhub/shared
map, collision, proximity, validation"]
+ livekit["LiveKit SFU
audio-only, by avatar distance"]
+
+ browser -->|HTTPS static| nginx
+ browser <-->|/colyseus WebSocket| nginx
+ nginx <--> server
+ server -.->|imports| shared
+ browser -.->|imports| shared
+ browser <-->|WebRTC audio| livekit
+ server -->|mints access tokens| livekit
+```
+
+Movement and chat are validated server-side at 20 ticks per second; the client predicts and interpolates. Chat and voice are proximity-scoped in `@pixelhub/shared`, so who you hear and read is a pure function of avatar distance. See [docs/architecture](docs/architecture/overview.md) for the full design.
+
## Quick Start
Requirements: Node >= 20 and pnpm >= 8 (`corepack enable` sets up pnpm).