Skip to work
Melbin Kuriakose
Back to work

eSanjeevani

A concept redesign of India’s national telemedicine service

  • Research
  • UX
  • UI & design system
  • AI
  • Full-stack
  • Solo

The waiting is not going away. So make the waiting do the work.

24
routes
74
components
21.7k
lines of TypeScript
7
taps to a doctor

Before anything else

What this is

An independent concept redesign of India’s national telemedicine service, built end to end.

What it is not

Not commissioned by, affiliated with, or endorsed by eSanjeevani or any government body.

What is real

The product is live and usable end to end - both the patient and the clinician sides.

What is stood in for

The roster and the queue rate are simulated, and sign-in is a PIN rather than a verified number. The product ships a page saying so, linked from every screen.

My role
Everything - research, UX, UI, design system, full-stack build, AI, deployment
The subject
India’s national telemedicine service, redesigned as an independent concept
Status
Live and usable end to end, both patient and clinician sides
Stack
Next.js 16, React 19, TypeScript, Tailwind 4, Postgres, LiveKit
The eSanjeevani landing page - a ₹0 price mark beside the headline 'Tell us what's wrong. In your own words.'

The person

Who is actually holding the phone?

Before any screen, before any feature - the description that settled every argument on this project.

One phone.
One hand.
Poor connectivity.
Someone is unwell.

  • Unwell

    Every extra step costs someone who has none to spare.

  • On a mid-range Android

    Every font file and every bundle is a real download.

  • On data that drops

    Anything that depends on a session staying alive will fail.

  • Possibly acting for someone else

    One phone covers a son’s fever and a mother’s knee.

  • Speaks more easily than they read

    So speech is the default input, everywhere.

  • Mixes Hindi and English

    Spoken language is tracked separately from interface language.

  • Has no medical vocabulary

    “Loose motions since two days”, not “acute diarrhoea, 48h onset”.

That single description killed more features than any other decision on the project.

The user is not choosing between apps

They are unwell, on a mid-range Android, on data that drops, possibly acting for someone else in the house. That single sentence killed more features than any other decision on the project.

They speak more comfortably than they read

And often mix Hindi and English mid-sentence. So speech is the default input everywhere, typing is always one tap away, and spoken language is tracked separately from the interface language.

A phone is a household, not an account

One number is used for a son’s fever, a mother’s knee and one’s own chest pain. Three people, three records, one login - which is why members carry their own health facts rather than sharing the account’s.

No medical vocabulary, ever

People say “loose motions since two days”, not “acute diarrhoea, 48h onset”. The intake had to accept the first and hand the doctor something closer to the second - without inventing anything in between.

The problems

Four things that fail before a doctor ever speaks

None of these are technology problems. They are all experience problems.

The wait produces nothing, and it is fragile

You hold a place in a browser tab. Close it, lose signal, get called away - and your turn can go with it. Meanwhile the waiting itself produces nothing at all.

The consultation starts from zero

A doctor opens the call knowing nothing. Three of four minutes go to questions the patient could have answered while waiting - and a patient who is unwell, nervous and on a clock tells the story badly.

Language is treated as a preference

Speaking to a doctor you cannot understand is not a degraded consultation. It is not a consultation. Yet language sat in settings rather than in the routing.

It ends with a PDF and no idea what happens next

A prescription you cannot read, no plan, no follow-up, and a fifth visit that looks exactly like the first because nobody linked them into a course of care.

The big idea

Move the intake in front of the queue

The queue is the one thing the service cannot remove. So the redesign changes what happens inside it.

  1. 1Describe the problem
  2. 2System structures the intake
  3. 3Join the queue
  4. 4Wait, while it processes
  5. 5Doctor receives a case sheet
  6. 6Consultation starts at care

The queue is the one thing the service cannot remove. So it stops being dead time: intake happens during it, and the clinician opens the call with a case sheet instead of a blank screen. The same four minutes of clinician time, spent differently.

The wait should do the work.

Everything else in the product follows from this one decision.

