Marsa Clinics
A booking and queue system for a four-doctor clinic, shown as a working demo — patients book and follow their turn from a phone, reception runs the day, a waiting-room screen calls numbers aloud in Arabic, the doctor writes the visit, and the owner reads the week.
Five screens on one database, each up to date with the others within seconds. Every role opens with one click — no account, no password — and the clinic starts over every day.

- Role
- Design & build, end to end
- Scope
- Patient site + four staff screens, 13 pages
- Stack
- Next.js 16 · TypeScript · Tailwind 4 · Postgres
- Type
- In-house demo, fictional clinic
The brief
A small clinic often runs on a paper list at reception and a full waiting room. Patients arrive early because they cannot know when their turn will come, reception answers the same question all day, and how fast the doctor is actually going is invisible to everyone outside the room.
The ask was a system the whole clinic could see itself in: booking from a phone without an account, a desk that runs the day, a screen in the waiting room that calls numbers aloud in Arabic, a visit sheet for the doctor and numbers for the owner — all in Arabic, all on one database, every screen current within seconds of the others.
It is a portfolio piece, which added a constraint of its own: a stranger has to be able to try every role in under a minute and leave nothing behind. So every seat opens with one click, everything in it is invented, and the clinic rebuilds itself every day. The project began as a single waiting-room page driven by an Arduino keypad and buzzer; this is what that page grew into.
What shipped
- Booking without an account — doctor, exam or follow-up, day and time, name and phone — up to 14 days ahead, at least 20 minutes before the slot, and no more than three open bookings per phone number
- A ticket page per booking with the stages of the visit, add-to-calendar, reschedule and cancel — and, after check-in, the place in line and an estimated wait that updates by itself
- Reception: today's list with search, check-in, walk-ins, emergencies that go to the front, no-shows, payments in cash or InstaPay, and a day-close that compares the counted drawer with what was recorded
- A waiting-room screen with seven-segment numbers per room, a chime and the number read aloud in Arabic, an urgent tone for emergencies, and a QR code to the patient's queue page
- The doctor's desk: the patient in the room and the line outside, a visit sheet with vital signs, complaint, diagnosis and notes that saves itself, previous visits, and an A5 prescription in English and Arabic
- The owner's numbers over 7 or 30 days, each compared with the period before
- One-click entry to every role, four doctors — a dentist among them — and 148 invented patients seeded around today, and a fresh start every day
- screens on one database
- 5
- automated tests on scheduling and the queue
- 41
- browser checks across every role
- 52
- click to enter any role
- 1





