pediatric-ai-scribe-v3/docs/database.md
Daniel b97c5f146b v6.1: Turnstile bot protection, LiteLLM provider, PPTX tables, audio backup fixes, docs
- Add Cloudflare Turnstile to login, register, and password reset forms
- Switch AI provider to LiteLLM, transcription to OpenAI Whisper
- Change domain to scribe.pedshub.com
- Fix PPTX export: add tables, bold/italic, numbered lists, code blocks, blockquotes
- Fix announcement banner close button (CSP was blocking inline onclick)
- Fix auth middleware: empty Bearer token now falls through to cookie auth
- Fix audio backups: only save on transcription failure, stop auto-deleting on success
- Soften AI correction injection to prevent model hallucination from correction history
- Fix LiteLLM TTS model name handling (no incorrect openai/ prefix)
- Expand AI instructions textarea in Learning Hub CMS
- Update README for v6 with all features and providers
- Add comprehensive docs/: architecture, API reference, database schema,
  authentication, AI providers, speech, learning hub, configuration, deployment
2026-04-04 22:56:24 +02:00

363 lines
11 KiB
Markdown

# 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.