Dönerbuch
Every Döner, on the record.
Dönerbuch is a journal for Döner. You log what you ate, where and for how much, rate it once you’ve finished, and everything ends up in your journal, your statistics, a map of your places and a leaderboard with your friends. It’s a mobile-first web app you install on your home screen, and the whole interface is in German.
What it is
- Log now, rate later. An entry is saved right away with time, type, meat, place, price, friends and an optional photo. It stays offen until you rate it, so you don’t have to type with sauce on your fingers.
- Ratings with detail. An overall score in stars, plus separate scores for meat, bread, sauce, salad, portion and value, and a note.
- Places from GPS. Your position is turned into a place via reverse geocoding. Places nearby that you or your friends have already used are suggested first, and two places with the same name within 50 m are treated as one.
- Friends. Add friends by code, QR code, search or invite link. You see their entries in a feed, compare yourselves in a ranking, and can tag the friends you ate with.
Why it exists
Everybody has an opinion on the best Döner in town, and nobody remembers what they actually paid or how the last one really was. A notes app doesn’t sort, count or put anything on a map, and a review platform is for strangers, not for the group you actually eat with.
Dönerbuch is the small, focused version of that: your own record first, and your friends second.
Core features
| Feature | What it does |
|---|---|
| 📓 Journal | Every entry with photo thumbnails, filterable by type, minimum rating and search. Unrated entries are marked offen. |
| ⭐ Rating | Overall stars plus six categories and a note. Open entries count as eaten but stay out of the averages. |
| 🗺️ Map | All your places as pins, with filters, search and your own location. |
| 📊 Statistics | Month, year and all time: how many, how much you spent, your best per type. |
| 👥 Friends | Ranking and feed, friend requests, invites by code, QR or link, tagged companions. |
| 🏅 Badges | From Erster Döner to Dönerlegende, plus ones like Stammgast, Entdecker and Nachteule. |
| 🧾 Own types & export | Add your own kebab types and export your entries as CSV. |
| 📱 PWA | Installable, with a service worker and an offline page. |
Vision
The MVP covers the loop of logging, rating and comparing. Next up are the things it deliberately left out: Web Push notifications instead of only the in-app dot on the friends tab, email verification, and cleaning up photos that were uploaded but never saved with an entry.
Technical details
Stack
| Layer | Choice |
|---|---|
| Framework | Next.js 16 (App Router, Server Actions, Turbopack), React 19 |
| Language | TypeScript (strict) |
| Styling | Tailwind CSS v4 |
| Database | PostgreSQL with Drizzle ORM |
| Auth | Better Auth - email and password, optional Google |
| Maps | MapLibre GL with OpenFreeMap tiles, Nominatim for reverse geocoding |
| Photos | S3-compatible storage (Cloudflare R2, MinIO, AWS) via presigned uploads |
| PWA | Serwist service worker |
| Testing | Vitest (unit and database tests), Playwright (end-to-end) |
| Deployment | Docker (standalone build) on Coolify |
Engineering decisions
- Rate later as a first-class state. An entry without a rating is a valid entry, not a draft. Open entries count towards totals, spending and the ranking, and are only left out where a rating is needed: averages and the best per type.
- Privacy by default for positions. Friends see the place, never the exact GPS fix. Entries are scoped to their owner, their friends, or the friends tagged in them, and every page, action and route checks that itself.
- Uploads the server controls. The browser uploads straight to the bucket with a presigned PUT, but only to keys the server issued, with size and type part of the signature. Photos are re-encoded in the browser first, which strips EXIF and GPS data.
- Stats in SQL. Statistics, ranking, feed and map are SQL aggregations in one place (
src/server/queries/), tested against a real database. Month boundaries follow a configured time zone, and every entry keeps its own currency - totals are shown per currency, never converted. - A replaceable geocoder. Nominatim sits behind an interface, rate-limited to one request per second and cached on a roughly 11 m grid, in line with its usage policy.
- Rate limits where they matter. Login, sign-up, new entries (database-backed, safe under parallel requests), uploads, friend requests, search and export each have their own limit.
Get & use Dönerbuch
Use it
Open doener.ntrapp.net on your phone, sign up, and add it to your home screen. Location needs to be allowed for places to be found automatically.
First run
- Tap the plus and log your Döner: type, meat, place, price, and who you were with.
- Eat it.
- Open the entry and rate it - the journal, statistics and map fill up from there.
- Add friends under Freunde to see their entries and the ranking.
Dönerbuch - every Döner, on the record.