- 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
11 KiB
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
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 | |
| 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 templatesros-- Review of systems preferencesencounter_format-- Note formatting preferencescustom-- Free-form user preferencestemplate_*-- 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 | |
| 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:
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.