Backend for GuessMyAlgorithm, a real-time multiplayer social game where friends connect their online accounts and guess which player a piece of activity belongs to.
Each round, the game surfaces a real activity from one player's connected accounts — a Spotify track, a YouTube video, a Steam session, an AniList watch — and everyone else has to guess whose it is. Points go to whoever guesses correctly, and to the player who stumped everyone.
Stack: Node.js · Express · Socket.IO · Supabase (PostgreSQL) · Redis
Client (React Native)
│
├── HTTP (Axios + Supabase JWT) → REST API routes
└── WebSocket (Socket.IO) → Real-time game engine
│
┌─────▼──────────────────┐
│ gameLoop.js │
│ context → guess → │
│ lock → reveal → │
│ reaction → score │
└─────┬──────────────────┘
│
Supabase (PostgreSQL)
Redis (optional, pub/sub)
The server owns all game state. Clients only send votes and reactions — they never drive phase transitions. This keeps all players synchronized regardless of connection quality and makes cheating structurally impossible.
Phase-driven round engine. Each round goes through:
context (show activity) → guess (players vote) → lock (timer end)
→ reveal (show answer) → reaction (emoji overlay) → score → next round
Flash rounds interrupt normal play at the game midpoint:
flash_input (25s) → flash_coin (8s) → flash_reveal (10s)
Transitions are setTimeout-based on the server. Per-socket rate limiters prevent guess spam (5 guesses/15 s) and reaction spam (5 reactions/10 s).
Picks which player's activity to use each round (balances exposure across players), calls the appropriate fetcher, and builds the round payload sent to all clients.
Supported platforms: Spotify · YouTube · YouTube Music · Deezer · AniList · TikTok · Twitch · Discord · Steam · Valorant (Riot) · Lichess · Chess.com · Twitter/X
Per-platform fetch functions that normalize activity data into a common shape: { type, label, sublabel, icon, metadata }. Encrypted OAuth tokens are decrypted on the fly using utils/encryption.js.
Room lifecycle: join, leave, play-again. Enforces max players, broadcasts player list updates, handles graceful disconnect and reconnect (players auto-rejoin their room on reconnect within the session).
In-memory store for active room state. Written on every phase transition; Supabase is only written at round end for stats and feed events. Redis pub/sub is used when scaling horizontally.
| Layer | Technology |
|---|---|
| HTTP framework | Express 4 |
| Real-time | Socket.IO 4 |
| Auth | Supabase JWT + custom middleware |
| Database | PostgreSQL via Supabase |
| Caching / pub-sub | Redis (Upstash, optional) |
| OAuth | 13 platform integrations |
| Security | Helmet · CORS · express-rate-limit · AES encryption |
| Testing | Jest · Supertest |
| Process management | Nodemon (dev) · Node (prod) |
- Node.js 18+
- A Supabase project (free tier works)
- At least one OAuth app configured (Spotify or AniList are easiest)
Redis is optional — the server falls back to in-memory state if REDIS_URL is not set.
npm installcp .env.example .envMinimum required variables:
| Variable | Where to get it |
|---|---|
SUPABASE_URL |
Supabase → Settings → API |
SUPABASE_ANON_KEY |
Supabase → Settings → API |
DATABASE_URL |
Supabase → Settings → Database → Connection string (Transaction Pooler) |
JWT_SECRET |
Any random 32+ character string |
ENCRYPTION_KEY |
Any random 32 character string |
| One OAuth pair | e.g. SPOTIFY_CLIENT_ID + SPOTIFY_CLIENT_SECRET from Spotify Developer Dashboard |
All other OAuth integrations are optional — the game works with any single connected service per player.
npm run db:migratenpm run dev # development (nodemon)
npm start # productionServer starts on http://localhost:3000. Health check: GET /api/health.
POST /api/auth/register
POST /api/auth/login
GET /api/oauth/:service → redirect to provider
GET /api/oauth/:service/callback → exchange code, store token
GET /api/rooms → list open rooms
POST /api/rooms → create room
GET /api/rooms/:id → room details
GET /api/services → connected services for current user
GET /api/friends → friend list + online status
GET /api/stats → player statistics
GET /api/shop → cosmetics catalogue
POST /api/shop/buy → purchase item
GET /api/feed → social activity feed
Client → Server
join-room { roomId, userId }
start-game { roomId }
vote { roomId, targetId }
reaction { roomId, emoji }
flash-input { roomId, text }
flash-vote { roomId, choice }
Server → Client
game-started { round, players, mode }
phase-change { phase, timeLeft }
player-voted { count, total }
round-reveal { correct, scores }
game-over { finalScores, rewards }
friend-online { userId }
src/
├── index.js Express app, middleware, route mounting
├── config/
│ ├── database.js PostgreSQL pool (Supabase)
│ ├── gameState.js In-memory room & session state
│ ├── redis.js Redis client (optional)
│ └── onlineUsers.js Presence tracking
├── middleware/
│ └── auth.js Supabase JWT verification
├── routes/ REST route handlers
│ └── oauth/ Per-platform OAuth callbacks
├── services/
│ ├── dataFetcher.js Fetch & normalize platform activity data
│ ├── roundGenerator.js Select player + activity, build round payload
│ └── eventGenerator.js Social feed event descriptions (i18n keys)
├── sockets/
│ ├── index.js Socket auth, connection lifecycle
│ ├── gameLoop.js Phase engine
│ ├── impostorLoop.js Impostor mode variant
│ ├── roomManager.js Room join/leave/play-again
│ └── presenceUtils.js Friend online/offline notifications
├── jobs/
│ ├── tiktokPoller.js Periodic TikTok data sync
│ ├── roomCleanup.js Idle room garbage collection
│ └── startupRecovery.js Restore in-progress rooms on restart
└── utils/
├── encryption.js AES encrypt/decrypt for stored OAuth tokens
└── socketRateLimit.js Per-socket rate limiter factory
MIT