Decisions, and why
Anyone can list a stack. The useful part of a case study is the calls that had a real alternative — so each of these says what the tradeoff was, not just what was chosen.
- 01
The database refuses a double booking
Overlapping visits for the same doctor are rejected by a Postgres exclusion constraint, not by application code.
Two phones can tap the same slot in the same second. A check in code reads the free slots and then writes, and both requests pass the check. A constraint on doctor, day and time range makes the second write fail inside the database, and that patient is told in plain words that the time was just taken.
Tradeoff · It needs the btree_gist extension and ties the schema to Postgres. Walk-ins are left out of the rule on purpose — they join the line as it stands and never owned a slot.
- 02
Every action is one statement
Each thing a person at the clinic does is a single UPDATE whose WHERE clause carries its precondition.
The serverless Postgres driver sends one statement per request, with no interactive transactions. So “call next” picks the first patient in line and marks them called in one statement that also checks nobody is already in the room — two receptionists pressing it together call one patient. A tap on a screen a few seconds out of date matches nothing, and is answered with a sentence and a refresh.
Tradeoff · Some of the logic lives in SQL rather than TypeScript, which makes it harder to read. It is covered by tests that run against a real Postgres in memory rather than a mock.
- 03
Live screens by polling, made cheap
Screens ask every two to three seconds, and the server answers “nothing changed” after one small query.
Every write bumps a revision number. A screen sends the revision it already has; if it still matches, the reply is an empty 204 and nothing is redrawn. Polling slows after five quiet minutes, slows again after thirty, and stops while the tab is hidden, so a screen left on overnight costs almost nothing. It runs on any serverless host, with no socket server to keep alive.
Tradeoff · A change can take up to one interval to appear — two seconds on the waiting-room screen. Server-sent events would remove that delay, and are the next step.
- 04
The wait estimate learns the doctor's pace
“About 25 minutes” comes from the line and from how long today's visits are actually taking.
Booked durations are a plan, and doctors run late. After three finished visits the estimate compares planned and actual time over the last six and stretches or shrinks the rest of the line — clamped between 0.7 and 1.6, so one long case cannot make the whole queue look hopeless. The same arithmetic runs on the server and in the browser, so the phone's countdown moves every second between two polls.
Tradeoff · It is an estimate, and the ticket says so in words. Until three visits are done it assumes the plan.
- 05
The clinic keeps Cairo time
A day is a date string and a time is minutes since midnight, both in Cairo time, wherever the server runs.
The host runs in UTC and a visitor's phone can be set to anything. Keeping clinic time as plain local values means “Tuesday, 5:00 PM” is the same for the server, the screens and the patient, across the daylight-saving change, and slot arithmetic stays whole numbers.
Tradeoff · The clinic can live in one timezone only — true of a clinic. The one place that needs real instants, the calendar file, converts explicitly.
- 06
The call is spoken by the browser
The waiting-room screen plays a chime, then reads the number aloud with the browser's own Arabic speech synthesis.
A call that is only on screen misses the patient looking at their phone. Recorded audio would need a file for every number and room; speech synthesis reads “رقم 24، اتفضل عيادة 1” from text. The screen asks for one click up front, because browsers keep audio locked until then, and picks the best Arabic voice on the machine, Egyptian first.
Tradeoff · Voice quality depends on the device. Chrome on a plain Windows install has no Arabic voice; the chime still plays and the screen says why the number was not read. Edge on Windows has a natural one.
- 07
One click per role, checked twice
Every seat opens without an account, through a signed cookie checked before a staff page renders and again inside every action.
A demo that asks for a password loses most visitors at the door, so each role is a button. The cookie is signed and names its seat. A visitor without one is sent back to the role picker before any staff page renders, and every server action checks the seat again, so a routing mistake cannot open a write — and a doctor cannot open another doctor's visit.
Tradeoff · Anyone can be the receptionist. That is the point of a demo; a clinic going live would need named staff accounts first.
- 08
The prescription is written the way Egypt writes them
Drug names in English under R/, instructions in Arabic, the doctor's name in both, on an A5 sheet.
The pharmacist reads the drug names and the patient reads the instructions, so each part is in its reader's language. Vital signs keep their Latin units. A short list of common drugs per specialty fills a line in one tap, and the sheet saves itself a moment after the doctor stops typing.
Tradeoff · Every name on it is invented, and the sheet is printed as not valid for dispensing.
- 09
The demo resets itself
The first request of a new Cairo day claims that day and rebuilds the clinic from scratch.
Visitors book, call and cancel all day. The seed is laid out relative to today — three weeks of history for the owner's numbers, a waiting room mid-shift, two weeks of half-booked days ahead — so the demo looks the same on any date. Claiming the day is one insert, so two first visitors cannot both rebuild it. And when the waiting room is empty, the first seat taken brings in a few invented walk-ins, so there is always something to press.
Tradeoff · Whatever a visitor does is gone by morning — right for a demo, and the first thing to remove for a real clinic.
Five screens, one moment
The hardest part was not any single screen. It was that five of them describe the same moment on different devices, through a host that keeps no connection open — and a patient deciding whether to leave the café round the corner depends on their phone being right.
Each screen holds a snapshot and its revision number, and recomputes what can be recomputed locally — the countdown, the estimate, the clock — every second. A screen that has just acted asks for a fresh snapshot at once rather than waiting for its turn. The waiting-room screen keeps its own queue of calls, so two calls in quick succession are both announced, one after the other, and a call that was already on the board when the screen was switched on is treated as history, not news.
It was checked end to end rather than by eye. A script drives a patient's phone, the reception desk, the waiting-room screen and the doctor in separate browsers at once — book, check in, call, recall, walk in, finish, close the day — and asserts that each screen changes within seconds. Those 52 checks pass on a local database and against the live deployment on Neon. The run surfaced one problem in the test itself: holding a dozen live pages open against a remote server exhausted the browser's own request limits, so the script now closes each part's pages when that part is done.
What was deliberately left out
Scope discipline is part of the work. Each of these was considered and cut for a stated reason.
- Online payment
- Payment is recorded at reception, in cash or InstaPay. A payment gateway is a contract with a clinic, not something to try in a demo.
- SMS verification
- Bookings are tied to a phone number and capped at three open ones. A code per booking costs money per message, and belongs to the day a real clinic goes live.
- A full medical record
- The doctor writes a visit sheet. A complete record needs permissions, retention rules and security on another level.
- Staff accounts and an audit trail
- Every role is one click by design. A clinic using it for real needs named users and a record of who did what.
- Push instead of polling
- Polling was chosen to work on any serverless host first. Server-sent events would remove the two-second delay.
- The hardware call button
- The original Arduino keypad and buzzer are not connected to this system. Bridging them over Web Serial is the next step.
Everything in the demo is invented: the clinic, the doctors, the patients and the phone numbers. The photographs are from Unsplash and show no faces. Scheduling, the queue and the database writes are covered by 41 automated tests, and every role by 52 browser checks; layouts were also reviewed by hand at phone and desktop widths. The interface is Arabic only. The spoken call depends on the device having an Arabic voice. No real clinic has used it.
Stack
- Next.js 16
- React 19
- TypeScript
- Tailwind CSS 4
- Postgres
- Drizzle ORM
- Neon
- Vercel
Services
- Product design
- Interface design
- Frontend engineering
- Backend engineering
- Data design
Need something built to this standard?
We design and build complete systems — a purchase path that works, roles, and an admin panel that actually runs the business. And Arabic here is never a translation layer bolted on at the end.
