Data model — what lives where#
This page is a simplified map of the data: which object is a row in the database, which one is a file on disk, and which one is never stored at all.
What lives where#
| Object | What it contains |
|---|---|
| Account | e-mail, password hash (Argon2id), role (user / mod / admin), session refresh tokens |
| Character | appearance (hair, hat, top, bottom, shoes, extras, colours), the handle name#tag, visibility on the map for other players |
| Route | name, description, an ordered list of stops, distance, estimated time, travel mode (on foot / by bike / by scooter) |
| Stop | position, completion radius (45 m by default), completion rule (GPS / puzzle / AR), description and media |
| Find | position, description, model (plain or AR), sponsor it belongs to |
| Run | one walk of a route: active → completed, or abandoned, with times and completed stops |
| Event | an immutable fact: a stop completed, a find collected, a route started or finished. Its unique ID is what prevents duplicates when a queue is synced after being offline |
| Offline pack | a versioned set of city content (routes, descriptions, graphics, models) to download to the phone |
| Media and 3D models | files in the content store on the server: photos, audio, models for the map and for AR |
| NPC bot | name, kind, icon, route loop (geometry along pavements plus pace), gift (a one-off find), on/off switch and a duty calendar. The database does not store its position — the server computes it deterministically from the time |
| Gift from a bot | who received a gift, from whom and on which day — one per player, per bot, per day, plus a 50 m radius measured by GPS |
| Reward rule | event (find, stop, route) → how many tokens, a daily limit, on/off. Alongside it a wallet (the balance is computed from ledger entries, not taken on trust) and a reward catalogue with redemptions |
| Lobby (exploring together) | a 6-character code, host, members, status (open / started / closed), a 2-hour time to live, and member positions (visible to members only, removed on leaving and after 5 minutes without a refresh) |
| Planner card | a card from the planner: title, description (the owner's note), column, deadline — plus the issue number and labels on the GitHub side (Workflow) |
| Player position | the last position used for “see others” — only with both sides' consent, kept for 5 minutes, within a 3 km radius |
A note on languages: the city is Warszawa, while the API and the database use the slug warsaw. When you see warsaw in a response, it is the same city the player sees.
Answers without plaintext passwords#
Stops with a puzzle work like this:
- In the creator you type the question and the answer.
- The answer is hashed — SHA-256 over a normalised form: lower case, trimmed spaces.
- Only the hash is stored; the database never holds the answer in readable form.
- A player may type the answer in any mix of upper and lower case, and it still matches.
The same principle applies to accounts: passwords exist only as Argon2id hashes, and sessions exist as tokens that can be revoked.
Files and models#
Content is split in two on purpose: descriptions and references in the database, bytes on disk.
| Kind | Where it lives | How the client sees it |
|---|---|---|
| Photos, audio, models | files in the content store on the production server | as a URL under /content/… |
| 3D models for the map and AR | the same content store, referenced by stop or find payload | a link plus scale and rotation in the payload |
| Character base mesh | a GLB file served to the app and to the website | one file, one atlas — both clients read the same source |
| Offline pack | a versioned manifest plus files, built ahead of time | download once, then work without a network (Offline mode) |
The offline pack manifest carries the version, the file list, checksums and the mapping from routes and stops to local paths — that mapping is what allows the app to show a route with no signal at all.
Two rules are enforced by the API rather than by convention:
- A find is never a stop on a route. Adding a find as a route stop is rejected with
400 collectible_is_not_a_route_stop, and turning an existing stop into a find is rejected with409 point_is_route_stop. If a route is meant to pass through the place where a find lies, it gets its own stop at the same coordinates (Map and markers). - Stops are shared, but the order belongs to the route. The same stop can appear in several routes; the sequence is a property of the route, not of the stop.
The content API mirrors that split: stops and routes are read and written under /points and /routes, their walking geometry comes from /routing/directions (the foot profile), media goes in through /media/upload, and offline packs are listed under /packs with /packs/:city/latest for the newest one. Progress has its own endpoints — runs, completing a stop, collecting a find and syncing queued activity — and they only ever touch the calling player's own data.
Documented for app version 0.120.1 (updated 2026-09-26).
Machine translation — the Polish version prevails.