The product decisions it produced

The wait does the work

The queue is the one thing the service cannot remove, so it stops being dead time. Intake happens during it, and the doctor opens the call with a case sheet instead of a blank screen. Everything else in the product follows from this one decision.

Position is derived, never stored

Your place comes from when you joined and how many were ahead of you then. Close the tab, flatten the battery, walk somewhere with signal - the answer is identical. That is the exact failure the real service is known for.

Language outranks speciality

In the routing ladder, a general physician who can understand you sits above a specialist who cannot. It is one line in a rules file and it is the most consequential product decision in the build.

Never read your own words back

An early version showed the patient their transcript to confirm. It tested as a wall of text somebody unwell will not proofread. It was replaced with the fielded case sheet the doctor will actually see - you confirm the output, not the input.

A course of care, not a pile of visits

Every consultation carries a thread id, so four follow-ups are one story. And when a check-in is answered “worse”, that is not filed as an answer - it opens the next consultation on the same thread, with the doctor already holding the history.

What it deliberately does not do

No account creation, no appointment picker in the main path, no doctor chooser, no medical questionnaire. Every one of those was cut on the same argument: it asks the user to do work the system can do for them.

The constraint

Seven taps from the dashboard to a doctor

Step through the actual flow. One of the seven is speaking.

1 of 8

The starting point. No account creation, no appointment picker.

Design constraint

Seven taps from the dashboard to a live consultation, one of which is speaking. No form fields typed, no medical vocabulary required, no appointment chosen. If a change makes it eight, the change is wrong.

This is the rule the build is held to - not a measured usability result.

Never read your own words back

A screen I had already built, and then deleted.

  1. Patient speaks
  2. Structured case sheet
  3. Confirm the information
  4. Doctor receives it

The thing being confirmed is now the thing the doctor will actually read. One less screen, one less tap - and the intake dropped from eight taps to seven.

Confirm the output, not the input.

Failure states

Designed the failures before the happy path

Every branch keeps what the person has already said. Pick one to see what happens.

What the interface says

The words were saved before the model ran.

What is preserved

The doctor receives the unstructured intake rather than nothing at all.

Every failure branch keeps what the person has already said. There are no dead ends, and nothing says only “something went wrong”.

TL;DRThere are no dead ends, and every failure has words.

Small rules that prevent large mistakes

Works before JavaScript arrives

Forms submit and validate server-side, filtering is plain links, and language switching is an anchor. The intake completes on a phone that never runs the client bundle.

Every failure has words

Permission refused, nothing heard, service unavailable, weak line - each says what happened and what to do instead. A button that does nothing and explains nothing is the worst outcome on a health screen.

Dates ISO in storage, Intl at render

And rendered in the reader’s timezone, not the server’s - a bug I shipped and then caught, where a deployed server in UTC told somebody in IST their turn was five and a half hours off.

Ages computed at read time

From date of birth, so a record does not quietly go wrong on a birthday. Under-twos render in months, because “1 year” and “14 months” are different things to a doctor.

No string lives in a component

All 731 keys come from the dictionary, and the Hindi file is typed against the English one. A missing or renamed key is a build failure, not a blank space somebody notices in production.

No stored sentence addresses a reader

One consultation is read by both the patient and the clinician. “Typed or spoken by you” was correct on one screen and a lie on the other - so the fact is stored and each screen writes its own line.

Design

A system rationed on purpose

Constraints that keep 24 routes reading as one product on a phone somebody is downloading over 2G.

Green is rationed

A solid fill in the action colour means “this is the button”, and appears nowhere else on any screen. Brand green is typographic only - marks, labels, rules. It is the single rule that keeps 24 routes reading as one product.

Font weights are rationed too

Every face is a file somebody on 2G downloads. Two sans weights and one serif, and the CSS is written so nothing can quietly ask for a weight that was never loaded.

Devanagari as a script, not a translation layer

