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):
- Client sends POST
/auth/signupwith{ role, name, email, password, ... } - Handler checks
role— branches toPatient.create()orDoctor.create() - Password is hashed:
bcrypt.hash(password, 10)— 10 salt rounds - Prisma creates the record. For patients:
ageisparseInt()'d,genderstored as string tokenGenerate()creates JWT with{ id, role, name, email }— 24h expiry- Returns
{ msg: "done signup", token, role }
Signin flow (auth/signin.js):
- Client sends POST
/auth/signinwith{ email, password, role } - Prisma
findUnique({ where: { email } })on Patient or Doctor table bcrypt.compare(password, user.password)— compares plaintext against hash- If match:
tokenGenerate()with user data - Returns
{ message: "Sign-in successful", token, role }
Files involved
auth/signup.js,auth/signin.js,auth/jwtToken.jsmiddleware/authMiddleware.js- Frontend:
Pages/Signin.jsx,Pages/Signup.jsx
DB queries
Patient.findUnique({ where: { email } })orDoctor.findUnique()Patient.create()orDoctor.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.jsL37-40) - Fallback JWT secret "mysecret" if env var missing (
jwtToken.jsL13) — 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
@uniqueconstraint. Two simultaneous signups with the same email: one fails with an unhelpful 400 "Error in creating user" - Fallback JWT secret — if
JWT_SECRETisn'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
- Doctor submits form → POST
/doctor/add-patientwith{ name, email, age, gender } authenticate()middleware verifies JWT, attachesreq.useraddPatient.jschecksreq.user.role === 'doctor'→ 403 if not- Checks if patient email already exists (
Patient.findUnique) → 400 if yes Patient.create()with:status: 'pending'(no password yet)password: nulldoctors: { connect: { id: doctorId } }— links to the creating doctor
- Generates invite token:
tokenGenerate({ email, purpose: 'password-setup', patientId })— 24h expiry sendInviteEmail()→ Nodemailer sends HTML email with link toFRONTEND_URL/set-password?token=xxx- Returns
{ patient, emailSent: true/false }
Patient sets password (auth/setPassword.js):
- Patient clicks email link → frontend reads
?token=from URL - POST
/auth/set-passwordwith{ token, password } tokenVerify(token)→ checks not expired,purpose === 'password-setup',patientIdexistsPatient.findUnique({ where: { id: decoded.patientId } })— existence checkbcrypt.hash(password, 10)Patient.update({ password: hash, status: 'active' })
Files involved
doctor/addPatient.js,auth/setPassword.js,auth/jwtToken.jsutils/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: falsereturned 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
patientIdin 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
- Frontend
AddPrescription.jsxsends POST/doctor/prescriptionwith:{ "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" } ] } - Validates:
patientIdrequired,medicinesmust be non-empty array, each medicine must have all 6 fields parseInt(patientId, 10)— converts string to int- Checks patient exists
Prescription.create()with nestedmedicines: { create: [...] }— single transaction creates parent + children- 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 checkPrescription.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 resultsEdge case: Prescription deletion
deletePrescription.js uses a Prisma transaction ($transaction) to cascade delete:
- Delete
MedicineAdherencerecords referencing this prescription - Delete
PrescribedMedicinerecords - Delete the
Prescriptionitself
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:
- Cron-based checks (
scheduleTasks.js): At 12:30 PM / 6:30 PM / 10 PM → mark pending meds as Missed - Continuous checker (every minute): Groups by time period, marks as Missed if past deadline
- 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
scheduledDatecolumn query fails (schema mismatch), falls back tocreatedAt-based date range query. - Deduplication: Uses
addedMedicinesSet 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):
- Generate all hourly slots from 9 AM to 5 PM for the given date (8 slots)
- Query existing non-cancelled appointments for that doctor + date
- Filter:
allSlots.filter(slot => !bookedTimes.includes(slot.getTime())) - Return available slots with formatted times
Create appointment (appointment/createAppointment.js):
- Verify patient role
normalizeToSlotStart(date)— zeros out minutes, seconds, milliseconds- Verify doctor exists
- Critical: Verify patient is assigned to this doctor (
Patient.findFirst({ doctors: { some: { id: doctorId } } })) - Fast-path conflict check:
Appointment.findFirst({ doctorId, slotStart, status != cancelled }) Appointment.create({ status: 'scheduled' })- 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):
- POST
/chatswith{ patientId, doctorId } - Check if chat already exists → return existing with
isExisting: true - If not →
Chat.create()→ return withisNew: true - Idempotent: calling create twice returns the same chat
Message sending:
- HTTP: POST
/chats/:chatId/messageswith{ content }- Verifies user is part of the chat
- Creates
Messagerecord withsenderIdandsenderType - Updates
Chat.updatedAt
- WebSocket: Client emits
send-message→ server broadcastsreceive-messageto 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
- Doctor opens chat → all patient messages get
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 thesenderIdcolumn 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):
- Finds user by email + role
- Security: Always returns 200 with same message regardless of whether email exists (prevents enumeration)
- Generates token:
tokenGenerate({ id, email, role, purpose: 'password-reset' }, '1h') - Sends password reset email with link to
FRONTEND_URL/reset-password?token=xxx
Reset password (auth/resetPassword.js):
tokenVerify(token)→ checkspurpose === 'password-reset'bcrypt.hash(password, 10)- Updates Patient or Doctor record based on
decoded.role
Edge case: Type coercion
- Patient IDs are integers, Doctor IDs are UUIDs
resetPassword.jsL35:parseInt(decoded.id)for patientsresetPassword.jsL40:decoded.idas-is for doctors- This asymmetry is scattered throughout the codebase