A collaborative bingo room. Single Node process: serves the static frontend and relays game state over a WebSocket. State is held in memory and persisted to a SQLite database on every change. Designed to drop onto a Digital Ocean droplet (or any Docker host) and stay up.
cd server
npm install
npm run build:client
npm run lint
node server.js
# → open http://localhost:3000The browser app is precompiled into public/bundle.js during the Docker build,
so the page no longer relies on Babel Standalone in the browser.
Environment variables (all optional):
| Var | Default | What |
|---|---|---|
PORT |
3000 |
HTTP + WS port |
ADMIN_PASSWORD |
nanohope |
Password to unlock the admin panel |
STATE_DB_FILE |
./data/state.sqlite |
Where in-memory state is persisted on every change |
STATE_FILE |
./data/state.json |
Optional legacy JSON source used once for migration into SQLite |
WORDS_FILE |
./bingo_words.json |
The word list. Read on boot; rewritten when admin saves via UI |
cd server
docker compose up -d --build
# → http://<host>:3000The compose file mounts:
./bingo_words.json→/app/bingo_words.json(edit on disk, restart, takes effect)- a named volume
bingo-data→/app/data(state survives container restart)
Change the admin password by exporting ADMIN_PASSWORD=… before docker compose up,
or by editing the compose file.
- Spin up a droplet (Ubuntu 22.04, $4/mo tier is more than enough).
- SSH in and install Docker + Compose plugin:
curl -fsSL https://get.docker.com | sh apt-get install -y docker-compose-plugin - Copy this
server/folder up (scp -r server root@droplet:/opt/bingo) orgit cloneyour fork. cd /opt/bingo && ADMIN_PASSWORD=<your-password> docker compose up -d --build- Point a domain at the droplet IP and either:
- expose port 3000 directly, or
- put Nginx/Caddy in front for HTTPS (recommended). Example Caddy block:
Caddy handles WebSocket upgrades automatically.
bingo.example.com { reverse_proxy 127.0.0.1:3000 }
Two ways:
- From the admin panel. Sign in to
/, hit "Admin sign-in", paste a JSON of shape{ "center_square": null | "FREE" | "<word>", "words": ["…", …] }, click "Save words". The server rewritesbingo_words.jsonon disk and pushes the new pool to every connected client. - On disk. Edit
bingo_words.jsondirectly and restart the container. The server re-reads the file on boot.
center_square: null picks a random word for the center per dealt board.
server.js— Express static +wsWebSocket server. Single global state. All actions are simple methods on astateobject; after each one the server broadcasts the full state to every connected socket. Presence is derived from the set of open WS connections, not stored.state.js— actions + SQLite persistence (debounced 250ms).public/— vanilla React app served as-is, transpiled in the browser via Babel Standalone. No build step. Same components as the design prototype; onlystore.jsxdiffers (WebSocket-backed instead of localStorage).- Broadcast banners default to session-only and can optionally persist for 1, 5, 10, or 30 minutes, or forever.
- Single room. If you want multiple concurrent games, run multiple containers on different ports/subdomains.
- In-memory + SQLite snapshot persistence.
- No rate-limiting on WS messages. For a public-internet deployment behind a reverse proxy, the proxy can shoulder that.
- The admin password is the only auth. Don't reuse it with anything sensitive.