Its own serif and sans faces, never preloaded so English visitors pay nothing for them. Faux-italic is switched off because slanted Devanagari is simply wrong, and uppercase tracking is off because that convention does not exist there.

Custom spot illustrations, not an icon set

Duotone drawings of the literal thing - a browser tab still spinning, a timer with the remaining slice shaded, a page of lines deliberately too dense to read. A row of stock outline icons reads as a toolbar rather than as something to look at.

Designed at 430×900 first

Navigation sits at the bottom within thumb reach, because the top of a phone screen is the part a single hand cannot get to. Safe areas, keyboard-aware heights and 44px targets throughout.

Motion is CSS-only

Scroll-driven reveals with a view timeline - nothing to download, nothing to hydrate, and browsers without the feature render the final state. Content is never trapped behind an animation that did not run.

Language is not a preference

Spoken and interface language are different things, and one of them affects routing.

Spoken language

What the patient is comfortable speaking - often Hindi and English in the same sentence.

Interface language

What they are comfortable reading. Not necessarily the same thing.

So they are tracked separately

And spoken language sits above speciality in the routing ladder. A general physician who can understand you outranks a specialist who cannot.

Devanagari as a script, not a translation layer

  • Its own serif and sans faces
  • Never preloaded, so English visitors pay nothing for them
  • Faux-italic switched off - slanted Devanagari is simply wrong
  • Uppercase tracking off - that convention does not exist there
  • 731 dictionary keys, Hindi typed against English
  • A missing key is a build failure, not a blank space in production

Where language sits in the routing

1As triagedShares a languageSame state2As triagedShares a languageAny state3General physicianShares a languageSame state4General physicianShares a languageAny state5As triagedNo shared languageAny state6General physicianNo shared languageAny stateTRIED IN THIS ORDER, FIRST MATCH WINSLanguage outranks speciality - rungs 3 and 4 sit above rung 5 deliberately.
The model contributes one speciality from a closed list. Everything after is ordinary rules over the roster - so who a patient is connected to is explainable and testable rather than argued into existence by a model.

A phone is a household

One login, three people, three records.

One phone

One login

  • A son’s fever

    Its own health record

  • A mother’s knee

    Its own health record

  • One’s own chest pain

    Its own health record

A phone is a household, not an account. Members carry their own health facts rather than sharing the account’s - which is a UX decision before it is a data model.

AI

The interesting part is the boundary

Four narrow jobs, and three things the model is never allowed to do.

Four narrow jobs it does

  • Structure the intake
  • Notice lasting health facts
  • Translate clinical text
  • Transcribe speech

Each is a separate module with its own prompt and its own validator. There is no general-purpose assistant anywhere in this product.

Three things it is never allowed to do

  • Diagnose
  • Prescribe
  • Decide a doctor is unnecessary

Enforced in the prompts, and stated in the product as prominently as the capability.

The model is not the source of truth

Facts are proposed, never asserted.

  1. Patient’s actual words
  2. AI
  3. Structured output
  4. Validator
  5. Human confirms
  6. Clinical record

Facts are proposed, never asserted. A human confirms before anything enters the record.

The words are saved before the model runs

So the worst case is an unstructured intake, not a lost one.

  1. Patient speaks
  2. Words saved
  3. AI structures them
  4. Doctor gets a case sheet

The words are stored before the model is called, so the worst case is an unstructured intake rather than a lost one.

Latency is a usability property

Which is why the model choice was a product decision.

  • Reasoning models8–70 seconds
  • moonshot-v1-autoabout 1 second

For someone unwell and waiting on a screen, latency is a usability property.

Four narrow jobs, never one big one

Structure the intake, notice lasting health facts, translate clinical text, transcribe speech. Each is a separate module with its own prompt and its own validator - there is no general-purpose assistant anywhere in this product.

The words are saved before the model runs

Order of operations as a safety property. The transcript is written to the draft first, so a slow or dead model can never cost somebody what they just said - it goes to the doctor unstructured instead.

One call, not two

