mobile-app
Starts the tournament management server (Preact UI + REST/SSE backend). Used on the day of the tournament for drawing pools, recording scores, and pushing real-time updates to spectator screens.
Refer to the Tournament app guide for a full walkthrough of the UI.
Flags
| Flag | Short | Default | Description |
|---|---|---|---|
--folder |
-f |
. |
Folder containing tournament.md and competitions/. Created at startup if it does not exist. |
--port |
-p |
8080 (or $PORT) |
Port to listen on |
--bind |
-b |
localhost (or $BIND_ADDRESS) |
Address to bind to. Use 0.0.0.0 to reach the server from other devices on the LAN. |
--lock-password |
(none) | unset (or $LOCK_PASSWORD=true) |
Switch to locked mode. Requires TOURNAMENT_PASSWORD_HASH. Refer to Authentication. |
Environment variables
| Variable | Used by | Description |
|---|---|---|
PORT |
--port default |
Initial value for --port if the flag isn't set. |
BIND_ADDRESS |
--bind default |
Initial value for --bind if the flag isn't set. |
LOCK_PASSWORD |
--lock-password default |
true enables locked mode without passing the flag. |
TOURNAMENT_PASSWORD_HASH |
locked mode auth | Bcrypt hash of the admin password. Required when --lock-password is set. Generate with hash-password. |
TOURNAMENT_ADMIN_PASSWORD_HASH |
nothing | Ignored. When it is set, the server logs a line at startup saying so. The admin password above covers every action. Refer to Upgrading from an earlier release. |
Authentication
The server has two authentication modes for the admin console; viewer routes are always public.
File mode (default)
The admin password is stored plaintext in tournament-data/tournament.md and compared by exact-string match. Set the password during the in-app Create tournament flow, or edit tournament.md directly.
POST /api/tournament/resetneeds no password but answers only a request from the machine running the server (a loopback address) that is addressed to a loopback name:localhost,127.0.0.1or[::1]. Any other request, from another machine or addressed by another name, gets403with the codereset_local_only. If you've forgotten the current password, browse tohttp://localhost:<port>/reseton the machine running the server to set a new one. In a Docker container run with-p, a browser on the host reaches the server through the container network, not loopback, so the reset is refused there too; edittournament.mdin the mounted data folder instead.GET /api/auth-configreturns{"mode": "file", "resetEnabled": true}.
Locked mode (--lock-password)
The on-disk password is ignored. Authentication compares the X-Tournament-Password header against a bcrypt hash from the TOURNAMENT_PASSWORD_HASH environment variable.
Generate the hash, piping the secret in rather than typing it (refer to hash-password for the input rules):
Start the server with the hash:
TOURNAMENT_PASSWORD_HASH='$2a$10$...' \
bracket-creator mobile-app --lock-password -f ./tournament-data
POST /api/tournament/resetreturns 404. The/resetSPA route still serves the embedded page, but renders an "operator-disabled" message instead of the form. The AuthModal also hides the "Forgot password?" link.GET /api/auth-configreturns{"mode": "locked", "resetEnabled": false}.- The server refuses to start if the env var is empty or malformed (fail-closed; no silent fallback to file mode).
- Rotation requires restarting with a new hash. The hash is read once at startup.
Recommended for any deployment reachable over the internet. The plaintext-in-file pattern of file mode is intended for trusted-network use only.
Operational caveats
POST /api/tournament/resetis limited to the machine running the server, judged on the connection's peer address and on the request'sHost, which must be a loopback name.X-Forwarded-Forand similar headers are not consulted. TheHostrule means a web page whose domain was rebound to127.0.0.1in a browser on the server machine is refused. Behind a reverse proxy on the same machine, every request arrives from the proxy's loopback address, so the reset is reachable by whoever the proxy exposes it to; block that path at the proxy or run with--lock-password(which 404sPOST /api/tournament/reset).- Locked mode has no bcrypt brute-force protection. The server runs a full bcrypt comparison (
bcrypt.DefaultCostapprox 50-100 ms) on everyX-Tournament-Passwordheader, but does not throttle repeated failed attempts. For internet-exposed locked-mode deployments, add a reverse proxy that rate-limits authenticated routes (for example, nginx'slimit_req). - Mode switching preserves the stored password. Switching from file mode to locked mode does NOT erase
tournament.md'spasswordfield; auth stops consulting it. A later switch back to file mode resurrects the original password. This is a deliberate rollback feature, but anyone with filesystem access can still read the value. To fully retire a file-mode credential before going locked,POST /api/tournament/resetit to a one-time throwaway first.
Competition configuration
A competition's pool and knockout settings are stored in its competitions/<id>/config.md YAML front matter, and are exposed by the API in JSON. Key names differ between the two:
config.md key |
JSON API key | Values | Default | Description |
|---|---|---|---|---|
extra_qualifiers |
extraQualifiers |
"", "larger-pools", "fill-bracket" |
"" (standard) |
How many finishers each pool sends to the knockout. Only meaningful for a Mixed competition with a minimum-players-per-pool size; requires pool_winners: 1 (poolWinners: 1 in JSON) for either non-default value. Refer to How many qualify from each pool. When updating a competition through the API, leaving the JSON key out keeps whatever is already stored; send the key with an empty string to set a competition back to standard. |
Examples
Local LAN, file mode, default port:
Bind to all interfaces on a custom port:
Locked mode for a public deployment, with the hash generated as shown under Locked mode: