How TwitchR is built: an offline-first wildlife recording app
For the technically curious. None of this is needed to use the app — it's here for people who want to know what's under the bonnet, or who want to get their records in and out programmatically.
Offline first, not offline tolerant
The app is a Progressive Web App: plain HTML, CSS and JavaScript ES modules with no build step and no framework. A service worker precaches the app shell so it opens with no connection at all, and every record you make is written to a local IndexedDB database on the device first. The server is a replica of your device, not the other way round.
Synchronisation runs in the background whenever the device is online. Changes go up in batches; changes from elsewhere (another device, a friend's shared records, an import) come down as a delta — the client asks for everything updated since the server timestamp it last saw, and soft-deleted records come back as tombstones so a deletion made while you were offline isn't missed. Every syncable record carries a version, and for your own records the rule is simply last write wins.
A documented REST API
The browser app is just one client of the API — the same JSON endpoints are available to anything else you'd like to connect. The contract was written with desktop synchronisation in mind (Microsoft Access was the original target), so it's deliberately conservative: versioned URLs, a consistent response envelope, cursor-based delta sync and predictable error codes.
- Browser sessions
- Short-lived JWT access tokens with a remember-me refresh flow
- Your own tools
- Per-user API keys, created and revoked from Account → API keys inside the app
- Delta sync
?updated_since=<ISO 8601>on locations, visits and observations, returning a server-side cursor for the next call- Reference data
- Species lists, taxon groups, regions and breeding-status codes are all readable so a client can build its own pick-lists
- Bulk data
- History exports and imports use plain CSV that round-trips — what you export can be imported back unchanged
Identification
The Identify feature scores the field marks you pick against a local table of species attributes first, entirely on the device. Only when that doesn't produce a confident answer does it fall back to an AI service — occasionally asking one follow-up question — and it tells you which route produced the suggestion.
Maps and species information
Maps are drawn with Leaflet over OpenStreetMap tiles; the Leaflet library is bundled with the app so map pages still load offline (the tiles themselves need a connection). Place names for recorded positions come from OpenStreetMap's Nominatim reverse geocoder, and species photographs and summaries come from Wikipedia and Wikimedia Commons. None of these require an account or key, and your position is only sent to look up a place name when you choose to record it.
The stack
- Front end
- Vanilla JavaScript ES modules, IndexedDB, service worker, Leaflet 1.9
- API
- PHP 8.2 with a small hand-written kernel and router — no framework
- Database
- MariaDB 10.11
- Auth
- JWT (firebase/php-jwt), Argon2id password hashing, email verification and password reset
- PHPMailer over SMTP — first-ever records of a species are notified to the administrator
- Hosting
- One Docker container (php:8.2-apache) plus a database container, behind a Caddy reverse proxy that handles TLS
- Testing
- Front-end tests render real views against seeded IndexedDB data under jsdom; every served module is parsed as ESM before a deploy so a single syntax error can't take the whole app down
Privacy in the design
Locations are personal to each recorder rather than a shared gazetteer, so a favourite patch is nobody else's business unless you share records with a friend. Exact coordinates are optional on every record — you can always just type a place name. There is no analytics, no advertising and no third-party tracking on any page.
Want to build something against the API? Create an account, generate a key under Account → API keys, and email admin@twitchr.app for the full API contract.