ClinicOSgithub ↗
A side project · 2025 → 2026

A clinic,
rewritten from
the schema up.

ClinicOS is a multi-tenant clinic management system for small Indian practices — built solo, one migration and one screen at a time. This page is the build log: the constraints, the trade-offs, and what finally shipped.

scroll
02
Why this exists

Small Indian clinics run on paper.
Every friction point is a real one.

I grew up watching clinics operate out of notebooks. Not because the staff were careless — because every "clinic software" on offer was priced for 300-bed hospitals, or needed a salesperson to install it, or locked the data inside their cloud. ClinicOS is the answer I wanted for that kind of clinic: small, honest, self-serve, and priced like nothing.
01

Paper registers, lost histories

A folder per patient, smudged pages, a file cabinet that grows. Past visits get lost when a receptionist rotates out.

02

Handwritten prescriptions

Pharmacists guess at dosages. Patients forget what was said. No digital record exists to reference next time.

03

Queues nobody trusts

Hand-written tokens, shouted names, angry relatives. No one in the waiting room knows how many people are ahead.

04

Day-end cash reconciliation

Carbon-copy receipt books, manual tallying at closing, disputes when a number doesn't match.

03
How it got built

Twelve sessions.
One module at a time, schema first.

S01
Kick-off

Schema first, then code

· foundation

12 tables, RLS policies, Flyway baseline. No endpoint was written until the data model was provably correct.

S02
Auth

JWT + RLS tenant context

· foundation

Spring Security with a custom TenantContextFilter that extracts clinic_id from the JWT and sets the Postgres session variable before any query.

S03
Patient

Registration + search

· feature

Registration number generator (P-00001 per clinic), full-text search on name/phone, duplicate detection on phone.

S04
Queue

Tokens + live SSE

· feature

One FIFO per doctor. State machine: WAITING → CALLED → IN_CONSULTATION → COMPLETED. Waiting-room TVs subscribe to SSE and update in under a second.

S05
Rx

Consultation + prescription

· feature

Auto-save drafts every few seconds. Medicine autocomplete from a master list. Finalize cuts an Rx number from a DB sequence and renders a PDF.

S06
Billing

Receipts + payment flow

· feature

Consultation fee + optional discount. Receipt number R-2026-NNNN issued on payment. Handoff is deliberate: doctor doesn't collect — the receptionist does.

S07
Admin

Dashboard + settings

· feature

Daily cash summary, top-diagnosis chart, clinic-level settings (doctors, services, working hours). Role-gated per RBAC matrix.

S08
Display

Queue TV display

· feature

Fullscreen public view for the waiting room. No auth — just a clinic-scoped token. Large type, quiet animation, server-sent updates.

S09
Deploy

Render + Vercel, $0/month

· ops

Backend Docker on Render. Frontend on Vercel. Supabase for Postgres + storage. Cold start budget accepted as a trade for the price tag.

S10
Rx v2

Prescription Workspace

· feature

Two-pane live workspace: left pane writes, right pane renders the PDF. Vitals inline, templates, doctor-profile letterhead overlay, atomic Rx numbering.

S12
UI

Full UI refactor

· polish

Every screen rebuilt against a tighter mockup set — TopBar, Sidebar, PageTransition, and every role-facing page. One consistent visual language, end-to-end.

S15
Brand

Pulse C + Cosmo

· polish

Pulse-C logo with a live beating dot. Cosmo, a sidebar mascot that blinks on human cadence. Split-pane login with a tenant chip.

04
The product

What the clinic actually sees.

Sign in
Frame 01

One front door, three roles

Split-pane login. Tenant chip bottom-left, app version bottom-right. One password for reception, doctor, and admin — RBAC decides the rest.

One front door, three roles
clinicos · frame 01
Frame 02

Mobile-first, not mobile-afterthought

Reception staff log in from ₹8,000 Android tablets. The split pane stacks, the ID badge shrinks, nothing else changes.

Mobile-first, not mobile-afterthought
clinicos · frame 02
Frame 03

The brand lives in every screen

Pulse-C mark with a 62-bpm heartbeat. Cosmo — our Saturday mascot — keeps the sidebar from feeling clinical.

The brand lives in every screen
clinicos · frame 03
Reception
Frame 04

Reception, at a glance

Today's tokens, waits, collection, and tomorrow's appointments. Built for someone who just walked in, not someone who has an hour to onboard.

Reception, at a glance
clinicos · frame 04
Frame 05

Patient list with guardrails

Search, paginate, register. Phone-number duplicate check runs server-side before a new patient row is ever written.

Patient list with guardrails
clinicos · frame 05
Frame 06

Register in under 30 seconds

Name, phone, DOB or age, gender. Registration number (P-NNNNN) issued on save. ABHA optional.

Register in under 30 seconds
clinicos · frame 06
Frame 07

History is the point

Three visits over five months. Every past diagnosis, prescription, and receipt reachable in two clicks.

History is the point
clinicos · frame 07
Queue
Frame 08

Issue a token, find the patient

Empty state shows recent patients by default — the most common case. Typing filters against name, phone, or reg number.

Issue a token, find the patient
clinicos · frame 08
Frame 09

Priority and follow-up, one tap each

Patient picked, doctor chosen, walk-in or appointment toggled. Token number assigned atomically when Issue is pressed.

Priority and follow-up, one tap each
clinicos · frame 09
Frame 10

The queue, live

Eight tokens across Waiting, Called, In Consultation, Completed. Reception manages the room; nobody shouts names.

The queue, live
clinicos · frame 10
Frame 12