The case sheet and the routing speciality come back from a single request. Providers cap in-flight calls per account, and asking twice doubles the wait for someone who is unwell. Latency is a usability property here, not a footnote.

Model output is untrusted input

Everything passes a shape check with length caps before it reaches application logic. A speciality outside the closed list silently becomes a general physician. A field returned as snake_case is rewritten before a clinician ever sees it.

Facts are proposed, never asserted

“I am allergic to penicillin” and “I am not allergic to penicillin” differ by one word, and a model that gets it backwards can kill someone. Extracted facts appear on the patient’s profile as a question, quoting what was actually said, and enter the record only when a person confirms.

Silence is never sent

Asked to transcribe a quiet room, the model invents plausible clinical speech - I saw it produce symptoms nobody had. So microphone level is measured in the browser and near-silence never leaves the device.

The other side

The clinician is a user too

This was designed as an ecosystem, not as a patient app with an admin screen attached.

Queue

Longest wait first, so nobody is forgotten by being unlucky.

Case sheet beside the write-up

The clinician should not have to remember what they just read.

Allergies and regular medicines pinned

The two things that change a prescription stay on screen.

Prescription cannot be submitted empty

The one mistake that is silent and serious.

Two sides, designed as one product. The patient app is only half of it.

The clinician console - the case sheet beside the write-up

A course of care, not a pile of visits

Answer the check-in and see what the product does with it.

  1. Consultation
  2. Prescription
  3. Check-in

“Worse” is not filed as an answer. It opens the next consultation on the same thread, with the doctor already holding the history.

Every consultation carries a thread id, so four follow-ups are one story rather than a pile of visits.

What was built

Patient app

Sign-in, onboarding, dashboard, the three-step intake, queue, call, the record, household members and their health profiles, settings.

Clinician console

A queue ordered by longest wait, the case sheet beside the write-up, allergies and regular medicines pinned above it, and a prescription form that cannot be submitted empty by accident.

Voice intake

Tap-to-start recording, live level metering, server-side transcription, and an auto-advance once the words come back - because somebody unwell should not have to press Continue on their own transcript.

Video consultations

LiveKit rooms scoped to one consultation, ownership checked server-side, tokens that expire in an hour and cannot enable recording. Video drops to voice by itself on a weak line, and says why.

Live in-call notes

Each side transcribes its own half of the call in 15-second chunks; the server assigns speaker and ordering. A four-section report updates on the clinician’s screen as they talk.

Prescription and follow-up

A printable prescription with a verification code, warning signs, and a dated check-in that asks Better / Same / Worse in one tap.

Doctor routing

53 doctors across all 35 states and union territories, each speaking three languages. Six-rung ladder, language above speciality, and the record stores why this doctor rather than just which one.

Two full languages

731 dictionary keys in English and Hindi. The Hindi file is typed against the English one, so a missing key is a compile error rather than a blank space on a screen.

Decisions

Eight calls, and the easier option in each

Open a card to see what would have been simpler, and why I did not do it.

01

Put the intake before the queue, not after it

The moment
The obvious shape is the one every clinic uses: join a queue, wait, then talk. Intake belongs in the call, where the clinician can ask follow-ups.
What it bought
The clinician opens a four-minute call already knowing the story. No extra clinician minutes were bought - the same four minutes simply start at care instead of at questions.
02

Derived queue position instead of storing it

The moment
Storing a position is trivial. Deriving it means recomputing from a join time on every read and reasoning about clocks.
What it bought
Closing the tab cannot cost anybody their turn - which is the single most cited complaint about the real service. Correctness came from removing state, not adding it.
03

Refused to let the model touch clinical decisions

The moment
A model that can already read symptoms into a case sheet is one prompt away from suggesting what they might mean. It would have demoed extremely well.
What it bought
The product is defensible to a clinician. A tool that guesses at diagnoses is one a doctor stops trusting the first time it is wrong - and it would have been wrong.
04

Ranked language above speciality in the routing

The moment
The clinically obvious ladder puts the right specialist first. Sorting by language ahead of it looks like a downgrade.
What it bought
A consultation you cannot conduct is not a consultation. This also made the roster work honest - where no specialist was free, the record says so rather than quietly downgrading.
05

Deleted the read-back screen I had already built

The moment
The confirm step showed the patient their own transcript to check. It felt respectful, it was built, and it was working.
What it bought
One less screen and one less tap, and the thing being confirmed is now the thing the doctor will actually read. The intake dropped from eight taps to seven.
06

Put a storage port between the rules and the database

The moment
A single Postgres data layer is less code and less indirection. The port costs an extra file and a second implementation to keep in step.
What it bought
A clone with no credentials runs the entire journey, which made both development and review frictionless. The memory adapter also became the reference the SQL is checked against.
07

Took the speaker from the session, never from the request

The moment
The in-call transcript needs to know who said what. The client obviously knows which side it is, and sending it is one field.
What it bought
A client that could label its own audio “doctor” could put words in a clinician’s mouth on a medical record. It costs one lookup to make that impossible.
08

Built the honesty page as a product surface

The moment
Every prototype has simulated parts. The convention is to leave them undocumented and hope the demo does not reach them.
What it bought
A reviewer is told before they find out. It also disciplined the build: anything embarrassing to write down tended to get fixed instead.

The things I cut

Each one on the same argument: it asks the user to do work the system can do for them.

Account creation in the main path

It asks the user to do work the system can do for them.

An appointment picker

A person who is unwell now should not be scheduling.

A doctor chooser

They have no basis on which to choose, and the ladder does it better.

A medical questionnaire

It demands vocabulary the patient does not have.

An AI “possible conditions” panel

It would have demoed extremely well, and been wrong.

The transcript read-back screen

Somebody unwell will not proofread a paragraph about themselves.

The build

How it was made

Ten stages, in the order they happened. Pick one.

01

Defined the problem in one sentence

Not “redesign a telemedicine app” but “the waiting is not going away, so make the waiting do the work”. Everything afterwards was measured against that sentence, and several good ideas were cut for failing it.

Technical decision, product consequence

Each of these exists because of something the user would otherwise feel.

Technical decisionProduct consequence
Memory adapterA reviewer can run the whole product without credentials.
AI provider abstractionThe product degrades when an external model is unavailable.
LiveKitMedia does not need to pass through my application server.
Derived queue positionClosing the tab cannot cost anybody their turn.
Speaker from the sessionA client cannot put words in a clinician’s mouth on a record.

The architecture underneath

BrowserReact 19 · works before hydrationServer components+ server actions, the only writesrepository.tsevery rule · and no SQLstorage portwhole-record reads/writesNeon PostgresDATABASE_URL setIn-memoryno credentials neededEXTERNAL, EACH DEGRADING ON ITS OWNKimicase sheet + triageGeminispeech to textLiveKitvideo and voiceEvery key is optional. Missing one degrades that feature and nothing else - no integration can produce a dead end.Everything under src/server is marked server-only - a client component importing it fails the build rather than leaking a key.
One storage port, two adapters. Postgres when a connection string is present, memory when it is not - so a clone with no credentials still runs the entire journey end to end.

What it is built with

Design

  • Figma

    Flows, wireframes, the design system

Front end

  • Next.js 16

    App Router, server components, server actions

  • React 19

    Client islands only where interaction demands it

  • TypeScript

    Strict - the Hindi dictionary is typed against English

  • Tailwind 4

    Design tokens in @theme, no config file

Backend & data

  • Neon Postgres

    Serverless driver, idempotent DDL, no migration step

AI

  • Kimi (Moonshot)

    Case sheet and triage - OpenAI-dialect compatible

  • Gemini

    Speech to text, Hindi in Devanagari

Realtime & hosting

  • LiveKit (WebRTC)

    Video and voice - media never touches my server

  • Vercel

    Deployed, with the database on Neon

What I proved

A concept, taken all the way

