Owning the codebase
A structured, verified walkthrough through the AI-Clinic codebase. While high-level specs document feature lists, this guide walks through where each system lives, how patient requests traverse the Fastify pipeline and Botpress AI triage, how clinical state machines enforce safety, and how to safely extend features without introducing regressions.
Every surface is kept concise. Full engineering depth sits one click away: ▸ expandable blocks reveal edge cases, cards marked open ↗ open deep inspection panels, and any teal mono link opens that declaration directly in the reference atlas. Press / to filter chapters, Esc to dismiss panels.
01How to read this guide
Three focused sessions, with the repository open beside this document. Paths are relative to the repository root. Every code snippet is taken verbatim from active production files.
Chapters 02–10: The architectural layers, Fastify bootstrap, request lifecycle, concurrency, and core Prisma persistence. Click for details.
Chapters 11–25: Botpress AI triage, clinical SOAP extraction, smart doctor matching, consultation state machines, H2H chat, and the financial pipeline. Click for details.
Chapters 26–38: Configuration, observability, test tiers, recipes, interactive data and type maps, Botpress SDK companion, and technical quiz. Click for details.
Focus on runtime foundations: read bootstrap, inspect Fastify plugin mounting, examine CsrfGuard and CookieAuthGuard, and understand how UserStatusCache enforces live account standing.
Deep-dive into clinical care delivery: follow AI symptom triage in BotpressService, examine SOAP parsing in SoapService, inspect the 40/25/20/15 doctor matching formula in MatchingService, and trace the 8-state consultation state machine in ConsultationService.
Master project maintenance: test tiers in auth.service.ts, configuration schemas in validateEnv, interactive database schema exploration in Chapter 33, and the companion integration of the Botpress Chat SDK in Part IX.
Start by reading bootstrap in server/src/main.ts (chapter 04) to see all registered Fastify plugins, session handlers, and security guards. Then inspect ConsultationService.create in server/src/consultation/consultation.service.ts and ChatGateway.handleMessage in server/src/chat/chat.gateway.ts. Together, they explain what services exist and how patient care is delivered.
How to navigate the interactive reference atlas navigation
Every code symbol highlighted in teal (e.g. bootstrap) is a direct portal into that file's declarations. Clicking any symbol opens a modal displaying its exact signature, doc comments, test file link, and line number directly extracted from the TypeScript AST.
02The shape of the thing
A modular NestJS 11 + Fastify 5 backend with PostgreSQL and Prisma 6, driving an authenticated React 18 client and real-time Socket.IO communications across five distinct tiers.
server/src/main.ts · Fastify 5 Engineserver/src/*/ handling HTTP routes, SSE streaming for AI chat, and Socket.IO namespaces for /chat and /matching.PrismaService.UserStatusCache (60s TTL), CacheService, argon2 password hashing, and multipart magic-byte file validation.server/src/main.ts bootstraps NestJS on top of FastifyAdapter rather than Express. Fastify provides high-throughput request handling and raw socket hijacking for SSE streams.
Controllers map directly to clinical REST endpoints. Real-time events use two dedicated Socket.IO namespaces:
- /chat — Managed by ChatGateway: handles direct human-to-human patient/doctor chat, message delivery, typing state, and unread notifications.
- /matching — Managed by MatchingGateway: dispatches 5-minute timed patient matching offers to candidate doctors.
- SSE Stream — Provided by AiAgentsController.stream: streams live triage tokens and
soap_readysignals viareply.raw.write.
Domain logic lives in specialized services under server/src/*/. Key modules include:
- AuthService: Registration, login verification against live ban cache, password rotation.
- ConsultationService: Enforces the 8-state consultation lifecycle from review to completion.
- MatchingService: Weighted algorithmic ranking of available verified doctors.
- BotpressService: Manages external AI chatbot client sessions and clinical symptom collection.
- NurseService: Delegation invite workflows and role upgrade transactions.
PostgreSQL schema defines 23 models managed by PrismaService. Migrations are executed at startup via prisma migrate deploy inside the production Docker image. All database relations use foreign key cascades and composite unique indexes to prevent orphaned medical records.
Essential infrastructure shared across modules:
- UserStatusCache (user-status.ts): In-memory cache holding ban and role status for 60 seconds; immediately invalidated when an admin bans an account or upgrades a nurse.
- CsrfGuard (csrf.guard.ts): Double-submit cookie verification applied globally to all mutating HTTP methods.
- RequestLoggingInterceptor: Logs incoming HTTP calls (method, path, status, duration) without exposing confidential body payloads.
Package size distribution across the codebase. Click any package bar to inspect its constituent files, declarations, and purposes.
Dependencies point strictly inward toward domain models and Prisma. Controllers depend on domain services; services never depend on HTTP controllers. Click to view import rules.
Unit tests reside directly beside source files as *.spec.ts (server) and *.test.ts (client). Integration suites live in client/integration/. Click to inspect runner configs.
Modules adhere to hexagonal architecture boundaries:
- Domain services (
consultation.service.ts,matching.service.ts) must never import controllers, Fastify request objects, or HTTP decorators. - Gateways (
chat.gateway.ts,matching.gateway.ts) share domain services with REST controllers, ensuring identical validation rules across HTTP and WebSocket paths. - Storage logic is encapsulated inside Prisma services and queries, never leaked into frontend client code.
The codebase maintains three rigorous testing tiers:
- Unit Tests:
server/src/**/*.spec.tsrun via Jest with in-memory mocks. Fast execution (~15s). - Server E2E Tests:
server/test/**/*.e2e-spec.tsboot the full NestJS Fastify app with Supertest. - Integration Suite:
client/integration/bootsserver/dist/main.jsand runs real HTTP and Socket.IO workflows with Vitest.
NestJS Module encapsulation and exports modules
Each feature directory defines a dedicated *.module.ts (e.g. ConsultationModule). Providers must be explicitly declared in the exports array to be injectable into sibling modules, preventing unintended tight coupling.
03Vocabulary
Core domain terms used across the code, schemas, and API contracts. Click any term to view its definition, database storage location, and enforcement invariants.
04Boot, run, shut down
The application starts in server/src/main.ts, configuring Fastify adapters, security cookies, and Swagger before binding to port 8080. Graceful shutdown closes open WebSocket rooms and database connections.
validateEnv executes before modules instantiate. Required variables: DATABASE_URL and SESSION_SECRET (at least 32 characters). Missing optional credentials (Botpress, Calendly, SMTP) log informative warnings without halting server startup.
Fastify is initialized with { rawBody: true }. This preserves the untouched request buffer required to compute HMAC signatures on incoming Calendly webhooks (CalendlyService.verifyWebhookSignature).
fastifySecureSession generates encrypted stateless session cookies. The 32-byte key is derived deterministically by hashing the configuration secret with SHA-256.
An onRequest hook checks for a csrf-token cookie. If absent, a random 32-byte hex token is generated and returned as a non-HttpOnly cookie so frontend Axios clients can read it and send it back as X-CSRF-Token.
IoAdapter binds Socket.IO on top of the Fastify HTTP server, establishing the /chat and /matching namespaces with cookie extraction hooks.
| Periodic Job | Interval | Target Method | Purpose |
|---|---|---|---|
| Cache Sweeper | Hourly | CacheService.cleanExpired | Purges expired key-value entries from in-memory cache. |
| Guest Context Pruner | Every 10 min | BotpressService.pruneGuestContexts | Evicts unauthenticated AI triage chat contexts older than 30 minutes. |
| Match Expiry Sweep | Every 30 sec | MatchingGateway.expirySweep | Flags unanswered doctor match offers exceeding 5 minutes and re-routes patients. |
Fastify traps SIGTERM and SIGINT, allowing ongoing requests to complete before closing sockets. Click to inspect shutdown sequence.
PrismaService.onModuleDestroy cleanly terminates the PostgreSQL connection pool without aborting transactions in flight.
Upon receiving SIGTERM from Docker or orchestrator:
- The HTTP listener stops accepting new TCP connections.
- Active WebSocket connections are informed and disconnected.
- In-flight HTTP requests have up to 10 seconds to finish.
- Fastify server closes, invoking NestJS module teardown hooks.
PrismaService.onModuleDestroy invokes (), safely closing client sockets and preventing lingering connection leaks on the PostgreSQL server.
Why Fastify plugin registration order matters security
In Fastify, plugin order is strictly hierarchical:
fastifyCookiemust register beforefastifySecureSessionso session cookies can be parsed from incoming headers.fastifySecureSessionmust register before theonRequestCSRF hook runs so authenticated user state is accessible.- The CSRF
onRequesthook must run before any route handler or guard executes, ensuring that newly arriving clients immediately receive their CSRF tokens before submitting mutations.
05One request's journey
From the incoming TLS socket through CSRF verification, rate limiting, and live account standing checks to controller execution and response envelope serialization.
CsrfGuard intercepts all POST, PUT, PATCH, and DELETE requests. It compares the header x-csrf-token with the cookie csrf-token. If they do not match or are missing, a 403 Forbidden is returned. Only endpoints marked with @SkipCsrf() (such as the Calendly webhook) are exempted.
FastifyThrottlerGuard tracks sliding-window request volume in memory. In development, rate limiting is disabled unless APP_ENV=production or NODE_ENV=production.
CookieAuthGuard extracts the user payload from request.session.get('user'). It does not blindly trust the session payload; instead, it checks UserStatusCache. If the user was banned or deactivated since their cookie was issued, the request is immediately rejected with 403 Forbidden.
RolesGuard reads metadata declared via @Roles(UserRolesEnum.DOCTOR). If the user lacks the role and is not an Administrator, access is denied. Admins bypass clinical role restrictions.
ResponseTemplateInterceptor transforms all returned values into:
{ status: true, message: 'Success', contents: data }. If an unhandled exception occurs, ExceptionTemplateFilter formats the error into a matching structure with status: false.
Inspect the live user status verification logic auth
From server/src/auth/guards/cookie-auth.guard.ts:
06Concurrency model
Node.js event loop handles concurrent Fastify HTTP requests, persistent WebSocket rooms, and streaming SSE sockets. Prisma transactions and memory caches coordinate shared state.
When an admin bans a user via PATCH /admin/users/:id/ban, UserStatusCache.invalidate(id) triggers gateway listeners to disconnect all open sockets immediately.
Consultation status updates use atomic conditional updates (where: { id, status: expectedStatus }) to prevent race conditions during concurrent review or payment.
Ban commands immediately update the database row and call UserStatusCache.invalidate(userId). Active WebSocket listeners in ChatGateway and MatchingGateway receive the event and forcefully sever client sockets.
ConsultationService updates consultation records inside Prisma transactions using exact matching on current state. If another worker advanced the record, the update matches 0 rows and rejects the mutation.
What may run concurrently for a single patient? concurrency
A patient can simultaneously maintain an SSE connection for AI triage, an active WebSocket room on /chat, and submit an appointment booking over REST. The system maintains isolation via:
- Independent socket IDs mapped to distinct user session rooms.
- Fastify session reading without locking or blocking the database connection pool.
- Unique Prisma indexes preventing duplicate consultations or double-booked calendar slots.
07State: the database and the cache
PostgreSQL holds 23 relational models through Prisma 6. Ephemeral caches and WebSocket rooms maintain live user presence and throttler tallies.
Click any model group to open its detailed field specifications, foreign keys, and indexes directly from the Prisma schema:
3 linear SQL migrations managed via Prisma Migrate. Click to view production deployment safeguards.
Patients only query their own consultations. Doctors only access their assigned patients. Click to inspect access filters.
All consultation fees and payments use Prisma Decimal types in PostgreSQL to eliminate floating-point inaccuracies.
Migrations in server/prisma/migrations/ execute sequentially. In production, Docker containers execute npx prisma migrate deploy inside the container entrypoint before spawning the application process.
Multi-tenancy isolation is enforced at the service layer:
- SoapService verifies ownership before returning clinical SOAP records.
- ConsultationService rejects queries where the calling user is neither participant nor administrator.
Monetary totals (e.g. DoctorProfile.consultationFee, Payment.amount) use the arbitrary-precision numeric SQL type (Decimal in Prisma). Floating-point conversions are prohibited in checkout calculation routines.
Lose the in-memory cache entirely — what happens? failover
If the Node.js process crashes or memory is cleared:
- UserStatusCache repopulates from PostgreSQL on the next request for that user ID.
- Socket rooms reconnect automatically via Socket.IO client retry mechanisms and rejoin
chat:<id>rooms. - Guest AI conversations resume seamlessly if within the 30-minute window because Botpress maintains external conversation state.
08Patterns you'll reuse
Core architectural patterns recurring throughout HTTP controllers, guards, filters, and WebSocket gateways.
Controllers extract the verified user context using @CurrentUser() after CookieAuthGuard validates live status:
All controller return values are automatically formatted into a consistent response envelope:
Server-Sent Events for AI chat stream real-time tokens using Fastify's socket hijacking:
WebSocket gateways share broadcasting logic with REST fallback controllers:
WebSockets enforce per-user sliding-window rate limiting to prevent spam:
Worked example: Fastify socket hijack versus NestJS interceptors lifecycle
Standard NestJS interceptors wait for an Observable stream or Promise to complete before wrapping the result in the HTTP envelope. For long-running Server-Sent Event (SSE) connections, this buffering model breaks streaming because the client receives nothing until the stream closes.
By executing reply.hijack() in AiAgentsController.stream, the controller removes Fastify's default response completion listeners. The application obtains raw TCP write privileges directly on reply.raw, enabling immediate chunk flushing while bypassing interceptor transformation.
WebSocket connection authentication and session cookie decryption security
Socket.IO handshakes pass the browser cookie jar in client.handshake.headers.cookie. In ChatGateway, incoming socket connections parse the session cookie, decrypt the authenticated user payload using the shared session secret, and reject unauthenticated connections before allocating socket rooms.
09Endpoints and the registry
The complete API surface comprises 21 controllers and 2 WebSocket gateways. Filter through the live endpoint registry below.
Session-based login, registration, password hashing, avatar upload, and user search. Click for endpoint details.
Botpress conversation lifecycle, SSE token streaming, and clinical SOAP records. Click for endpoint details.
8-state consultations, doctor decisions, slot booking, and Calendly sync. Click for endpoint details.
Socket.IO messaging on /chat, typing events, and unread notifications. Click for endpoint details.
Anchored by AuthController and PatientController:
POST /auth/register: Argon2 password hashing, creates user record, initializes session.POST /auth/login: Verifies credentials, sets encrypted Fastify session cookie.POST /auth/logout: Destroys session and clears client cookie.GET /patient/profile: Fetches demographics, emergency contact, and medical history.PUT /patient/profile: Updates patient records with payload validation.
Managed by AiAgentsController and SoapService:
POST /ai-agents/start: Begins conversational triage session with Botpress Cloud.POST /ai-agents/send: Sends patient symptom message to triage agent.GET /ai-agents/stream/:conversationId: Raw SSE socket pushing assistant response tokens.GET /soap/:id: Retrieves structured SOAP note with clinical RBAC enforcement.
Operated by ConsultationController and DoctorController:
POST /consultation: Initiates new care record linked to triage SOAP note.PATCH /consultation/:id/decide: Doctor submits clinical decision and mode.PATCH /consultation/:id/advance-to-payment: Advances consultation to fee settlement.PATCH /consultation/:id/confirm-payment: Confirms consultation fee.PATCH /consultation/:id/complete: Terminal completion transition.
Governed by ChatGateway and ChatService:
POST /chat: REST creation fallback validating doctor-patient relationship.GET /chat: Lists active chat conversations and unread counts.WS /chat:join: Joins broadcast and presence rooms.WS /chat:send: Transmits message with rate-limiting and room broadcasts.WS /chat:read: Marks conversation messages as read and updates unread badge.
| Route / Socket Event | Method / Handler | File | Description |
|---|
10Onboarding and the profile
From initial registration through role-specific clinical qualification, document verification, and availability configuration.
Patient or Doctor signs up with email and password via POST /auth/register. Argon2 hashes the password; Fastify session cookie is issued.
Patients fill demographics and emergency contacts. Doctors submit license numbers, consultation fees, and medical specialties via POST /doctor.
Doctors upload medical licenses and certification PDFs via POST /doctor/documents. Files pass magic-byte MIME validation.
Platform administrators inspect credentials via GET /admin/doctors/pending and verify the doctor via PATCH /admin/doctors/:id/verify.
Verified doctors (or permitted nurses) define weekly working hours via POST /scheduling/availability to become available for patient matching.
Input validation and sanitization rules validation
Incoming request payloads are rigorously validated by class-validator:
- Email addresses are normalized to lowercase and validated against RFC 5322.
- Doctor fees must be non-negative decimals.
- Uploaded documents are restricted to PDF, JPEG, PNG, and WebP, capped at 10 MB.
11AI Triage & Streaming SSE
Patients converse with Botpress AI to record clinical history. Tokens and completion triggers stream back over an unbuffered SSE socket.
Tokens stream as they are generated by Botpress, avoiding polling delays and buffering issues. Click for architecture.
Unauthenticated patients can chat in guest mode; guest conversations are retained in memory and evicted after 30 minutes.
When the AI finishes clinical synthesis, a soap_ready SSE event prompts the client to proceed to doctor matching.
AiAgentsController.stream leverages native Fastify socket hijacking. By bypassing NestJS HTTP interceptors, chunks flush over TCP immediately without buffer aggregation.
Guests can conduct full clinical triage before registering. In BotpressService, guest contexts are indexed by a cryptographic UUID token and stored with an explicit TTL.
When Botpress outputs the terminal ***SOAP*** block, the parser automatically persists the structured record and dispatches a soap_ready SSE payload carrying the generated record ID.
Guest conversation eviction and memory bounds cleanup
To prevent unbounded memory growth from abandoned guest sessions, BotpressService.pruneGuestContexts executes every 10 minutes, purging guest contexts whose last activity exceeds 30 minutes.
12SOAP Generation & Extraction
The triage bot wraps synthesized clinical notes in special delimiters, which the server parses into structured database records.
Splits clinical notes into Subjective, Objective, Assessment, and Plan subsections. Click for parser mechanics.
Persisted in patient_soaps with foreign key to patient and optional consultation link. Click for schema details.
Strict RBAC restricts access to patient, assigned doctor, permitted nurse, and auditor. Click for access rules.
The parser targets ***SOAP*** bounding markers. Each section is parsed with case-insensitive regular expressions, capturing multiline clinical observations while extracting recommended triage urgency levels.
Stored in PostgreSQL via the PatientSOAP model. Each record stores triageLevel, suggestedSpecialty, narrative summaries, and references the initiating patient ID.
SoapService.getById validates caller credentials against ownership, assignment, and nurse delegation scopes before returning medical text.
Clinical access control rules for SOAP records security
SoapService.getById enforces strict medical record privacy:
- The owning patient may always read their own SOAP notes.
- A doctor may read the note only if it is attached to a consultation assigned to that doctor.
- A nurse may read the note only if they hold the
VIEW_SOAPSpermission for that attending doctor. - Platform administrators possess audit access for legal compliance.
13Doctor-Patient Matching Engine
A weighted multi-criteria ranking algorithm scores candidate doctors and dispatches real-time timed offers via WebSockets.
Highest weight awarded when doctor specialties match the SOAP note's suggested specialty.
Normalized patient review ratings (1.0 to 5.0 stars) across completed consultations.
Prioritizes doctors with open UTC booking slots within the next 48 hours.
Years in active practice and total verified patient consultations handled.
Evaluates whether the candidate doctor's verified medical specialties intersect with the suggested specialty extracted from the patient's triage SOAP note.
Doctors begin with a default baseline rating of 5.0. Scores scale linearly based on patient reviews upon consultation completion.
Queries doctor availability slots in the next 48 UTC hours. Candidates with open slots in the immediate window receive full availability marks.
Normalized against verified years of clinical practice, rewarding senior practitioners for complex triage assessments.
Matching offer timeout and rollover handling matching
When an offer is dispatched, MatchingGateway starts an active 5-minute timer. If the doctor does not accept within 300 seconds, the background sweeper executes MatchingService.rejectMatch, logs a rejection, and immediately dispatches the offer to the second-highest ranked candidate.
14Consultation Lifecycle & State Machine
Care delivery advances through an 8-state finite state machine with strict transition guards and optimistic concurrency controls.
| Current State | Allowed Target States | Authorized Actor |
|---|---|---|
CREATED | PENDING_DOCTOR_REVIEW, CANCELLED | Patient / System |
PENDING_DOCTOR_REVIEW | DOCTOR_DECIDED, CANCELLED | Doctor |
DOCTOR_DECIDED | PENDING_PAYMENT, CANCELLED | Doctor / Patient |
PENDING_PAYMENT | PAYMENT_CONFIRMED, CANCELLED | Patient / Admin |
PAYMENT_CONFIRMED | IN_PROGRESS, CANCELLED | Doctor |
IN_PROGRESS | COMPLETED, CANCELLED | Doctor |
COMPLETED | (Terminal) | None |
CANCELLED | (Terminal) | None |
Doctor decision modalities and concurrency guards concurrency
When a doctor reviews a consultation via doctorDecide, they choose between three modalities: ASYNC (written notes), ONLINE (video/chat appointment), or IN_PERSON. The update query verifies that the consultation remains in PENDING_DOCTOR_REVIEW, preventing concurrent race conditions.
15Real-time H2H Chat & Presence
Direct human-to-human doctor/patient messaging over WebSocket namespace /chat with chat-open room suppression.
Patients may initiate chats with any doctor. Doctors may only initiate chats with patients who have an active consultation or appointment.
Users actively viewing a chat join chat-open:<id>. Senders broadcasting to the room bypass external email/push notifications for active viewers.
Enforced by ChatService.validateChatRules: To prevent unprompted outreach or harassment, medical practitioners cannot arbitrarily message patients without an active consultation record.
The gateway tracks active room presence. When an active message arrives, users inside chat-open:<id> receive the WebSocket event directly, while absent participants trigger background push notifications.
Heartbeats, typing indicators, and unread counters presence
Clients emit ephemeral chat:typing events with a 3-second debounce. Unread message badges are calculated dynamically via database queries counting unread messages authored by other participants.
16Doctor Scheduling & Calendly Integration
Weekly shifts and slot duration arithmetic generate bookable UTC appointment slots, synchronized with Calendly v2 webhooks.
Doctors define weekly shifts by day of week with UTC start and end minute boundaries.
Configurable consultation length (e.g. 30 min) plus changeover buffer (e.g. 10 min).
Syncs external bookings and cancellations via HMAC-SHA256 signature verified webhooks.
Shifts are saved in SchedulingService.setAvailability. Times are converted to whole minutes from midnight (0 to 1440) for rapid arithmetic.
Slots are derived dynamically on query. The generator steps through each shift by durationMinutes + bufferMinutes, excluding existing appointments.
Incoming Calendly booking events create mirrored appointment records in PostgreSQL, reserving the corresponding internal time slot.
Why all scheduling instants are strictly stored as UTC timezone
To avoid daylight-saving anomalies and cross-timezone scheduling confusion:
- Doctors configure availability shifts with explicit start and end times interpreted in UTC.
- Slot generation divides shifts by slot duration + buffer minutes in whole UTC minutes.
- Client date pickers format display labels with explicit "(UTC)" tags.
17Nurse Delegation & RBAC Permissions
Doctors delegate clinical and administrative authority through accepted invitations, governed by granular permission bitmasks.
Doctors send invitations via POST /nurse/assign. No privileges are granted until the candidate accepts.
Accepting an invite converts the candidate's account to NURSE role and invalidates the user cache in one transaction.
Six granular flags: VIEW_PATIENTS, VIEW_SOAPS, CHAT_WITH_PATIENTS, MANAGE_SCHEDULE, MANAGE_APPOINTMENTS, VIEW_CONSULTATION_NOTES.
Initiated via NurseService.assignNurse. Creates a DoctorNurseAssignment record with status PENDING.
When the invitee accepts via NurseService.acceptInvitation, a Prisma transaction updates the user role to NURSE and activates the assignment.
Permissions are checked at runtime via NurseService.assertNursePermission, ensuring delegated nurses only access authorized medical records.
| Scope Flag | Operations Allowed | Clinical Invariant |
|---|---|---|
VIEW_PATIENTS | Read patient demographics | Restricted to attending doctor's patients. |
VIEW_SOAPS | Inspect clinical SOAP notes | Cannot edit or author new SOAP notes. |
CHAT_WITH_PATIENTS | Send chat messages on doctor's behalf | Messages are logged under the nurse's sender ID. |
MANAGE_SCHEDULE | Create/update availability shifts | Cannot exceed doctor's declared working hours. |
MANAGE_APPOINTMENTS | Book or cancel patient appointments | Requires active payment confirmation. |
VIEW_CONSULTATION_NOTES | Read post-consultation doctor notes | Audit trail records all nurse note inspections. |
18The map and its laws
Where clinical liability meets financial transactions: the consultation lifecycle from triage handoff to fee settlement and calendar booking.
Consultations cannot be initiated anonymously. The patient must possess an authenticated account, a verified email, and link an existing PatientSOAP record generated during AI triage.
Doctors cannot be bypassed. The assigned doctor must explicitly review the patient's symptoms and submit their clinical decision via PATCH /consultation/:id/decide before payment is accepted.
Payments are tracked through Payment records. All fees use decimal precision, and status changes are recorded idempotently.
Appointment booking requires confirmed payment. The booking engine locks the UTC time slot to prevent double-booking across concurrent patient requests.
No consultation may exist without an active patient account and an attached clinical SOAP note or triage narrative.
No doctor calendar slot or video appointment may be confirmed until the consultation fee is confirmed.
Consultation status updates use atomic compare-and-swap (CAS) transitions. States never regress backward.
How atomic compare-and-swap (CAS) transitions prevent double-transitions transactions
When advancing state in ConsultationService, update queries specify both the record ID and the expected prerequisite status:
If two concurrent requests attempt to transition the same consultation simultaneously, exactly one updates the row while the other fails with zero rows updated.
19Its foundation
The four Prisma models anchoring the care pipeline and their database integrity constraints.
| Model | Primary Key | Foreign Keys | Critical Invariant |
|---|---|---|---|
Consultation | id: String (cuid) | patientId, doctorId, soapId | Status updates validate against ALLOWED_TRANSITIONS matrix. |
Payment | id: String (cuid) | userId, consultationId | Unique 1-to-1 relation with Consultation; Decimal amounts. |
Appointment | id: String (cuid) | consultationId, patientId, doctorId | Time range enforced in UTC; unique constraint on doctor + slot. |
DoctorAvailability | id: String (cuid) | doctorId | Weekly recurring day-of-week slots with start/end time validation. |
URL-friendly, collision-resistant string IDs generated via Prisma cuid() prevent enumeration attacks.
Fees are mapped to PostgreSQL Decimal types, avoiding IEEE 754 floating-point rounding errors.
Unlike sequential auto-incrementing integers, CUIDs provide cryptographic pseudo-randomness and time clustering. This protects sensitive consultation IDs in URL paths from enumeration by unauthorized callers.
In Payment and Consultation, fee amounts use Decimal(10, 2) in PostgreSQL and Prisma's Decimal type in TypeScript, guaranteeing exact currency arithmetic without rounding drift.
PostgreSQL constraint guarantees vs application-level guards integrity
The database enforces referential integrity through foreign key constraints with ON DELETE RESTRICT on consultations and payments. Medical records can never be orphaned or deleted while dependent financial transactions exist.
20The outside systems it talks to
Integration boundaries for Calendly scheduling, mock payment settlement, and Botpress AI triage.
Calendly syncs appointment booking and cancellations via HMAC-SHA256 signature-verified webhooks:
Payment operations interface through PaymentService. In production, this wraps external gateways (e.g. Stripe); currently provided as a stub provider with patient self-confirmation.
Botpress Chat SDK (@botpress/chat) provides conversational symptom collection and generates tagged clinical SOAP notes.
Webhook replay attack defense and clock tolerance security
verifyWebhookSignature enforces a 180-second clock-skew tolerance window. Any webhook payload whose signed timestamp differs from local server time by more than 3 minutes is rejected, protecting against captured replay attacks.
21How it computes
Consultation fee calculations, doctor rating moving averages, and UTC booking slot division.
Increments start minutes by duration + buffer, verifying each slot against booked appointment sets.
Consultations capture the doctor's active fee at creation time, preserving historical pricing.
Executed by SchedulingService.getAvailableSlots. Existing bookings for the day are pre-fetched into a Set, allowing O(1) conflict detection per generated time window.
If a doctor subsequently updates their consultation fee in their profile, existing pending or completed consultations retain their original snapshot fee, preventing retro-active price inflation.
Worked example of consultation fee checkout math math
If Dr. Smith sets a consultation fee of $120.00:
- The consultation record initializes with fee $120.00 in state
PENDING_PAYMENT. - A payment record is created referencing the consultation and user ID.
- Upon successful confirmation, the status transitions to
PAYMENT_CONFIRMED, and a single payment notification is dispatched.
22The main operation, start to finish
The complete clinical transaction from triage note to doctor review, fee payment, and visit completion.
Patient selects matched doctor and submits SOAP note ID: POST /consultation. Initial state: PENDING_DOCTOR_REVIEW.
Doctor reviews symptoms and decides modality (ASYNC, ONLINE, IN_PERSON) via PATCH /consultation/:id/decide. Advances to DOCTOR_DECIDED.
Consultation advances to PENDING_PAYMENT; patient confirms fee payment. Status becomes PAYMENT_CONFIRMED.
Patient selects an open UTC slot on the doctor's calendar. Appointment record created and synced with Calendly.
Doctor completes consultation via PATCH /consultation/:id/complete. Terminal state: COMPLETED; unlocks patient review.
| Refusal Trigger | HTTP Status | Error Message |
|---|---|---|
| Missing SOAP note | 400 Bad Request | A valid SOAP note must be attached to the consultation. |
| Unverified Doctor | 403 Forbidden | Doctor profile is pending administrative verification. |
| Invalid State Jump | 400 Bad Request | Invalid consultation state transition. |
| Double Booking | 409 Conflict | The requested appointment slot has already been reserved. |
Refusal reasons and client error handling refusals
Each refusal returns a structured exception envelope containing an explicit error code and remediation action for the user interface, preventing ambiguous form states.
23Reconciliation and the state machine
Visualizing the 8 consultation states, permitted transitions, and error recovery paths.
Unpaid consultations in PENDING_PAYMENT automatically expire after 24 hours, releasing reserved provisional holds.
Optimistic concurrency checks reject simultaneous doctor decision and patient cancellation attempts.
If a patient fails to settle the consultation fee within 24 hours of doctor approval, a background cron sweep cancels the consultation and alerts both parties.
Prisma transactions compare the consultation status in the where clause before writing, ensuring only one conflicting mutation commits.
State machine transition invariant checks in validateTransition invariants
validateTransition checks an immutable dictionary of permitted forward state edges. Any transition from a terminal state (COMPLETED or CANCELLED) immediately throws an unhandled BadRequestException.
24Its supporting mechanism
Security bypasses, webhook signature verification, and raw buffer capture.
Fastify plugin preserves unaltered request body buffers for bit-for-bit cryptographic signature hashing.
Targeted decorator bypasses double-submit cookie checks solely for webhook endpoints using HMAC verification.
If JSON middleware parses and re-stringifies request bodies, key ordering or whitespace differences invalidate the HMAC hash. Fastify attaches the exact bytes received over the wire to req.rawBody.
@SkipCsrf sets custom metadata read by CsrfGuard, exempting third-party webhooks that do not carry browser cookies.
Why raw body capture is critical for cryptographic HMAC verification cryptography
HMAC-SHA256 signatures are computed by Calendly over the raw bytes sent in the HTTP payload. Even a single transformed newline or escaped Unicode character causes the computed digest to diverge, causing valid webhooks to be rejected.
25The reverse operation, and its screens
Orderly cancellation, slot release, fee refund flags, and participant notifications.
Any non-terminal consultation can be cancelled by patient, doctor, or admin. Associated appointments transition to CANCELLED.
Cancelled appointment slots are immediately unlocked in the doctor's calendar, allowing other patients to book the vacated time.
Executed by ConsultationService.cancel. Validates authorization, flags the payment record for refund settlement, and updates consultation status to CANCELLED.
In SchedulingService.cancelAppointment, appointment status updates to CANCELLED. Subsequent availability queries immediately treat that time window as open.
Handling appointment cancellation when synchronized with external Calendly calendar calendly
When an appointment is cancelled locally, the server invokes Calendly's cancellation endpoint using the stored external event URI. If Calendly is unreachable, the local database transaction completes and marks the Calendly sync state as pending retry.
26Configuration and settings
Centralized environment loading, strict validation schemas, and runtime configuration defaults.
| Key | Required | Default | Description |
|---|---|---|---|
DATABASE_URL | Yes | — | PostgreSQL connection URI with pooled credentials. |
APP_PORT | No | 8080 | HTTP port bound by Fastify server. |
APP_ENV | No | development | Environment switch controlling rate-limiting and debug flags. |
CORS_ORIGIN | No | http://localhost:5173 | Allowed frontend origins for credentialed cookies. |
| Key | Required | Description |
|---|---|---|
BOTPRESS_WEBHOOK_ID | No | Webhook ID for external Botpress AI chat agent. |
CALENDLY_API_KEY | No | Personal access token for Calendly API v2. |
CALENDLY_WEBHOOK_SIGNING_KEY | No | HMAC-SHA256 signing secret for Calendly appointment events. |
STORAGE_TYPE | No | Storage adapter selection: local (default) or s3. |
| Key | Required | Description |
|---|---|---|
SESSION_SECRET | Yes | At least 32 characters; used to derive SHA-256 session encryption key. |
SESSION_COOKIE_NAME | No | Cookie name for session token (default: sid). |
VAPID_PUBLIC_KEY | No | Web Push VAPID public key for browser alerts. |
Strict schema validation halts process launch if any required environment variable is absent or malformed.
Session encryption keys are derived from SESSION_SECRET using SHA-256, ensuring high-entropy session security.
validateEnv transforms and parses process.env against an explicit class-validator schema. Startup fails immediately with an actionable error list if credentials are invalid.
The session secret must be at least 32 characters long. The encryption module derives a 256-bit symmetric key to encrypt and sign cookie payloads before transmission.
How environment validation prevents startup with missing secrets failfast
NestJS executes validateEnv before any module initialization or database connection attempts. If an operator deploys a container without DATABASE_URL or with an insufficient SESSION_SECRET, the container terminates immediately rather than running with insecure fallbacks.
27Observability and errors
Structured request logging without data leakage, unified exception normalization, and health monitoring.
RequestLoggingInterceptor logs HTTP method, path, response status, and duration in ms. Sensitive medical bodies are excluded.
All errors format into a standard error envelope, hiding internal database stack traces in production.
RequestLoggingInterceptor instruments incoming requests using high-resolution performance timers. It records method, route, status code, and latency without logging sensitive patient payloads.
ExceptionTemplateFilter traps all unhandled exceptions, mapping them to uniform JSON envelopes with status code, message, and timestamp while logging the raw stack trace internally.
Error envelope format and production stack trace sanitization errors
In production (APP_ENV=production), database connection strings, SQL query fragments, and internal file paths are completely scrubbed from error responses. The client receives only clean, localized error messages.
28Tests
Over 3,500 automated tests across three tiers: unit tests, server E2E suites, and full-stack integration suites.
| Tier | Location | Runner | Coverage Target |
|---|---|---|---|
| Unit Tests | server/src/**/*.spec.ts | Jest | Isolated domain services, guards, and scoring algorithms. |
| Server E2E | server/test/**/*.e2e-spec.ts | Jest + Supertest | Full Fastify HTTP request lifecycle with database rollback. |
| Integration | client/integration/**/*.test.ts | Vitest | Full-stack client and server execution against test PostgreSQL. |
External dependencies (Botpress, Calendly, S3) provide zero-network in-memory test stubs for deterministic CI runs.
E2E and integration suites run against an ephemeral PostgreSQL container with automated schema migrations.
Testing harnesses inject mock implementations for external APIs. Botpress stubs return predefined clinical SOAP notes without latency or API key requirements.
Each E2E test file truncates or rolls back modified database tables in an afterEach hook, preventing cross-test pollution during parallel execution.
Fastify test injection with app.inject() fastify
E2E integration tests use Fastify's native app.inject() method, routing synthetic HTTP requests directly into the application pipeline without binding real TCP sockets. This executes test suites up to 5x faster than socket-bound Supertest suites.
29Recipes
Step-by-step instructions for implementing the most common engineering tasks.
Recipe 1: Add a new clinical REST endpoint endpoint
- Define request payload in
server/src/<domain>/dto/<name>.dto.tsusing class-validator decorators. - Add business logic method in
server/src/<domain>/<domain>.service.ts. - Add controller method in
server/src/<domain>/<domain>.controller.tswith@UseGuards(CookieAuthGuard, RolesGuard). - Add corresponding unit test in
server/src/<domain>/<domain>.service.spec.ts.
Recipe 2: Add a new database column and Prisma migration database
- Update model in
server/prisma/schema.prisma. - Generate migration:
npx prisma migrate dev --name add_new_field. - Verify migration SQL in
server/prisma/migrations/. - Update affected service DTOs and re-run test suite.
Recipe 3: Add a new WebSocket gateway event websocket
- Add event handler in
server/src/chat/chat.gateway.tsdecorated with@SubscribeMessage('event:name'). - Validate socket payload with class-validator.
- Broadcast to room via
this.server.to(`room:${id}`).emit('event:name', data).
30Invariants and gotchas
Critical system rules that must never be broken during development or maintenance.
Always verify active standing via UserStatusCache to enforce instant bans and deactivations.
All availability and appointments are UTC. Conversions occur solely at presentation boundaries.
The server runs Fastify 5. Express middleware causes fatal startup crashes.
Mutations require matching X-CSRF-Token and csrf-token cookies unless decorated with @SkipCsrf.
UserStatusCache caches active/banned status in process memory. Session cookies are stateless; checking the cache prevents banned users from continuing authorized sessions.
Doctor availability shifts in SchedulingService are calculated from 0 to 1440 UTC minutes. Never apply local daylight-saving offsets in backend storage.
Fastify manages request streams differently than Express. Always use Fastify-native hooks (onRequest, preHandler) and Fastify plugins for cross-cutting concerns.
CsrfGuard enforces double-submit token matching on all state-altering requests (POST, PUT, PATCH, DELETE), protecting authenticated users from cross-site forged transactions.
Security at the boundary, integrity at the core. Guards, filters, and schema validators protect the perimeter; strict database relational constraints protect clinical and financial truth.
31Where it stands; what's next
180 of 196 task milestones completed across Phases 0 through 16. Upcoming phases expand social and calling features.
Peer-to-peer friend requests and contact discovery between users sharing identical clinical roles.
Friendship-based chat initiation enabling doctor-to-doctor and patient-to-patient conversations.
Real-time audio/video consultations via Socket.IO signaling and STUN/TURN media relay servers.
Next architectural milestones and migration path roadmap
Phase 17 introduces friendship edge tables in PostgreSQL. Phase 18 redesigns the chat namespace to support general conversations outside of consultations. Phase 19 adds WebRTC signaling gateways for direct browser video telemedicine.
32The reference atlas
Interactive declaration browser across all 275 source files. Search by name, filter by symbol kind, or navigate the directory tree.
The reference atlas indexes 275 source files (34,072 lines of code excluding test suites), documenting 313 types, 666 functions and methods, and 23 database models extracted via TypeScript compiler AST analysis.
33The data model, as a map
Interactive schema map of all 23 PostgreSQL database models, showing relational foreign keys, live caches, and clinical transaction flows.
Prisma enforces relational integrity directly in PostgreSQL, with cascading deletes on child records (e.g. ChatParticipant on Chat delete).
The ghost node UserStatusCache represents live process memory tracking active accounts and real-time ban states.
Click any flow button above the map to step through live transactional sequences and highlight participating tables.
Reading the map in two minutes: an ordered tour of the schema tour
The schema centers around User in the center column. Triage and medical history radiate to the left through PatientSOAP. Clinical consultations, appointments, and payments flow downward through Consultation. Communication and doctor availability anchor the right-hand zones.
34The type map
Map of the core services, guards, and controllers driving runtime execution. Function links open declaration sources directly in the atlas.
NestJS registers all domain services as singletons within AppModule. Gateways and controllers receive dependencies via constructor injection.
Every function label on edge connections (fn) is clickable, opening the exact source file and declaration in the reference atlas.
Touring the type transformations from socket to store types
Incoming HTTP requests and WebSocket packets enter through edge controllers and gateways, validate against DTO schemas, and pass into domain services before persistence in Prisma models.
35Where the rest is written
Cross-reference index to documentation files stored in the repository root.
Comprehensive architectural distillation, stack components, completed phases, and key architectural decisions.
Task tracking across Phases 0 through 19, tracking 180 completed items and upcoming feature scopes.
Security analysis and deployment caveats S-01 through S-16, covering session handling and pre-launch verification.
Detailed verification records of fixed bugs BUG-01..12 and deployment enhancements INF-01..07.
Original product requirements specification and technical design architecture documentation.
Test harness execution plan, coverage matrix, and full-stack integration verification guidelines.
Document hierarchy and where decisions are tracked documentation
All core architectural decisions are archived in CONTEXT.md. Sprint execution and roadmap items are maintained in TASKS.md. Pre-deployment audits and vulnerability checklists are documented in SECURITY.md.
36What Botpress is
A cloud-hosted conversational AI triage engine integrated via @botpress/chat (pinned v0.5.5), collecting patient symptoms and generating structured SOAP notes.
Caches authenticated Botpress client instances per user ID, reusing connections across triage chat turns. Click for details.
Fastify socket hijacking pushes assistant tokens to the browser as Server-Sent Events without intermediate buffering. Click for details.
Unauthenticated patients receive temporary guest conversation IDs, kept in memory and pruned after 30 minutes. Click for details.
Listens for delimiter tags (***SOAP***), parsing clinical sections and auto-titling the triage record. Click for details.
BotpressService manages client instances using user credentials. Instances are cached in memory to eliminate handshake overhead during multi-turn symptom collection.
AiAgentsController.stream attaches a message listener to BotpressService and writes directly to Fastify's raw TCP socket with X-Accel-Buffering: no.
Guest conversations operate without a PostgreSQL user record. Contexts are stored in guestContexts with an expiration timestamp and evicted every 10 minutes by pruneGuestContexts.
When the assistant finishes synthesizing symptoms, it generates a SOAP block. The service regex extracts Subjective, Objective, Assessment, and Plan, saves a record, and emits soap_ready.
Botpress connection pooling and webhook keep-alive performance
To minimize HTTPS handshake latency during rapid patient symptom typing, BotpressService maintains an active HTTP Agent with TCP keep-alive enabled, reusing underlying sockets across consecutive chat turns.
37How the project uses it, and running it
Wiring the Botpress Chat SDK into NestJS modules, test mocking conventions, and operational failure boundaries.
AiAgentsModule registers BotpressService as an injectable singleton provider.
The client requests POST /ai-agents/start; the service calls Botpress API to create or resume a conversation ID.
Incoming user messages are dispatched via BotpressService.send(), triggering AI clinical synthesis.
The listener detects ***SOAP*** tags, extracts clinical sections, and emits soap_ready over the SSE stream.
BotpressClient: SDK instance handling webhook authentication and event listeners.
ConversationContext: Active conversation ID, message buffer, and associated patient ID.
Unit and integration tests mock BotpressService using test stubs that emit predefined symptom prompts and valid tagged SOAP notes, avoiding external network calls during CI test runs.
Common verbs on BotpressService: start(), send(), listen(), pruneGuestContexts().
Requires BOTPRESS_WEBHOOK_ID in the server environment. If unset, the service initializes in stub mode and logs an alert.
Never bypass delimiter validation. Raw AI messages must be checked for ***SOAP*** boundaries before attempting clinical extraction. Unvalidated text must never be inserted into the patient_soaps medical record table.
Handling network dropouts and Botpress API rate limits resilience
If the external Botpress Cloud endpoint returns an HTTP 429 (Too Many Requests) or 503 (Unavailable), BotpressService logs the failure and notifies the patient client over the SSE stream with a friendly retry prompt, preserving the active triage conversation context.
38Check yourself
12 technical questions testing deep architectural understanding of the AI-Clinic codebase. Click each question to reveal its answer and file references.
1. Why does CookieAuthGuard query UserStatusCache instead of relying only on the session cookie?
Session cookies are stateless and valid for 7 days. If an administrator bans an abusive user, fires a doctor, or demotes an admin, relying only on the decrypted cookie would allow them to continue making authenticated requests for up to a week. CookieAuthGuard queries UserStatusCache on every single request, denying access immediately upon ban or deactivation.
2. How does the double-submit CSRF protection work, and which single endpoint is exempted?
A Fastify onRequest hook sets a random 32-byte hex token in a non-HttpOnly cookie named csrf-token. Mutating HTTP requests (POST, PUT, PATCH, DELETE) must return this exact token in the X-CSRF-Token header, verified by CsrfGuard. The only exempted endpoint is POST /calendly/webhook, decorated with @SkipCsrf() because external Calendly webhooks authenticate via HMAC-SHA256 signatures instead.
3. What are the exact scoring weights in the doctor-patient matching algorithm?
Calculated in MatchingService.scoreDoctors: Specialty match contributes 40 points, Patient rating average contributes 25 points, Upcoming availability within 48 hours contributes 20 points, and Years of clinical experience contributes 15 points (Total: 100 points).
4. What happens when a doctor ignores a matching offer for more than 5 minutes?
MatchingGateway arms a 300,000 ms timer, backed by periodic cron sweeps. When 5 minutes elapse, MatchingService.rejectMatch records a rejection, and the matching engine automatically dispatches the offer to the second-highest ranked doctor.
5. Why are all doctor availabilities and appointment bookings stored strictly in UTC?
To eliminate daylight-saving transitions, leap hour ambiguities, and cross-timezone scheduling conflicts between doctors and patients located in different time zones. Whole-day slot queries cover UTC day bounds, and frontend clients format display strings explicitly marked with "(UTC)".
6. How does the server stream real-time AI triage tokens to the browser without buffering?
AiAgentsController.stream executes reply.hijack() on the Fastify reply object, taking direct ownership of the underlying TCP socket. It sets X-Accel-Buffering: no to prevent reverse proxy caching by Nginx and writes Server-Sent Events directly as tokens arrive from Botpress.
7. What prevents a user from arbitrarily declaring themselves a Nurse?
Self-registration only permits the PATIENT (default) or DOCTOR roles. To become a Nurse, an authenticated user must receive an explicit invitation from a verified doctor (NurseService.assignNurse). The candidate must then accept via POST /nurse/invitations/:id/accept, which updates their role to NURSE and activates permissions in a single atomic database transaction.
8. How does the real-time chat gateway avoid sending spam push notifications to active readers?
When a client opens a conversation window, they emit chat:join, entering both the general broadcast room chat:<id> and a presence room chat-open:<id>. In ChatGateway.announceNewMessage, external push notifications and emails are dispatched only to room participants who are not present in chat-open:<id>.
9. How does CalendlyService verify that webhook events are genuine?
CalendlyService.verifyWebhookSignature inspects the calendly-webhook-signature header. It extracts timestamp t and signature v1, rejects requests where |now - t| > 180 seconds, and computes an HMAC-SHA256 hash over t.rawBody, comparing signatures via timingSafeEqual.
10. What prevents race conditions when updating consultation states?
ConsultationService executes compare-and-swap database updates with explicit where: { id, status: expectedCurrentStatus } clauses inside Prisma transactions, preventing duplicate advances if payment confirmations and review decisions overlap.
11. How does the upload service prevent malicious executable files disguised as images?
FileUploadService reads the initial buffer bytes of multipart uploads to inspect file magic signatures (e.g. FF D8 FF for JPEG, 89 50 4E 47 for PNG, 25 50 44 46 for PDF). Extensions are derived solely from validated MIME signatures rather than user-supplied filenames.
12. Why does the server run Fastify 5 instead of Express?
Fastify provides superior HTTP request throughput, built-in schema serialization, native raw buffer handling for HMAC webhooks, and raw socket hijacking for Server-Sent Events streaming, which are essential for high-concurrency telemedicine chats.