# Technical Architecture — Milestone 1
## AI-Guided Cardio Thoracic Surgery Patient Intake System

**Version:** 3.0
**Date:** 2026-05-12
**Status:** Draft

---

## 1. Architecture Overview

The system is composed of three runtime services communicating over a private Docker network. The frontend is a React PWA served over HTTPS. FastAPI is the single backend — it handles auth, business logic, session management, database writes, and all Claude API calls. Audio transcription happens entirely in the browser via Web Speech API — no audio is ever sent to the backend.

```mermaid
graph TD
    CLIENT["React PWA\n(Tablet / Mobile / Laptop)\nWeb Speech API — transcription in browser"]

    subgraph Docker Network
        FAST["FastAPI\n(Port 8000)\nAuth · Sessions · Business Logic · DB · Claude API"]
        PG["PostgreSQL\n(Port 5432)\nStructured Data"]
        RD["Redis\n(Port 6379)\nSession State · Locks · TTL"]
    end

    CLAUDE["Claude API\nclaude-sonnet-4-6"]

    CLIENT <-->|"REST\n(HTTPS)"| FAST
    FAST <-->|"SQLAlchemy"| PG
    FAST <-->|"aioredis"| RD
    FAST <-->|"HTTPS"| CLAUDE
```

---

## 2. Component Responsibilities

### React PWA (Frontend)
- Renders the intake list, patient info form, chat-style Q&A interface, completion screen, summary view, and account management form
- Captures speech via browser mic using Web Speech API; supports English, Hindi, Telugu
- Displays transcription in an editable text area — staff can correct before submitting
- Sends plain text (never audio) to FastAPI on each response submission
- Stores JWT in memory (not localStorage) — cleared on tab close, re-login required

### FastAPI (Single Backend)
- Auth: validates email + PIN, issues JWT, enforces session timeout
- Staff account management: create, update, deactivate accounts via minimal account management form
- Session lifecycle: create, resume, complete, ask-more, data retention wipe
- Patient daily ID management and duplicate session check
- Writes all persistent data to PostgreSQL via SQLAlchemy
- Manages Redis for session state, concurrency enforcement, patient locks
- Calls Claude API for: next question generation, summary generation
- At INTAKE_COMPLETE: scans `question_responses` and marks non-substantive answers as `is_flagged = true`
- Hourly background task purges expired patient data from PostgreSQL

### PostgreSQL
- Source of truth for all structured, persistent data
- Stores: departments, staff accounts, patients, sessions, question responses, summaries, protocols
- All tables are department-scoped via `department_id` foreign key

### Redis
- `patient:{deptId}:{dailyId}:sessionId` — fast session lookup; TTL = `DATA_RETENTION_HOURS × 3600` seconds
- `patient_lock:{deptId}:{dailyId}` — enforces one active writer per patient session; 30min TTL
- `staff:{staffId}:session` — tracks each active staff session; 30min TTL; count of keys = concurrent session count

---

## 3. Department-Aware Design

Every PostgreSQL table has a `department_id` foreign key. FastAPI scopes all queries by the `departmentId` from the JWT — no cross-department data leakage is possible.

On every Claude call, FastAPI fetches and passes:
```
departments.llm_system_prompt  ─┐
departments.llm_summary_prompt  ├─► injected into every Claude call

protocols.question_bank ──────────► passed as coverage checklist on every intake call
question_responses (all so far) ──► passed as conversationHistory
```

Claude decides follow-up questions at runtime using the `question_bank` as a coverage checklist. Branching logic is not stored.

---

## 4. Data Flow: Key Sequences

### 4.1 Login

```mermaid
sequenceDiagram
    participant FE as React PWA
    participant FA as FastAPI
    participant RD as Redis
    participant PG as PostgreSQL

    FE->>FA: POST /auth/login { email, pin }
    FA->>PG: Find staff by email
    PG-->>FA: Staff record { pinHash, role, departmentId, isActive, lockedAt }
    FA->>FA: Check isActive and lockedAt
    FA->>FA: bcrypt.verify(pin, pinHash)
    FA->>RD: SCAN staff:*:session → count active keys
    RD-->>FA: current count
    FA->>FA: Validate count ≤ 5
    FA->>RD: SET staff:{staffId}:session TTL 30min
    FA->>FA: Sign JWT { staffId, role, departmentId }
    FA-->>FE: 200 { accessToken, staff }
```

### 4.2 Start or Resume Intake Session

