This README documents the current backend for the MedPharma assignment. It reflects the implementation completed during development: authentication, secure admin onboarding, appointment & doctor management, queue logic, and real‑time notifications.
Provide a backend that allows patients to book online consultations and gives them live visibility of:
- their position in the queue;
- an estimated waiting time (ETA);
- whether the doctor is running late;
- when it is their turn to join the call.
The backend exposes REST APIs, enforces authentication and validation, computes queues, and broadcasts real‑time events through Socket.IO.
- Clients (mobile/web) interact with the Express REST API for CRUD and use Socket.IO to subscribe to real‑time updates.
- MongoDB Atlas (Mongoose) stores users, doctors, appointments, and invites.
- Socket.IO runs in the same process, using rooms for
doctor:{id}andappointment:{id}. - Controllers emit broadcasts after DB writes so events reflect committed state.
Architecture (simplified):
Client (Web/Mobile)
├─ HTTPS REST API ──> Express (auth, CRUD, queue endpoints)
└─ WebSocket (Socket.IO) ──> receive queue:state / queue:update / turn:now
Express API <──> MongoDB Atlas (Mongoose)
- Node.js (ESM)
- Express
- MongoDB Atlas (Mongoose)
- Socket.IO (real-time)
- Joi (input validation)
- bcryptjs + jsonwebtoken (auth)
- nodemon (development)
Core features implemented:
-
Project structure and modular code layout. ✅
-
MongoDB connection via
src/config/db.js. ✅ -
Models:
User,Doctor,Appointment,Invite. ✅ -
Authentication:
- Patient registration (public) — forced to
role: patient. ✅ - Login and
GET /api/auth/me. ✅ - JWT token issuance and
protectmiddleware. ✅ authorizemiddleware for role-based access. ✅
- Patient registration (public) — forced to
-
Admin onboarding:
- One‑time
scripts/seedAdmin.jsto create the initial admin. ✅ - Invite flow:
Invitemodel and endpoints for admins to create single‑use, expiring invite tokens. ✅
- One‑time
-
Doctor management: CRUD and
PATCH /api/doctors/:id/statusto setisRunningLate,lateByMinutes, andavgConsultMinutes. ✅ -
Appointment management: create (patient-bound), list, update status, and admin/doctor views. ✅
-
Queue utilities:
getQueueForDoctor()andestimateForAppointment()used to compute position and ETA. ✅ -
Real‑time:
src/sockets/io.jsandsrc/sockets/queueSocket.jsplus broadcasts from controllers after DB changes. ✅ -
Joi validation for request bodies and a generic validator middleware. ✅
-
README, API summary, and operational/security notes. ✅
Optional / future improvements:
- Postman collection & Newman CI (recommended).
- Swagger/OpenAPI documentation.
- Refresh tokens, MFA, SSO/SCIM for enterprise admin management.
- Production logging & monitoring, rate limiting.
src/
config/db.js
index.js
models/
User.js
Doctor.js
Appointment.js
Invite.js
controllers/
authController.js
authInviteController.js
doctorController.js
appointmentController.js
inviteController.js
routes/
auth.js
doctors.js
appointments.js
admin.js
queue.js
middleware/
auth.js # protect + authorize
validate.js # Joi wrapper
validations/
authValidation.js
doctorValidation.js
appointmentValidation.js
sockets/
io.js
queueSocket.js
utils/
estimateWait.js
scripts/
seedAdmin.js
README.md
.env.example
Create a .env at the project root (do not commit). Required keys:
PORT=5000
MONGO_URI=mongodb+srv://<user>:<password>@cluster0.../medpharma
JWT_SECRET=<strong-random-secret>
JWT_EXPIRES_IN=7d
CLIENT_ORIGIN=http://localhost:3000
Optional keys for seed script:
SEED_ADMIN_EMAIL=admin@example.com
SEED_ADMIN_PASSWORD=ChangeThisNow!
Generate a secure JWT_SECRET locally:
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"# install deps
npm install
# dev server
npm run devVerify:
GET http://localhost:5000/→{ "message": "MedPharma API is running..." }- Server logs show
MongoDB connectedwhen DB is available.
Base URL: http://localhost:<PORT>/api
-
POST /api/auth/register— register patient (public). Requests cannot create admin directly.- Body:
{ name, email, password }
- Body:
-
POST /api/auth/login— login returns{ token, user }. -
GET /api/auth/me— protected: returns current user. -
POST /api/auth/register/invite— register using an invite token (creates admin/doctor as invited).
POST /api/admin/invites— create an invite token (admin only).
GET /api/doctorsPOST /api/doctors— create doctor (admin)GET /api/doctors/:idPUT /api/doctors/:id— update doctor (admin)PATCH /api/doctors/:id/status— updateisRunningLate,lateByMinutes,avgConsultMinutes(admin)DELETE /api/doctors/:id— delete doctor (admin)
-
POST /api/appointments— create appointment (protected patient)- Body:
{ patientName, patientEmail, patientPhone, doctor, date, reason? }
- Body:
-
GET /api/appointments/my— list appointments for logged-in patient -
GET /api/appointments/doctor/:doctorId— get doctor's queue (doctor/admin) -
PATCH /api/appointments/:id/status— update status (cancel, confirm, complete) -
GET /api/appointments— admin view of all appointments
GET /api/queue/doctor/:doctorId— current queue listGET /api/queue/appointment/:id/position— position & ETA for an appointment
Client → Server
queue:subscribe{ doctorId?, appointmentId? }— join rooms; server responds with current state.queue:unsubscribe{ doctorId?, appointmentId? }— leave rooms.
Server → Client
queue:state{ doctorId, list }— full queue list for the doctor.queue:update{ appointmentId, position, etaMinutes }— single appointment update.turn:now{ appointmentId, joinUrl }— prompt patient to join the call.doctor:status{ doctorId, isRunningLate, lateByMinutes }— doctor status broadcast.
Rooms:
doctor:{doctorId}— broadcasts for a doctor's queue.appointment:{appointmentId}— targeted broadcasts for a single appointment.
-
Queue = appointments with
statusin [pending,confirmed], sorted ascending bydate. -
Position = number of appointments with an earlier
datein that queue. -
ETA (minutes) =
position * avgConsultMinutes + doctor.lateByMinutes.avgConsultMinutesstored on Doctor model; default = 12.lateByMinutesis applied when doctor marks themselves late.
This method is deterministic and explainable. It is easy to improve later (e.g., incorporate historical consult durations, no-shows, or machine learning estimates).
-
Broadcasts are invoked in controllers after DB operations complete. That ensures events reflect committed state.
-
Emissions are wrapped in
try/catchand are best-effort; failures don’t break HTTP responses. -
Typical events emitted after operations:
- After
createAppointment:queue:state(doctor room) +queue:update(appointment room). - After
updateAppointmentStatusor cancellation: same as above. - After
updateDoctorStatus:doctor:status+queue:state.
- After
- Seed admin:
scripts/seedAdmin.jscreates the first admin (run once). Do not commit secrets used for seeding. - Invite flow: admins generate single-use, expiring invite tokens for additional admins/doctors. In production, send tokens via email.
- Public registration is limited to patients; admin creation is disabled on the public route.
- Protect admin endpoints using
protect+authorize('admin')middleware.
Operational recommendations:
- Use email verification for invites.
- Log invite creation and admin actions for audit.
- Enforce MFA for admin accounts in production.
Create a Postman environment with BASE_URL, TOKEN, DOCTOR_ID, APPOINTMENT_ID, INVITE_TOKEN.
Flow:
- Register patient → login → save token.
- Seed/create admin or create invite via an admin account.
- Create a doctor (admin credentials required) → save
DOCTOR_ID. - As patient, create appointment using
DOCTOR_ID→ saveAPPOINTMENT_ID. - Verify
GET /api/queue/appointment/:id/positionreturnspositionandetaMinutes. - Start Socket.IO client,
queue:subscribeand verifyqueue:state/queue:update/turn:nowon mutations.
Export the Postman collection to docs/postman_collection.json for repeatable tests.
- Use managed secrets (AWS Secrets Manager / Azure Key Vault). Don’t store
.envin the repo. - For horizontal scaling of Socket.IO, use a Redis adapter or managed Pub/Sub and sticky sessions.
- Add monitoring/alerts for failed broadcasts, high error rates, and suspicious auth behavior.
- Consider short access tokens + refresh tokens, and SSO for enterprise admin provisioning.
- Ensure
.gitignoreexcludesnode_modules/,.env, and log files. - Commit
.env.example(no secrets). - Verify no secrets are present in staged commits (
git grep JWT_SECRET || true). - Run
npm ciand any tests/linting scripts. - Commit and push to remote.
If a secret has been pushed previously, rotate it immediately and purge the history using git filter-repo or BFG.
MIT — Samuel Mensah Quaye