Skip to content
CIYAhqPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Chess

A minimal two-player chess server. Create a game, send the link, play. No accounts, no lobby, no matchmaking.

Built to be self-hosted on Coolify at chess.ciya.so.

Features

  • Share-a-link games with no sign-up. The first person to open the link takes the open seat; everyone after that watches.
  • Server-authoritative rules and clocks, so a tampered client cannot cheat.
  • Time controls from 3 minutes to 30, with increment, plus unlimited.
  • Resign, draw offers, and rematch with colours swapped.
  • In-game chat between the two players, readable by spectators.
  • Sound cues for moves, captures, checks, castling, and game end.
  • Games survive a redeploy: state lives in SQLite on a volume.
  • Works on a phone. The board scales and the layout stacks.

Running it locally

Requires Node 24 or newer (the server uses the built-in node:sqlite).

npm install
npm run dev

That starts the API and WebSocket server on port 3000 and the Vite dev server on port 5173. Open http://localhost:5173.

To test both sides yourself, open the invite link in a private window. Seat ownership is stored in localStorage, so a second tab in the same browser is recognised as the same player and will show the same colour.

Other commands

Command What it does
npm run build Build the client and bundle the server into dist/
npm start Run the production build
npm run typecheck Typecheck everything
npm test Game-rule checks: flag-fall, draws, seating
npm run test:e2e Protocol checks against a running server

npm run test:e2e expects a server already running, and takes an optional base URL: npm run test:e2e -- http://localhost:3000.

Deploying on Coolify

  1. Point an A record for chess.ciya.so at your Coolify host.
  2. In Coolify, create a new Application from this Git repository and choose the Dockerfile build pack.
  3. Set the domain to https://chess.ciya.so. Coolify's Traefik handles the Let's Encrypt certificate and proxies WebSocket upgrades with no extra configuration.
  4. Set the port to 3000.
  5. Under Storages, add a persistent volume mounted at /data. Without it, games are lost on every redeploy.
  6. Deploy.

The container runs as the non-root node user (uid 1000). If you mount a host directory rather than a Docker volume, give it to that user or the database cannot be written:

chown -R 1000:1000 /path/on/host

The server does not crash when /data is unwritable. It logs the problem, keeps games in memory only, and reports "persistent": false from /healthz, so this shows up as lost games after a redeploy rather than an outage.

The client works out which WebSocket URL to use from the page it was served from, so there is no build-time environment variable to keep in sync with the domain.

If the connection keeps dropping

Coolify can enable Gzip compression on a resource, which interferes with streamed connections. If games connect but never receive moves, turn Gzip off in the application's advanced settings.

Configuration

Every value is optional; the defaults are what a personal instance wants. See .env.example.

Variable Default Purpose
PORT 3000 HTTP port
DATA_DIR /data in Docker Where chess.db is written
CREATE_PASSPHRASE empty When set, starting a game requires this passphrase
RETENTION_HOURS 168 How long finished games are kept
ABANDONED_HOURS 24 How long a game nobody joined is kept
MAX_GAMES_PER_IP_PER_HOUR 30 Game-creation rate limit
MAX_CHAT_PER_MINUTE 30 Chat rate limit

Because the instance is on the public internet, game creation is rate-limited by IP. If you would rather nobody but you could start games, set CREATE_PASSPHRASE; invite links still work for whoever you send them to.

How it fits together

Browser ──HTTP──> Node server ──> static client build
        └──WS───> Node server ──> in-memory games ──> SQLite (/data/chess.db)

One process serves the client, owns every game, and holds the WebSocket connections. Games live in memory and are written to SQLite on each change, so a restart reloads them from the stored PGN.

Seats without accounts

There is no login, so a game seat is proved by a random token:

  • Creating a game reserves your colour and hands your browser a token, stored in localStorage under chess:seat:<gameId>.
  • Opening the link without a token claims whichever seat is still free and gets a token for it.
  • Opening it once both seats are taken makes you a spectator.
  • Reconnecting with a token always returns you to your own side, which is what makes refreshing safe.

Clocks

Clocks are kept and enforced on the server; the browser only interpolates between updates so the countdown looks smooth. Times are sent as remaining milliseconds plus which side is running, and the client measures from when it received the message, so a client whose system clock is wrong still shows the right time.

The clock starts on White's first move rather than at creation, so no one burns time waiting for a friend to open the link. Flagging is detected by a timer on the server. Running out of time against an opponent who cannot possibly mate (a lone king, or king and a single minor piece) is scored a draw, matching the usual rule.

After a restart the clock resumes when a player reconnects rather than charging anyone for the downtime.

Licence

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages