How checkmate.blue Works#
checkmate.blue is a real-time chess game built entirely on the ATmosphere. There is no application server, no database, no WebSocket server. The browser is the app, each player's PDS is their database, and AT Protocol is the infrastructure.
This document walks through how a single move flows through the system -- from one player's browser to the other's.
The Setup#
Both players have the game page open in their browsers. Each player has their own blue.checkmate.game record on their own PDS (Personal Data Server). These records contain the full PGN (Portable Game Notation) -- the complete move history. The players are connected to each other via Jetstream, a WebSocket service that broadcasts AT Protocol record changes in real-time.
Player A Makes a Move#
Player A drags a piece on the board. The app asks chess.js: "is this legal?" If it's a pawn reaching the back rank, a promotion modal appears first. Otherwise, the move is applied to the local chess.js instance immediately -- the board updates before anything hits the network.
The app generates the updated PGN from chess.js (including standard headers like player DIDs and the event name) and writes it to Player A's own PDS via putRecord. This is the only write -- one record update containing the full PGN, the game status, and a timestamp. Player A cannot write to Player B's PDS. AT Protocol enforces this: you can only write to your own repository.
The Move Reaches Player B#
Jetstream, which indexes the AT Protocol firehose, sees that Player A's blue.checkmate.game record changed. Player B's browser has an open WebSocket to Jetstream filtered to Player A's DID and the blue.checkmate.game collection. Jetstream pushes the event.
Player B's app receives the updated record. It extracts the PGN and compares it to the local chess.js state. If the incoming PGN has more moves (it should -- exactly one more), the app replaces the local chess.js instance with the new PGN. The board updates. A sound plays.
If Jetstream disconnects (network issues, server restart), the app falls back to polling Player A's PDS directly every 3 seconds until the WebSocket reconnects. Either way, the data source is the same: Player A's record on their PDS.
Player B Responds#
Now it's Player B's turn. The same flow happens in reverse. Player B moves, chess.js validates, the PGN is written to Player B's PDS, Jetstream delivers it to Player A.
Why Two Records?#
Each player maintains their own copy of the game because AT Protocol is built around personal data repositories -- you own your data, you write your data. There's no shared database to update. This means at any given moment, one record is one move ahead of the other (whichever player moved last). When the game page loads or reloads, the app reads both records and uses the longer PGN, ensuring no moves are lost.
When the Game Ends#
If chess.js detects checkmate, stalemate, or a draw condition after a move, the writing player includes status: "completed" and the result in their record update. The opponent receives this via Jetstream, verifies the result by replaying the PGN through chess.js, and mirrors the completion to their own record. Resignations and draw agreements follow the same pattern -- the acting player writes to their record, the opponent verifies and syncs.
The Flow#
sequenceDiagram
participant A as Player A (Browser)
participant CJ as chess.js
participant A_PDS as Player A's PDS
participant JS as Jetstream
participant B_PDS as Player B's PDS
participant B as Player B (Browser)
Note over A,B: Both players have the game open.<br/>Each has their own game record on their own PDS.<br/>Both are connected to Jetstream.
rect rgb(30, 40, 55)
Note over A,CJ: Player A makes a move
A->>CJ: Drag piece (orig, dest)
CJ->>CJ: Is this legal?
CJ-->>A: Yes -- board updates instantly
end
rect rgb(30, 40, 55)
Note over A,A_PDS: Write to own PDS
A->>A_PDS: putRecord(blue.checkmate.game)<br/>Updated PGN + status + timestamp
Note over A,A_PDS: Player A can only write to<br/>their own repository
end
rect rgb(40, 35, 50)
Note over A_PDS,B: Move delivered via Jetstream
A_PDS-->>JS: Record change event
JS-->>B: Filtered by Player A's DID<br/>+ blue.checkmate.game collection
end
rect rgb(30, 40, 55)
Note over B,CJ: Player B receives the move
B->>CJ: Compare PGN lengths
CJ-->>B: Incoming PGN is longer -- accept
B->>B: Update board + play sound
end
Note over A,B: Now it's Player B's turn.<br/>The same flow happens in reverse.
rect rgb(30, 40, 55)
Note over B,CJ: Player B responds
B->>CJ: Drag piece (orig, dest)
CJ->>CJ: Is this legal?
CJ-->>B: Yes -- board updates instantly
end
rect rgb(30, 40, 55)
Note over B,B_PDS: Write to own PDS
B->>B_PDS: putRecord(blue.checkmate.game)<br/>Updated PGN + status + timestamp
end
rect rgb(40, 35, 50)
Note over A,B_PDS: Move delivered via Jetstream
B_PDS-->>JS: Record change event
JS-->>A: Filtered by Player B's DID
end
rect rgb(30, 40, 55)
Note over A,CJ: Player A receives the move
A->>CJ: Compare PGN lengths
CJ-->>A: Incoming PGN is longer -- accept
A->>A: Update board + play sound
end
Two Records, One Game#
flowchart TB
subgraph A_PDS["Player A's PDS"]
A_REC["blue.checkmate.game/3abc...<br/><br/>pgn: 1. e4 e5 2. Nf3<br/>status: active<br/>white: did:plc:playerA<br/>black: did:plc:playerB"]
end
subgraph B_PDS["Player B's PDS"]
B_REC["blue.checkmate.game/3xyz...<br/><br/>pgn: 1. e4 e5 2. Nf3 Nc6<br/>status: active<br/>parentGameUri: at://playerA/..."]
end
A_REC -.-|"Player B's record points<br/>back via parentGameUri"| B_REC
LOAD["On page load or reconnect:<br/>Read BOTH records.<br/>Use the longer PGN."]
A_REC --> LOAD
B_REC --> LOAD
Jetstream Fallback#
stateDiagram-v2
[*] --> Connected: WebSocket opens to Jetstream
Connected --> Connected: Receive opponent's moves
Connected --> Disconnected: WebSocket closes
Disconnected --> Polling: Start polling opponent's PDS (3s interval)
Disconnected --> Reconnecting: Exponential backoff (1s to 30s)
Polling --> Connected: WebSocket reopens
Reconnecting --> Connected: WebSocket reopens
Note right of Polling: Same data source either way --<br/>the opponent's record on their PDS