AI-Clinic · Telemedicine Platform Architecture Guide

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.

0source files
0lines, no tests
0types & models
0functions & methods
0Prisma tables
0database migrations
0automated tests
Part I · Orientation

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.

①Session 1 · Shape & Runtime

Chapters 02–10: The architectural layers, Fastify bootstrap, request lifecycle, concurrency, and core Prisma persistence. Click for details.

②Session 2 · Domain & Care Engine

Chapters 11–25: Botpress AI triage, clinical SOAP extraction, smart doctor matching, consultation state machines, H2H chat, and the financial pipeline. Click for details.

③Session 3 · Engineering & Verification

Chapters 26–38: Configuration, observability, test tiers, recipes, interactive data and type maps, Botpress SDK companion, and technical quiz. Click for details.

🧭 The fastest entry points into the system

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 Engine
Entrypoint configuring FastifyAdapter, raw body extraction, secure session cookies, double-submit CSRF, and global validation pipes.
▼ registers and mounts
Interface Adapters · REST Controllers & WebSocket Gateways
21 controllers in server/src/*/ handling HTTP routes, SSE streaming for AI chat, and Socket.IO namespaces for /chat and /matching.
▼ delegates business operations
Domain Layer · Clinical & Communication Services
Auth, Botpress AI triage, SOAP parser, doctor matching engine, consultations, Calendly scheduling, nurse delegation, and notifications.
▼ queries and mutates
Data Store · PostgreSQL & Prisma 6
23 Prisma models, 22 enums, 3 migrations, seed scripts, and connection pool managed via PrismaService.
Cross-Cutting · Security, Cache & Infrastructure
In-memory UserStatusCache (60s TTL), CacheService, argon2 password hashing, and multipart magic-byte file validation.
Where the lines are

Package size distribution across the codebase. Click any package bar to inspect its constituent files, declarations, and purposes.

🔒Dependency Direction

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.

🧪Where Tests Live

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.

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.

Part II · Runtime

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.

Step 1Environment ValidationValidates DATABASE_URL and SESSION_SECRET at startup.
Step 2Fastify InitializationCreates HTTP adapter with rawBody capture enabled for Calendly HMAC.
Step 3Security PluginsMounts secure-session, cookie parser, compression, and helmet headers.
Step 4Guards & CSRF HookAdds fastify onRequest hook setting csrf-token and global CsrfGuard.
Step 5Port Binding & SocketsAttaches Socket.IO adapter and listens on 0.0.0.0:8080.
Periodic JobIntervalTarget MethodPurpose
Cache SweeperHourlyCacheService.cleanExpiredPurges expired key-value entries from in-memory cache.
Guest Context PrunerEvery 10 minBotpressService.pruneGuestContextsEvicts unauthenticated AI triage chat contexts older than 30 minutes.
Match Expiry SweepEvery 30 secMatchingGateway.expirySweepFlags unanswered doctor match offers exceeding 5 minutes and re-routes patients.
🛑Signal Trapping & Shutdown

Fastify traps SIGTERM and SIGINT, allowing ongoing requests to complete before closing sockets. Click to inspect shutdown sequence.

🗄Prisma Connection Drainage

PrismaService.onModuleDestroy cleanly terminates the PostgreSQL connection pool without aborting transactions in flight.

Why Fastify plugin registration order matters security

In Fastify, plugin order is strictly hierarchical:

  1. fastifyCookie must register before fastifySecureSession so session cookies can be parsed from incoming headers.
  2. fastifySecureSession must register before the onRequest CSRF hook runs so authenticated user state is accessible.
  3. The CSRF onRequest hook 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.

Layer 1Fastify HookExtracts cookies and validates incoming CSRF token double-submit pair.
Layer 2ThrottlerGuardChecks IP rate limits (100 req/min global; 5 req/min on login routes).
Layer 3CookieAuthGuardDecodes session cookie and queries UserStatusCache for active status.
Layer 4RolesGuardValidates that request.user.role matches required @Roles() annotations.
Layer 5Envelope InterceptorExecutes controller method and serializes contents into standard envelope.
TLS Socket CSRF Check Double-Submit Status Check Live Cache Controller Domain Method Envelope JSON Output
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.

Fastify Event Loop HTTP & SSE Streams Socket.IO Server /chat & /matching UserStatusCache In-Memory 60s TTL Real-time Ban Invalidation Disconnects Active Sockets PostgreSQL & Prisma Connection Pool CAS Status Updates
⚡In-Memory Status Invalidation

When an admin bans a user via PATCH /admin/users/:id/ban, UserStatusCache.invalidate(id) triggers gateway listeners to disconnect all open sockets immediately.

🔒Compare-and-Swap Transitions

Consultation status updates use atomic conditional updates (where: { id, status: expectedStatus }) to prevent race conditions during concurrent review or payment.

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:

📦Migrations Policy

3 linear SQL migrations managed via Prisma Migrate. Click to view production deployment safeguards.

👁Visibility & Tenancy

Patients only query their own consultations. Doctors only access their assigned patients. Click to inspect access filters.

💰Financial Types

All consultation fees and payments use Prisma Decimal types in PostgreSQL to eliminate floating-point inaccuracies.

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.
Part III · The Interface Layer

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.

🔐Auth & Users

Session-based login, registration, password hashing, avatar upload, and user search. Click for endpoint details.

🤖AI Triage & SOAPs

Botpress conversation lifecycle, SSE token streaming, and clinical SOAP records. Click for endpoint details.

🩺Care Delivery

8-state consultations, doctor decisions, slot booking, and Calendly sync. Click for endpoint details.

💬Real-Time Chat

Socket.IO messaging on /chat, typing events, and unread notifications. Click for endpoint details.

Route / Socket EventMethod / HandlerFileDescription

10Onboarding and the profile

From initial registration through role-specific clinical qualification, document verification, and availability configuration.

1
Public Registration

Patient or Doctor signs up with email and password via POST /auth/register. Argon2 hashes the password; Fastify session cookie is issued.

2
Profile Declaration

Patients fill demographics and emergency contacts. Doctors submit license numbers, consultation fees, and medical specialties via POST /doctor.

3
Credential Upload

Doctors upload medical licenses and certification PDFs via POST /doctor/documents. Files pass magic-byte MIME validation.

4
Admin Review & Verification

Platform administrators inspect credentials via GET /admin/doctors/pending and verify the doctor via PATCH /admin/doctors/:id/verify.

5
Weekly Schedule Setup

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.
Part IV · The Domain: Core Engine

11AI Triage & Streaming SSE

Patients converse with Botpress AI to record clinical history. Tokens and completion triggers stream back over an unbuffered SSE socket.

⚡Low Latency SSE

Tokens stream as they are generated by Botpress, avoiding polling delays and buffering issues. Click for architecture.

👤Guest Mode Support

Unauthenticated patients can chat in guest mode; guest conversations are retained in memory and evicted after 30 minutes.

🔔soap_ready Signal

When the AI finishes clinical synthesis, a soap_ready SSE event prompts the client to proceed to doctor matching.

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.

📑Section Parser

Splits clinical notes into Subjective, Objective, Assessment, and Plan subsections. Click for parser mechanics.

💾Relational Schema

Persisted in patient_soaps with foreign key to patient and optional consultation link. Click for schema details.

🛡Privacy Controls

Strict RBAC restricts access to patient, assigned doctor, permitted nurse, and auditor. Click for access rules.

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_SOAPS permission 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.

🎯Specialty Match (40%)

Highest weight awarded when doctor specialties match the SOAP note's suggested specialty.

⭐Rating Score (25%)

Normalized patient review ratings (1.0 to 5.0 stars) across completed consultations.

📅Availability (20%)

Prioritizes doctors with open UTC booking slots within the next 48 hours.

💼Experience (15%)

Years in active practice and total verified patient consultations handled.

Patient Request Score & Dispatch Weights: 40/25/20/15 Offer to Top Doctor 5-Min Timeout Timer Doctor Accepts Timeout / Next Doctor
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.

Part V · The Domain: Consultations & Care Delivery

14Consultation Lifecycle & State Machine

Care delivery advances through an 8-state finite state machine with strict transition guards and optimistic concurrency controls.

CREATED→ PENDING_REVIEW→ DOCTOR_DECIDED→ PENDING_PAYMENT→ PAYMENT_CONFIRMED→ IN_PROGRESS→ COMPLETED
Current StateAllowed Target StatesAuthorized Actor
CREATEDPENDING_DOCTOR_REVIEW, CANCELLEDPatient / System
PENDING_DOCTOR_REVIEWDOCTOR_DECIDED, CANCELLEDDoctor
DOCTOR_DECIDEDPENDING_PAYMENT, CANCELLEDDoctor / Patient
PENDING_PAYMENTPAYMENT_CONFIRMED, CANCELLEDPatient / Admin
PAYMENT_CONFIRMEDIN_PROGRESS, CANCELLEDDoctor
IN_PROGRESSCOMPLETED, CANCELLEDDoctor
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.

💬Chat Initiation Rules

Patients may initiate chats with any doctor. Doctors may only initiate chats with patients who have an active consultation or appointment.

🔕Notification Suppression

Users actively viewing a chat join chat-open:<id>. Senders broadcasting to the room bypass external email/push notifications for active viewers.

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.

📅Weekly Recurring Shifts

Doctors define weekly shifts by day of week with UTC start and end minute boundaries.

⏱Duration & Buffer

Configurable consultation length (e.g. 30 min) plus changeover buffer (e.g. 10 min).

🔗Calendly v2 Webhooks

Syncs external bookings and cancellations via HMAC-SHA256 signature verified webhooks.

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.

✉️Pending Invitations

Doctors send invitations via POST /nurse/assign. No privileges are granted until the candidate accepts.

🔄Atomic Promotion

Accepting an invite converts the candidate's account to NURSE role and invalidates the user cache in one transaction.

🛡Permission Scopes

Six granular flags: VIEW_PATIENTS, VIEW_SOAPS, CHAT_WITH_PATIENTS, MANAGE_SCHEDULE, MANAGE_APPOINTMENTS, VIEW_CONSULTATION_NOTES.

Scope FlagOperations AllowedClinical Invariant
VIEW_PATIENTSRead patient demographicsRestricted to attending doctor's patients.
VIEW_SOAPSInspect clinical SOAP notesCannot edit or author new SOAP notes.
CHAT_WITH_PATIENTSSend chat messages on doctor's behalfMessages are logged under the nurse's sender ID.
MANAGE_SCHEDULECreate/update availability shiftsCannot exceed doctor's declared working hours.
MANAGE_APPOINTMENTSBook or cancel patient appointmentsRequires active payment confirmation.
VIEW_CONSULTATION_NOTESRead post-consultation doctor notesAudit trail records all nurse note inspections.
Part VI · The Riskiest Subsystem: Consultations, Booking & Payments

18The map and its laws

Where clinical liability meets financial transactions: the consultation lifecycle from triage handoff to fee settlement and calendar booking.

Level 1 · Client Initiation
Patient completes triage; chooses doctor or accepts smart match offer.
▼ creates consultation
Level 2 · Clinical Review & Decision
Doctor inspects SOAP note and selects consultation mode (ASYNC, ONLINE, IN_PERSON).
▼ authorizes payment
Level 3 · Financial Settlement
Fee calculation and payment confirmation transition state to PAYMENT_CONFIRMED.
▼ reserves schedule
Level 4 · Calendar Booking & Calendly Sync
UTC appointment slot reserved and synchronized with Calendly webhook events.
⚖️Law 1 · Clinical Pre-requisite

No consultation may exist without an active patient account and an attached clinical SOAP note or triage narrative.

💳Law 2 · Payment Before Booking

No doctor calendar slot or video appointment may be confirmed until the consultation fee is confirmed.

🔒Law 3 · Monotonic State Transitions

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.

ModelPrimary KeyForeign KeysCritical Invariant
Consultationid: String (cuid)patientId, doctorId, soapIdStatus updates validate against ALLOWED_TRANSITIONS matrix.
Paymentid: String (cuid)userId, consultationIdUnique 1-to-1 relation with Consultation; Decimal amounts.
Appointmentid: String (cuid)consultationId, patientId, doctorIdTime range enforced in UTC; unique constraint on doctor + slot.
DoctorAvailabilityid: String (cuid)doctorIdWeekly recurring day-of-week slots with start/end time validation.
🔑CUID Primary Keys

URL-friendly, collision-resistant string IDs generated via Prisma cuid() prevent enumeration attacks.

💰Decimal Currency Types

Fees are mapped to PostgreSQL Decimal types, avoiding IEEE 754 floating-point rounding errors.

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.

📅Slot Stepping

Increments start minutes by duration + buffer, verifying each slot against booked appointment sets.

💵Fee Inheritance

Consultations capture the doctor's active fee at creation time, preserving historical pricing.

Worked example of consultation fee checkout math math

If Dr. Smith sets a consultation fee of $120.00:

  1. The consultation record initializes with fee $120.00 in state PENDING_PAYMENT.
  2. A payment record is created referencing the consultation and user ID.
  3. 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.

1
Consultation Creation

Patient selects matched doctor and submits SOAP note ID: POST /consultation. Initial state: PENDING_DOCTOR_REVIEW.

2
Doctor Decision

Doctor reviews symptoms and decides modality (ASYNC, ONLINE, IN_PERSON) via PATCH /consultation/:id/decide. Advances to DOCTOR_DECIDED.

3
Payment Settlement

Consultation advances to PENDING_PAYMENT; patient confirms fee payment. Status becomes PAYMENT_CONFIRMED.

4
Appointment Booking

Patient selects an open UTC slot on the doctor's calendar. Appointment record created and synced with Calendly.

5
Visit Completion

Doctor completes consultation via PATCH /consultation/:id/complete. Terminal state: COMPLETED; unlocks patient review.

Refusal TriggerHTTP StatusError Message
Missing SOAP note400 Bad RequestA valid SOAP note must be attached to the consultation.
Unverified Doctor403 ForbiddenDoctor profile is pending administrative verification.
Invalid State Jump400 Bad RequestInvalid consultation state transition.
Double Booking409 ConflictThe 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.

CREATED PENDING_REVIEW DOCTOR_DECIDED PENDING_PAYMENT PAYMENT_CONFIRMED IN_PROGRESS COMPLETED (✓) CANCELLED (✕)
⏰Payment Timeout Recovery

Unpaid consultations in PENDING_PAYMENT automatically expire after 24 hours, releasing reserved provisional holds.

⚔️Concurrent Decision Conflict

Optimistic concurrency checks reject simultaneous doctor decision and patient cancellation attempts.

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.

📦Raw Body Buffering

Fastify plugin preserves unaltered request body buffers for bit-for-bit cryptographic signature hashing.

🛡@SkipCsrf Scoping

Targeted decorator bypasses double-submit cookie checks solely for webhook endpoints using HMAC verification.

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.

🔄Cancellation Trigger

Any non-terminal consultation can be cancelled by patient, doctor, or admin. Associated appointments transition to CANCELLED.

📅Slot Release

Cancelled appointment slots are immediately unlocked in the doctor's calendar, allowing other patients to book the vacated time.

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.

Part VII · Working on It

26Configuration and settings

Centralized environment loading, strict validation schemas, and runtime configuration defaults.

KeyRequiredDefaultDescription
DATABASE_URLYes—PostgreSQL connection URI with pooled credentials.
APP_PORTNo8080HTTP port bound by Fastify server.
APP_ENVNodevelopmentEnvironment switch controlling rate-limiting and debug flags.
CORS_ORIGINNohttp://localhost:5173Allowed frontend origins for credentialed cookies.
KeyRequiredDescription
BOTPRESS_WEBHOOK_IDNoWebhook ID for external Botpress AI chat agent.
CALENDLY_API_KEYNoPersonal access token for Calendly API v2.
CALENDLY_WEBHOOK_SIGNING_KEYNoHMAC-SHA256 signing secret for Calendly appointment events.
STORAGE_TYPENoStorage adapter selection: local (default) or s3.
KeyRequiredDescription
SESSION_SECRETYesAt least 32 characters; used to derive SHA-256 session encryption key.
SESSION_COOKIE_NAMENoCookie name for session token (default: sid).
VAPID_PUBLIC_KEYNoWeb Push VAPID public key for browser alerts.
⚙️Schema Validation

Strict schema validation halts process launch if any required environment variable is absent or malformed.

🔐Cryptographic Derivation

Session encryption keys are derived from SESSION_SECRET using SHA-256, ensuring high-entropy session security.

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.

📝Zero-Leak Request Logging

RequestLoggingInterceptor logs HTTP method, path, response status, and duration in ms. Sensitive medical bodies are excluded.

🛡Unified Exception Filter

All errors format into a standard error envelope, hiding internal database stack traces in production.

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.

TierLocationRunnerCoverage Target
Unit Testsserver/src/**/*.spec.tsJestIsolated domain services, guards, and scoring algorithms.
Server E2Eserver/test/**/*.e2e-spec.tsJest + SupertestFull Fastify HTTP request lifecycle with database rollback.
Integrationclient/integration/**/*.test.tsVitestFull-stack client and server execution against test PostgreSQL.
🧪Service Test Fakes

