Your board. Your games. Your data.
A small multiplayer-first chess app with no platform lock-in. Play asynchronous games mirrored between two Pubky Homeservers, or use the browser bot in Solo mode.
- Play legal chess against a bundled easy or medium bot
- Save the game after every move under
/pub/chess/games/ - Browse previous games and replay them move by move
- Find Pubky opponents, start correspondence games, and see them grouped by whose turn it is
- Mirror each accepted opponent move into browser storage and the signed-in user's Homeserver
- Sign in through Pubky Ring using legacy cookie auth, with a testnet-only temporary identity shortcut
- Dark mode by default, plus a light-mode switch
Multiplayer is asynchronous rather than live: each player publishes one legal move at a time. There is no Chessky backend and no external chess API.
Prerequisites: Node 20.19+ (or 22.12+) and Docker with Compose.
Clone Chessky and pubky-docker into the same parent directory:
git clone https://github.com/gcomte/chessky.git
git clone https://github.com/pubky/pubky-docker.gitStart the local Pubky Homeserver:
cd pubky-docker
cp -n .env-sample .env
docker compose up homeserver -dhomeserver.config.toml must use signup_mode = "open" for the temporary development identity. Keep the development ports bound to localhost; this Docker stack is not a production deployment.
Then start Chessky:
cd ../chessky
cp .env.example .env.local
npm ci
npm run devOpen http://127.0.0.1:5173. Use Pubky Ring for a durable identity, or the temporary-identity link for a quick local test. A temporary identity is never persisted, so closing the page loses access to that identity's history.
When the browser runs on a different machine over SSH, forward the app and browser-facing Pubky ports:
ssh -L 5173:127.0.0.1:5173 \
-L 15411:127.0.0.1:15411 \
-L 15412:127.0.0.1:15412 \
-L 6286:127.0.0.1:6286 USER@HOSTChessky follows the Pubky developer guide and uses @synonymdev/pubky 0.11:
Pubky.testnet()connects the development build to the local testnet.- Pubky Ring legacy cookie auth requests only
/pub/chess/:rw. - Computer games remain under
/pub/chess/games/. Opening an opponent creates the empty marker/pub/chess/<opponent-pubky>/index.json; no separate player directory is used. - Multiplayer games live beside that marker as
/pub/chess/<opponent-pubky>/<game-id>.json. The other player stores the identical record and game ID under the inverse directory on their Homeserver. - Loaded records are byte-limited, schema-checked, and replayed through
chess.jsto prove every stored move is legal before display. - An initial full sync reads the user's Homeserver first, then Chessky listens to the opponent's public, path-filtered Homeserver event stream. A validated game-file event only wakes the existing authoritative sync; event data is never treated as game state. Cursor bootstrap avoids replaying the opponent's entire history, reconnects resume after the last accepted cursor, and polling is dormant while the stream is healthy. If the stream is unavailable or reconnecting, the bounded adaptive poller temporarily takes over. Both mechanisms pause in hidden, offline, or inactive views and perform a fresh reconciliation on resume. Writes use uncached reads and a short propagation-aware verification window. A genuinely pending runtime write retries automatically with bounded jittered backoff rather than requiring a manual retry. Sync skips remote reads for finished games and games already on the user's turn, then accepts only an unknown one-move invitation or exactly one legal opponent move extending the trusted local history.
- The initiator is always White and publishes the first move. The second mover learns and preserves the initiator's UUID, so both copies stay at the same game ID.
Pubky currently exposes /pub data publicly. Chessky says this in the interface: opponent directory markers and complete games are public, while profile names and avatars are only browser caches.
Player metadata and multiplayer game caches are scoped by signed-in Pubky, opponent, and network. Small searchable metadata and validated game checkpoints live in localStorage; validated raster avatars up to 4 MiB live in IndexedDB. The signed-in profile uses the same validated avatar pipeline but remains labeled “You” in games. Browser game data is treated as an untrusted cache and never wins over a valid user-Homeserver checkpoint. Stale profiles refresh asynchronously on startup or when a tab becomes visible, at most once per day and with bounded concurrency; failed profile resources retry after five minutes. Testnet builds resolve only testnet profiles, and mainnet builds resolve only mainnet profiles.
The current Basic Pubky App Template was downloaded and reviewed before implementation. Chessky carries over its v0.11 client configuration, Ring authentication lifecycle, local development signup, and Homeserver storage patterns.
The template itself is a vanilla-TypeScript CRUD example built with HTML strings and direct DOM mutation. Its file manager and event-stream UI would have been replaced almost entirely to support board state, bot turns, and replay state. React was therefore used for the product shell while Pubky remains isolated in src/pubky. For now, authentication uses the SDK's deprecated legacy cookie flow as a temporary compatibility mode: the Homeserver holds the credential in an HTTP-only cookie and Chessky persists only non-secret session metadata. Cookie auth inherits Pubky's cross-site request and shared-cookie limitations, so it should be replaced with grant auth when compatibility allows. Other deliberate security differences are narrow capabilities, in-memory auth flow state, no root-key persistence, and strict validation instead of loose JSON coercion.
npm test
npm run typecheck
npm run build
npm run auditThe chess rules, bounded bot, game-record validation, terminal positions, replay generation, opponent-path validation, one-ply reconciliation, cache bounds, and multiplayer grouping have unit coverage. Inter and Noto Sans Symbols 2 are bundled locally under the SIL Open Font License; no font CDN or third-party asset request is used.
Vercel uses npm run build:mainnet from vercel.json. The command explicitly disables the local testnet and its temporary-identity shortcut, even if stale Vercel environment variables exist.
The deployed app uses Pubky's production network, HTTPS relay, and resolved mainnet Homeservers. Local development remains on the Docker-backed testnet and keeps every development service bound to localhost.
Chessky is available under the MIT License.