A tool for building and sharing double-elimination tournament brackets.
-
The Admin (
/admin) page allows administrators to create, edit, and manage brackets: seed the field, set the rest rule, draw the bracket, pick winners, and assign per-game dates/times/venues. Every change autosaves to the server.
-
The View (
/b/<id>) page allows the public a read-only view of a single bracket. Anyone with the link can see the bracket and schedule, but they can't edit or pick winners:
server/
server.js Express REST API + static/page routes
storage.js pluggable storage backend (Azure Blob or filesystem)
public/ browser-served assets (everything here is public)
bracket-core.js shared engine + renderer (read-only aware)
admin.html editable admin page (talks to the API, autosaves)
view.html read-only viewer page
styles.css shared styles
data/ filesystem backend: one JSON file per bracket (git-ignored)
Each bracket has a server-assigned, unguessable id (crypto.randomBytes → base64url).
A record stores the raw inputs plus the per-game pick / date / time / venue overrides,
keyed by stable match id.
| Method | Route | Auth | Purpose |
|---|---|---|---|
| GET | /api/brackets |
token | list brackets (id, name, time) |
| POST | /api/brackets |
token | create, returns the new record |
| GET | /api/brackets/:id |
public | read one (used by the viewer) |
| PUT | /api/brackets/:id |
token | update |
| DELETE | /api/brackets/:id |
token | delete |
Reading a single bracket is public so read-only links work without a login. All
writes (and the full list) require the admin token via the x-admin-token header,
when ADMIN_TOKEN is set. If ADMIN_TOKEN is unset/empty, the server runs open
(handy for local dev).
docker compose up --buildThen open http://localhost:3000. The compose file sets ADMIN_TOKEN=dev-secret-token
— change it for anything but local use. Brackets are stored in ./data on the
host (bind-mounted), so they survive container restarts and rebuilds.
The first time you open the admin page it will ask for the admin token (paste the
value of ADMIN_TOKEN). It's remembered in your browser afterward.
Requires Node 18+.
npm install
# PowerShell: $env:ADMIN_TOKEN='dev-secret-token'; npm start
# bash: ADMIN_TOKEN=dev-secret-token npm start| Env var | Default | Meaning |
|---|---|---|
PORT |
3000 |
HTTP port |
ADMIN_TOKEN |
(empty) | required for writes; empty = open (no auth) |
DATA_DIR |
./data |
filesystem backend: where bracket JSON files are stored |
AZURE_STORAGE_ACCOUNT |
(empty) | Blob backend via managed identity (set this in Azure) |
AZURE_STORAGE_CONNECTION_STRING |
(empty) | Blob backend via key/Azurite (alternative to the above) |
AZURE_STORAGE_CONTAINER |
brackets |
Blob container name (auto-created) |
The storage backend is chosen automatically: if either AZURE_STORAGE_ACCOUNT or
AZURE_STORAGE_CONNECTION_STRING is set it uses Azure Blob, otherwise it falls
back to the local filesystem. One blob per bracket (<id>.json); the bracket name and
updatedAt are mirrored into blob metadata so listing doesn't download every blob.
Stores brackets in an Azure Storage account you already have — no mounted disk, so the app is stateless and runs well on Azure Container Apps (scale-to-zero).
Quick path: deploy.azure.ps1 does all of the below in one idempotent
run (builds the image in ACR, creates the app with a managed identity, wires the env vars,
and grants the blob role). All parameters are explicit — nothing deployment-specific is
baked into the script. From the repo root:
./deploy.azure.ps1 `
-TenantId <tenant-guid> `
-ResourceGroup <rg> `
-Location eastus `
-AcrName <globally-unique-acr> `
-StorageAccount <account> `
-StorageContainer brackets `
-AppName brackets `
-Environment brackets-env `
-ImageTag brackets:latestIt scales 0..1, generates an admin token if you don't pass -AdminToken, and prints the
app URL + token when done. The manual steps below are the same thing spelled out.
-
Push the image to a registry the host can pull (ACR or GHCR).
-
Create the app with a managed identity (system-assigned shown here) and point it at your account. With Azure Container Apps:
az containerapp create \ --name brackets --resource-group <rg> --environment <aca-env> \ --image <registry>/brackets-app:latest \ --system-assigned \ --ingress external --target-port 3000 \ --env-vars ADMIN_TOKEN=<your-token> AZURE_STORAGE_ACCOUNT=<account> \ --min-replicas 0 --max-replicas 1
-
Grant the identity access to the storage account (data-plane RBAC):
PRINCIPAL=$(az containerapp show -n brackets -g <rg> --query identity.principalId -o tsv) ACCOUNT_ID=$(az storage account show -n <account> --query id -o tsv) az role assignment create --assignee $PRINCIPAL \ --role "Storage Blob Data Contributor" --scope $ACCOUNT_ID
Notes:
- Keep
--max-replicas 1: storage is one blob per bracket with last-write-wins, not built for concurrent writers across instances. DefaultAzureCredentialis used, so the same code authenticates locally via youraz loginif you ever run against the real account from your machine.- The container (
brackets) is created automatically on first write; the identity needs Storage Blob Data Contributor (it covers creating the container and read/write).
Open a bracket in the admin page and copy its Read-only link
(/b/<id>) from the left rail. Send that link to anyone — they get the read-only
viewer for that bracket. Picking winners and editing the schedule stays on the admin
side, behind the token.