Files
lemonade/server
jcoffey-dev 87474ad5ec Say plainly that there is no player limit, and stop one classroom hitting it
The README heading read 'Two people, two browsers', which sounds like a
cap. It is not one: the server holds no game state at all, so any number
of people play independent games and the server only ever sees a
finished score. The only real cap is four to a game, and that is a
keyboard sharing the same stand rather than anything technical.

Checking that turned up a case where it would not have been true. A
classroom, an office or a household all arrive from one address, and the
submission limit of thirty an hour was low enough that a class finishing
together would have started losing scores to a 429. Raised to 120 and
verified with a burst of 130: the first 120 land, the rest are refused.
What actually keeps rubbish off the board is the plausibility check, not
this limit.
2026-09-08 17:58:40 -07:00
..

Lemonade scores service

The leaderboard everybody shares. A single Node process with SQLite, no build step and no native modules: node:sqlite ships with the runtime and Node runs the TypeScript directly.

API

Method Path Purpose
GET /api/health liveness, used by the container healthcheck
GET /api/scores the top 50, best first
POST /api/scores submit 14 players from one finished season
// POST /api/scores
{ "entries": [ { "name": "ADA", "assets": 4635, "days": 12,
                 "glasses": 800, "seed": 1234, "broke": false } ] }
// 201 -> { "ids": ["17"], "scores": [ ...the new board... ] }

What it does and does not guarantee

Everything in a request is treated as hostile. Names are forced to a printable uppercase subset and cut to 12 characters. Every number must be an integer in range, and a score is refused if it could not have happened: assets above $2.00 + $25 a day, or glasses above 400 a day, are rejected as impossible for the days claimed. Bodies are capped at 4 KB and submissions at 120 an hour per address, which is why TRUST_PROXY=1 matters behind nginx — otherwise every request looks like it came from the proxy and one busy classroom would lock everyone else out.

It cannot prove a score is real. There are no accounts and no signing, so anyone willing to craft a request can post a plausible score under any name. The checks stop nonsense and casual spam, not a determined forger. That is a deliberate trade for a game with no sign-up; if the board ever needs to be trustworthy it needs identities, which is a different piece of work.

Only the best 500 rows are kept; the rest are pruned on write.

Running it

npm run scores          # from the repo root, on :5184
npm run dev:all         # game and scores service together

Environment: PORT (5184), DB_PATH (./data/scores.db), TRUST_PROXY (1 behind a proxy).

Deploying

docker compose -f server/compose.yml up -d --build

The container is read-only apart from the named volume holding the database — that volume is the one piece of state that must survive a redeploy. In front of it, nginx terminates TLS and proxies /api/ through:

location /api/ {
    proxy_pass         http://127.0.0.1:5184;
    proxy_set_header   Host              $host;
    proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Proto $scheme;
}

The game itself is a static bundle and needs no server; point it at another origin with VITE_SCORES_API=https://example.org/api at build time, or serve both from one origin and leave it on the default /api.