Skip to content
Fyndi docs
Polski

Architecture — what Fyndi is made of#

Fyndi is four cooperating parts — an Android app, an API, a website with a staff panel, and a server that hosts them — and each part has one clearly defined role, which is why content can be changed without shipping a new APK.

What Fyndi is made of#

PartTechnologyWhat it is responsible for
Android appKotlin, Jetpack Compose, Mapbox, ARCore Geospatialmap and the character pawn, routes, finds, AR view, navigation, notifications, offline pack and the character creator (drawn natively, the same model as on the website)
APINode.js 22 (in server/api), PostgreSQL 16 (the database container)accounts and sessions (Argon2id plus JWT and refresh tokens), routes and stops, completing stops, finds, offline packs, player presence, the panel's AI features — and since 14 September also lobbies (exploring together), NPC bots (movement computed from time), tokens and rewards, the planner bridge to GitHub Issues and serving the project brain graph
Website and panelstatic HTML/CSS/JS served by nginxlanding page, /account, /character (the same creator as in the app) and the staff panel: dashboard, content studio, planning, media, 3D models, character generator
Servernginx with TLS (Let's Encrypt), containers, OSRMserving the website and the API under /api/, the content store, documentation screenshots, the APK mirror and walking/cycling routing (since 13 September: all of Poland, one OSRM data file of roughly 11 GB)

The same picture as a diagram:

Android app (Kotlin)
  UI | Domain | Data
        |
        v
  ArExperience (interface) --> ArCoreImpl (today)
                            --> UnityImpl (later)
        |
        | HTTPS, JSON
        v
API on the production server
  Auth | Routes | Packs | Content | Coop
  Bots | Rewards | Brain | Texts | Users/RBAC | Assets
        |
   +----+----+
   v         v
PostgreSQL  file storage (models, media)

The API is not one program but a set of modules with separate responsibilities — this is also what the documentation gate reads when it checks that a described feature has an owner:

API moduleResponsibility
auth/registration, login, refresh, roles (user / mod / admin)
content/stops, routes, sponsors, media
progress/a player's routes, completing stops, finds, history, reward hooks
packs/offline packs
staff/panel: tasks, notes, media, texts, releases
brain/the project brain graph (staff only)
ai/character generator and the content studio
texts/interface text overrides, so labels can change without a new APK
coop/exploring together — lobbies with a code, no coordinates in the responses
bots/NPCs walking along zones, with a gift as a one-off find
rewards/token economy: rules, ledger, catalogue, redemptions

Data flow#

The app talks to the API over HTTPS in JSON, and the server is the source of truth. The phone keeps a local cache and a queue of events so that it works without a network, but statistics, completed stops and character appearance live in the database.

  1. The app reads the content it needs (routes, stops, finds, bots) and writes only its own events.
  2. When the network is back, the queued events are pushed to the server.
  3. Every event carries its own identifier, so a repeated sync cannot create a duplicate — the same completion sent twice lands once.

There is deliberately no second source of truth: the app does not keep a private copy of progress that the server could disagree with. Sessions use short-lived tokens with a refresh token; a session list lives on the server side, which is also what the account screen shows.

Client layers (app and website)#

Both clients show the same things, but each uses the tools of its platform. The numbers below come from the same recipe on both sides — the parity is checked automatically.

LayerAndroid appWebsite / panel
MapMapbox Maps SDK 11.31.1 with the Standard style, plus our own GL layers (CustomLayer) for figures and markersMapbox GL JS 3.30, with marker sprites drawn from the same palette as in the app
Characternative blocky renderer: its own GLB parser and shader, no texturing through a WebViewa model viewer on /character, the same atlas and the same blocks
LogicJetpack Compose; pawn movement, the camera, markers and the draw plan are pure Kotlin, testable on a server without a phonestatic HTML/CSS/JS with no framework; the marker and bot modules have node tests

On the Android side the code is split into Gradle modules:

  • :app — UI, navigation, character creator, map;
  • :core — networking, auth storage, shared models, including the lobby models and API client;
  • :ar — the ARCore implementation behind the ArExperience interface;
  • :puck3d — the shared 3D pawn renderer (GLB, animation clips, emote, shadow, rings and NPC layers). Since 0.120.0 both the main map and the AR mini-map use it, so one pawn has one definition.

Why it is built this way#

  1. The AR engine sits behind an interface (ArExperience). Today it is ARCore Geospatial; if it ever needs replacing, the exploration logic does not have to be rewritten.
  2. Content is data, not code. Routes, stops and models live in the database and on disk, so a change goes live without a new APK.
  3. No secrets in the app. The APK knows only the public API address; keys and the release keystore stay on the server and in the release secrets (APK releases).
  4. One creator everywhere. The app, /character and the panel read a single table of wardrobe items, so a character looks identical on every screen (3D character).
  5. The domain layer knows nothing about the map engine. It sees only the AR port, so the game logic stays testable without a device and without a GPU.

Documented for app version 0.120.1 (updated 2026-09-26).

Machine translation — the Polish version prevails.

App version: 0.120.1 · API version: 0.35.0 ·

docs.fyndi.app · 2026-09-26