```mermaid
sequenceDiagram
    participant FE as React PWA
    participant FA as FastAPI
    participant RD as Redis
    participant PG as PostgreSQL
    participant CL as Claude API

    FE->>FA: POST /sessions { dailyPatientId, name, age, gender }
    FA->>RD: GET patient:{deptId}:{dailyPatientId}:sessionId
    alt Session exists (within retention window)
        RD-->>FA: existing sessionId
        FA->>RD: GET patient_lock:{deptId}:{dailyPatientId}
        alt Lock held by another staff
            FA-->>FE: 409 { error: "SESSION_IN_USE" }
        else Lock free
            FA->>RD: SET patient_lock:{deptId}:{dailyPatientId} = staffId TTL 30min
            FA->>PG: INSERT session_contributors if staff not already contributor
            FA->>PG: GET all question_responses for session
            FA-->>FE: 200 { session, responses, intakeHistory, resumeFromQuestion }
        end
    else No session (new patient)
        FA->>PG: INSERT patient { expires_at = now() + DATA_RETENTION_HOURS }
        FA->>PG: INSERT intake_session { status = ACTIVE }
        FA->>RD: SET patient:{deptId}:{dailyPatientId}:sessionId TTL = DATA_RETENTION_HOURS × 3600s
        FA->>RD: SET patient_lock:{deptId}:{dailyPatientId} = staffId TTL 30min
        FA->>PG: INSERT session_contributors { sequence_number = 1 }
        FA->>CL: Claude API — empty history → question overview + first question
        CL-->>FA: firstQuestion
        FA-->>FE: 201 { session, firstQuestion }
    end
```

### 4.3 Per-Response Turn (Core Intake Loop)

Staff submits a transcribed answer. Transcription already happened in the browser.

```mermaid
sequenceDiagram
    participant FE as React PWA
    participant FA as FastAPI
    participant CL as Claude API
    participant PG as PostgreSQL
    participant RD as Redis

    FE->>FA: POST /sessions/{id}/respond { questionId, text, inputType, inputLanguage }
    FA->>RD: EXPIRE patient_lock:{deptId}:{dailyId} 30min
    FA->>RD: EXPIRE staff:{staffId}:session 30min
    FA->>PG: INSERT question_response { questionId, questionText, transcribedText, inputType, inputLanguage }
    FA->>CL: Claude API — full conversation history + question bank
    CL-->>FA: nextQuestion OR INTAKE_COMPLETE
    alt INTAKE_COMPLETE
        FA->>PG: UPDATE question_responses SET is_flagged = true WHERE non-substantive
        FA-->>FE: 200 { intakeComplete: true, flaggedQuestions: [{ questionText, sequenceNumber }] }
    else next question
        FA-->>FE: 200 { nextQuestion: { id, text } }
    end
```

### 4.4 Summary Generation

```mermaid
sequenceDiagram
    participant FE as React PWA
    participant FA as FastAPI
    participant CL as Claude API
    participant PG as PostgreSQL

    FE->>FA: POST /sessions/{id}/complete
    FA->>PG: UPDATE intake_sessions SET status = COMPLETED, completed_at = now()
    FA->>PG: GET all question_responses for session
    FA->>CL: Claude API — summary prompt + full conversation history
    CL-->>FA: Structured patient summary
    FA->>PG: INSERT or UPDATE patient_summaries { summaryText, generatedAt, latencyMs }
    FA->>RD: DEL patient_lock, DEL staff:{staffId}:session
    FA-->>FE: 200 { summaryId, summaryText, generatedAt, latencyMs }
```

### 4.5 Ask More

```mermaid
sequenceDiagram
    participant FE as React PWA
    participant FA as FastAPI
    participant CL as Claude API
    participant PG as PostgreSQL
    participant RD as Redis

    FE->>FA: POST /sessions/{id}/ask-more
    FA->>RD: SCAN staff:*:session → check count ≤ 5
    FA->>PG: UPDATE intake_sessions SET status = ACTIVE, completed_at = NULL
    FA->>RD: SET patient_lock:{deptId}:{dailyId} = staffId TTL 30min
    FA->>RD: SET staff:{staffId}:session TTL 30min
    FA->>PG: INSERT session_contributors if staff not already contributor
    FA->>PG: GET all question_responses for session
    FA->>CL: Claude API — full conversation history → next relevant question
    CL-->>FA: nextQuestion
    FA-->>FE: 200 { nextQuestion }
```

