- 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
363 lines
11 KiB
Markdown
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.
|