01-architecture-and-flow.md

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 list

Data 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 PurposeExpiryContainsUsed By
Session token24hid, email, name, roleAll authenticated routes
Invite token24hemail, purpose: 'password-setup', patientId/auth/set-password
Reset token1hid, 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.message

Sync vs Async Operations

OperationSync/AsyncMechanism
API requests (CRUD)SynchronousExpress → Prisma → Postgres → JSON response
Email sending (invites, reminders, resets)Async fire-and-forgetsendReminders().catch(console.error) — doesn't block response
Medication reminder triggerAsync per-requesttriggerMedicationReminder().catch() in middleware — non-blocking
Missed medication checksAsync cronnode-cron runs every minute, marks past-due meds
Chat messagesAsync WebSocketSocket.IO send-message → broadcast to room
Medication update notificationsAsync WebSocketServer pushes medications-updated event after cron marks meds missed

Environment Separation

ConcernDevelopmentProduction (Vercel)
Frontend serverVite dev server (:5173)Static build served by Vercel CDN
Backend servernodemon app.js (:8000)Serverless function via api/index.js + vercel.json rewrite
DatabaseSame Neon PostgreSQL (no local DB)Same Neon PostgreSQL
DB accessPrisma Accelerate (connection pooling)Prisma Accelerate (required for serverless)
CORS originFRONTEND_URL=http://localhost:5173FRONTEND_URL=<production-domain>
Socket.IOWorks on long-lived serverBroken on Vercel — serverless functions are stateless, can't hold WebSocket connections
Cron jobsnode-cron runs in-processBroken on Vercel — no persistent process to run cron
EmailGmail SMTP via NodemailerSame 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.