I built a chess web app where you play entirely through algebraic notation instead of a visual board.
It runs on a React frontend, a Node.js server service, and a Kafka-powered Stockfish microservice, all Dockerized and deployed on AWS.
🔎 App Overview
This app lets you play chess in the browser—with a twist:
- Moves are entered in algebraic notation (no drag-and-drop board)
- You play against a Stockfish-based bot (Elo ~2000)
- Games run in real time over WebSockets
Click “Start Game” and you w randomly assigned White or Black against the bot. A blinking cursor means it’s your turn—type a move (e.g., e4
, Nf3
) and press Enter. When the bot replies, the top bar updates with the new move in the list.
At any moment you can:
- Peek board — quickly visualize the current position
- Restart game — begin a new session
🌱 Why I Built This
I’ve been playing chess casually for the past few years, and I got fascinated by the idea that you can actually play without a board — just by remembering every move in your head and keeping track of the board state mentally.
That sparked an idea: what if I built an app that focused on the moves themselves, not a fancy board UI?
At the same time, I had a personal goal. Most of my professional experience has been backend (mainly Java), and I wanted to push myself to build and deploy a full-stack app end-to-end. Not just a local prototype, but something I could share via a link.
This project was the perfect excuse to:
- Deepen my experience with Node.js and React beyond the basics
- Practice Docker and cloud deployment
- Ship a portfolio piece that I own end-to-end: idea → code → infra → deployment
As the project evolved, I also started experimenting with microservices (Kafka + a separate bot service), because I wanted to practice real-world architecture patterns I hadn’t touched in my day job yet.
♟️ What the App Does
Right now the app is focused on a very specific experience:
- Play vs. Stockfish bot → you make a move, the bot responds.
-
Moves are in algebraic notation only (e.g.,
e4
,Nf3
). - Guest login → the server issues a JWT token automatically when you connect.
- Real-time play → all communication runs over WebSockets.
- Peek board mode → temporarily visualize the current board state if you don’t want to keep it fully in your head. What’s not there yet:
- No user accounts.
- No human-vs-human mode.
- If you close the page, you can’t rejoin the same game.
These are things I plan to add later, but the current foundation is already robust: the Game object on the server service is generalized enough that exposing new modes (like human-vs-human) will just mean connecting the right APIs and adding persistence.
Here’s a gif of the app in action:
🏗️ Architecture Deep Dive
When I started this project, my main goal wasn’t just “make a chess app” — it was to practice thinking in systems. That meant splitting responsibilities, experimenting with microservices, and forcing myself to deal with infra pieces like Docker, Kafka, and AWS.
Here’s the architecture as it stands today:
🌐 Frontend (React/Preact)
- UI built in React (Vite + Tailwind + shadcn).
- Users type their moves in algebraic notation (e.g.,
e4
,Nf3
). - Communicates with the server via WebSockets for real-time play.
- Deployed on Vercel for quick hosting and iteration.
For v1 I skipped real user accounts to keep the product accessible. I still wanted the JWT plumbing, so the app performs an automatic guest login: on load (or when a token expires), the client requests a guest JWT and uses it as a temporary user ID for socket auth. This is exposed via an AuthContext at the app root, alongside a GameContext for game state.
The frontend is intentionally light right now; there’s more to build, but the structure feels solid (GameContext + AuthContext).
More to come!
⚙️ Server Service — Node.js, Express, WebSocket
The server is deliberately lightweight right now. It:
- Acts as the entry point and handles HTTP routing
- Issues JWTs for guest login (used for socket auth)
- Manages WebSocket connections and keeps game state in memory
- Validates moves with chess.js
- Publishes move requests to Kafka when the opponent is a bot
Because the game logic is handled by a generalized Game object, adding new features like human-vs-human play or persistent sessions will be straightforward. For example, today if a user closes the browser, they can’t return to the game. Once I add a proper user system and persistence layer, it will be simple to support re-login to active games without changing much in the game server itself.
Here’s a simplified version of how I modeled the Game and players. The idea was to make the game logic generic enough so it doesn’t care if the opponent is a human or a bot.
game.js: Watch on Github
//...
class Game {
constructor() {
this.chess = new Chess();
this.whitePlayer = null;
this.blackPlayer = null;
this.currentPlayer = null;
this.currentMoveId = uuidv4();
this.gameId = uuidv4();
this.winner = null;
this.gameOver = false;
}
startGame() {
this.whitePlayer.setOnMoveCallback(data => this.makeMove(this.whitePlayer, { "move": data.move, "moveId": data.moveId }));
this.whitePlayer.setColor('white');
this.blackPlayer.setOnMoveCallback(data => this.makeMove(this.blackPlayer, { "move": data.move, "moveId": data.moveId }));
this.blackPlayer.setColor('black');
this.currentPlayer = this.whitePlayer;
this.currentPlayer.requestMove(this.currentMoveId);
}
makeMove(player, moveDetails) {
console.log(JSON.stringify(moveDetails))
let move = null;
if (player === this.currentPlayer && this.currentMoveId === moveDetails.moveId) {
try {
move = this.performMove(moveDetails.move);
}
catch (error) {
this.getNewMove(moveDetails.move)
return;
}
this.notifyMove(move)
if (this.isGameOver()) {
this.endGame();
}
else {
this.swapTurn()
this.requestMove()
}
}
}
//...
HumanPlayer.js (Extend Player): Watch on Github
//...
class HumanPlayer extends Player {
constructor(socket, playerDetails) {
super();
this.socket = socket;
this.onMove = null;
this.playerDetails = playerDetails;
}
requestMove(moveId) {
if (this.onMove == null) {
throw new Error("player doesn't have a callback!");
}
this.sendMessage("make move", moveId);
this.socket.once(`move ${moveId}`, (move) =>
this.onMove({ move: move, moveId: moveId })
);
}
//...
BotPlayer.js (Extend Player): Watch on Github
//....
class BotPlayer extends Player {
constructor(elo) {
super();
if (!isValidNumber(elo)) {
throw new Error("elo must be between 1320 and 3000!");
}
this.elo = elo
this.board = new Chess();
this.onMove = null;
this.nextMoveId = null;
}
requestMove(nextMoveId) {
this.nextMoveId = nextMoveId;
assignMoveIdToBotPlayer(nextMoveId, this);
sendMoveRequest({
moveId: nextMoveId,
fen: this.board.fen(),
elo: this.elo
}).catch(err => {
logger.error("Failed to send move request:", err);
});
}
//...
♟️ Bot Service (Stockfish + Kafka Consumer)
- Runs Stockfish inside a Docker container as an independent microservice.
- Subscribes to a Kafka topic for move requests.
- Calculates the best move with Stockfish.
- Publishes results back to another Kafka topic, which the Server Service consumes.
This decoupling keeps the bot logic isolated: the server doesn’t need to know how the bot works, and if the bot crashes, it can restart independently without breaking the rest of the system.
🐳 Infrastructure (Docker + Kafka + AWS)
-
Local development:
- Orchestrated with Docker Compose (server service, bot service, Kafka, Zookeeper).
-
Production:
- Deployed on AWS EC2.
- Server + bot service run as Docker containers.
- Kafka + Zookeeper containerized as well.
- Nginx reverse proxy in front, handling SSL termination.
This setup worked well for a first deployment. The next step is to move to ECS Fargate for more flexibility and easier scaling.
Overview Sketch
🔄 Flow of a Bot Game
Here’s what happens when you play against the Stockfish bot:
- Player submits a move → frontend → Server service via WebSocket.
- Server validates the move with
chess.js
. - If the opponent is a bot → Server publishes a
move.request
to Kafka. - Bot service consumes the request, runs Stockfish, and publishes
move.response
. - Server consumes the response → sends the bot’s move back to the frontend via WebSocket.
- The frontend updates the board instantly.
🔮 Future Architecture Ideas
- OAuth login — real user accounts and persistent sessions
- Rejoin active games — let users reconnect after a disconnect
- Human-vs-human mode — built on the generalized game object
- Matchmaking service — pair players automatically
- Redis caching — keep active games/queues outside process memory
- NoSQL DB (MongoDB Atlas or DynamoDB) — users, game history, stats
- ECS Fargate migration — run services independently and scale cleanly
- Move processing reliability — retries/acks to ensure every request is handled
- Spectator mode — broadcast moves to watcher sockets
🚀 Deployment Journey
Early on I had a single service (including Stockfish) and briefly deployed it on Railway to get something live. I then split the bot into its own service and added Kafka via Docker Compose.
Once that worked locally (server service, bot service, Kafka, Zookeeper), I redeployed on AWS EC2. Both EC2 and ECS Fargate were new to me, but EC2 let me ship fastest—and the deployment was smooth.
Production setup:
- Server Service and Bot Service as Docker containers on EC2
- Kafka and Zookeeper as containers on the same instance
- Nginx reverse proxy as the entry point, handling routing and SSL (Let’s Encrypt)
The only tuning was operational: security groups, container networking, and SSL certificates. Getting it online quickly made the project feel real and validated the Docker setup.
Next step: migrate to ECS Fargate so each service can run independently in a more cloud-native, scalable way.
✅ Wrapping Up (Part 1)
This post covered the overview, motivation, core features, architecture, and deployment journey of my chess app — from the idea of “chess without a board” to running it live on AWS with Docker, Kafka, and a Stockfish bot service.
For me this project is more than just a chess app: it was a way to practice building and deploying an end-to-end system, learning how pieces like WebSockets, JWTs, Kafka, and Docker actually fit together in production.
In Part 2, I’ll share:
- The challenges and small gotchas I hit along the way
- The lessons learned while working across the stack
- My roadmap: OAuth login, rejoin active games, human-vs-human play, Redis caching, NoSQL persistence, ECS Fargate, and more
Stay tuned for Part 2: Challenges, Lessons Learned, and What’s Next.
🔗 Live demo: your-app-link.com
💻 Code: GitHub repo
Top comments (0)