Waiting-room TV, public view

No login. Big numbers, room IDs, called tokens pulsing. Any ₹8,000 Android TV does the job.

Waiting-room TV, public view
clinicos · frame 12
Doctor
Frame 11

Doctor's two-pane queue

Left: the line. Right: the selected patient's vitals, last visit, and open consultation button. One keyboard flow.

Doctor's two-pane queue
clinicos · frame 11
Frame 13

Prescription Workspace, fresh

Editable vitals up top, empty medicine list, live preview on the right. Zero modal dialogs; everything is inline.

Prescription Workspace, fresh
clinicos · frame 13
Frame 14

Typing a prescription, watching it render

Medicine search with composition + strength autocomplete. 1-0-1 dosage shorthand. Right pane updates on every keystroke.

Typing a prescription, watching it render
clinicos · frame 14
Frame 16

A PDF a pharmacist can actually read

OpenPDF output, A5, clinic letterhead margin-aware. Rx number (RX-DEMOCLI-2026-NNNNNN) issued atomically from a DB sequence.

A PDF a pharmacist can actually read
clinicos · frame 16
Billing
Frame 17

Billing lives with reception, not the doctor

Pending payments sit here until reception collects. Doctor is never on the money side — that was a deliberate design choice.

Billing lives with reception, not the doctor
clinicos · frame 17
Frame 18

Receipt R-2026-NNNN, issued atomically

The receipt number is the same on screen, on print, and in the database. No drift, no disputes.

Receipt R-2026-NNNN, issued atomically
clinicos · frame 18
Admin
Frame 19

Admin sees the clinic, not the line items

Today's collection, pending dues, bill count. Staff performance and revenue trends live behind the same screen.

Admin sees the clinic, not the line items
clinicos · frame 19
05
The vocabulary

A small system,
carefully named.

Colors · click to copy
Type · three families
Hero
clamp(51→100)px
Manrope 600
Rewritten
Display
clamp(38→64)px
Manrope 600
The build
Section
clamp(30→42)px
Manrope 600
Under the hood
Body
16–18px
Inter 400
A clinic, rewritten from the schema up.
Mono
12–14px
JetBrains
RX-DEMOCLI-2026-000003
Brand · live components
Pulse-C · 62 bpm

A single SVG path. Beats once per second with a secondary echo ring. Respects prefers-reduced-motion.

Cosmo · blinks every 4–9s

Hover to wiggle. The waiting-room face of the app — why this was built on a Saturday.

06
What it runs on

Three boxes,
one migration at a time.

Architecture
Vercel
Vercel
Edge CDN · React 18 · Vite build
Render
Render
Spring Boot 3.3 · Docker · Asia-south
Supabase
Supabase
Postgres 16 · RLS · Storage
$0monthly hosting
1 GBprod RAM
21Flyway migrations
< 300msSSE queue update
Stack · real versions
Java 21·Spring Boot 3.3·Hibernate 6·Flyway·MapStruct·OpenPDF 2·PostgreSQL 16·Supabase·React 18·Vite 5·TanStack Query 5·Tailwind 3.4·Framer Motion·Render·Vercel·Java 21·Spring Boot 3.3·Hibernate 6·Flyway·MapStruct·OpenPDF 2·PostgreSQL 16·Supabase·React 18·Vite 5·TanStack Query 5·Tailwind 3.4·Framer Motion·Render·Vercel·
Decisions · five that shaped the build

Multi-tenant with Row-Level Security

ADR
Context
Small clinics in India can't justify a dedicated database each. But data leaks across tenants would be an existential bug.
Decision
Shared PostgreSQL schema, RLS enforced at the row level. Every request sets a tenant context via a Spring Security filter before any query runs.
Consequence
One database, one deploy, zero cross-tenant leak paths. The filter is the single enforcement point — if it runs, isolation holds.

JWT (15min) + Refresh cookie (7d)

ADR
Context
Sessions need to be revocable, survive browser refreshes, and never put credentials in localStorage.
Decision
Short-lived JWT in memory for API calls. Refresh token in an HttpOnly, Secure, SameSite=Strict cookie. Access token rotates every 15 minutes without the user noticing.
Consequence
XSS can't steal the refresh token. A compromised access token expires in 15 minutes. The cost is one interceptor and one silent-refresh endpoint.

Flyway migrations, never manual

ADR
Context
A schema drift between dev and prod in a multi-tenant system means every tenant is at risk.
Decision
No hand-edits on the database — ever. Every change is a versioned V###__description.sql. The CI migration run is the source of truth.
Consequence
21 migrations shipped, zero drift. Rolling back is a new migration, not a SQL surgery.

SSE for the queue, not WebSockets

ADR
Context
Waiting-room displays and receptionists need token changes live. Bidirectional messaging was overkill.
Decision
Server-Sent Events via Spring SseEmitter. Queue updates flow one-way from server to every connected display.
Consequence
No ws:// infra, no sticky sessions, no socket library on the client. Reconnect is built-in. The receptionist ticks 'Call' — TVs update in ~300ms.

OpenPDF with clinic letterhead overlay

ADR
Context
Indian clinics print prescriptions on pre-printed letterheads. The PDF has to lay ink exactly where the letterhead leaves room.
Decision
OpenPDF renderer with configurable top/bottom margins, optional clinic logo and doctor signature from Supabase Storage. A RX-DEMOCLI-2026-NNNNNN number is issued atomically from a DB sequence.
Consequence
Prescriptions print clean on any letterhead. The Rx number is the same on screen, on print, and in the DB — dispute-proof.