Polish Among Us community site. Two things live here:
- a role search engine for the Town of Us mod — 60 legacy roles plus 77 from TOU:Mira, with each role's live lobby settings;
- the Drama Afera league tracker — games, players, per-season ELO rankings, session summaries and a host panel, fed by a companion mod that uploads a match the moment it ends.
All user-facing text is Polish. Code, commits and issues are English.
Next.js 16.2 (App Router, Turbopack) · React 19 · TypeScript strict · Tailwind 3.4 · Zod 4
Cloudflare Workers via @opennextjs/cloudflare · Cloudflare D1 (SQLite) · R2
Prisma 7 with @prisma/adapter-d1 · Jest 30
npm ci # also runs cf-typegen + prisma generate
cp .dev.vars.example .dev.vars
npm run dev # :3000That is enough for most of the site. The role search engine reads its 60 legacy and 77 Mira
roles from plain TypeScript files, so the home page, the role pages, /custom and /tajemniczy
need no database at all.
The 25 pages under /dramaafera read real games out of D1 and do need data. db-backups/ is
gitignored, so a fresh clone has no dump to seed from — see
First time on this repo for how to get
one and which commands are safe to run.
npm run preview builds through OpenNext and serves the real Worker on :8787. Use it whenever
the thing you are testing touches a Cloudflare binding — npm run dev fakes those.
main is protected: open a pull request, do not push to it directly.
- Branch, commit, open a PR.
pr.ymlruns typecheck and tests. Copilot reviews every PR — that is an enforced rule, not a courtesy, and its instructions live in.github/copilot-instructions.md.- Merge. That deploys staging automatically.
- Production is a separate, manual
workflow_dispatchon the Deploy workflow. Releasing is a deliberate act; nothing reaches players because a PR merged.
Commit messages and code are English; everything a player sees is Polish.
The league's stats are not entered by hand. The mod's host uploads a JSON payload at the end of every match:
| endpoint | payload | |
|---|---|---|
| v1 | POST /api/games |
aggregated — 24 pre-computed counters per player |
| v2 | POST /api/v2/games |
event-based — a flat actions[] the server aggregates itself |
v2 is what the mod sends today. Both are live: v1 still serves the 735 games played before the cutover, and its rows are never rewritten.
Three rules govern ingest, and breaking any of them corrupts data silently:
- The mod scores, the server sums.
totalPointsis the sum of every action'spointsChangeand nothing else. No bonus is added server-side, and the disconnect clamp arrives already applied. The 24 counters exist for display; the ranking readstotalPointsalone. - The era comes from the data, never from the request. A game's season is derived from its own timestamp, and which role registry applies follows from that — never from "what season is it now".
- Ingest must be chronological. The ranking calculator refuses a game older than one already
scored, because a ranking row's
previousRatingis only meaningful in order.
POST /api/v2/games is idempotent on gameIdentifier: a re-submit returns 409 and writes
nothing, across all five tables.
wrangler.toml's top level is a dev config that deploys nowhere useful. Every real target is
a named environment, so nothing ships by accident.
| worker | D1 | |
|---|---|---|
| staging | townofus-pl-staging |
townofus_pl_preview |
| production | townofus-pl |
townofus-pl |
Named environments do not inherit bindings — [[d1_databases]], [assets] and [vars] are
redeclared in full in each [env.*] block. [env.*.secrets] required is enforced: a deploy is
refused outright if a listed secret is unset.
Push to main deploys to staging. Production is workflow_dispatch only. Both run
check.yml (typecheck, tests, manifest check) first, and migrations run in the same job as the
deploy — so a failed migration means no code ships against a half-migrated database.
npm run dev / build / preview
npm test # Jest
npm run typecheck # tsc --noEmit
npm run validate # integrity checks; --target local|staging|production
npm run ranking:oracle # replay a season's ELO and diff it against what is stored
npm run replay -- --file <p.json> # POST a payload and print the per-table row delta
npm run db:migrate:create # scaffold a migration + diff it against local D1
npm run db:migrate:apply:local # …:staging, …:production
npm run db:import:local # seed local D1 from a dump
npm run db:seed:staging # wipe and reseed staging
npm run mod:publish # does latest.json match the files beside it?
npm run mod:publish -- --write # rewrite latest.json from public/mod/client/validate prints one PASS/FAIL line per check and exits non-zero on any failure. Its
thresholds are deliberately loose floors, not exact counts — read the real totals from its
output, never from a document.
Interactive docs at /api/docs, health at /api/status, OpenAPI JSON at /api/schema.
Endpoints are protected with HTTP Basic Auth (API_USERNAME / API_PASSWORD, from .dev.vars
locally and wrangler secrets per environment) — except GET /api/dramaafera/*, which is
public so client components and embeddable widgets can read it. Destructive methods under that
same prefix are still authenticated.
AGENTS.md(symlinked asCLAUDE.md) — conventions, structure, the D1 and Prisma trapsdocs/ops/LOCAL_TESTING.md— the local loop, and a symptom → cause → fix tabledocs/v2-role-counter-mapping.md— the action → counter specaggregate.tsimplementsdocs/agents/— issue tracker, triage labels, domain docs.agents/skills/— reusable agent workflows, symlinked into.claude/skills/
Issues live on GitHub. The scoring weights the mod applies are deliberately not published here or on the site: the site names which actions score, not what each is worth.
MIT