External dependencies (Botpress, Calendly, S3) provide zero-network in-memory test stubs for deterministic CI runs.

🗄Database Isolation

E2E and integration suites run against an ephemeral PostgreSQL container with automated schema migrations.

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
  1. Define request payload in server/src/<domain>/dto/<name>.dto.ts using class-validator decorators.
  2. Add business logic method in server/src/<domain>/<domain>.service.ts.
  3. Add controller method in server/src/<domain>/<domain>.controller.ts with @UseGuards(CookieAuthGuard, RolesGuard).
  4. Add corresponding unit test in server/src/<domain>/<domain>.service.spec.ts.
Recipe 2: Add a new database column and Prisma migration database
  1. Update model in server/prisma/schema.prisma.
  2. Generate migration: npx prisma migrate dev --name add_new_field.
  3. Verify migration SQL in server/prisma/migrations/.
  4. Update affected service DTOs and re-run test suite.
Recipe 3: Add a new WebSocket gateway event websocket
  1. Add event handler in server/src/chat/chat.gateway.ts decorated with @SubscribeMessage('event:name').
  2. Validate socket payload with class-validator.
  3. 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.

⚠️UserStatusCache

Always verify active standing via UserStatusCache to enforce instant bans and deactivations.

🌐Strict UTC Instants

