Patients at small clinics in Lahore get a paper token and no idea how long they'll wait, so they stay glued to the waiting room and the receptionist answers "how much longer?" all evening. QueueCare is a live clinic queue: urgent patients are seen first, everyone else keeps their place, and every patient can follow the queue on their own phone.
Live: queuecare-zeta.vercel.app · PRD: docs/PRD.md · BRD: docs/BRD.md · Tests: plan · report · Pilot: plan · Lessons: lessons learned · Board: project
Try it in a minute (demo data, public accounts): open the live site and press Try the live demo; one click signs you in as reception. Register a patient and scan the QR code with your phone, or use the demo strip to open the patient's phone page and the waiting-room display in new tabs. Then press Switch to the doctor and call the next patient: every open screen changes by itself.
| Login | Password | |
|---|---|---|
| Reception | reception@queuecare.demo |
QueueCare-Reception-1 |
| Doctor | doctor@queuecare.demo |
QueueCare-Doctor-1 |
I interviewed a clinic receptionist and three patients in Lahore (findings). All four said the worst part isn't the wait itself but not knowing how long it will be: both patients who said what they expected waited longer than they thought, people are afraid to step out in case they miss their turn, and the receptionist spends the evening answering the same question. Urgent and booked patients also jump the queue with no explanation, which feels unfair. QueueCare is the second project of my 3-week, 10-project portfolio, and the one where I practised the whole path from interviews to a deployed, tested product.
- Staff sign-in with roles: reception and the doctor each see only their own screen
- Register a patient in a few taps: walk-in or booked, Normal, Urgent or Emergency; the next token is issued with a wait estimate and a QR code, and can be printed as a receipt-sized slip
- A fair priority queue: urgent first, waiting time moves everyone up, bookings are kept; reception changes urgency from a badge on each patient, and can show a patient's QR code again if they lost it
- Call next: the doctor's button says exactly who it will call and who it will mark finished
- Live on every screen: reception, doctor, the patient's phone and the waiting-room TV update within about a second, without refreshing
- The patient's page, without an app or account: token, patients ahead, a wait range and whether it is safe to step out (come back when 2 patients are ahead); when called, the whole screen says Please go in now; no other patient's name is ever shown
- Waiting-room display: "Now serving" and the next three tokens in very large type, readable across the room
| Doctor | Patient's phone | Waiting-room TV |
|---|---|---|
![]() |
![]() |
![]() |
Next.js 16 (App Router, Server Components, Server Actions) · TypeScript · Supabase (Postgres with row-level security, Auth, Realtime) · Tailwind CSS · shadcn/ui · Vitest · Playwright · GitHub Actions · Vercel (Mumbai region)
flowchart TB
subgraph Browser
R[Reception screen]
D[Doctor screen]
P[Patient's phone]
W[Waiting-room TV]
end
subgraph Vercel["Vercel (Mumbai)"]
X[proxy.ts: session check]
S[Server Components and Server Actions]
L["src/lib: pure rules<br/>priorityQueue, access, registration, callNext..."]
end
subgraph Supabase["Supabase Postgres (Mumbai)"]
RLS[Row-level security and column grants]
V[(visits, profiles)]
F["Public functions<br/>visit_status, waiting_room"]
T[(queue_ticks)]
end
R & D --> X --> S
P & W --> S
S --> L
S -->|staff, as the signed-in user| RLS --> V
S -->|public pages| F --> V
V -->|trigger on every change| T
T -.->|Realtime: refresh| R & D & P & W
Pure core, imperative shell. Every decision (queue order, who may open which page, input validation, what Call next does) is a pure TypeScript function in src/lib, tested without a browser or a database. Pages and Server Actions only read data, call those functions and save the result. The database is the real security boundary: staff reads and writes go through row-level security as the signed-in user, and public pages can only call two read-only functions that return token numbers, never names.
The queue is a priority queue ordered by three rules, applied in turn:
- Level, highest first. Emergency (2) > Urgent (1) > Normal (0). Every 30 minutes of waiting raises a patient by one level, up to Urgent, so a Normal patient can't wait forever behind a stream of urgent ones, but no amount of waiting outranks an emergency.
- Then earliest arrival. A booked patient counts as arriving 10 minutes before their booking (or when they actually arrived, if later).
- Then the lower token number.
Example, at 7:00 pm. A-03 (Normal, arrived 6:26), A-07 (Urgent, arrived 6:55), A-09 (Normal, arrived 6:48) and A-12 (Emergency, arrived 6:58) are waiting. A-12 goes first (Emergency). A-03 has waited 34 minutes, so it now counts as Urgent and ties with A-07; it arrived earlier, so it goes second. Then A-07, then A-09.
Wait estimate: patients ahead × the average of today's last five consultations (10 minutes until there are any), shown as a range from 80% to 125%, rounded to 5 minutes: "About 15–25 min".
Complexity: ordering n waiting patients is a sort, O(n log n); a busy clinic has 15–20 waiting at its peak, so the whole queue is recomputed on every change rather than maintained incrementally.
| Decision | Why | Trade-off |
|---|---|---|
Rules as pure functions in src/lib |
Fast, exact unit tests (86) and one source of truth for every screen | The server reads the queue before deciding, so two simultaneous registrations can both count the same people ahead (only the estimate is affected; token numbers are protected by a database lock) |
| Row-level security as the security boundary | A missing check in a page can't leak data; proved by a script that acts as each role (16 checks) | Rules live in SQL migrations as well as in the app |
| Public pages read through two database functions | Patients don't sign in, and no secret key is needed in the app; the functions return only what a patient or the waiting room may see | The waiting-room order is computed in SQL too, so a check compares it with the TypeScript rules on a real mix of patients |
Live updates through a one-row queue_ticks signal |
Public screens can listen without seeing any visit data; each screen re-loads through the normal permission checks | One extra round trip per change; a 30-second safety refresh covers missed messages |
| Visits are never deleted | Estimated vs actual waits can be measured in the pilot | Test data has to be tidied (marked "missed"), not removed |
| Server code in Vercel's Mumbai region | The database is in Mumbai; every page makes several round trips | Measured: live updates went from 0.9–2.7 s (local) to 0.8–0.9 s |
| A calm-clinic design system (v1.1): Manrope, forest and sage, motion that explains what changed | Designed and approved as a clickable prototype before any code; every screen was then built to match it | Animations are switched off for people who ask for reduced motion, and kept short so a busy receptionist never waits on them |
- Interviews before code: one receptionist and three patients on 7 October 2026, then a BRD (as-is and to-be processes, 27 requirements (5 business, 15 functional, 7 non-functional) with MoSCoW priorities, use cases, traceability) and a PRD (user stories, success metrics, priority and estimate rules)
- 38 issues on a GitHub Project board, one commit per issue, conventional commit messages, CI on every push
- A code review before committing the registration feature found 7 issues, including one no test had caught (a failed registration left the previous patient's QR code on screen); all were fixed before commit
- Testing: a test plan and test report; nine defects found and fixed, each traced to an issue
- Redesign (v1.1): a workflow review of every screen, a clickable prototype approved before building, then one issue and one commit per screen
- Releases: v1.0, v1.1
Measured, not estimated. QueueCare has not yet been used by a real clinic, so there are no patient numbers yet; the pilot plan is how they will be collected.
| What | Result |
|---|---|
| Unit tests | 86 / 86 pass |
| Security checks (each role, plus public functions) | 16 / 16 pass |
| System tests, Edge, Chrome and a 375 px phone view, on the live site | 98 pass (4 skipped by design) |
| Live update to another open screen (target: 2 s) | 0.8–1.4 s after the change is saved, on the live site (v1.1) |
| Patient page on throttled mobile data (target: 3 s) | Token visible after 0.5–0.7 s, page fully loaded after 2.2–2.3 s (v1.1) |
| Public pages | No names and no urgency in anything the public can read |
- Run the pilot at a small clinic and measure estimate accuracy (target: 70% of real waits inside the range)
- Mark a called patient as missed and re-queue them (#10)
- Show estimated vs actual waits to the clinic (#12), and an optional patient account (#11)
- Bring live updates back under 1 second on every browser: v1.1 measured 1.4 s in Chrome (v1.0: 0.9 s), possibly because the new header reads from the database on every refresh
- Say "Under 5 min" consistently when an estimate starts at 0 (the token slip currently says "About 0–5 min")
QueueCare can count how it is used with PulseMetrics, a small analytics tool that never learns who did something. It sends four fixed event names (status_viewed, step_out_advice_seen, called_in, patient_registered) and a random browser id, and nothing else: never a page address (a patient's link is private), a name or a health detail. It is off unless three settings are present at build time (NEXT_PUBLIC_PULSE_KEY, NEXT_PUBLIC_PULSE_SRC, NEXT_PUBLIC_PULSE_API). A browser test checks that a patient's private link is never sent, and the privacy page says what is counted.
- Create a Supabase project. In its SQL Editor, run
supabase/migrations/001_init.sql,002_patient_status.sqland003_waiting_room.sql, create the two demo users in Authentication, then runsupabase/seed/demo.sql. - Create
.env.local:NEXT_PUBLIC_SUPABASE_URL=https://<project>.supabase.co NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_... DEMO_RECEPTION_EMAIL=reception@queuecare.demo DEMO_RECEPTION_PASSWORD=... DEMO_DOCTOR_EMAIL=doctor@queuecare.demo DEMO_DOCTOR_PASSWORD=... - Run it (Node 22):
npm install npm run dev # http://localhost:3000 npm test # unit tests npm run check:security # row-level security, acting as each role npm run test:e2e # system tests on the production build
- Waiting-room photo on the home page: Adhitya Sibikumar on Unsplash, used under the Unsplash License.
- Type: Manrope (SIL Open Font License).
Ahmed Malik · Part of my portfolio covering software development, business analysis and product management.



