Architecture and Flow — How It All Connects
Detailed Component Diagram
┌─────────────────────────────────────────────────────────────────────────┐
│ CLIENT LAYER │
│ │
│ React 19 (Vite dev server :5173) │
│ ├── Pages/ │
│ │ ├── Signin.jsx ← Login form, role selector │
│ │ ├── Signup.jsx ← Registration form (doctor/patient) │
│ │ ├── DoctorDashboard.jsx ← 823-line mega-component, tabbed UI │
│ │ └── PatientDashboard.jsx← 504-line mega-component, tabbed UI │
│ ├── Components/ │
│ │ ├── AddPatient.jsx ← Doctor invites patient │
│ │ ├── AddPrescription.jsx ← Multi-medicine prescription form │
│ │ ├── AppointmentBooking ← Slot picker + booking │
│ │ ├── AppointmentList ← Shared between doctor/patient │
│ │ ├── MedicationTracker ← Patient daily meds view │
│ │ ├── ViewPrescription ← Patient reads prescriptions │
│ │ ├── Chat/ ← ChatList, NewChat, EnhancedChatInterface │
│ │ ├── SetPassword.jsx ← Invited patients set initial password │
│ │ ├── ForgotPassword.jsx ← Email entry for reset flow │
│ │ └── ResetPassword.jsx ← New password after token verification │
│ └── lib/ │
│ └── apiClient.js ← Axios instance, baseURL from env │
│ │
│ State: React useState + useEffect (no Redux/Zustand) │
│ Routing: react-router-dom v7 │
│ Styling: Tailwind CSS v4 + shadcn/ui components │
│ Icons: lucide-react │
│ Real-time: socket.io-client │
└────────────────────┬───────────────────────────┬────────────────────────┘
│ HTTP (REST) │ WebSocket
▼ ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ SERVER LAYER │
│ │
│ Express 4 + HTTP Server (port 8000) │
│ ├── app.js ← Central route registry + middleware │
│ ├── client.js ← Prisma client singleton + Accelerate │
│ │ │
│ ├── middleware/ │
│ │ ├── authMiddleware.js ← JWT verification → req.user │
│ │ ├── MedicationReminder.js ← Fires on every request (async) │
│ │ └── MissedMedicationChecker.js← Marks overdue meds as Missed │
│ │ │
│ ├── auth/ │
│ │ ├── jwtToken.js ← tokenGenerate() + tokenVerify() │
│ │ ├── signup.js ← Creates Doctor or Patient + returns JWT │
│ │ ├── signin.js ← bcrypt.compare + returns JWT │
│ │ ├── setPassword.js ← Token-gated password setup for invites │
│ │ ├── forgotPassword.js ← Generates reset token, sends email │
│ │ └── resetPassword.js ← Verifies reset token, updates password │
│ │ │
│ ├── doctor/ │
│ │ ├── addPatient.js ← Creates patient + sends invite email │
│ │ ├── assign-patient.js ← Connects existing patient to doctor │
│ │ ├── remove-patient.js ← Disconnects patient from doctor │
│ │ ├── retrievePatients.js ← Lists doctor's assigned patients │
│ │ ├── getAllDoctors.js ← Lists all patients with their doctors │
│ │ ├── prescription.js ← Creates Prescription + PrescribedMedicine│
│ │ ├── getPrescriptions.js ← Doctor views patient's prescriptions │
│ │ ├── deletePrescription.js ← Cascading delete (adherence+meds+rx) │
│ │ └── medications.js ← Doctor views patient's today meds │
│ │ │
│ ├── patient/ │
│ │ ├── prescriptions.js ← Patient views own prescriptions │
│ │ └── medications.js ← getTodayMeds, updateStatus, history, │
│ │ adherenceStats │
│ │ │
│ ├── appointment/ │
│ │ ├── createAppointment.js ← Book slot (concurrency-safe) │
│ │ ├── getAvailableSlots.js ← Generate 9AM-5PM minus booked │
│ │ ├── getDoctorAppointments.js ← Filter by status/date │
│ │ ├── getPatientAppointments.js ← Filter by status │
│ │ └── updateAppointmentStatus.js ← Complete/cancel/miss │
│ │ │
│ ├── chat/ │
│ │ ├── chatController.js ← Create chat, list by role, get by ID │
│ │ └── messageController.js ← Create message, get messages, read │
│ │ receipts │
│ │ │
│ ├── scheduleTasks.js ← node-cron: 8AM/1PM/8PM reminders, │
│ │ 12:30/6:30/10PM missed checks, │
│ │ every-minute continuous checker │
│ │ │
│ └── utils/ │
│ ├── emailConfig.js ← Nodemailer transporter (Gmail SMTP) │
│ ├── emailService.js ← sendInviteEmail, sendPasswordResetEmail │
│ ├── emailServiceAlert.js ← Medication reminder emails + DB log │
│ ├── medicineData.js ← CSV parser for 32MB medicine dataset │
│ ├── medicationHelpers.js ← isStillActive(), resolvePatient() │
│ └── slotUtils.js ← normalizeToSlotStart() for appointments│
└────────────────────┬────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ DATA LAYER │
│ │
│ PostgreSQL (hosted on Neon, accessed via Prisma Accelerate) │
│ ├── 10 tables (see 03-database-design.md) │
│ ├── 17 migrations │
│ ├── 1 partial unique index (appointment concurrency guard) │
│ └── Implicit M:N join table: _DoctorToPatient │
│ │
│ File Storage: │
│ └── data/A_Z_medicines_dataset_of_India.csv (32MB, read-only) │
└─────────────────────────────────────────────────────────────────────────┘Request Lifecycle: Typical User Request
Example: Patient marks a medication as "Taken"
1. PatientDashboard.jsx → MedicationTracker.jsx
User clicks "Mark as Taken" button
2. Frontend sends POST /patient/medications/update-status
Headers: { Authorization: "Bearer <jwt>" }
Body: { id, status: "Taken", patientId, medication, prescriptionId, medicineId, scheduledTime }
3. Express receives request → middleware pipeline:
a. express.json() → parse body
b. cors() → allow FRONTEND_URL origin
c. triggerMedicationReminder() → async check if reminders need sending (4-min cooldown)
d. checkForMissedMedications() → decode JWT, find overdue meds, mark as Missed
4. Route match: app.post("/patient/medications/update-status", authenticate, updateMedicationStatus)
5. authenticate middleware (authMiddleware.js):
- Extract token from "Bearer <jwt>"
- tokenVerify(token) → jwt.verify() with JWT_SECRET
- Attach decoded payload to req.user
- Call next()
6. updateMedicationStatus handler (patient/medications.js L242-311):
- Re-verifies token manually (redundant with middleware — legacy code)
- Checks decoded.role === 'patient' && decoded.id == patientId
- Queries MedicineAdherence.findFirst() for existing record (today + time + medicineId)
- If found: UPDATE adherenceStatus → "Taken", missedDoses → 0
- If not found + isNewMedication: CREATE new MedicineAdherence record
- Returns the upserted record as JSON
7. Response flows back:
- 200 OK → { id, patientId, medication, adherenceStatus: "Taken", ... }
- Frontend updates local state, re-renders medication listData Flow Diagrams
Doctor Adds Patient
Doctor Dashboard (AddPatient.jsx)
│
▼
POST /doctor/add-patient { name, email, age, gender }
│
▼
authenticate() → req.user.role === 'doctor' check
│
▼
addPatient.js:
1. Check if email already exists → 400 if yes
2. prisma.Patient.create({ status: 'pending', doctors: { connect: doctorId } })
3. tokenGenerate({ purpose: 'password-setup', patientId }) → invite token (24h)
4. sendInviteEmail(email, name, token) → Nodemailer → Gmail SMTP
│
▼
Patient receives email → clicks link → /set-password?token=xxx
│
▼
SetPassword.jsx → POST /auth/set-password { token, password }
│
▼
setPassword.js:
1. tokenVerify(token) → check purpose === 'password-setup'
2. bcrypt.hash(password, 10)
3. prisma.Patient.update({ password: hash, status: 'active' })Appointment Booking (Concurrency-Safe)
PatientDashboard → AppointmentBooking.jsx
│
├─ GET /appointments/available-slots?doctorId=x&date=y
│ → Generate 9AM-5PM hourly slots
│ → Query existing non-cancelled appointments
│ → Filter out booked times
│ → Return available slots
│
▼ User selects a slot
POST /appointments { doctorId, appointmentDate, purpose }
│
▼
createAppointment.js:
1. Verify patient role
2. normalizeToSlotStart(date) → zero out minutes/seconds
3. Check doctor exists
4. Check patient is assigned to this doctor
5. Fast-path: findFirst({ doctorId, slotStart, status != cancelled })
6. If no conflict: prisma.Appointment.create()
7. If CONCURRENT insert hits DB:
→ Partial unique index triggers P2002
→ Caught in catch block → 409 "This time slot is already booked"
┌────────────────────────────────────┐
│ DB-Level Concurrency Guard: │
│ │
│ CREATE UNIQUE INDEX ... ON │
│ "Appointment" ("doctorId", │
│ "slotStart") │
│ WHERE status <> 'cancelled' │
│ │
│ Even if two requests pass the │
│ fast-path check simultaneously, │
│ only one INSERT will succeed. │
│ The loser gets P2002. │
└────────────────────────────────────┘Authentication Flow
┌──────────┐ POST /auth/signup ┌──────────────┐
│ Client │ ──────────────────────────────→ │ signup.js │
│ │ { role, name, email, │ │
│ │ password, ... } │ bcrypt.hash │
│ │ │ prisma.create│
│ │ ← { token, role } │ tokenGenerate│
│ │ └──────────────┘
│ │
│ │ POST /auth/signin ┌──────────────┐
│ │ ──────────────────────────────→ │ signin.js │
│ │ { email, password, role } │ │
│ │ │ findUnique │
│ │ ← { token, role } │ bcrypt.compare│
│ │ │ tokenGenerate│
│ │ └──────────────┘
│ │
│ localStorage.setItem('token', token)
│ localStorage.setItem('role', role)
│ │
│ │ GET /doctor/retrievePatients
│ │ Authorization: Bearer <token>
│ │ ──────────────────────────────→ ┌──────────────┐
│ │ │authMiddleware│
│ │ │ tokenVerify │
│ │ │ → req.user = │
│ │ │ { id, role,│
│ │ │ email, │
│ │ │ name } │
│ │ └──────┬───────┘
│ │ │ next()
│ │ ▼
│ │ [route handler]
└──────────┘JWT Payload Structure:
{
"id": "uuid-or-int",
"email": "user@example.com",
"name": "Dr. Smith",
"role": "doctor",
"specialization": "Cardiology", // doctor only
"iat": 1723449508,
"exp": 1723535908 // 24h expiry
}Token types used in the system:
| Token Purpose | Expiry | Contains | Used By |
|---|---|---|---|
| Session token | 24h | id, email, name, role | All authenticated routes |
| Invite token | 24h | email, purpose: 'password-setup', patientId | /auth/set-password |
| Reset token | 1h | id, email, role, purpose: 'password-reset' | /auth/reset-password |
Error Propagation
Client Request
│
▼
┌── Express Middleware ──────────────────────────────────────────────┐
│ │
│ authMiddleware → 401 { message: "Unauthorized: No token" } │
│ → 401 { message: "Unauthorized: Invalid token" } │
│ │
│ MissedMedicationChecker → catches errors silently, calls next() │
│ MedicationReminder → catches errors silently, calls next() │
│ (never blocks the request) │
│ │
└────────────────────────────────────────────────────────────────────┘
│
▼
┌── Route Handler ──────────────────────────────────────────────────┐
│ │
│ Validation errors: 400 { message: "Patient ID is required" } │
│ Authorization: 403 { message: "Only doctors can..." } │
│ Not found: 404 { message: "Patient not found" } │
│ Conflict: 409 { error: "This time slot is already..." }│
│ Server error: 500 { message: "Error creating..." } │
│ │
│ ⚠ INCONSISTENCY: Some use { message }, some use { error } │
│ ⚠ Some handlers expose internal error messages to client │
│ (e.g., "Error creating prescription: " + err.message) │
│ │
└────────────────────────────────────────────────────────────────────┘
│
▼
Frontend catches in try/catch → displays error.response.data.messageSync vs Async Operations
| Operation | Sync/Async | Mechanism |
|---|---|---|
| API requests (CRUD) | Synchronous | Express → Prisma → Postgres → JSON response |
| Email sending (invites, reminders, resets) | Async fire-and-forget | sendReminders().catch(console.error) — doesn't block response |
| Medication reminder trigger | Async per-request | triggerMedicationReminder().catch() in middleware — non-blocking |
| Missed medication checks | Async cron | node-cron runs every minute, marks past-due meds |
| Chat messages | Async WebSocket | Socket.IO send-message → broadcast to room |
| Medication update notifications | Async WebSocket | Server pushes medications-updated event after cron marks meds missed |
Environment Separation
| Concern | Development | Production (Vercel) |
|---|---|---|
| Frontend server | Vite dev server (:5173) | Static build served by Vercel CDN |
| Backend server | nodemon app.js (:8000) | Serverless function via api/index.js + vercel.json rewrite |
| Database | Same Neon PostgreSQL (no local DB) | Same Neon PostgreSQL |
| DB access | Prisma Accelerate (connection pooling) | Prisma Accelerate (required for serverless) |
| CORS origin | FRONTEND_URL=http://localhost:5173 | FRONTEND_URL=<production-domain> |
| Socket.IO | Works on long-lived server | Broken on Vercel — serverless functions are stateless, can't hold WebSocket connections |
| Cron jobs | node-cron runs in-process | Broken on Vercel — no persistent process to run cron |
| Gmail SMTP via Nodemailer | Same Gmail SMTP |
Critical production gap: Socket.IO and node-cron both require a persistent Node.js process. On Vercel's serverless, both silently fail. The medication-reminder-on-every-request middleware (app.js L74-77) is a partial workaround for cron, but chat is fundamentally incompatible with serverless.