Restaurant discovery
JoyTop
joy-top.uz is live: a food guide to Tashkent with a map, search, editors’ collections and a path for venue owners. The website, the mobile app and the Telegram bot run on one database.

Find · Taste · Connect: where Tashkent decides where to eat.
A guide to eating in Tashkent, chosen by local writers rather than by advertising: selected places, verified menus and real prices, editors' collections, and a map for when you already know what you want. JoyTop is our own product: Nodirkhon is its co-founder and CTO.
Product strategy · UI/UX design · Full-stack engineering · Editorial system
In restaurant lists, whoever paid ranks higher, and the menus are long out of date.
People in Tashkent choose where to eat from lists where the one who paid comes first. Menus and prices in those lists go stale.
JoyTop was designed as a guide where local writers choose the places and every card carries a real menu with real prices, on the website, in the mobile app and in a Telegram bot at once.
We wrote the guide’s rules into the database and the map, not into people’s memory.
A rule in the code decides how a pin looks: an expert’s pick gets the most prominent pin, and a venue without a menu and photo is a plain dot. The database decides what a venue’s plan includes. The website, the app and the bot read the same records.
Use one shared place model across web and mobile so every surface reads from the same source.
Use geographic data for nearby discovery and map browsing instead of maintaining separate location lists.
Keep ranking editorial, with operator tooling for menus, collections, and verification.
Set each map pin’s look by rule: an expert’s pick gets the most prominent pin, and a venue without full details is a plain dot.
This is JoyTop’s design, rebuilt from its own code.
The window below is not a picture. The Fraunces and DM Sans type, the blue and orange from its tokens, the bowl-shaped map pin and the rating seal are rebuilt live from JoyTop’s source. The map is drawn and shows no real city, and the venues on it are samples. Beside it are screenshots of the public joy-top.uz to compare against.
Образец А
Национальная
Образец Б
Плов
Образец В
Шашлык
This is not a screenshot: it is live HTML and CSS rebuilt from JoyTop’s source. Pick a pin: it grows the way it does on joy-top.uz, and the venue’s row appears below.
- Fonts
- Fraunces · 300–700
- DM Sans · 300–700
- JetBrains Mono · 500
- Colours
- --jt-blue-700 #0f1a6c
- --jt-blue-900 #060a38
- --jt-orange-500 #f6a34e
- --jt-orange-700 #c77e2e
- --jt-cream-100 #fbf8f4
- --jt-cream-300 #f3efeb
- --jt-ink-900 #15140f
- --jt-ink-500 #514e42
- --border-default rgba(15, 26, 108, 0.16)
- primary #2D4AA8
- Radius
- 4px · .seal
- 12px · .searchBtn
- 16px · .searchBar
- Motion
- scale(1.34)setPinActive()
- 0.18s cubic-bezier(.2,.8,.2,1)pin image
- Source
- web/src/styles/tokens.css
- web/tailwind.config.ts
- web/app/fonts.ts
- web/app/[locale]/map/map.module.css
- web/app/[locale]/map/components/MapNav.tsx
- web/app/[locale]/map/markerDom.ts
- web/app/[locale]/map/pinAsset.ts
- web/public/brand/logomark.svg
- web/components/collections/Seal.tsx
- web/components/collections/Seal.module.css


We measured before fixing, and put the rules where nobody can get around them.
One spatial data system powers the public web product, native mobile application, Telegram bot and the operational tools behind the catalogue.
- Surfaces
- Next.js web, Expo mobile and a Telegraf bot
- Data
- PostgreSQL, Supabase, PostGIS, storage and row-level access policies
- Discovery
- MapLibre plus full-text, trigram and prefix search fused in Postgres
Search is a ranking system, not a text box
Weighted full-text, fuzzy trigram and prefix results are combined with reciprocal-rank fusion, so exact names, inflected terms and misspellings can resolve through one query path.
Location is part of the data model
PostGIS powers nearby discovery and the tiered map from the same restaurant records used by web, mobile and operations.
Every role sees a governed surface
Reader, owner, expert and administrator workflows share one backend while row-level policies and server functions protect tenant and editorial boundaries.
External discovery is a controlled fallback
When the internal catalogue has too few results, cached Yandex results can be blended only after restaurant-category filtering, without replacing JoyTop's own source of truth.
The slow map, taken apart by measurement
Without cached data, the map was slow to open. On 14 August 2026 we measured it on the live site: almost half of the response was the same field names, repeated for every venue. The new format sends each name once, and on the same data the response is half the size. Apps already installed on people’s phones keep receiving the old format.
The cause was found by measuring, not guessing, and nothing broke for people who already have the app.
One SMS instead of two — on a test send
A login code written in Cyrillic went out as two paid SMS parts. We wrote the text in Latin script and checked every character: even the Uzbek ʻ would have brought the second part back. A test send on 4 August 2026 arrived as one part: 110 soʻm instead of 220–320.
This login text costs two to three times less, according to a real send, not a calculation.
One complaint, several causes
To a user, every login failure looks the same: the code did not arrive. We wrote down each cause behind that symptom and the real error for each. The login service hides the error text, so the runbook records how to read it.
A login failure is found from the record, not from one person’s memory.
A catalogue without guesses
The catalogue was filled venue by venue from open sources, and every accepted fact records its source and a confidence level. Automation only fills empty fields; only a person can replace what is already recorded. If a venue has closed, changed its name, lost its website to someone else or duplicates another, the system does not guess: it sets the venue aside for manual review with the reason named.
An empty field is better than a stranger’s phone number on a venue’s card.
The database holds the plan
The database, not the app screen, checks what a venue’s plan includes. When a subscription ends, the venue returns to the basic plan, and there the database will not accept an 81st menu item. If the plan cannot be determined, the basic one applies, and two simultaneous additions cannot slip past the limit together.
A plan means the same thing on the website, in the app and in the database itself.
429 database migrations · 58 website pages on the repository’s main branch. Counted by us on 26 September 2026.
No card will show a stranger’s phone number, and no one gets around a plan through the app.
The database checks what a plan includes, so the website, the app and the bot cannot drift apart. A doubtful catalogue record waits for a person instead of being guessed. If SMS login breaks, the cause is found from the record. And a slow spot is measured first and rewritten only after.
Want a system like this for your business?
Describe the problem in two or three sentences. We reply the same working day and suggest a time to talk.
Reach usDiscuss your project on Telegram
- We reply the same working day, Tashkent time.
- You write to Nodirkhon; he brings in whichever of us your problem needs.