---

## 5. Redis Data Structures

| Key | Type | Value | TTL |
|---|---|---|---|
| `patient:{deptId}:{dailyId}:sessionId` | String | PostgreSQL session UUID | `DATA_RETENTION_HOURS × 3600` seconds |
| `patient_lock:{deptId}:{dailyId}` | String | staffId of current writer | 30 min — refreshed on every `/respond` call |
| `staff:{staffId}:session` | String | Active sessionId | 30 min — refreshed on every `/respond` call |

**Concurrency:** Max 5 sessions enforced by counting `staff:*:session` keys. Checked on login and `/ask-more`. Auto-cleans on TTL expiry or logout.

**Data wipe:** FastAPI APScheduler runs hourly — `DELETE FROM patients WHERE expires_at < NOW()`. CASCADE handles all child rows. Redis keys self-expire.

**Auto-save:** Every `POST /sessions/{id}/respond` writes to `question_responses` immediately — auto-save is a side effect of normal operation.

---

## 6. API Surface

All endpoints are FastAPI. All require `Authorization: Bearer {jwt}` except `POST /auth/login`.

| Method | Path | Description |
|---|---|---|
| POST | `/auth/login` | Email + PIN login, returns JWT |
| POST | `/auth/logout` | Invalidate session, release concurrency slot |
| POST | `/staff` | Admin creates staff account |
| PATCH | `/staff/{id}` | Update staff, reset PIN, deactivate, unlock |
| GET | `/sessions` | List all intake sessions for the department |
| POST | `/sessions` | Start new or resume existing patient session |
| GET | `/sessions/{id}` | Get session state and conversation history |
| POST | `/sessions/{id}/respond` | Submit text answer; refreshes locks |
| POST | `/sessions/{id}/complete` | Complete session and generate summary (synchronous, ≤30s) |
| GET | `/sessions/{id}/summary` | Fetch already-generated summary |
| POST | `/sessions/{id}/ask-more` | Re-open completed session for continued Q&A |

Claude calls are internal Python service functions within FastAPI — not HTTP endpoints.

---

## 7. Auth & Security

- PIN stored as bcrypt hash — never plain text
- JWT payload: `{ sub: staffId, role, departmentId, iat, exp }`
- JWT expiry: 30 minutes (re-login required after inactivity)
- All endpoints require valid JWT except `/auth/login`
- HTTPS enforced on all client-facing traffic (standard TLS, nginx)
- 5 failed PIN attempts locks the account — Admin resets via `PATCH /staff/{id}`

---

## 8. Deployment — Docker Compose (MVP)

```yaml
services:
  frontend:   # React PWA built + served via nginx
  fastapi:    # FastAPI on port 8000
  postgres:   # PostgreSQL on port 5432
  redis:      # Redis on port 6379

networks:
  internal:   # All services communicate here
  public:     # Only frontend and fastapi exposed
```

Four services total. FastAPI is the only backend service.

---

## 9. Non-Functional Architecture Decisions

| Concern | Decision |
|---|---|
| Single backend | FastAPI handles everything — no separate orchestration layer needed for M1 scale |
| Browser-side transcription | Web Speech API runs in browser — no audio sent to backend, no server transcription cost |
| Summary latency ≤ 30s | `POST /sessions/{id}/complete` is synchronous — client waits, response contains the summary |
| Adaptive question latency ≤ 2s | Claude call uses only conversation history — minimal context, fast response |
| Data retention window | Controlled by `DATA_RETENTION_HOURS` env var; Redis TTL and PostgreSQL `expires_at` both set from this value at session creation |
| Max 5 concurrent sessions | Enforced by counting `staff:*:session` keys in Redis at login and ask-more |
| Session auto-save | Every `POST /respond` writes to PostgreSQL immediately — always current |
| No permanent patient storage | PostgreSQL rows deleted by hourly APScheduler task. Redis keys self-expire. |
| Ask More | Re-opens session to ACTIVE, passes full history to Claude; summary is UPDATE not INSERT on re-completion |
| is_flagged detection | At INTAKE_COMPLETE, FastAPI scans all `question_responses` and sets `is_flagged = true` where `transcribed_text` matches non-substantive criteria ("no", "NA", "I don't know", empty, or equivalent) |
| Multilingual input | Claude's system prompt instructs it to understand Hindi and Telugu input and always respond in English — no separate translation step needed |
