# Pediatric AI Scribe - Database Schema ## Overview The application uses PostgreSQL 16 with the **pgvector** extension enabled for vector similarity search. The database runs in a `pgvector/pgvector:pg16` container with a persistent `pgdata` volume. ### Connection Pool | Setting | Value | |---|---| | Max connections | 20 | | Idle timeout | 30 seconds | | Connection timeout | 5 seconds | --- ## Extensions ```sql CREATE EXTENSION IF NOT EXISTS vector; -- pgvector for embedding search ``` --- ## Tables ### users Core user accounts with authentication, TOTP two-factor, OIDC federation, and per-user preferences. | Column | Type | Notes | |---|---|---| | id | SERIAL PRIMARY KEY | | | email | VARCHAR UNIQUE NOT NULL | | | password | VARCHAR | bcrypt hash; NULL for OIDC-only users | | name | VARCHAR | Display name | | role | VARCHAR | `user`, `admin` | | totp_secret | VARCHAR | TOTP shared secret (encrypted) | | totp_enabled | BOOLEAN | Whether 2FA is active | | email_verified | BOOLEAN | | | verify_token | VARCHAR | Email verification token | | verify_expires | TIMESTAMP | Expiry for verify_token | | reset_token | VARCHAR | Password reset token | | reset_expires | TIMESTAMP | Expiry for reset_token | | oidc_sub | VARCHAR | OpenID Connect subject identifier | | disabled | BOOLEAN | Soft-disable account | | nextcloud_url | VARCHAR | User's Nextcloud/WebDAV server URL | | nextcloud_user | VARCHAR | WebDAV username | | nextcloud_pass | VARCHAR | WebDAV password (encrypted) | | stt_model | VARCHAR | Preferred speech-to-text model | | tts_voice | VARCHAR | Preferred text-to-speech voice | | webdav_learning_path | VARCHAR | WebDAV path for learning exports | | created_at | TIMESTAMP | DEFAULT NOW() | | updated_at | TIMESTAMP | DEFAULT NOW() | --- ### app_settings Key-value store for application configuration. Values are cached in memory with a 2-minute TTL to avoid repeated DB reads on every request. | Column | Type | Notes | |---|---|---| | key | VARCHAR PRIMARY KEY | Setting name | | value | TEXT | JSON or plain text value | | updated_at | TIMESTAMP | DEFAULT NOW() | | updated_by | INTEGER | FK to users.id | --- ### audit_log High-level audit trail for security-relevant and AI-related actions. | Column | Type | Notes | |---|---|---| | id | SERIAL PRIMARY KEY | | | user_id | INTEGER | FK to users.id | | action | VARCHAR | Action name (e.g., `generate_note`, `login`) | | category | VARCHAR | Grouping category | | details | TEXT | Free-form detail string or JSON | | ip_address | VARCHAR | Client IP | | user_agent | VARCHAR | Client User-Agent header | | model_used | VARCHAR | LLM model identifier (if applicable) | | tokens_used | INTEGER | Total tokens consumed | | duration_ms | INTEGER | Wall-clock time of the operation | | status | VARCHAR | `success`, `error`, etc. | | timestamp | TIMESTAMP | DEFAULT NOW() | --- ### api_log Per-request API telemetry with cost tracking. | Column | Type | Notes | |---|---|---| | id | SERIAL PRIMARY KEY | | | user_id | INTEGER | FK to users.id | | endpoint | VARCHAR | Route path | | method | VARCHAR | HTTP method | | status_code | INTEGER | Response status | | request_size | INTEGER | Request body bytes | | response_size | INTEGER | Response body bytes | | model_used | VARCHAR | LLM model identifier | | tokens_input | INTEGER | Input/prompt tokens | | tokens_output | INTEGER | Output/completion tokens | | cost_estimate | NUMERIC | Estimated USD cost | | duration_ms | INTEGER | Request duration | | ip_address | VARCHAR | Client IP | | error | TEXT | Error message if status >= 400 | | timestamp | TIMESTAMP | DEFAULT NOW() | --- ### access_log Lightweight authentication event log. | Column | Type | Notes | |---|---|---| | id | SERIAL PRIMARY KEY | | | user_id | INTEGER | FK to users.id | | action | VARCHAR | `login`, `logout`, `failed_login`, etc. | | ip_address | VARCHAR | Client IP | | user_agent | VARCHAR | Client User-Agent header | | success | BOOLEAN | Whether the action succeeded | | timestamp | TIMESTAMP | DEFAULT NOW() | --- ### saved_encounters Transcribed clinical encounters with generated notes. Rows auto-expire after 7 days. | Column | Type | Notes | |---|---|---| | id | SERIAL PRIMARY KEY | | | user_id | INTEGER | FK to users.id | | label | VARCHAR | User-assigned label | | enc_type | VARCHAR | Encounter type (e.g., `well_child`, `sick`) | | transcript | TEXT | Raw transcript text | | generated_note | TEXT | AI-generated clinical note | | partial_data | JSONB | In-progress form state | | status | VARCHAR | `draft`, `complete`, etc. | | idempotency_key | VARCHAR | Prevents duplicate submissions | | created_at | TIMESTAMP | DEFAULT NOW() | | updated_at | TIMESTAMP | DEFAULT NOW() | | expires_at | TIMESTAMP | DEFAULT NOW() + INTERVAL '7 days' | --- ### user_memories Persistent per-user preferences and correction history that the AI uses to personalize output. | Column | Type | Notes | |---|---|---| | id | SERIAL PRIMARY KEY | | | user_id | INTEGER | FK to users.id | | category | VARCHAR | See categories below | | name | VARCHAR | Human-readable label | | content | TEXT | Memory content | | created_at | TIMESTAMP | DEFAULT NOW() | | updated_at | TIMESTAMP | DEFAULT NOW() | **Categories:** - `physical_exam` -- Default physical exam templates - `ros` -- Review of systems preferences - `encounter_format` -- Note formatting preferences - `custom` -- Free-form user preferences - `template_*` -- User-defined note templates (prefix pattern) - `correction_*` -- Learned corrections from user edits (prefix pattern) --- ### audio_backups Temporary storage for raw audio recordings. Data is gzip-compressed before storage. Rows auto-expire after 24 hours. | Column | Type | Notes | |---|---|---| | id | SERIAL PRIMARY KEY | | | user_id | INTEGER | FK to users.id | | module | VARCHAR | Source module (e.g., `encounter`, `dictation`) | | mime_type | VARCHAR | Original audio MIME type | | size_bytes | INTEGER | Original uncompressed size | | compressed_bytes | INTEGER | Stored compressed size | | audio_data | BYTEA | Gzip-compressed audio binary | | created_at | TIMESTAMP | DEFAULT NOW() | | expires_at | TIMESTAMP | DEFAULT NOW() + INTERVAL '24 hours' | --- ### user_documents Metadata for user-uploaded documents stored in S3-compatible object storage. | Column | Type | Notes | |---|---|---| | id | SERIAL PRIMARY KEY | | | user_id | INTEGER | FK to users.id | | s3_key | VARCHAR | Object storage key | | filename | VARCHAR | Original filename | | mime_type | VARCHAR | File MIME type | | size_bytes | INTEGER | File size | | description | TEXT | User-provided description | | created_at | TIMESTAMP | DEFAULT NOW() | --- ### learning_categories Top-level groupings for educational content. | Column | Type | Notes | |---|---|---| | id | SERIAL PRIMARY KEY | | | name | VARCHAR | Category display name | | slug | VARCHAR UNIQUE | URL-safe identifier | | description | TEXT | Category description | | sort_order | INTEGER | Display ordering | | created_at | TIMESTAMP | DEFAULT NOW() | --- ### learning_content Educational articles and reference material with vector embeddings for semantic search. | Column | Type | Notes | |---|---|---| | id | SERIAL PRIMARY KEY | | | title | VARCHAR | Content title | | slug | VARCHAR UNIQUE | URL-safe identifier | | body | TEXT | Full content body (Markdown or HTML) | | category_id | INTEGER | FK to learning_categories.id | | subject | VARCHAR | Subject area tag | | content_type | VARCHAR | `article`, `reference`, `case`, etc. | | published | BOOLEAN | Visibility flag | | author_id | INTEGER | FK to users.id | | embedding | VECTOR(768) | pgvector embedding for similarity search | | created_at | TIMESTAMP | DEFAULT NOW() | | updated_at | TIMESTAMP | DEFAULT NOW() | --- ### learning_questions Quiz questions attached to learning content. | Column | Type | Notes | |---|---|---| | id | SERIAL PRIMARY KEY | | | content_id | INTEGER | FK to learning_content.id ON DELETE CASCADE | | question_text | TEXT | The question prompt | | question_type | VARCHAR | `multiple_choice`, `true_false`, etc. | | explanation | TEXT | Post-answer explanation | | sort_order | INTEGER | Display ordering within content | --- ### learning_options Answer options for quiz questions. | Column | Type | Notes | |---|---|---| | id | SERIAL PRIMARY KEY | | | question_id | INTEGER | FK to learning_questions.id ON DELETE CASCADE | | option_text | TEXT | Answer text | | is_correct | BOOLEAN | Whether this is the correct answer | | explanation | TEXT | Option-specific explanation | | sort_order | INTEGER | Display ordering within question | --- ### learning_progress Tracks user scores on learning content quizzes. | Column | Type | Notes | |---|---|---| | id | SERIAL PRIMARY KEY | | | user_id | INTEGER | FK to users.id ON DELETE CASCADE | | content_id | INTEGER | FK to learning_content.id ON DELETE CASCADE | | score | INTEGER | Number of correct answers | | total | INTEGER | Total number of questions | | completed_at | TIMESTAMP | DEFAULT NOW() | --- ### developmental_milestones Pediatric developmental milestone reference data, organized by age group and domain. | Column | Type | Notes | |---|---|---| | id | SERIAL PRIMARY KEY | | | age_group | VARCHAR | e.g., `2 months`, `4 months`, `6 months` | | domain | VARCHAR | e.g., `motor`, `language`, `social`, `cognitive` | | milestone_text | TEXT | Description of the milestone | | sort_order | INTEGER | Display ordering within age group + domain | | created_at | TIMESTAMP | DEFAULT NOW() | | updated_at | TIMESTAMP | DEFAULT NOW() | --- ## Indexes The schema defines 22 indexes to support query patterns across the application: | # | Table | Index | Columns | |---|---|---|---| | 1 | users | unique | email | | 2 | users | index | oidc_sub | | 3 | users | index | verify_token | | 4 | users | index | reset_token | | 5 | audit_log | index | user_id | | 6 | audit_log | index | timestamp | | 7 | audit_log | index | action | | 8 | audit_log | index | category | | 9 | api_log | index | user_id | | 10 | api_log | index | timestamp | | 11 | api_log | index | endpoint | | 12 | access_log | index | user_id | | 13 | access_log | index | timestamp | | 14 | saved_encounters | index | user_id | | 15 | saved_encounters | index | expires_at | | 16 | saved_encounters | index | idempotency_key | | 17 | user_memories | index | user_id, category | | 18 | audio_backups | index | user_id | | 19 | audio_backups | index | expires_at | | 20 | user_documents | index | user_id | | 21 | learning_content | index | category_id | | 22 | learning_progress | index | user_id, content_id | --- ## Auto-Cleanup Expired rows are automatically purged by a scheduled cleanup job: | Target | Expiry Rule | Affected Table | |---|---|---| | Encounters | `expires_at < NOW()` (default 7 days after creation) | saved_encounters | | Audio backups | `expires_at < NOW()` (default 24 hours after creation) | audio_backups | ### Schedule - The cleanup function runs **hourly** via `setInterval`. - An initial cleanup also runs **10 seconds after server startup** to clear any rows that expired while the application was down. ### Behavior The cleanup executes two `DELETE` statements inside the hourly tick: ```sql DELETE FROM saved_encounters WHERE expires_at < NOW(); DELETE FROM audio_backups WHERE expires_at < NOW(); ``` Deleted row counts are logged at the `info` level via the Winston logger.