Event registry schema¶
event-data/config/events.json — the live event/day/facility registry for
sage-tools-api's sync feature, fetched and cached at runtime. See sync
pipeline for how it's fetched/cached/validated; this page
is the shape.
Editing and committing this file (on main) is how you add an event, add a
day, or fix a wrong sheet ID — no sage-tools-api redeploy needed;
every running instance re-checks it within SYNC_CONFIG_TTL_MS (~60s by
default).
Shape¶
{
"version": 1,
"livePush": true, // optional: false turns live push off (written by Control Center's Sync method switch)
"defaults": {
"matchesSheetName": "CSV",
"standingsSheetName": "STANDINGSCSV"
},
"events": {
"<event-key>": {
"type": "dual-meet", // or "standard" or "team" — required, picks the console's layout
"archived": false, // optional, console-only: hides from the event picker
"title": "PNF × BUP Dual Meet", // optional, console-only: masthead label
"attendance": "desks", // optional: "console" | "desks" — staff check-in; absent means none
"scoreEntry": "links", // optional: "console" | "links" — entering scores; absent means off
"days": {
"<day-key>": {
"label": "Day 1 · Aug 15",
"date": "2026-08-15", // optional, console-only: chronological ordering + "today" default
"isLive": "auto", // "auto" | true | false — the go-live override target
"facilities": [
{ "name": "Main", "sheetId": "1hbjqbH3H1..." }
]
}
},
"display": { // optional — console shows raw codes without it
"divisions": { "LI": "Low Intermediate" },
"events": { "MD": "Men's Doubles" },
"clubs": { "PNF": "Pickle & Friends Community" }
}
}
}
}
<event-key>must match this event's folder name underevents/insage-match-control.github.ioand its folder name inevent-data.typeis read only by Control Center —sage-tools-apinever looks at it. Not inferred: an unmatched code would fail silently the moment the code shape ever changes, so it's required and explicit.- A
"team"event takes its team names fromSTANDINGSCSVand needs nodisplayblock, exceptdisplay.pairswhen its pair labels aren't the defaults anddisplay.teamColorswhen its teams have colours.sage-tools-apinever readstypeordisplay, so it needs no backend change. display.pairs(team events only) labels each pair of a matchup, keyed by the pair number in a team code (the3ofA_3):"3": { "full": "Mixed Doubles", "short": "XD" }. Both labels are non-empty strings. A type that repeats is numbered ("XD 1", "XD 2"). Without it the labels are MD, WD, XD 1 and XD 2; a malformed map falls back to those with a console warning on the page, and a pair number the map lacks reads "Pair". The Hub, the board, the scorer page and Control Center read it. display.teamColors(team events only, optional) gives each team a colour, keyed by its group-stage team code:"Red": "#D55E00". Values are hex colours (#RGBor#RRGGBB); any other value is dropped with a console warning on the page. Each coloured team gets a mark, its colour on a shape of its own, before its name wherever the name appears, and a matchup card or board card is tinted from one team's colour to the other's. The shapes follow the map's order: the first team is a circle, then square, triangle, diamond, star, hexagon, plus and an upside-down triangle, and a ninth team starts them again. A playoff side gets its team's mark once the organiser has typed the team into its slot; a team the map lacks gets none. The Millennium Invitational's colours are from the Okabe–Ito palette, which stays apart for most colour-blind readers, and the shape tells teams apart where colour can't. The Hub, the board and Control Center read it.teamScoring(team events only, optional) is the event's scoring rule."points", the default, decides a matchup on total points and ranks a bracket on points scored."games"decides a matchup on matches won and then total points, and ranks a bracket on matches won, total points, head-to-head wins and head-to-head points. Anything else reads as"points", with a console warning on the page. The Hub, the board, Control Center and its Awards podium read it;sage-tools-apidoes not.displaymaps are allcode → label; order comes from key order, so there's no separate ordering config to keep in step. No logo field —display.clubsmaps to a plain name string only; the console shows the 3-letter code, not a logo, on every row.attendanceturns on event attendance:"console"lets operators mark people in Control Center,"desks"also allows desk links. Anything else is rejected on load. A day used with desk links needs adate.scoreEntryturns on entering a match's score from Control Center and the API (see Entering a score):"console"for signed-in operators,"links"for operators and scorer links (see Scorer page). Absent means off. Anything else is rejected on load. Mission Control's Scorer links switch moves an event between"links"and"console"by writing this value (PUT /v3/events/{event}/score-entry); it never turns score entry on or off, so adding the setting is a commit here. Every facility workbook of the event must be shared with the API's service account as Editor, as for attendance, or a save fails with a message naming the account.- A facility with an empty/missing
sheetIdis treated as "not set up yet" and skipped rather than fetched — lets you add a day's entry before its spreadsheet exists. matchesSheetName/standingsSheetNamecan be overridden per day, if that spreadsheet's tabs are literally named something else. So canrosterSheetName, the team roster tab a"team"event publishes (defaultTeams; other types publish no roster). Deliberately no GID equivalent — a GID is assigned per-workbook and doesn't carry over if a spreadsheet is ever duplicated from another event's; a tab name does.- A
"_comment"string key is allowed anywhere in the tree for notes that have no other home in JSON; ignored by validation.
Validation¶
Enforced on every load; a file failing any rule is rejected wholesale — the service keeps serving whatever it last loaded successfully (or its bundled fallback seed) rather than partially applying a broken commit:
versionmust match the supported version.- Every event key and day key must match
^[a-z0-9][a-z0-9-]*$— both become path segments/filenames, so an invalid key is refused rather than sanitized. - Day keys must be globally unique across every event — a day key is
also a path segment of every sync (
/v3/days/{day}/…), so two events sharing one would race to publish into each other's data. - Each day needs a non-empty
labeland afacilitiesarray (can be empty). isLive, if present, must betrue,false, or the literal string"auto".attendance, if present, must be"console"or"desks", andscoreEntry, if present, must be"console"or"links".livePush, if present, must betrueorfalse.falsemakes every sync and Live/Hide publish to GitHub alone; absent ortrueleaves live push to Cloud Run's environment. Normally written by Control Center's Sync method switch, not by hand.- Facility names must be unique within a day.
- At least one event, each with at least one day.
If unsure whether an edit is valid before committing, check GET
/v3/diagnostics/sync (X-Sync-Secret header) after committing — it reports which
config revision is actually live, and whether the service fell back to its
bundled seed because the commit failed validation.
Sheet IDs are effectively public¶
event-data is public (GitHub Pages requires it on the free plan), so this
file publishes every registered facility spreadsheet's ID. That's
expected — sheet IDs aren't secrets, the sheets are already
link-shareable by design, and the published snapshots already contain
everything on their synced tabs. But it does make the set of
spreadsheets enumerable: only register spreadsheets that are already
meant to be public, and never keep private notes in an extra tab
of a spreadsheet that's registered here.
Full reference: event-data/config/README.md (this page is a condensed version).
Related: Adding a new event, sync pipeline.