A production-leaning prototype of a real-time multiplayer chess app built with Angular, Spring Boot, WebSockets/STOMP, and PostgreSQL (Supabase). It's designed to showcase solid full-stack fundamentals, clear architecture, and the precise features required for a comprehensive chess application.
- 👤 Authentication: Register + login with JWT tokens
- 🟢 Presence: List currently online users (live updates)
- ✉️ Invitations: Invite online players + accept/decline (live DM via STOMP user destinations)
- 🎮 Game creation upon invitation acceptance
- 🧩 8×8 chessboard UI built with Angular
- 🔁 Real-time move synchronization via WebSockets
- 💾 Minimal persistence: Each move stored in database
- 🔄 Reconnection/resume for active games
- ⏯️ Replay system: Step through past moves sequentially
- ✅ Server-side move validation with sanity checks
- 🧭 UX enhancements: Side panel with move list, game status, etc.
repo-root/
├── backend/ # Spring Boot 3.5.x (Java 17/22)
│ ├── src/main/java/com/example/chess/
│ │ ├── config/ # Security, CORS, WebSocket, STOMP interceptor (JWT on CONNECT)
│ │ ├── auth/ # AuthController, AuthService, JwtService
│ │ ├── user/ # User entity + repository
│ │ ├── lobby/ # LobbyService + LobbyController (presence, invites)
│ │ ├── game/ # Game, Move, GameStatus, repos, GameService, GameController, GameRules
│ │ └── common/ # Dto.java (single holder for REST/WS DTOs)
│ └── src/main/resources/
│ └── application.properties # Uses env vars for DB + JWT
└── frontend/ # Angular 17+ (Node 18+)
└── chess-client/
├── src/app/core/ # Auth service/guard/interceptor, STOMP service, models
├── src/app/shared/ # UI components (Navbar, etc.)
└── src/app/features/ # auth/, lobby/, game/ modules & pages
- STOMP over WebSockets (
/ws), with broker prefixes:/app(client → server),/topic(broadcast),/user(DM) - JWT verified during STOMP CONNECT via a ChannelInterceptor (no unsecured WS traffic)
- Spring's user destinations:
convertAndSendToUser(<principalName>, "/queue/...")where the principal name is the userId (string) to guarantee correct routing
- Java 17 or Java 22 (JDK) + Maven/Gradle
- Node 18+ + Angular CLI
- Supabase account (PostgreSQL)
- IntelliJ IDEA (recommended for environment variable setup)
In Supabase, create a project and copy:
- Host (e.g.
aws-1-eu-north-1.pooler.supabase.com) - Database:
postgres(default) - User:
postgres.<project-ref>(e.g.,postgres.vbdtwmmxdijmbzwbboqk) - Password: Set/reset a strong password (you control this)
- Port:
5432(Session Pooler) or6543(Transaction Pooler)
postgresql://postgres.<project-ref>:<DB_PASSWORD>@<region>.pooler.supabase.com:5432/postgres?sslmode=require
In Supabase SQL editor, run your schema script (users, games, moves, optional invitations, and views).
⚠️ SECURITY WARNING: Never commit real database credentials or secrets to version control!
In IntelliJ IDEA, set these environment variables:
- Go to Run → Edit Configurations
- Select your Spring Boot configuration
- In Environment Variables, add:
DB_URL=jdbc:postgresql://aws-1-eu-north-1.pooler.supabase.com:5432/postgres?sslmode=require
DB_USER=postgres.vbdtwmmxdijmbzwbboqk
DB_PASSWORD=your_actual_supabase_password
JWT_SECRET=ChessApp2024!MultiplayerGame#SecureAuth$Token9876543210ABCDEF
SERVER_PORT=8081Important Notes:
- Replace
your_actual_supabase_passwordwith your real Supabase password - The JWT_SECRET should be a long, random string (minimum 256 bits)
- Use different secrets for production environments
- Consider using a
.envfile locally (add to.gitignore)
Your application.properties should reference environment variables:
# Database Configuration
spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USER}
spring.datasource.password=${DB_PASSWORD}
spring.datasource.driver-class-name=org.postgresql.Driver
# JPA/Hibernate Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.open-in-view=false
spring.jpa.properties.hibernate.jdbc.time_zone=UTC
# Server Configuration
server.port=${SERVER_PORT:8081}
# JWT Configuration
jwt.secret=${JWT_SECRET}
# Logging (tune during development)
logging.level.org.springframework.web=INFO
logging.level.org.hibernate.SQL=INFO
logging.level.com.zaxxer.hikari=INFOFrom backend/ directory:
./mvnw spring-boot:run
# or with Gradle
./gradlew bootRunServer should boot at http://localhost:8081.
Edit frontend/chess-client/src/environments/environment.ts:
export const environment = {
production: false,
apiUrl: 'http://localhost:8081/api',
wsUrl: 'ws://localhost:8081/ws'
};cd frontend/chess-client
npm install
ng serveApp available at: http://localhost:4200
- Register:
POST /api/auth/register→{ token, userId, displayName } - Login:
POST /api/auth/login→{ token, userId, displayName } - Current user:
GET /api/users/me(withAuthorization: Bearer <token>)
Frontend stores the token in localStorage. An HTTP interceptor adds the Authorization header. AuthGuard protects routes like /lobby and /game/:id.
- Endpoint:
/ws - Prefixes:
- Client → Server:
/app/... - Broadcast:
/topic/... - Direct to user:
/user/queue/...
- Client → Server:
- Heartbeats: Enabled both sides to keep connections healthy
- JWT: Sent via
Authorizationconnect header; validated in aChannelInterceptor - Principal name:
userId(string) - critical forconvertAndSendToUser(userId, "/queue/invitations", ...)routing
- Client requests snapshot:
SEND /app/presence.request - Server broadcasts:
/topic/online-users
- Client sends:
SEND /app/invite.send(payload{ toUserId }) - Server DMs recipient:
/user/queue/invitations - Recipient replies:
SEND /app/invite.reply(payload{ invitationId, accept }) - On accept, server DMs both:
/user/queue/game-created
- Client sends move:
SEND /app/games/{id}/move - Server validates, persists, and broadcasts:
/topic/games/{id}
id,email(unique),password_hash,display_name, timestamps
id,white_player_id,black_player_idstatus(CREATED|STARTED|FINISHED)last_fen(optional snapshot)- timestamps
id,game_id,move_number,from_square,to_squaresan(optional),promotion(optional),played_by_user_id,played_at,fen_after(optional)- Indexes for
game_id,(game_id, move_number)
id,from_user_id,to_user_id,status(PENDING|ACCEPTED|DECLINED),game_id?- timestamps
v_user_active_games— quick active games lookup
- Auth: Service tests for register/login (hashing, duplicate email, JWT generation)
- GameController:
@WebMvcTestwith mockedGameService,SimpMessagingTemplate - GameService: Unit tests for
recordMove, turn validation, basic rules (viaGameRules) - WebSocket: Slice tests for
JwtChannelInterceptor(CONNECT with token OK/KO)
Run tests:
./mvnw test
# or
./gradlew test- Services (Auth/Lobby/Game) with
HttpClientTestingModule - Components for form validation, navigation, and dialog behavior
ng test- Start backend, then start frontend
- Register two users (e.g., in Chrome and Firefox, or Chrome normal vs Incognito)
- Both login →
/lobbylists 2 online users - User A clicks Invite on User B → User B receives a dialog to accept/decline
- On accept, both are routed to
/game/:id. Moves are synced in real-time - Reload either tab — the game resumes (moves fetched from DB; live WS resumes)
- (Bonus) Use replay controls in the game view to step through moves
- PowerShell scaffold scripts (backend Java packages/classes) included for clean structure generation
- Angular CLI commands listed in docs to generate core/shared/features modules and components
- Spring Boot 3.5.x (Web, Security, WebSocket, Validation, Data JPA, Actuator)
- PostgreSQL (Supabase), Hikari, Lombok
- JWT:
io.jsonwebtoken:jjwt-* - Testing: JUnit 5, Mockito
- Angular 17+
- Angular Material (dialogs)
- STOMP over WebSockets
- RxJS, Reactive Forms
DB_URL=jdbc:postgresql://<region>.pooler.supabase.com:5432/postgres?sslmode=require
DB_USER=postgres.<project-ref>
DB_PASSWORD=<YOUR_SUPABASE_PASSWORD>
JWT_SECRET=<A_LONG_RANDOM_STRING>
SERVER_PORT=8081- ❗ Never commit secrets to version control
- Use
.gitignorefor.envfiles - Rotate secrets regularly
- Use different secrets for different environments
- Verify your CI/CD (if any) injects runtime secrets securely
This repository is for technical assessment/demo purposes. You may adapt it for your own portfolio; be sure to rotate and remove all secrets before sharing or deploying.