torque/scrims

Public: anyone can read this; nobody can change it.

A scrim board for the cs crew. Three things, each a resource in contract.json with its api so the forge can knock on every door: - announcement: only an admin posts, edits or removes one; anyone, signed in or not, reads and lists them. - match: any signed-in person posts a match (map, time, slots); the person who posted it, or an admin, edits or cancels it; anyone reads and lists. - signup: a signed-in person joins a match once and can leave it; the match's poster and an admin can see who signed up; anyone can see how many slots are left, never who. Keep the notes example only if it still earns its place; otherwise replace it. Every door asks permit; the contract test acts as every identity at every door. Plain server-rendered pages, the JSON API, the MCP tools and the terminal all serve the same actions. Small, fast, no dependencies.

forked from trevato/scrims

workbenchthis machine

This repository has no app yet.

following trevato/scrims in step

Everything here is up to date with trevato/scrims.

Proposes updates from trevato/scrims.

README.md

scrims

A scrim board for the cs crew: announcements from an admin, matches anyone signed in can put up, and a slot you take or give back. Three things, each a resource in contract.json with its own API path, so the forge can knock on every door.

  • announcement — only an admin posts, edits or removes one; anyone,

signed in or not, reads and lists them.

  • match — any signed-in person posts a match (map, time, slots); the

person who posted it, or an admin, edits or cancels it; anyone reads and lists.

  • signup — a signed-in person joins a match once and can leave it; the

match's poster and an admin can see who signed up; anyone can see how many slots are left, never who.

All times are UTC. Slots left is slots − signups, floored at zero, and the count is on the match, which everyone may read — the names are on the signups, which they may not.

This is an app: the forge runs it. Production follows the default branch; every open pull gets a preview at its own address with its own copy of the data. Everything the app keeps lives under FORGE_APP_DATA, which is what makes "production data" and "preview data" two different things.

PORT=8080 FORGE_APP_DATA=./data bun run src/server.ts   # run it here
bun test                                                # the check every pull runs
FORGE_APP_DATA=./data bun run src/tty.ts                # the terminal
FORGE_APP_DATA=./data bun run src/db.ts --check         # schema version, integrity

Nothing here is simulated: the board is real SQLite, the identities are the forge's signed ones, and /events is a real stream. Without the forge in front of it — running it by hand — nobody is signed in, so the page is the guest's view: matches and counts, no forms.

The shape

One implementation, many doors. src/actions.ts is every operation as a plain function — (db, who, input) in, data or a named failure out — and the page, the JSON API, the MCP server and the terminal all call it and decide nothing themselves. AGENTS.md says how to add to it.

filewhat it is
src/actions.tsannouncements, matches, signups: every rule, once
src/time.tsa match time: read as UTC, stored in one shape, shown to a person
contract.jsonwho may do what: roles, and the least role each verb needs
src/contract.ts, src/can.tsthe table, read at boot; the forge's evaluator, verbatim
src/who.tswho is asking, from the forge's signed headers
src/db.tsSQLite under FORGE_APP_DATA, WAL, forward-only migrations
src/live.tsthe in-process bus and GET /events
src/mcp.tsthe actions as MCP tools at POST /mcp
src/routes.tsevery path, in one table
src/page.tsthe board, rendered on the server
src/http.tsthe floor: headers, body cap, same-origin, rate limit, files
src/server.tsboot: open the database, serve
src/tty.tsthe terminal, over the same actions
src/env.tswhat the forge told this process
static/app.css, static/live.jsthe look, and the board that stays current

No dependencies: Bun, bun:sqlite and the standard library are all of it.

Who you are

The forge says who is asking on every request it proxies, and the app verifies the signature (src/who.ts); nothing else is trusted. Three identities, and two roles a row hands out:

  • anon — anyone. Reads the announcements, the matches and the counts.
  • user — signed in to the forge and able to see this app. Puts up

matches, joins and leaves them.

  • author — held by whoever a row names: the person who put up a match

edits and cancels it, the person who joined can leave.

  • host — held by the match's poster, named on each signup (host_id),

so they can see who signed up for their own match.

  • admin — able to write this repository. Posts announcements, and may

do anything the roles below can.

"Sign in" is /__forge/signin?to=<path> and "Sign out" is /__forge/signout; the forge answers both on the app's own host.

Any interface

The same actions, the same rules, at every door. A script is the person whose forge grant it carries; a bare script is anonymous.

curl https://<app>/api/matches
curl https://<app>/api/announcements
curl -X POST https://<app>/api/matches -H 'content-type: application/json' \
  -d '{"map":"de_dust2","starts_at":"2026-09-30T20:00","slots":10}'
curl -X POST https://<app>/api/signups -H 'content-type: application/json' -d '{"match_id":1}'
curl https://<app>/api/signups?match=1     # your own, and your matches'
curl -X DELETE https://<app>/api/signups/1 # leave

A time may be written 2026-09-30T20:00 (UTC), with seconds, with a Z, or with an offset such as +02:00, which is converted. It comes back as 2026-09-30T20:00:00Z.

As an MCP server, for Claude Code or Claude Desktop — list_announcements, post_announcement, remove_announcement, list_matches, post_match, edit_match, cancel_match, join_match, leave_match, list_signups:

claude mcp add --transport http scrims https://<app>/mcp
{ "mcpServers": { "scrims": { "type": "http", "url": "https://<app>/mcp" } } }

The board stays current over /events: a match someone puts up, or a slot someone takes, shows without a reload. What goes on the wire is the id and the count, never a name — the page each viewer then refetches is rendered for them. Without JavaScript it is the same page, one reload behind.

The terminal opens with the exact command in FORGE_APP_SSH when the app runs; the page and the terminal both print that command verbatim, and say so when it is unset. It is src/tty.ts on a real pty over the same data, as the account at the keyboard: j/k move, s join, l leave, n a new match, c cancel, a announce, q quit.

The forge sets PORT, FORGE_APP_DATA, FORGE_APP_ENV (production or preview), FORGE_APP_URL, FORGE_APP_COMMIT, FORGE_APP_SSH and FORGE_APP_IDENTITY_KEY when it starts the app. forge.json says how.

↑↓ navigate ↵ open esc close

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs