02-feature-deep-dives.md

Feature Deep Dives — How Each Feature Works

1. User Registration & Authentication

What the user does

Doctor or patient fills out signup form → receives JWT → redirected to dashboard.

Under the hood (step-by-step)

Signup flow (auth/signup.js):

  1. Client sends POST /auth/signup with { role, name, email, password, ... }
  2. Handler checks role — branches to Patient.create() or Doctor.create()
  3. Password is hashed: bcrypt.hash(password, 10) — 10 salt rounds
  4. Prisma creates the record. For patients: age is parseInt()'d, gender stored as string
  5. tokenGenerate() creates JWT with { id, role, name, email } — 24h expiry
  6. Returns { msg: "done signup", token, role }

Signin flow (auth/signin.js):

  1. Client sends POST /auth/signin with { email, password, role }
  2. Prisma findUnique({ where: { email } }) on Patient or Doctor table
  3. bcrypt.compare(password, user.password) — compares plaintext against hash
  4. If match: tokenGenerate() with user data
  5. Returns { message: "Sign-in successful", token, role }

Files involved

  • auth/signup.js, auth/signin.js, auth/jwtToken.js
  • middleware/authMiddleware.js
  • Frontend: Pages/Signin.jsx, Pages/Signup.jsx

DB queries

  • Patient.findUnique({ where: { email } }) or Doctor.findUnique()
  • Patient.create() or Doctor.create()

Error handling

  • Missing JWT_SECRET → 500 with "Server configuration error"
  • Invalid credentials → 401 "Invalid email or password" (same message for wrong email AND wrong password — prevents email enumeration)
  • Duplicate email → Prisma P2002 caught as generic 400

Edge cases handled

  • JWT secret validation before token generation (signin.js L37-40)
  • Fallback JWT secret "mysecret" if env var missing (jwtToken.js L13) — dangerous but prevents crash
  • dotenv.config() called in multiple files to handle import-order issues

What could go wrong

  • No email uniqueness check in signup — relies on DB @unique constraint. Two simultaneous signups with the same email: one fails with an unhelpful 400 "Error in creating user"
  • Fallback JWT secret — if JWT_SECRET isn't set, tokens are signed with "mysecret". Anyone who reads the source code can forge tokens.

2. Doctor Adds Patient (Invite Flow)

What the user does

Doctor fills in patient name, email, age, gender → patient receives email with "Set Your Password" link.

Under the hood

  1. Doctor submits form → POST /doctor/add-patient with { name, email, age, gender }
  2. authenticate() middleware verifies JWT, attaches req.user
  3. addPatient.js checks req.user.role === 'doctor' → 403 if not
  4. Checks if patient email already exists (Patient.findUnique) → 400 if yes
  5. Patient.create() with:
    • status: 'pending' (no password yet)
    • password: null
    • doctors: { connect: { id: doctorId } } — links to the creating doctor
  6. Generates invite token: tokenGenerate({ email, purpose: 'password-setup', patientId }) — 24h expiry
  7. sendInviteEmail() → Nodemailer sends HTML email with link to FRONTEND_URL/set-password?token=xxx
  8. Returns { patient, emailSent: true/false }

Patient sets password (auth/setPassword.js):

  1. Patient clicks email link → frontend reads ?token= from URL
  2. POST /auth/set-password with { token, password }
  3. tokenVerify(token) → checks not expired, purpose === 'password-setup', patientId exists
  4. Patient.findUnique({ where: { id: decoded.patientId } }) — existence check
  5. bcrypt.hash(password, 10)
  6. Patient.update({ password: hash, status: 'active' })

Files involved

  • doctor/addPatient.js, auth/setPassword.js, auth/jwtToken.js
  • utils/emailService.js → sendInviteEmail()
  • Frontend: Components/AddPatient.jsx, Components/SetPassword.jsx

Error handling

  • Token expired → 400 "Token has expired, please request a new invitation"
  • Token has wrong purpose → 400 "Invalid token purpose"
  • Patient already exists → 400 "Patient with this email already exists"
  • Email fails → emailSent: false returned but patient IS created (partial success)

Edge cases

  • Email delivery failure doesn't rollback patient creation — doctor sees "Patient added" but patient never gets invite. No retry mechanism.
  • Token contains patientId in plain JWT (not encrypted) — intercepting the token reveals the patient's DB ID

3. Prescription Creation

What the user does

Doctor selects a patient, searches medicines from the database, adds dosage/timing/instructions/duration for each, submits.

Under the hood

  1. Frontend AddPrescription.jsx sends POST /doctor/prescription with:
    { "patientId": 5, "condition": "Hypertension", "medicines": [ { "id": "med-123", "name": "Amlodipine", "dosage": "5mg", "timing": { "morning": true, "afternoon": false, "evening": true }, "instructions": "After food", "duration": "30 days" } ] }
  2. Validates: patientId required, medicines must be non-empty array, each medicine must have all 6 fields
  3. parseInt(patientId, 10) — converts string to int
  4. Checks patient exists
  5. Prescription.create() with nested medicines: { create: [...] } — single transaction creates parent + children
  6. Returns the created prescription with included patient name, doctor name, and all medicines

Files involved

  • doctor/prescription.js, doctor/getPrescriptions.js, doctor/deletePrescription.js
  • Frontend: Components/AddPrescription.jsx, Components/ViewPrescription.jsx
  • utils/medicineData.js → CSV parser for medicine autocomplete

DB queries

  • Patient.findUnique() — existence check
  • Prescription.create({ include: { medicines: true, patient, doctor } }) — nested create + eager load

The medicine search pipeline

GET /api/medicines?search=amlo │ ▼ medicineData.js → getMedicinesHandler() 1. fs.createReadStream('data/A_Z_medicines_dataset_of_India.csv') 2. Pipe through csv-parser 3. Collect ALL rows into array (entire 32MB file) 4. If ?search param: filter by name.toLowerCase().includes(search) 5. Return filtered results

Edge case: Prescription deletion

deletePrescription.js uses a Prisma transaction ($transaction) to cascade delete:

  1. Delete MedicineAdherence records referencing this prescription
  2. Delete PrescribedMedicine records
  3. Delete the Prescription itself

Without the transaction, a crash mid-deletion would leave orphaned adherence records pointing to non-existent prescriptions.


4. Medication Tracking & Reminders

What the user does

Patient opens dashboard → sees today's medications grouped by morning/afternoon/evening → clicks "Taken" or "Missed".

Under the hood — generating today's medication list

This is the most complex flow in the system. patient/medications.js → getTodayMedications():

Step 1: Query MedicineAdherence for today's date (scheduledDate = YYYY-MM-DD) ├─ If records exist → merge with prescriptions to fill gaps └─ If no records → fall through to Step 2 Step 2: Query all Prescriptions for this patient (include medicines) Step 3: For each prescription → for each medicine: ├─ Check isStillActive(prescriptionDate, duration) │ └─ Parses "7 days" / "2 weeks" / "1 month" → calculates end date │ └─ Returns false if today > endDate │ └─ For each time period (morning/afternoon/evening): └─ If timing[timeOfDay] === true AND not already in records: → Push { medicineId, medicineName, dosage, instructions, scheduledTime, adherenceStatus: 'Pending', prescriptionId } Step 4: Return [...existingRecords, ...generatedPending]

The reminder pipeline

node-cron: 8:00 AM → sendReminders("morning") 1:00 PM → sendReminders("afternoon") 8:00 PM → sendReminders("evening") sendReminders(timeOfDay) in scheduleTasks.js: 1. Find all patients with at least one prescription 2. For each patient: a. Get prescriptions + medicines b. Get existing adherence records for today + timeOfDay c. Build medicationStatusMap[medicineId] = status d. Filter: timing[timeOfDay] === true AND NOT already "Taken" e. If untaken meds exist → emailServiceAlert(token) 3. emailServiceAlert.js: a. Re-query prescriptions (yes, again) b. Filter for current timeOfDay's untaken meds c. Send HTML email via Nodemailer d. Create/update MedicineAdherence records with reminderSent: true Additionally on EVERY HTTP request (app.js L74-77): triggerMedicationReminder() → checks 4-minute cooldown → sendReminders()

Auto-marking missed medications

Three mechanisms, overlapping:

  1. Cron-based checks (scheduleTasks.js): At 12:30 PM / 6:30 PM / 10 PM → mark pending meds as Missed
  2. Continuous checker (every minute): Groups by time period, marks as Missed if past deadline
  3. Middleware (MissedMedicationChecker.js): On every patient request, check for overdue pending meds

Edge cases

  • Duration parsing handles "days", "weeks", "months" via regex (\d+)\s*(\w+). Anything else (e.g., "as needed") defaults to active.
  • Fallback query: If scheduledDate column query fails (schema mismatch), falls back to createdAt-based date range query.
  • Deduplication: Uses addedMedicines Set to avoid showing the same medicine twice for the same time slot.

What could go wrong

  • isStillActive() is duplicated in 3 files (patient/medications.js, doctor/medications.js, utils/medicationHelpers.js) — if one is updated and others aren't, behavior diverges.
  • Medication reminders fire on every HTTP request with a 4-minute cooldown. If the server receives no requests for hours, reminders silently stop (mitigated by cron, but not on Vercel).

5. Appointment Booking

What the user does

Patient selects a doctor → picks a date → sees available 1-hour slots → books one.

Under the hood

Available slots (appointment/getAvailableSlots.js):

  1. Generate all hourly slots from 9 AM to 5 PM for the given date (8 slots)
  2. Query existing non-cancelled appointments for that doctor + date
  3. Filter: allSlots.filter(slot => !bookedTimes.includes(slot.getTime()))
  4. Return available slots with formatted times

Create appointment (appointment/createAppointment.js):

  1. Verify patient role
  2. normalizeToSlotStart(date) — zeros out minutes, seconds, milliseconds
  3. Verify doctor exists
  4. Critical: Verify patient is assigned to this doctor (Patient.findFirst({ doctors: { some: { id: doctorId } } }))
  5. Fast-path conflict check: Appointment.findFirst({ doctorId, slotStart, status != cancelled })
  6. Appointment.create({ status: 'scheduled' })
  7. If concurrent insert hits the partial unique index → Prisma throws P2002 → caught → 409

The concurrency guard (deep dive)

-- From migration 20250806120000 CREATE UNIQUE INDEX "Appointment_doctorId_slotStart_active_key" ON "Appointment" ("doctorId", "slotStart") WHERE status <> 'cancelled'

This is a partial unique index — it only enforces uniqueness for non-cancelled appointments. This means:

  • Cancel an appointment → that slot becomes bookable again
  • Two concurrent INSERTs for the same slot → only one wins at the DB level
  • The loser gets a unique constraint violation (P2002)

Tested with: scripts/concurrentBookingTest.js fires 50 simultaneous booking requests and verifies exactly 1 succeeds.

Edge cases

  • Cancelled appointments are excluded from conflict checks, so re-booking is possible
  • normalizeToSlotStart() prevents booking at 9:30 — it snaps to 9:00
  • Patient can only book with assigned doctors — prevents unauthorized access

6. Real-time Chat

What the user does

Doctor or patient opens Messages tab → selects a conversation → sends/receives messages in real-time.

Under the hood

Chat creation (chat/chatController.js):

  1. POST /chats with { patientId, doctorId }
  2. Check if chat already exists → return existing with isExisting: true
  3. If not → Chat.create() → return with isNew: true
  4. Idempotent: calling create twice returns the same chat

Message sending:

  1. HTTP: POST /chats/:chatId/messages with { content }
    • Verifies user is part of the chat
    • Creates Message record with senderId and senderType
    • Updates Chat.updatedAt
  2. WebSocket: Client emits send-message → server broadcasts receive-message to all in room

Read receipts (chat/messageController.js):

  • When fetching messages, automatically marks opponent's unread messages as read:
    • Doctor opens chat → all patient messages get readStatus: true
    • Patient opens chat → all doctor messages get readStatus: true

Socket.IO room model

socket.on("join-chat", (chatId) => socket.join(chatId)) socket.on("send-message", (data) => io.to(data.chatId).emit("receive-message", data))

Edge cases

  • No offline message queue — if recipient is offline, they get messages on next HTTP fetch, but miss the real-time push
  • No typing indicators — could be added with Socket.IO events
  • senderId is string-cast (String(decoded.id)) — because Doctor IDs are UUIDs and Patient IDs are integers, and the senderId column is a string

7. Password Recovery

What the user does

Clicks "Forgot Password" → enters email + role → receives email → clicks link → sets new password.

Under the hood

Forgot password (auth/forgotPassword.js):

  1. Finds user by email + role
  2. Security: Always returns 200 with same message regardless of whether email exists (prevents enumeration)
  3. Generates token: tokenGenerate({ id, email, role, purpose: 'password-reset' }, '1h')
  4. Sends password reset email with link to FRONTEND_URL/reset-password?token=xxx

Reset password (auth/resetPassword.js):

  1. tokenVerify(token) → checks purpose === 'password-reset'
  2. bcrypt.hash(password, 10)
  3. Updates Patient or Doctor record based on decoded.role

Edge case: Type coercion

  • Patient IDs are integers, Doctor IDs are UUIDs
  • resetPassword.js L35: parseInt(decoded.id) for patients
  • resetPassword.js L40: decoded.id as-is for doctors
  • This asymmetry is scattered throughout the codebase