All availability and appointments are UTC. Conversions occur solely at presentation boundaries.

🛡Fastify Plugins

The server runs Fastify 5. Express middleware causes fatal startup crashes.

🔑CSRF on Mutations

Mutations require matching X-CSRF-Token and csrf-token cookies unless decorated with @SkipCsrf.

💎 Core Principle

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.

Phases 0–16 Completed (180 Items)→ Phase 17 · Friend System (Next)→ Phase 18 · H2H Chat Redesign→ Phase 19 · WebRTC Calls
👥Phase 17 · Friend System

Peer-to-peer friend requests and contact discovery between users sharing identical clinical roles.

💬Phase 18 · H2H Chat Redesign

Friendship-based chat initiation enabling doctor-to-doctor and patient-to-patient conversations.

📞Phase 19 · Calls & WebRTC

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.

Part VIII · Reference

32The reference atlas

Interactive declaration browser across all 275 source files. Search by name, filter by symbol kind, or navigate the directory tree.

💡 Atlas Coverage

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.

🔗Foreign Key Constraints

Prisma enforces relational integrity directly in PostgreSQL, with cascading deletes on child records (e.g. ChatParticipant on Chat delete).

⚡In-Memory State

The ghost node UserStatusCache represents live process memory tracking active accounts and real-time ban states.

