An independent, locally curated guide and booking platform for Sharm el-Sheikh, shipped to both app stores and to the web by one person. Two faces in one product: travellers get 115+ curated places across 6 neighbourhoods, 9 service verticals, 4 languages and a live radio station; the businesses get a console holding listings, bookings, replies, campaigns and takings, with a scanner and an approvals queue closing the loop.
Drag the seam · wireframe ↔ shippeddesigned and built by one person
FIG.Measured Render
The UI, measured. The shipped screen, drawn to its own griddesigned and built by one person
A measured render: the same screen you ship, drawn as a technical drawing. Every number is a constraint someone has to hold. That is why a simple screen is so hard to build.
A city deserved a better map
Search engines show you the same ten tourist traps. Booking sites show you whoever paid the most. Sharm el-Sheikh has a reef, a dive industry, and six neighbourhoods with six different characters, and none of that survives a results page ranked by ad budget.So I drew the map. Sharm.app is an independent, locally curated guide to where to eat, drink, dive, and stay, with an events calendar, day trips run by local operators, food ordering, emergency information, and a 24/7 radio station broadcasting from the Red Sea coast. It is two-sided: travellers on one face, a business console on the other. One person, two native platforms, a web app, and the backend they both talk to. Version 1.0.0, in both stores.
1, The rule that makes it worth reading
Advertising is always labelled, and ranking is not for sale. That is the whole product thesis, and it is a constraint before it is a promise. Sponsored placements carry a label in the same position, in the same type, every time. There is no paid boost, no suppressed review, no editorial slot with a price. The curation is local and it is ours to defend.A second rule governs everything numeric. The app never invents an aggregate it cannot read. If a rating has 3 reviews behind it, it says 3. If a count is not computable at that moment, the number does not render at all, and the layout is designed to survive its absence. Trust is the product, so the numbers have to be real.Both rules are worth nothing as copy. They hold because of a queue and a console, which is where sections 7 and 8 go.
Advertising is always labelled. Nobody can pay to rank higher or hide a bad review.
2, Nine verticals, one switch
Launching a service is a configuration change, not a release. Places, events, trips, food ordering, stays, diving, transfers, the radio, and the business console all sit behind a single service registry. Each entry carries a remote state: live, coming soon, or hidden. Flip a switch and a vertical appears in navigation, in search, and on the home feed, everywhere at once, with the coming-soon copy already translated.The home feed is remotely composed the same way. Card design, section order, and hero content are configuration, not code. That matters when the map changes faster than a store review queue. A season starts, a dive operator opens, the feed reshapes that afternoon.The app itself is a ground-up rewrite. It replaced an off-the-shelf template that could not carry any of this, and I ran the replacement against a documented parity inventory so nothing quietly went missing on the way across.
3, One design language, two platforms
Mobile and web share the design system 1:1, at the token level, not by resemblance. Colour, type scale, spacing, radius, elevation, and motion are defined once and consumed by both. A hairline is the same hairline in both places. When the scale moves, it moves everywhere.Light and dark are both first class, and dark is neutral graphite, never warm black, so photography of a turquoise sea does not sit on a brown ground. A few surfaces stay ink in both themes, deliberately, because they read as instruments rather than pages.The icon system is hand drawn on a shared 28pt grid, duotone, with exactly one spark per glyph. One accent, one emphasis, no glyph shouting over its neighbour. Motion honours Reduce Motion everywhere, which means every animation has a defined resting state that is a design in its own right, not an absence.
4, A feed that pays for itself as it is read
Mounting the whole home screen at once fired 26 requests in a second and got a development address banned for 2 hours. That was the lesson, and it is now the architecture. Sections reveal as they are read. Each one asks for its data at the moment it enters view, so the feed costs what the reader actually consumes and nothing more.Underneath, the app is offline first. Queries persist locally, and the entire shell renders from shipped fallbacks, so a phone with one bar in a dive centre still opens to a usable map instead of a spinner. The contract between server and client is typed end to end, from procedure to the hook that renders, which is why 117 procedures can move without a screen quietly breaking.
5, Objects, not pages
Three screens are drawn as physical objects, because the interaction is physical.The radio is an instrument faceplate. Hairlines, silkscreened mono labels, a plotter drawing the live spectrum. The stream survives navigation, so you keep listening while you read a listing, and now-playing arrives over a live connection where the server is the sole publisher. The chat beside it is moderated on the same channel.A booking is a perforated ticket. The tear line is real: 18 dashes, individually tinted, carrying a gradient across a gap that a single gradient cannot cross. It is a small trick and it is the difference between a card that says ticket and a ticket.The welcome mark draws itself in the order a hand would draw it, before a single photo has loaded. First impression, zero payload.
6, A station that runs itself
Sharm Radio broadcasts around the clock, and nobody is standing in a studio. The station is its own machine: a broadcast stack I run on its own box, with a music library, scheduled playlists, and an automation engine that walks them 24/7. When a DJ goes live, from anywhere, the automation steps aside for the set and takes the air back the moment it ends. The app never knows the difference, it just shows who is playing.The station reaches the product through the same discipline as everything else. Its credentials live server-side and never travel to a browser. Its artwork arrives addressed to the container that rendered it, so every URL is rewritten to the public origin before a client sees one. Now-playing is pushed to every open screen over a live channel rather than polled for, and the chat rides the same channel with the one rule that makes moderation possible at all: the server is the only publisher. A message passes the profanity filter and the rate limit before anyone hears it, or it does not exist. The listener meter reads what the stream can prove, total, unique, and current, and adds one for you.Mixes from local DJs come in through a submission flow that opens with a contributor agreement, waits for a human review, and answers either way by mail. A copyright takedown path stands next to it, because a station that plays other people's work owes them a door. It is a small radio station with the paperwork of a real one, which is what makes it worth putting a dial on.
7, The other face
The same app, turned around, is the console the businesses run on. Sign in as an owner and the map inverts. Instead of browsing 115 places you hold one, with its gallery, hours, amenities, and prices under your own hand. Flip the device beside this text to the manager side and you are looking at it: the half of the product that never appears in a store screenshot, and the half that decides whether the other half stays honest.Bookings arrive in one book whether they came through the app or over the phone, because a restaurant that takes half its covers by voice will not keep two lists. A manual booking is a first-class booking: same record, same ticket, same seat count, same effect on the night.Reviews come with a reply box. Nothing is deleted for being unflattering, and the owner gets the last word in public, which is the only version of review moderation I am willing to defend. Campaigns are bought in the same console, with a budget, a window, and a placement that carries the label. The wallet reconciles the rest: earned, owed, paid out.
8, The queue in front of the map
What goes live is a decision, not an upload. New places, and edits to places that already exist, land in a submissions queue and wait there. Photos, hours, prices, claims about a dive boat, all of it read by a person before it reaches the map. The queue is slower than a form that publishes on submit, and the slowness is the entire point.That is section 1 again, this time as a workflow rather than a promise. Curation is only a position until something enforces it, and the thing enforcing it is a queue somebody has to work through on a Tuesday. The label on a sponsored placement works the same way: it is drawn from the record at render time, in a fixed position, and there is no field anywhere in the console that moves it or buys rank.A rejection carries a reason, translated, because an owner who cannot see what was wrong will submit the same thing again next week.
9, The loop that closes
A booking is not finished when it is paid, it is finished when someone walks in. Guests pay by card, by native wallet, or in cash on arrival. The pass lands in the phone's wallet and the countdown runs as a live activity on the lock screen, both delivered by two small native modules I wrote because nothing off the shelf carried the ticket design.The far end of that is the scanner. A signed check-in code turns a booking into an arrival and an arrival into takings, in one motion at a door, which is why the console reports what was earned rather than what was requested. Statistics follow the same rule as the rest of the product: a number the console can prove, or no number. Push notifications route straight into the relevant screen, so a message about a booking opens the booking.
10, Four languages and the guards around them
Zero hardcoded strings, in four languages, at roughly 551 KB of translated copy. English, German, Russian, and Italian, with every surface, error, and empty state translated, including the ones a reviewer only sees when something fails.Around it, the guards. Roughly 70 web routes and 117 procedures are covered by about 585 tests. Head content is rewritten per request, so a client-rendered app still unfurls correctly when a link is pasted into a chat. A machine-readable index sits at the root for the crawlers that read instead of scrape. And after a 6-minute production outage, I added a build guard that makes that entire failure class impossible rather than unlikely.Two store rejections landed during review. Both were diagnosed the same day, one of them a missing purpose string for motion sensors, and the fix shipped in build 7.
What it demonstrates
Layer
What I did
Product
Positioning thesis, labelled advertising enforced by workflow, 9 verticals behind remote switches, an approvals queue as the curation guarantee, parity inventory for a full rewrite, store submission and review
Two-sided
A business console holding listings, one book for online and manual bookings, public replies to reviews, campaigns, wallet and takings, plus the scanner that closes booking to arrival
UX / Design
One token system shared by mobile and web, light and dark as peers, hand-drawn icon system on a 28pt grid, instrument and ticket surfaces, reduced-motion resting states
Engineering
Typed contract end to end, offline-first data, pay-as-you-read feed, a self-run 24/7 station with pushed now-playing and moderated chat, payments and signed check-in, two custom native modules
The seam
The product rule, the design token, and the query that fetches it are the same decision held in three places by one person
Role: Product, UX, and engineering, end to end. Timeline: 2026 · Version 1.0.0 live on the web and in both app stores.