# Pediatric AI Scribe — Developer Guide **Version:** 3.15 | **Stack:** Node.js / Express / PostgreSQL / Vanilla JS --- ## Table of Contents 1. [Project Overview](#1-project-overview) 2. [Architecture](#2-architecture) 3. [Directory Structure](#3-directory-structure) 4. [Environment Variables](#4-environment-variables) 5. [Database Schema](#5-database-schema) 6. [Authentication System](#6-authentication-system) 7. [Backend API Reference](#7-backend-api-reference) 8. [Frontend Architecture](#8-frontend-architecture) 9. [AI Integration](#9-ai-integration) 10. [Learning Hub & CMS](#10-learning-hub--cms) 11. [Deployment](#11-deployment) 12. [Known Issues & Security Notes](#12-known-issues--security-notes) 13. [Adding New Features](#13-adding-new-features) 14. [Resetting Admin Password via Console](#14-resetting-admin-password-via-console) --- ## 1. Project Overview Pediatric AI Scribe is a clinical documentation platform for pediatric healthcare providers. It uses AI (via OpenRouter, AWS Bedrock, or Azure OpenAI) to generate: - HPI notes from live encounter recordings - SOAP notes from dictation - Hospital course summaries - Chart reviews - Well-visit notes (including SSHADESS, ROS/PE, milestones) - Sick visit notes - Learning Hub content (articles, quizzes, clinical pearls, presentations) **Key design principle:** Single-page application. All tabs are lazy-loaded HTML components (`/public/components/*.html`). JavaScript modules initialize only when their tab is first activated via the `tabChanged` custom event. --- ## 2. Architecture ``` Browser (Vanilla JS + Tiptap) | | HTTP (JWT Bearer token in Authorization header) | Express.js (Node.js) — server.js | |— Helmet (CSP, security headers) |— CORS (restricted to APP_URL in production) |— express-rate-limit (login: 5/hr, register: 5/hr, general: 100/15min) |— cookie-parser |— Routes (/src/routes/) | PostgreSQL (pg driver, no ORM) | |— users, app_settings, audit_log, saved_encounters |— user_memories, learning_*, access_log, api_log ``` **AI providers** (configured via environment variables, in priority order): 1. AWS Bedrock (if `AWS_BEDROCK_REGION` is set) 2. Azure OpenAI (if `AZURE_OPENAI_ENDPOINT` is set) 3. OpenRouter (default, requires `OPENROUTER_API_KEY`) --- ## 3. Directory Structure ``` / ├── server.js # Express app entry point, route registration, CSP ├── package.json ├── Dockerfile ├── docker-compose.yml # Production compose ├── docker-compose.local.yml # Local development (port 3552) ├── DEVELOPER_GUIDE.md # This file │ ├── src/ │ ├── db/ │ │ └── database.js # DB connection pool, schema init, migrations │ ├── middleware/ │ │ ├── auth.js # authMiddleware, adminMiddleware, moderatorMiddleware │ │ └── logging.js # Access log middleware │ ├── routes/ │ │ ├── auth.js # Login, register, 2FA, password reset, /me │ │ ├── admin.js # User management (admin only) │ │ ├── adminConfig.js # Site settings, feature flags, AI prompts, models │ │ ├── encounters.js # Save/load/delete draft encounters │ │ ├── memories.js # User templates (physical exam, ROS, etc.) │ │ ├── hpi.js # Generate HPI from encounter/dictation transcript │ │ ├── soap.js # Generate SOAP note │ │ ├── hospitalCourse.js # Generate hospital course summary │ │ ├── chartReview.js # Generate outpatient chart review │ │ ├── milestones.js # Generate developmental milestone narrative │ │ ├── wellVisit.js # Well-visit note generation (ROS/PE/ICD-10) │ │ ├── sickVisit.js # Sick visit note generation │ │ ├── refine.js # Refine/shorten any generated document │ │ ├── transcribe.js # Whisper audio transcription │ │ ├── tts.js # Text-to-speech (if configured) │ │ ├── nextcloud.js # Nextcloud WebDAV connect/export/disconnect │ │ ├── learningHub.js # User-facing: feed, content, quiz submission │ │ ├── learningAdmin.js # CMS: categories, content, questions CRUD │ │ ├── learningAI.js # AI generation for Learning Hub content │ │ └── logs.js # Usage/audit/API/access logs + client error │ └── utils/ │ ├── ai.js # callAI() — multi-provider AI client │ ├── models.js # Model ID lists, defaults, Bedrock ID mapping │ ├── prompts.js # Prompt templates, DB override loader │ ├── config.js # App configuration helpers │ └── logger.js # Winston logger (file + console) │ ├── public/ │ ├── index.html # Single HTML shell, loads all components │ ├── 404.html # Custom 404 page │ ├── css/ │ │ └── styles.css # All CSS (single file, ~750 lines) │ ├── js/ │ │ ├── app.js # Core: tab switching, loadComponent, global helpers │ │ ├── auth.js # Login/register UI, JWT localStorage, getAuthHeaders() │ │ ├── admin.js # Admin panel UI │ │ ├── liveEncounter.js # Live recording tab │ │ ├── voiceDictation.js # Dictation tab │ │ ├── hospitalCourse.js # Hospital course tab │ │ ├── chartReview.js # Chart review tab │ │ ├── soap.js # SOAP tab │ │ ├── milestones.js # Milestones tab (inside Well Visit) │ │ ├── wellVisit.js # Well Visit guide + vaccine schedule │ │ ├── shadess.js # SSHADESS form + Well Visit note generation │ │ ├── sickVisit.js # Sick Visit tab │ │ ├── nextcloud.js # Nextcloud settings UI │ │ ├── encounters.js # Save/load encounter UI (all tabs) │ │ ├── memories.js # User templates UI (Settings) │ │ ├── learningHub.js # Learning Hub + CMS (entire module, ~1400 lines) │ │ ├── milestonesData.js # Static milestone data by age group │ │ └── pediatricScheduleData.js # Vaccine schedule data │ ├── components/ # Lazy-loaded tab HTML (injected by loadComponent) │ │ ├── encounter.html ├── dictation.html ├── hospital.html │ │ ├── chart.html ├── soap.html ├── wellvisit.html │ │ ├── sickvisit.html ├── vaxschedule.html ├── catchup.html │ │ ├── learning.html ├── cms.html ├── admin.html │ │ └── settings.html │ └── vendor/ │ └── tiptap.bundle.js # Tiptap 2 + extensions (esbuild bundle, self-hosted) ``` --- ## 4. Environment Variables Set in `.env` file (copy `.env.example` to get started): ```bash # ── Required ────────────────────────────────────────────────── DATABASE_URL=postgresql://user:@host:5432/dbname JWT_SECRET=change-this-to-a-random-64-char-string # ── AI Provider (choose one or let it default to OpenRouter) ── OPENROUTER_API_KEY=sk-or-... # Default provider # OR AZURE_OPENAI_ENDPOINT=https://... # Azure (HIPAA eligible) AZURE_OPENAI_API_KEY=... AZURE_DEPLOYMENT_NAME=gpt-4o-mini AZURE_OPENAI_API_VERSION=2024-08-01-preview # OR AWS_BEDROCK_REGION=us-east-1 # AWS Bedrock (HIPAA eligible) AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... # ── Optional ────────────────────────────────────────────────── OPENAI_API_KEY=sk-... # For Whisper transcription only APP_URL=https://yourdomain.com # Enables secure CORS + Secure cookies NODE_ENV=production # Enables production optimizations PORT=3000 # Default: 3000 # ── Email (for password reset, registration verification) ────── SMTP_HOST=smtp.example.com SMTP_PORT=587 SMTP_USER=noreply@example.com SMTP_PASS=... SMTP_FROM=Pediatric AI Scribe ``` **Note:** If no SMTP is configured, registration auto-verifies and password reset won't work. Configure SMTP or use the console reset method (see Section 14). --- ## 5. Database Schema All tables are created automatically on first run by `src/db/database.js`. The file runs `CREATE TABLE IF NOT EXISTS` for every table, followed by `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` migrations for upgrades. ### Core Tables #### `users` | Column | Type | Notes | |--------|------|-------| | id | SERIAL PK | | | email | TEXT UNIQUE | Lowercase | | password | TEXT | bcrypt hash (cost 12) | | name | TEXT | Display name | | role | TEXT | `'user'` \| `'moderator'` \| `'admin'` | | totp_enabled | BOOLEAN | 2FA status | | totp_secret | TEXT | TOTP secret (base32) | | disabled | BOOLEAN | Soft disable | | email_verified | BOOLEAN | | | verify_token / verify_expires | TEXT / BIGINT | Email verification | | reset_token / reset_expires | TEXT / BIGINT | Password reset | | nextcloud_url / nextcloud_user / nextcloud_token / nextcloud_folder | TEXT | Nextcloud integration | | webdav_learning_path | TEXT | Default WebDAV path for Learning Hub file picker | | created_at | TIMESTAMPTZ | | #### `app_settings` Key-value store for all site configuration. Read via `db.getSetting(key)`, written via admin panel or direct DB. Important keys: - `registration_enabled` — `'true'` / `'false'` - `announcement.enabled` / `announcement.text` / `announcement.type` - `smtp.*` — SMTP config (overrides env vars) - `ai.prompt.*` — AI prompt overrides - `model.*` — enabled/disabled models #### `saved_encounters` Draft encounters (7-day auto-expiry). Columns: `label`, `enc_type`, `transcript`, `generated_note`, `partial_data` (JSON), `status`, `expires_at`. #### `user_memories` User templates fed into AI generation. `category` is one of: `physical_exam`, `ros`, `encounter_format`, `family_history`, `assessment_plan`, `custom`. ### Learning Hub Tables #### `learning_categories` Simple category list with `name`, `slug`, `sort_order`. #### `learning_content` Articles, quizzes, pearls, presentations. Key columns: `title`, `slug`, `body` (HTML for articles/pearls/quizzes; Marp markdown for presentations), `content_type` (`article` | `quiz` | `pearl` | `presentation`), `published`, `author_id`. #### `learning_questions` Quiz questions linked to `learning_content`. `question_type`: `mcq` | `true_false` | `multi`. `explanation` = general explanation shown after answering. #### `learning_options` Answer options for quiz questions. `is_correct: boolean`, `explanation` = shown when this wrong option is chosen. #### `learning_progress` Quiz attempt scores per user per content item. --- ## 6. Authentication System **Current implementation: JWT in localStorage** ### Flow 1. `POST /api/auth/login` → returns `{ success, token, user }` 2. Frontend stores token in `localStorage` as `ped_scribe_token` and in `window.AUTH_TOKEN` 3. All API calls include `Authorization: Bearer ` header via `getAuthHeaders()` 4. `src/middleware/auth.js` validates the Bearer token, attaches `req.user` 5. Logout: `clearSession()` removes token from localStorage (client-side only) ### Token - Signed with `JWT_SECRET` env var - 7-day expiry - Payload: `{ userId: number }` ### Roles - `user` — standard access (clinical tools only) - `moderator` — can create/edit Learning Hub content - `admin` — full access including user management and site settings ### Middleware - `authMiddleware` — validates JWT, populates `req.user` - `adminMiddleware` — run after auth, requires `role === 'admin'` - `moderatorMiddleware` — run after auth, requires `role === 'admin' OR 'moderator'` ### 2FA Uses TOTP (speakeasy). If enabled, login returns `{ requires2FA: true }` and the client must POST the TOTP code to complete login. ### Session Check on Page Load (auth.js) ```javascript var savedToken = localStorage.getItem('ped_scribe_token'); if (savedToken) { fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + savedToken } }) .then(/* if ok → enterApp(), else → clearSession() */); } ``` The `has-session` CSS class on `` hides the auth screen immediately when a localStorage token exists, preventing a white flash. --- ## 7. Backend API Reference All routes are prefixed `/api`. Routes requiring auth are marked (A). Admin-only: (ADM). Moderator+: (MOD). ### Auth — `/api/auth/` | Method | Path | Auth | Description | |--------|------|------|-------------| | POST | `/login` | — | Email + password login. Returns `{ token, user }` | | POST | `/register` | — | Create account (checks `registration_enabled` setting) | | GET | `/me` | A | Returns current user object | | POST | `/logout` | — | Clears server-side state (currently no-op, kept for future) | | POST | `/setup-2fa` | A | Generates TOTP secret + QR code | | POST | `/verify-2fa` | A | Confirms TOTP code, enables 2FA | | POST | `/disable-2fa` | A | Disables 2FA (requires password) | | POST | `/forgot-password` | — | Sends reset email | | POST | `/reset-password` | — | Sets new password via reset token | | GET | `/registration-status` | — | Returns `{ registrationEnabled: bool }` | | GET | `/verify-email` | — | Verifies email via token in query string | ### Clinical — AI Generation | Method | Path | Auth | Description | |--------|------|------|-------------| | POST | `/generate-hpi-encounter` | A | HPI from live encounter transcript | | POST | `/generate-hpi-dictation` | A | HPI from dictation | | POST | `/generate-soap` | A | SOAP note | | POST | `/generate-hospital-course` | A | Hospital course summary | | POST | `/generate-chart-review` | A | Chart review | | POST | `/generate-milestone-narrative` | A | Milestone narrative | | POST | `/generate-milestone-summary` | A | 3-sentence milestone summary | | POST | `/well-visit/note` | A | Full well-visit note | | POST | `/sick-visit/note` | A | Sick visit SOAP | | POST | `/transcribe` | A | Whisper audio → text (multipart/form-data, field: `audio`) | | POST | `/refine` | A | Refine existing document | | POST | `/shorten` | A | Shorten existing document | | POST | `/clarify` | A | Find missing info in a document | ### Encounters (Save/Load) | Method | Path | Auth | Description | |--------|------|------|-------------| | GET | `/encounters` | A | List user's saved encounters | | POST | `/encounters` | A | Save/update encounter draft | | DELETE | `/encounters/:id` | A | Delete a draft | ### User Templates | Method | Path | Auth | Description | |--------|------|------|-------------| | GET | `/memories` | A | List user's templates | | POST | `/memories` | A | Create template | | PUT | `/memories/:id` | A | Update template | | DELETE | `/memories/:id` | A | Delete template | | GET | `/memories/context` | A | Returns templates formatted for AI injection | ### Nextcloud | Method | Path | Auth | Description | |--------|------|------|-------------| | POST | `/nextcloud/connect` | A | Connect + test Nextcloud credentials | | POST | `/nextcloud/export` | A | Export text file to Nextcloud | | POST | `/nextcloud/disconnect` | A | Remove Nextcloud credentials | ### Learning Hub (User-Facing) | Method | Path | Auth | Description | |--------|------|------|-------------| | GET | `/learning/categories` | A | List categories | | GET | `/learning/feed` | A | Paginated published content | | GET | `/learning/category/:slug` | A | Content by category | | GET | `/learning/content/:slug` | A | Single content item + questions | | POST | `/learning/submit-quiz` | A | Submit quiz answers, returns scored results | | GET | `/learning/search` | A | Full-text search | ### Learning Hub CMS (Moderator+) | Method | Path | Auth | Description | |--------|------|------|-------------| | GET | `/admin/learning/categories` | MOD | All categories with counts | | POST | `/admin/learning/categories` | MOD | Create category | | PUT | `/admin/learning/categories/:id` | MOD | Update category | | DELETE | `/admin/learning/categories/:id` | MOD | Delete category | | GET | `/admin/learning/content` | MOD | All content (including drafts) | | GET | `/admin/learning/content/:id` | MOD | Single item with questions | | POST | `/admin/learning/content` | MOD | Create content | | PUT | `/admin/learning/content/:id` | MOD | Update content | | DELETE | `/admin/learning/content/:id` | MOD | Delete content + questions | | POST | `/admin/learning/content/:id/questions` | MOD | Add question to content | | PUT | `/admin/learning/questions/:id` | MOD | Update question + options | | DELETE | `/admin/learning/questions/:id` | MOD | Delete question | | GET | `/admin/learning/stats` | MOD | Dashboard stats | ### Learning Hub AI (Moderator+) | Method | Path | Auth | Description | |--------|------|------|-------------| | POST | `/admin/learning/ai-generate` | MOD | Generate content from topic/file/Nextcloud (multipart/form-data) | | POST | `/admin/learning/ai-refine` | MOD | Refine body HTML with instructions | | POST | `/admin/learning/preview-slides` | MOD | Render Marp markdown → `{ css, slides[] }` for preview | | POST | `/admin/learning/generate-pptx` | MOD | Marp markdown → `.pptx` download (pptxgenjs) | | GET | `/admin/learning/webdav-browse` | MOD | PROPFIND Nextcloud folder | | POST | `/admin/learning/webdav-path` | MOD | Save user's default WebDAV path | ### Admin (Admin Only) | Method | Path | Auth | Description | |--------|------|------|-------------| | GET | `/admin/users` | ADM | List all users | | POST | `/admin/users` | ADM | Create user | | PUT | `/admin/users/:id` | ADM | Update user (role, disable) | | DELETE | `/admin/users/:id` | ADM | Delete user | | GET/POST | `/admin/config/*` | ADM | Site settings (announcement, SMTP, models, prompts, etc.) | ### Logs & Health | Method | Path | Auth | Description | |--------|------|------|-------------| | GET | `/health` | — | Returns `{ status: 'running', version, provider }` | | GET | `/models` | — | Returns available AI models list | | POST | `/logs/client-error` | — | Client-side error logging (public) | | GET | `/logs/usage` | ADM | API usage log | | GET | `/logs/audit` | ADM | Audit log | --- ## 8. Frontend Architecture ### Tab Loading (Lazy Components) Every tab's HTML lives in `/public/components/.html`. When a tab button is clicked, `loadComponent()` in `app.js` fetches the HTML, injects it into the tab section, then fires `tabChanged` event. ```javascript // app.js document.dispatchEvent(new CustomEvent('tabChanged', { detail: { tab: tabName } })); ``` **Critical pattern:** Every JS module that needs to access tab DOM elements MUST listen for `tabChanged`, not `DOMContentLoaded`: ```javascript // Correct pattern for every tab module (function() { var _inited = false; document.addEventListener('tabChanged', function(e) { if (e.detail.tab !== 'myTab' || _inited) return; _inited = true; // Now safe to querySelector elements — they exist in the DOM var btn = document.getElementById('my-btn'); btn.addEventListener('click', ...); }); })(); ``` If you use `DOMContentLoaded` instead, the elements won't exist yet (they're loaded async) and you'll get `null.addEventListener` errors. ### Global Functions (defined in app.js) These are available everywhere — no imports needed: | Function | Description | |----------|-------------| | `getAuthHeaders()` | Returns `{ 'Content-Type': 'application/json', 'Authorization': 'Bearer ' }` | | `getSelectedModel()` | Returns model ID from active tab's selector or global selector | | `showLoading(msg)` | Shows full-screen loading overlay | | `hideLoading()` | Hides loading overlay | | `showToast(msg, type)` | Shows toast notification. `type`: `'success'`\|`'error'`\|`'info'`\|`'warning'` | | `setOutputText(el, text)` | Sets text on contenteditable div, converting `\n` to `
` | | `transcribeAudio(blob)` | Sends audio blob to `/api/transcribe`, returns `{ success, text }` | | `createSpeechRecognition()` | Returns Web Speech API recognition instance | | `createTimer(el)` | Returns timer object with `.start()` / `.stop()` | ### Rich Text Editor (Tiptap) The body editor in the CMS uses Tiptap 2 (headless, no styling framework). The bundle is pre-built at `/public/vendor/tiptap.bundle.js` and exposes `window.Tiptap = { Editor, StarterKit, Link, Underline, TextStyle, Color }`. To rebuild the bundle after updating Tiptap packages: ```bash cat > tiptap-entry.js << 'EOF' import { Editor } from '@tiptap/core'; import StarterKit from '@tiptap/starter-kit'; import Link from '@tiptap/extension-link'; import Underline from '@tiptap/extension-underline'; import { TextStyle } from '@tiptap/extension-text-style'; import { Color } from '@tiptap/extension-color'; window.Tiptap = { Editor, StarterKit, Link, Underline, TextStyle, Color }; EOF npx esbuild tiptap-entry.js --bundle --format=iife --minify --outfile=public/vendor/tiptap.bundle.js rm tiptap-entry.js ``` --- ## 9. AI Integration ### `src/utils/ai.js` — `callAI(messages, options)` The single function used by all routes. It routes to the correct provider automatically. ```javascript const { callAI } = require('../utils/ai'); const result = await callAI( [{ role: 'user', content: 'Generate a note...' }], { model: 'google/gemini-2.5-flash', // optional, uses default if omitted temperature: 0.3, // optional, default 0.3 maxTokens: 4000 // optional, default 4000 } ); // result = { success: true, content: '...', model: '...', provider: '...', duration: ms } ``` ### Prompt System Prompts are defined in `src/utils/prompts.js`. Admins can override any prompt via the Admin panel (`/admin/config/prompts`). Overrides are stored in `app_settings` table and loaded into memory on startup (with 3s grace period for DB readiness). To add a new prompt: 1. Add a default in `prompts.js` 2. Use `PROMPTS.get('your-prompt-key')` in your route 3. The admin panel will auto-discover it ### AI Generate for Learning Hub The `src/routes/learningAI.js` file handles all Learning Hub AI generation. **For presentations:** The AI is prompted to return raw Marp markdown (not JSON). The response is stored in the `body` column. Detection: `content_type === 'presentation'`. **For articles/quizzes/pearls:** The AI returns JSON: ```json { "title": "...", "subject": "...", "body": "

HTML content

", "questions": [ { "question_text": "...", "question_type": "mcq", "explanation": "...", "options": [ { "option_text": "...", "is_correct": true, "explanation": "..." } ] } ] } ``` --- ## 10. Learning Hub & CMS ### Content Types | Type | Body format | Has questions | |------|-------------|---------------| | `article` | HTML (Tiptap) | Optional | | `quiz` | HTML (brief intro) | Always | | `pearl` | HTML | Optional | | `presentation` | Marp markdown | Never | ### Quiz Question Types - `mcq` — Single choice (radio buttons), 4 options, 1 correct - `true_false` — 2 options: "True" / "False", 1 correct - `multi` — Multiple select (checkboxes), scoring: all correct chosen AND no incorrect chosen ### PPTX Generation `POST /admin/learning/generate-pptx` parses Marp markdown (splits on `---`), extracts `#` headings as slide titles, bullet points as content, and uses `pptxgenjs` to create a real `.pptx`. **No Chromium required** — pure Node.js. ### Slide Preview `POST /admin/learning/preview-slides` uses `@marp-team/marp-core` to render Marp markdown to HTML, then extracts individual `
` elements. Returns `{ css, slides[] }`. The frontend renders these one at a time in a full-screen modal with arrow key + swipe navigation. ### Content Display In the Learning Hub viewer, content `body` is rendered via `sanitizeHtml()` in `learningHub.js`. This function allows a safe subset of HTML tags only (no `` 3. Create `public/js/myTab.js`: ```javascript (function() { var _inited = false; document.addEventListener('tabChanged', function(e) { if (e.detail.tab !== 'mytab' || _inited) return; _inited = true; // Wire up DOM elements here }); })(); ``` 4. Create `src/routes/myTab.js` with the API route 5. Register in `server.js`: `app.use('/api', require('./src/routes/myTab'));` ### Adding a New AI Prompt 1. In `src/utils/prompts.js`, add to the defaults object: ```javascript 'my-prompt': 'You are a pediatric physician...' ``` 2. In your route: `const prompt = PROMPTS.get('my-prompt') + '\n\n' + userInput` 3. The admin panel will show an editor for this prompt automatically. ### Adding a New Learning Hub Content Type 1. Add the new type to the `content_type` selector in `cms.html` 2. Handle it in `toggleEditorMode()` in `learningHub.js` 3. Add to the type detection in `buildGeneratePrompt()` in `learningAI.js` 4. Handle rendering in `learningHub.js` `loadContent()` function 5. No DB migration needed — `content_type` is a free-text column --- ## 14. Resetting Admin Password via Console If you lose admin access and have no SMTP for password reset, use the Docker console: ```bash # Step 1: Get a shell in the running app container docker exec -it pediatric-ai-scribe sh # Step 2: Open Node.js REPL node # Step 3: Hash your new password const bcrypt = require('bcryptjs'); const hash = await bcrypt.hash('YourNewPassword123!', 12); console.log(hash); // Copy the hash output # Step 4: Exit Node REPL .exit # Step 5: Open a DB shell # (Exit app container first, then:) docker exec -it pedscribe-db psql $POSTGRES_USER $POSTGRES_DB # Step 6: Update the password (paste the hash) UPDATE users SET password = '$2a$12$...(your-hash-here)...' WHERE email = 'your-admin@email.com'; # Verify: SELECT email, left(password, 7) as hash_prefix FROM users WHERE email = 'your-admin@email.com'; # Exit: \q ``` ### Enabling Registration via Console ```bash docker exec -it pedscribe-db psql $POSTGRES_USER $POSTGRES_DB UPDATE app_settings SET value = 'true' WHERE key = 'registration_enabled'; \q ``` ### Creating First Admin User (empty database) The first user to register is automatically made admin. Enable registration, register, then disable registration again. Or directly: ```bash # In the Node REPL inside the app container: const bcrypt = require('bcryptjs'); const { Pool } = require('pg'); const pool = new Pool({ connectionString: process.env.DATABASE_URL }); const hash = await bcrypt.hash('YourPassword', 12); await pool.query( "INSERT INTO users (email, password, name, role, email_verified) VALUES ($1, $2, $3, 'admin', true)", ['admin@yourdomain.com', hash, 'Admin'] ); pool.end(); ``` --- *Last updated: March 2026 — v3.15* *Generated for developer handover.*