Patient Tracker System — Project Overview
What It Does
A full-stack healthcare platform where doctors manage patients and patients track their treatment. Two user roles, one shared data layer.
Feature List
| Feature | Who Uses It | What It Does |
|---|---|---|
| Role-based Auth | Both | Separate signup/signin flows for doctors and patients. JWT-based session. Doctors can invite patients via email. |
| Patient Management | Doctor | Add patients (creates pending account + sends email invite), assign existing patients, remove patients from roster. |
| Prescription System | Doctor | Create prescriptions with multiple medicines, each with dosage, timing (morning/afternoon/evening), instructions, and duration. Delete prescriptions with cascading cleanup. |
| Medication Tracking | Patient | Daily view of scheduled medications grouped by time-of-day. Mark as Taken/Missed. Adherence stats and history. |
| Automated Reminders | System | Cron-based email reminders at 8 AM, 1 PM, 8 PM. Auto-marks medications as Missed after their time window expires. |
| Appointment Booking | Patient | Book 1-hour slots (9 AM – 5 PM) with assigned doctors. Concurrency-safe via partial unique DB index. |
| Appointment Management | Both | Doctors: complete/cancel/mark missed. Patients: cancel only. Filter by date/status. |
| Real-time Chat | Both | Socket.IO-powered messaging between doctor and assigned patient. Read receipts. Chat history. |
| Medicine Database | System | 32 MB CSV dataset of Indian medicines parsed on-demand for prescription autocomplete. |
| Password Recovery | Both | Forgot password → email with time-limited reset token → set new password. |
Why This Project Exists
Clinics — especially small ones — still manage patient medications, reminders, and appointments through phone calls, WhatsApp, or paper. This system digitizes the doctor ↔ patient relationship end-to-end:
- Doctor adds a patient → patient gets an email → sets password → immediately sees prescriptions and medication schedule.
- Patient doesn't need to remember what to take when — the system generates a daily medication schedule from active prescriptions and emails reminders.
- Appointments are booked without phone tag — patient picks from available slots, double-booking is prevented at the DB level.
- Doctor can monitor which patients are actually taking their meds via adherence stats.
Real-world analog: Think of it as a self-hosted version of Practo or MyChart, scoped to the prescription-and-adherence workflow.
High-Level Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ BROWSER (React 19 + Vite) │
│ ┌──────────┐ ┌──────────────┐ ┌────────────┐ ┌──────────────────┐ │
│ │ Signin │ │ Doctor Dash │ │Patient Dash│ │ Chat Interface │ │
│ │ Signup │ │ - Patients │ │ - Meds │ │ (Socket.IO) │ │
│ │ SetPwd │ │ - Prescribe │ │ - Rx View │ │ │ │
│ │ ResetPwd│ │ - Appts │ │ - Appts │ │ │ │
│ └──────────┘ └──────────────┘ └────────────┘ └──────────────────┘ │
│ │ │ │ │ │
│ └──────────────┴──────────────┴───────────────┘ │
│ │ axios (HTTP) │ socket.io-client │
└────────────────────────────┼─────────────────┼─────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ EXPRESS SERVER (Node.js, port 8000) │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐│
│ │ Middleware Pipeline ││
│ │ express.json() → cors() → triggerMedicationReminder() ││
│ │ → checkForMissedMedications() → [route handler] ││
│ └─────────────────────────────────────────────────────────────────┘│
│ │
│ ┌──────────┐ ┌──────────┐ ┌────────────┐ ┌──────────┐ │
│ │ auth/ │ │ doctor/ │ │ patient/ │ │ chat/ │ │
│ │ signup │ │ addPat │ │ meds │ │ chatCtrl │ │
│ │ signin │ │ prescRx │ │ prescRx │ │ msgCtrl │ │
│ │ resetPwd │ │ assignPat│ │ adherence │ └──────────┘ │
│ │ forgotPwd│ │ removePat│ │ history │ │
│ │ setPwd │ │ getMeds │ │ stats │ ┌──────────────┐ │
│ └──────────┘ │ getPresc │ └────────────┘ │ appointment/ │ │
│ └──────────┘ │ create │ │
│ │ getSlots │ │
│ ┌───────────────────┐ │ updateStatus │ │
│ │ scheduleTasks.js │ │ getDr/Pt │ │
│ │ (node-cron) │ └──────────────┘ │
│ │ 8AM/1PM/8PM │ │
│ │ + every-minute │ ┌───────────────────┐ │
│ │ missed checker │ │ Socket.IO Server │ │
│ └───────────────────┘ │ (real-time chat + │ │
│ │ med notifications)│ │
│ └───────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Prisma ORM Client + Accelerate Extension │ │
│ └─────────────────────────────┬────────────────────────────────┘ │
└────────────────────────────────┼─────────────────────────────────┘
│
▼
┌───────────────────────┐
│ PostgreSQL (Neon) │
│ via Prisma Accelerate│
│ │
│ Tables: │
│ Patient, Doctor, │
│ Prescription, │
│ PrescribedMedicine, │
│ MedicineAdherence, │
│ Appointment, │
│ Chat, Message, │
│ MedicalRecord, │
│ HealthMetrics │
└───────────────────────┘How Layers Connect
Browser (React)
│
├─ HTTP (axios) ──────────→ Express Routes
│ │
│ ├─ authenticate() middleware verifies JWT
│ │
│ ├─ Route handler (e.g., doctor/prescription.js)
│ │ │
│ │ ├─ Validates input
│ │ ├─ Checks authorization (role + ownership)
│ │ ├─ Calls Prisma ORM
│ │ │ └─ PostgreSQL query
│ │ └─ Returns JSON response
│ │
│ └─ Global middleware on EVERY request:
│ ├─ triggerMedicationReminder() ← fires emails asynchronously
│ └─ checkForMissedMedications() ← auto-marks missed meds
│
└─ WebSocket (socket.io) ──→ Socket.IO Server
├─ join-chat / leave-chat (room management)
├─ send-message → broadcast to room
└─ medications-updated ← server pushes to patientsNo cache layer, no queue, no CDN. Everything is synchronous request → DB → response, except:
- Email sending (fire-and-forget via Nodemailer)
- Cron jobs (background medication checks)
- WebSocket events (real-time chat + medication update notifications)
Key Design Decisions and Trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
| Modular file-per-handler (no router files) | Each handler is self-contained — easy to read in isolation | app.js becomes a 186-line import-and-route registry |
| JWT with all user data in payload | No DB lookup needed for auth check — pure stateless | Token bloat; changing user data requires re-login to get new token |
| Medication reminders fire on every HTTP request | Ensures reminders trigger even if cron fails (Vercel kills long-running processes) | Adds ~0ms overhead per request (async fire-and-forget), but it's unconventional |
| Prisma Accelerate | Connection pooling for serverless (Vercel) where direct Postgres connections would exhaust pool | Adds latency (~5-15ms) per query vs direct connection |
| CSV medicine dataset loaded on-demand | Avoids 32MB memory resident data; streaming parse via csv-parser | Every /api/medicines call re-reads the file — no caching |
| Partial unique index for appointment concurrency | DB-level guarantee — no double-booking even under concurrent inserts | Requires raw SQL migration outside Prisma's declarative schema |
Socket.IO on global io | Quick way to share the socket instance across middleware and cron jobs | Global mutable state; not testable in isolation |
| No separate router layer | Routes defined inline in app.js | No route grouping, middleware reuse, or prefix-based organization |
What I Would Do Differently If Starting Over
-
Use Express Router with grouped route files — one router per domain (auth, doctor, patient, appointment, chat) instead of 30+ individual imports in
app.js. -
Extract business logic from controllers — current handlers mix validation, authorization, business logic, and DB queries in one function. A service layer would make each testable independently.
-
Don't fire medication reminders on every HTTP request — the middleware approach in
app.jsline 74-77 is a workaround for serverless cold starts. A proper solution: use a dedicated cron service (Vercel Cron, Railway, or a standalone worker). -
Cache the medicine CSV — 32MB re-parsed on every search request is wasteful. Load once on startup, or import into a DB table.
-
Use refresh tokens — current JWT expires in 24h with no refresh mechanism. User gets silently logged out.
-
Add input validation library — current validation is manual
if (!field)checks. Zod or Joi would make schemas declarative and consistent. -
Separate WebSocket concerns — chat and medication notifications share the same Socket.IO instance with no namespace separation. At scale, these should be different namespaces or even different services.
-
Add proper error types — current error handling returns inconsistent shapes (
{ message }vs{ error }vs plain strings). A customAppErrorclass with status codes would standardize this.