Offline that converges
A festival crowd saturates cells, and courtyards eat signal, so
every write — visit, completion, score — goes through a typed
queue in local storage and replays when the network returns.
Rejections (4xx) drop; network failures retry; a completion the
server hasn't graded yet is honestly is_correct: null,
never a fabricated "wrong". App updates re-download only the
shell — the 30-day artwork cache stays put.
Players without accounts
Players aren't users: a device-generated token is the whole
session, and a name + email is the whole signup. A signed 7-day
link verifies the address in the background — play is never gated
on it — and switching phones re-binds identity with an emailed
6-digit code. No passwords, no OAuth, nothing to leak; the admin's
real auth lives in a fully separate world with roles and an audit
log.
A game engine, not an event app
The schema is deliberately content-agnostic —
game → locations → activities plus config
flags for sequencing, unlock rules, basemap mode, and guidance.
"Street art" appears nowhere in API logic. The season model
(waves of locations, an explore-mode afterlife) is configuration,
not a fork.
Push that respects the platform
The service worker is hand-written (injectManifest) so precache
and web-push handlers coexist. Announcements are staged as
batches, decoupled from content activation, and fanned out by the
scheduler off-request. iOS gets the truth: web push exists only in
the installed PWA on 16.4+, and the UI gates its promises
accordingly.
An admin the organizer runs alone
Locations are placed on a MapLibre picker with a live capture-radius
circle; photos are cropped and compressed in the browser, and
iPhone HEIC uploads convert server-side. Waves activate in batches,
every game string is editable per-game in both languages, and a
KPI funnel shows registrations → visits → completions
live during the event.
Proof it holds
Built test-first: 594 Pest cases on the API and admin, 721
Vitest cases on the SPA — including a guard that fails the build
if backend and frontend ever disagree about activity types — plus
Playwright end-to-end happy paths. The stack ships as Docker
Compose behind Caddy with auto-TLS.
The map has no server
The whole basemap is one self-hosted PMTiles file read by HTTP
range requests — no tile service, no API keys, no third-party
trackers. A single palette restyles every Protomaps layer to the
brand: solid orange roads where width alone carries hierarchy,
in matching light and dark flavors.