Telemedicine Platform Backend
End-to-end NestJS backend for a telemedicine product — consultations, messaging, payments, wallets, uploads, calls, and notifications under a versioned API surface.
Context
Brought in as a contract backend engineer to build and extend the production API for a telemedicine platform serving patients, doctors, and admins. The product needed reliable auth across three roles, consultation lifecycle management, wallet-based billing, real-time messaging, and integrations for payments, video calls, and file uploads.
- Owned modular backend delivery across auth, consultations, payments, wallets, messaging, and infrastructure modules.
- Designed for production use with structured error handling, API documentation, and webhook-first payment reconciliation.
- Client and product names are omitted; this case study focuses on engineering decisions and system design.
Architecture
The backend follows a modular NestJS layout with clear separation between feature modules, shared infrastructure, and cross-cutting concerns. All business APIs live under a global /api/v1 prefix, with health probes and documentation routed separately.
Admin app
Doctor app
Patient app
NestJS API
/api/v1 · guards · validation · throttling
Auth
OTP · JWT
Consultations
lifecycle
Messages
REST + Socket.IO
Payments
Razorpay
Wallets
ledger
Upload
S3 presigned
Calls
video tokens
Notifications
FCM
Shared
reference data
PostgreSQL
Prisma · split schemas
Redis / KeyDB
cache · Socket.IO scale
External services
OTP · Razorpay · S3 · video
- Feature modules: auth, admin, doctors, patient, consultations, messages, payments, wallets, upload, calls, notifications, billing, and shared reference data.
- Infrastructure layer: Prisma database access, Redis/KeyDB caching, external service adapters (OTP, Razorpay, S3, video), and a Redis-backed Socket.IO adapter for horizontal scaling.
- Split Prisma schema by domain — identity, auth, consultations, financial, and reference data — to keep models maintainable as the product grew.
- Global validation, throttling, structured error codes, and Swagger/OpenAPI docs for client and admin teams.
Authentication & access control
Auth is role-aware from the first request. Patients and doctors authenticate via phone OTP; admins use email/password or OTP depending on the client surface. Sessions are JWT-based with short-lived access tokens and rotating refresh tokens.
- Three actor types — admin, doctor, patient — enforced with JWT guards, role decorators, and route-level permission checks.
- OTP flows include rate limiting, expiry, attempt caps, and role immutability so a patient OTP cannot be replayed as a doctor session.
- Session service tracks active tokens; logout and account suspension invalidate sessions immediately.
- DoctorActive guard blocks suspended doctors from protected routes even with a valid token.
Consultations & workflow
Consultations are the core domain object connecting patients, doctors, billing, messaging, and calls. Status transitions are explicit so downstream systems — wallets, notifications, and settlement — can react predictably.
- Lifecycle: requested → accepted → active → ended → settled.
- Consultation end triggers automatic settlement logic tied to wallet debits and credits.
- Admin tooling supports manual settlement and operational oversight when automated flows need intervention.
- Availability, reviews, and reference data (specializations, symptoms, languages) support booking and discovery flows.
Payments & wallets
Financial logic uses a ledger model rather than a mutable balance column. Wallet balance is derived from credited and debited transactions, which makes reconciliation and audit trails easier to reason about.
Wallet recharge request
Patient requests recharge with idempotency key to prevent duplicate orders on retries.
Razorpay order created
Backend creates a payment order and returns checkout details to the client.
Payment captured
User completes checkout via Razorpay. Webhook is the primary reconciliation path.
Webhook verified & wallet credited
Signature-checked event writes a ledger credit — balance is derived from transactions, not a mutable field.
Consultation ends
Lifecycle moves to ended, triggering settlement logic across patient, doctor, and platform wallets.
Settlement split
Debit patient wallet, credit doctor earning and platform commission. GST tracked as separate ledger entries.
Fallback paths: order status polling and admin verify-order when webhooks are delayed.
- Patient wallet recharge via Razorpay orders with idempotency keys for safe client retries.
- Webhook-primary reconciliation: signature-verified Razorpay events credit wallets on successful capture.
- Fallback paths include order status polling and admin verify-order endpoints when webhooks are delayed.
- Consultation settlement splits value across patient debit, doctor earning credit, platform commission, and GST components.
- Separate wallet contexts for patient, doctor, and platform with payout request flows for doctor withdrawals.
Real-time & integrations
Messaging is real-time; calls and notifications use dedicated integration paths suited to their delivery model.
- Socket.IO gateway for consultation messaging, scaled with a Redis adapter for multi-instance deployments.
- REST endpoints for message history alongside the real-time channel.
- Video/audio calls via third-party token generation — consultation-scoped call start, end, and token endpoints.
- Push notifications through FCM device token registration.
- AWS S3 presigned PUT for uploads and presigned GET for private documents with short expiry and owner/admin access checks.
Trade-offs & learnings
Building a telemedicine backend under contract meant optimizing for clarity and operability, not just feature count. A few decisions that paid off:
- Ledger-based wallets made payment debugging and webhook retries safer than storing a single balance field.
- Webhook-first payments with explicit fallbacks reduced duplicate-credit risk without blocking the happy path.
- Splitting Prisma schemas and Nest modules early kept auth, financial, and consultation logic from becoming a monolith.
- OpenAPI generation gave frontend teams a stable contract while the backend evolved quickly.
Key takeaways
- Modular NestJS architecture with /api/v1 versioning and domain-split data models.
- Role-based OTP + JWT session design with guard layering for production safety.
- Ledger wallets and Razorpay webhook reconciliation with idempotent recharge flows.
- Socket.IO messaging scaled via Redis, alongside S3 presigned uploads and third-party call integration.