A small Express API for creating and joining shared "sessions": a session has a list of players and two lists of picks (attackers and defenders), stored in a JSON file with lowdb.
I wrote this in October 2024 as the backend for a Rainbow Six Siege operator randomizer on my personal website. A group of friends needed to share one lobby: one person creates a session, the rest join with the session id, and whatever operators get rolled are written back so everyone sees the same attackers and defenders. I kept the API generic enough (players + two arrays of strings) that it works for any "a few people share a small piece of state" use, like a pick/ban list or a quick team draft.
It was also my first go at a full CI/CD loop: GitHub Actions builds a Docker image, pushes it to Docker Hub, and a self-hosted runner pulls and restarts the container.
git clone https://github.com/joelstephen97/sessions-node-api.git
cd sessions-node-api
npm ci
npm test# 3 tests, in-process, against a temp db file
npm start # Server running on port 3001Try it:
curl -X POST http://localhost:3001/create-session \
-H 'Content-Type: application/json' -d '{"playerName":"Joel"}'# {"sessionId":"a84bdcd6-...","playerId":"139d1230-..."}With Docker:
docker build -t sessions-node-api .
docker run -p 3001:3001 -e CORS_ORIGIN='*' sessions-node-apiAll request bodies are JSON. Ids are UUID v4 strings and are validated with Joi.
| Method | Route | Body | Returns |
|---|---|---|---|
GET | / | hello message | |
POST | /create-session | { "playerName" } | { sessionId, playerId } |
POST | /join-session | { "sessionId", "playerName"? } | { session, playerId } (name defaults to Player N) |
POST | /change-player-name | { "sessionId", "playerId", "newPlayerName" } | { message, session } |
GET | /session/:sessionId | { session } | |
POST | /session/:sessionId/update-operators | { "playerId", "attackers": [...], "defenders": [...] } | { message } (player must be in the session) |
DELETE | /session/:sessionId | { message } |
A session object looks like:
{
"players": [{ "id": "uuid", "name": "Joel" }],
"attackers": ["Ash", "Sledge"],
"defenders": ["Jager"]
}Errors come back as { "error": "..." } with 400 (validation), 403 (player not in session) or 404 (session/player not found).
Configuration by environment variable (a .env file is loaded on start, see .env.example):
| Var | Default | Meaning |
|---|---|---|
PORT | 3001 | Port to listen on |
CORS_ORIGIN | http://localhost:3000 | Comma-separated allowed origins, or * for any |
DB_FILE | db.json | Where lowdb writes its JSON file |
app.mjs builds the Express app: JSON body parsing, CORS, and the seven routes above. Each handler validates the body with a Joi schema from validation.mjs, then reads the whole lowdb document, mutates db.data.sessions[sessionId], and writes it back. db.mjs opens the JSON file once at import time. index.mjs loads .env and calls listen(). The test in test/ imports the app, listens on a random port, and drives it with the built-in fetch, pointing DB_FILE at a temp directory.
index.mjs entry point: dotenv + listen
app.mjs Express app and routes
db.mjs lowdb JSON file adapter
validation.mjs Joi schemas for request bodies
test/api.test.mjs node:test smoke test of the whole lifecycle
Dockerfile node:20-alpine image
.github/workflows/ ci.yml (test, build + push image), cd.yml (self-hosted pull + run)
- Works and tested with Node 22 (needs 18+ for lowdb 7 and built-in fetch).
- Storage is one JSON file read and rewritten on every request. Fine for a handful of friends, not for concurrent traffic: two writes at the same moment can lose one of them.
- No auth. Anyone with a session id can join it, and any player id in the session can overwrite the operator lists. Sessions are never expired or cleaned up.
- The CD workflow targets my own self-hosted runner and Docker Hub secrets (
DOCKER_U,DOCKER_P); it will not run in a fork without those. - There is no randomizer logic here. The rolling of operators happened in the frontend; this API only stores the result.
MIT, see LICENSE.