🔄Interactive Flows

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.

🧱Dependency Injection

NestJS registers all domain services as singletons within AppModule. Gateways and controllers receive dependencies via constructor injection.

🔌Clickable Method Doors

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.

📄CONTEXT.md

Comprehensive architectural distillation, stack components, completed phases, and key architectural decisions.

📋TASKS.md

Task tracking across Phases 0 through 19, tracking 180 completed items and upcoming feature scopes.

🛡SECURITY.md

Security analysis and deployment caveats S-01 through S-16, covering session handling and pre-launch verification.

🐞BUG-REPORT-FIXED.md

Detailed verification records of fixed bugs BUG-01..12 and deployment enhancements INF-01..07.

📑PRD.md & TDD.md

Original product requirements specification and technical design architecture documentation.

🧪INTEGRATION-TESTS-PLAN.md

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.

Part IX · Wrapping Up: Companion — Botpress AI

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.

Patient Browser AI-Clinic Fastify Backend AiAgentsController & SSE Stream BotpressService Adapter SOAP Delimiter Parser Botpress Cloud API @botpress/chat v0.5.5
🔌Client Session Adapter

Caches authenticated Botpress client instances per user ID, reusing connections across triage chat turns. Click for details.

📡SSE Stream Relaying

Fastify socket hijacking pushes assistant tokens to the browser as Server-Sent Events without intermediate buffering. Click for details.

👤Ephemeral Guest Triage

Unauthenticated patients receive temporary guest conversation IDs, kept in memory and pruned after 30 minutes. Click for details.

📝SOAP Note Extraction

Listens for delimiter tags (***SOAP***), parsing clinical sections and auto-titling the triage record. Click for details.

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.

1
Module Registration

AiAgentsModule registers BotpressService as an injectable singleton provider.

2
Conversation Initialization

The client requests POST /ai-agents/start; the service calls Botpress API to create or resume a conversation ID.

3
Message Dispatch

Incoming user messages are dispatched via BotpressService.send(), triggering AI clinical synthesis.

4
SOAP Synthesis & Event Emission

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.

🚨 Critical Invariant

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.

AI-Clinic codebase guide · October 2026 · Built against commit 6ec2568 · Rebuild at every docs update