Terminal-native collaborative pixel board built with OpenTUI, React, and Yjs. This repository is open source and self-host friendly: it includes the terminal client, a Cloudflare Worker collaboration server, and local build and release scripts.
For a Korean-language overview, see README.ko.md.
If the public SSH entrypoint is available, the fastest way to try the project is from a real terminal:
ssh pxpx.sh
ssh -t pxpx.sh facebook/react
ssh -t pxpx.sh torvalds/linuxssh pxpx.sh is the default public entrypoint when the SSH gateway is available. You do not need a fixed SSH username such as pxpx@; the gateway accepts plain ssh pxpx.sh and ignores the presented SSH username. You can also jump straight into a repository-scoped room with any owner/repo slug, such as torvalds/linux. The rest of this README explains how to run and self-host the same stack from source.
- Included and supported: terminal client, Cloudflare Worker, local build and release scripts
- Included as an advanced deployment option: SSH gateway in
src/ssh-gateway.ts - Out of scope for this repository: any maintainer-run shared worker or SSH host
- Local multiplayer via
y-websocketfor development or self-hosted play - Remote multiplayer via the Cloudflare Worker in this repository
- Room routing by explicit room name or GitHub
owner/reposlug - Live cursors, recent paint activity, short-lived paint highlights, and board growth on the south and east frontier
- Optional GitHub device login for identity labels and protected repository rooms
- Standalone binary builds and GitHub release packaging
pnpmbun- A Cloudflare account only if you want to run your own worker
If pnpm is not installed yet:
corepack enable
corepack prepare pnpm@10.13.1 --activateInstall dependencies:
pnpm installStart the local collaboration server:
pnpm dev:serverStart the client:
pnpm dev:clientBy default the source checkout connects to ws://127.0.0.1:1234 and joins room pixel-game.
Useful local variations:
pnpm dev:client -- facebook/react
PIXEL_NAME=alice pnpm dev:client
PIXEL_ROOM=design-review pnpm dev:clientRun a second client in another terminal to verify real-time sync.
Run the Worker locally with Wrangler:
pnpm dev:server:cloudflareThen point gameplay and login at the local Worker:
PIXEL_SERVER_URL=ws://127.0.0.1:8787 pnpm dev:client
PIXEL_AUTH_SERVER_URL=ws://127.0.0.1:8787 pnpm dev:client -- loginDeploy your own Worker:
pnpm deploy:server:cloudflareAfter deployment:
PIXEL_SERVER_URL=wss://<your-worker-url> pxboard owner/repo
PIXEL_AUTH_SERVER_URL=wss://<your-worker-url> pxboard loginRun directly from a checkout:
pnpm install
pnpm dev:server
pnpm dev:clientInstall a local binary from this checkout:
./install.shinstall.sh uses dist/pxboard if it already exists, otherwise it builds a local binary from source when the checkout has pnpm, bun, and dependencies available.
The installed binary keeps the same defaults as the source build. Start a local server first or set PIXEL_SERVER_URL or --server-url to a deployed Worker before running pxboard.
Install from GitHub release assets:
curl -fsSL https://raw.githubusercontent.com/tolluset/pxpx/main/install.sh | shOverride the default release source if you are running a fork:
curl -fsSL https://raw.githubusercontent.com/<owner>/<repo>/main/install.sh | PIXEL_GAME_REPO=<owner>/<repo> shThe installed binary is pxboard. From a source checkout, the equivalent pattern is pnpm dev:client -- <args>.
| Task | Installed binary | From source |
|---|---|---|
| Play the default room | pxboard |
pnpm dev:client |
| Join a repository room | pxboard facebook/react |
pnpm dev:client -- facebook/react |
| Join a named room | pxboard --room design-review |
pnpm dev:client -- --room design-review |
| Override player name | pxboard --name alice |
pnpm dev:client -- --name alice |
| Start GitHub login | pxboard login |
pnpm dev:client -- login |
| Show stored identity | pxboard whoami |
pnpm dev:client -- whoami |
| Clear stored identity | pxboard logout |
pnpm dev:client -- logout |
| Show repo access policy | pxboard access status owner/repo |
pnpm dev:client -- access status owner/repo |
| Enable protected mode | pxboard access enable owner/repo |
pnpm dev:client -- access enable owner/repo |
| Grant an editor | pxboard access grant owner/repo alice |
pnpm dev:client -- access grant owner/repo alice |
Other project scripts:
pnpm build:client
pnpm dev:ssh-gateway
pnpm package:client:release
pnpm dev:server:cloudflare
pnpm deploy:server:cloudflare
pnpm typegen:worker
pnpm typecheckLogin is optional for open rooms. For repository rooms that have protected mode enabled, the owner and invited editors can paint while everyone else stays read-only.
pxboard login
pxboard whoami
pxboard logout
pxboard access status owner/repo
pxboard access enable owner/repo
pxboard access grant owner/repo alice
pxboard access revoke owner/repo aliceIf you have a local or deployed Worker, point login at it:
pxboard login --server-url ws://127.0.0.1:8787
PIXEL_AUTH_SERVER_URL=wss://<your-worker-url> pxboard loginIf the Worker login flow is unavailable, pxboard login can fall back to GitHub's device flow when PIXEL_GITHUB_CLIENT_ID or GITHUB_CLIENT_ID is set locally.
Successful Worker-backed logins also upsert the GitHub user profile into a server-side Durable Object-backed registry. The Worker still does not store GitHub access tokens.
Repository access management commands require a Worker-backed login because they depend on the Worker-signed session token.
This repository includes a Durable Object-backed Yjs collaboration Worker in cloudflare/worker.ts.
Repository rooms also support an owner-managed protected mode. When enabled, editing is limited to the repository owner plus the invited editor list stored in the room Durable Object.
If you change wrangler.toml bindings, regenerate the Worker runtime declarations before typechecking:
pnpm typegen:workerRun the Worker locally with Wrangler:
pnpm dev:server:cloudflareThen point the client at the local Worker URL printed by Wrangler. A typical local URL is:
PIXEL_SERVER_URL=ws://127.0.0.1:8787 pnpm dev:client
PIXEL_AUTH_SERVER_URL=ws://127.0.0.1:8787 pxboard loginDeploy your own Worker:
pnpm deploy:server:cloudflareAuthenticate Wrangler with either pnpm exec wrangler login or CLOUDFLARE_API_TOKEN plus CLOUDFLARE_ACCOUNT_ID.
To enable Worker-backed GitHub login on your deployment:
pnpm exec wrangler secret put GITHUB_CLIENT_ID
pnpm exec wrangler secret put GITHUB_SESSION_SECRET
pnpm exec wrangler secret put ROOM_RESET_TOKENThen connect clients to the deployed Worker:
PIXEL_SERVER_URL=wss://<your-worker-url> pnpm dev:client
PIXEL_SERVER_URL=wss://<your-worker-url> pxboard facebook/reactTo reset a room back to an empty 16x16 board:
curl -X POST \
-H "Authorization: Bearer $ROOM_RESET_TOKEN" \
https://<your-worker-url>/admin/rooms/pixel-game/resetTo paint a single pixel over HTTP:
curl -X POST \
-H "Content-Type: application/json" \
https://<your-worker-url>/api/rooms/tolluset%2Fpxpx/pixels \
-d '{"x":0,"y":0,"color":"sky","playerName":"pxpx-bot"}'The pixel API accepts palette ids such as sky or rose, plus custom colors like #38bdf8. Open rooms accept unauthenticated writes. Protected repository rooms require the same Worker GitHub session token used for websocket editing, sent as Authorization: Bearer <token> or ?github_auth=<token>.
This repository also includes a custom SSH gateway for hosted entrypoints. It is an advanced deployment option and not required for local development or self-hosting the Worker.
Example hosted entrypoints:
ssh pxpx.sh
ssh -t pxpx.sh facebook/reactRun it locally on a high port:
PXPX_GATEWAY_PORT=22222 \
PXPX_GATEWAY_HOST=127.0.0.1 \
PXPX_GATEWAY_HOST_KEYS=/tmp/pxpx-hostkey \
PXPX_GATEWAY_COMMAND=/usr/local/bin/pxboard \
PXPX_GATEWAY_RUN_AS_USER=pxpx \
PXPX_GATEWAY_RUN_HOME=/home/pxpx \
pnpm dev:ssh-gatewayThe gateway accepts SSH public-key authentication, ignores the presented SSH username, launches only the configured board command, and stores GitHub auth state by SSH public-key fingerprint via PIXEL_GITHUB_AUTH_FILE.
Build a standalone binary for the current OS and architecture:
pnpm build:client
./dist/pxboard facebook/reactPackage the current platform binary as a GitHub release asset:
pnpm package:client:releaseThis creates:
dist/pxboardartifacts/pxboard-<os>-<arch>.tar.gzartifacts/pxboard-<os>-<arch>.tar.gz.sha256
| Variable | Purpose | Default |
|---|---|---|
PIXEL_SERVER_URL |
Gameplay websocket server URL | ws://127.0.0.1:1234 |
PIXEL_AUTH_SERVER_URL |
GitHub login worker URL | ws://127.0.0.1:8787 |
PIXEL_ROOM |
Explicit room name | pixel-game |
PIXEL_REPO |
Repository slug alias for the room | none |
PIXEL_NAME |
Player label override | stored GitHub login or random player-xxxx |
PIXEL_GITHUB_AUTH_FILE |
Override the stored GitHub auth session path | XDG config path under ~/.config/pxboard |
PIXEL_GITHUB_CLIENT_ID |
Direct GitHub device-login fallback | none |
GITHUB_CLIENT_ID |
Same fallback, alternate name | none |
GITHUB_SESSION_SECRET |
Worker-side HMAC secret for signed GitHub sessions | none |
ROOM_RESET_TOKEN |
Worker-side bearer token for room reset operations | none |
Room selection precedence:
--room- Positional
owner/repo --repoPIXEL_ROOMPIXEL_REPOpixel-game
Arrow keys,WASD, orHJKL: move the cursorEnter,Space, or left click: paint when editing is allowedX: clear the current cell when editing is allowed1-8: select a palette colorC: open custom color input mode (#RRGGBB,Enterto apply,Escto cancel)EscorQ: quit
Painting or pushing beyond the south or east edge grows the shared board by 8 cells in that direction.