This is a concept redesign, so these are things the build demonstrates - not clinical or business outcomes.

  • The entire patient journey works end to end.
  • The clinician and patient sides work together, in a live two-sided call.
  • The seven-tap constraint is implemented, not just asserted.
  • Intake can happen before the queue, and the case sheet reaches the doctor.
  • The AI is constrained to four narrow jobs, with a validator and a human confirming.
  • Language is treated as part of routing rather than as a setting.
  • Realtime consultation works, and degrades to voice on a weak line.
  • Follow-up exists as a care thread rather than a filed answer.
  • The product is usable in English and Hindi as first-class scripts.
  • It is deployed, and a reviewer can walk both sides of it.

The same four minutes of clinician time, spent differently.

No extra clinician minutes were bought. The consultation simply starts at care instead of at questions.

What I would do next

The honest gaps, in the order I would close them.

  1. 01

    Replace the simulated queue with real clinician availability

    The people ahead of you currently clear at a fixed rate, because there is no real roster behind it. The honest version reads live duty state and actual consultation durations - which changes the wait from a countdown into a forecast, and makes the whole "book a time instead" branch load-bearing.

  2. 02

    Move sign-in to a real OTP, and sessions to signed tokens

    A PIN opens a household today because there is no number to verify in a prototype full of invented people. The swap is contained on purpose: one file goes, the session cookie becomes a signed short-lived token, and no call site changes.

  3. 03

    Put the AI evaluation on a harness rather than my judgement

    The intake prompt is guarded by a validator, but "did it keep every clinical detail" is currently checked by reading the output. A labelled set of real transcripts with assertions on recall, on refusal to diagnose, and on the denial case - "I am not allergic to penicillin" - would turn prompt changes into something measurable rather than something felt.

  4. 04

    Take the live report from polling to streaming

    The clinician’s in-call notes refresh on a timer, which is right for a demo and wasteful in a clinic. Server-sent events would cut the latency and the request count, and let the report update mid-sentence rather than every few seconds.

  5. 05

    Add the accessibility work a health service actually owes

    Landmarks, focus, contrast and reduced motion are done. What is missing is the harder half: a screen-reader pass on the call controls, a genuinely low-literacy mode, and text-to-speech for the summary - because the person who most needs a doctor is often the least able to read the outcome.

  6. 06

    Scale the roster and prove the routing with data

    Fifty-three doctors is enough to exercise six rungs. A real deployment has thousands, and the interesting question becomes load: the ladder currently takes the first match rather than balancing across equally valid ones, which would concentrate work on whoever sits earliest in the list.

  7. 07

    Close the loop on outcomes

    The follow-up asks Better / Same / Worse and files it. Nothing yet reads those answers back across a course of care, and that is where the clinical value would actually be - a doctor seeing that their last three prescriptions for this thread all came back "same" is the beginning of something useful.

What I did on it

  • Product definition - what this is, who it is for, and what it deliberately does not do
  • Research and problem framing - the user, the constraints, and the one sentence the build answers to
  • UX - the flows, the failure branches, and the seven-tap constraint the whole app is held to
  • UI and visual design - type scale, colour tokens, spot illustrations, every screen
  • Design system - built in Figma, then encoded as Tailwind tokens rather than redrawn in CSS
  • Front-end engineering - Next.js 16 App Router, React 19, 74 components, server/client split
  • Backend engineering - domain layer, storage port, two adapters, idempotent schema
  • AI engineering - prompt design, output validation, guardrails, provider abstraction
  • Realtime - WebRTC consultations, in-call transcription and a live clinician report
  • Internationalisation - English and Hindi as first-class scripts, not a translation layer
  • Accessibility - landmarks, focus, 44px targets, reduced motion, works without JavaScript
  • QA - browser-driven testing of every flow, including the two-sided call
  • Deployment and operations - Vercel, Neon, environment contract, and the demo affordances around it

The point of it

I did not try to remove the wait. I redesigned what happens inside it - and then built the whole thing, both sides, to find out whether the idea survives contact with a real implementation.