- Drop first/second-person voice; reference-style prose throughout - Remove stale information; align with current code (argon2id primary, hybrid cookie/Bearer auth, sliding 24h idle, AES-256-GCM PHI at rest, backup codes, node-pg-migrate, collation-drift guard, multi-arch Docker, auto-version pipeline) - Preserve all technical accuracy and code examples - Remove any remaining references to separate PedsHub Quiz app - Keep consistent tone across files (tables + code blocks, imperatives where needed) - api-reference.md and developer-guide.md route tables expanded to reflect current routes (billing, sessions)
14 KiB
Developer guide
How the codebase is organized, how the main subsystems work, and where to extend them.
Layout
server.js Express entry, middleware stack, route mount
Dockerfile node:20-alpine, argon2 native compile deps
docker-compose.yml app + postgres services
migrations/ node-pg-migrate versioned schema changes
scripts/
maintenance.js REINDEX / drift CLI
release.sh semver bump + tag + push
import-milestones.js one-off seed import
src/
db/
database.js pool, baseline init, query helpers
migrate.js programmatic node-pg-migrate runner
middleware/
auth.js JWT + session-table validation + sliding idle
logging.js request log
utils/
ai.js callAI() multi-provider router + model whitelist
models.js built-in model registry
prompts.js templates (DB-overridable)
promptSafe.js <UNTRUSTED_*> wrapping + INJECTION_GUARD
crypto.js AES-256-GCM (PHI at rest)
passwords.js argon2id + bcrypt fallback + rehash
sessions.js token hash, UA parse, session id
platform.js isMobileClient()
redact.js PHI redactor for audit details
auditQueue.js batched audit/api/access writer
fileType.js magic-byte upload verifier
errors.js generic 500 responder
logger.js audit + api + access + Loki shipper
embeddings.js Vertex / LiteLLM / OpenAI embeddings
notify.js ntfy push
transcribe*.js, tts*.js STT / TTS provider clients
routes/ 27 routers
public/
index.html SPA shell, version-stamped asset refs
sw.js cache shell, network-first API
manifest.json PWA
js/ 24 vanilla JS modules (no bundler)
components/ per-tab HTML fragments loaded on demand
css/styles.css
models/ bundled Whisper WASM
mobile/ Capacitor 6 wrapper (Android + iOS)
.github/workflows/ CI (auto-version, APK, docker)
Backend
Middleware stack (server.js)
request
→ helmet CSP, HSTS, X-Content-Type-Options
→ CORS fail-closed in prod if APP_URL/CORS_ORIGINS missing
→ cookieParser
→ express.json 10 MB cap
→ rate limiters 200/min general; 10/15min login; 20/15min sensitive
→ static public/ with per-filetype Cache-Control; ?v=BUILD_ID cache-busts HTML on deploy
→ route handlers
→ 404 fallback serves index.html for SPA routes
On boot:
APP_VERSIONread frompackage.json, printed + returned by/api/health/detailed.BUILD_ID= short git HEAD SHA (or random on non-git deploys). Rewritten into HTML at startup.JWT_SECRET/DATA_ENCRYPTION_KEYfail-fast if missing in production.initDatabase()→runMigrations()→ collation drift check.- SIGTERM / SIGINT handler drains the audit queue and closes the pool.
DB helpers (src/db/database.js)
await db.get(sql, params); // first row or null
await db.all(sql, params); // array of rows
await db.run(sql, params); // { lastInsertRowid, changes }
await db.query(sql, params); // raw pg result
await db.getSetting(key); // app_settings value (2 min cache)
await db.setSetting(key, v); // writes + invalidates cache
db.pool // pg.Pool instance for transactions
SQL uses ? placeholders (auto-converted to $1, $2, ...) OR native $N.
INSERTs without explicit RETURNING get RETURNING id appended.
Auth middleware (src/middleware/auth.js)
var { authMiddleware, adminMiddleware, moderatorMiddleware } = require('../middleware/auth');
router.post('/thing', authMiddleware, handler); // requires auth
router.post('/admin-thing', authMiddleware, adminMiddleware, handler);
Sets req.user with { id, email, name, role, totp_enabled, disabled } and
req.sessionId.
AI (src/utils/ai.js)
var { callAI } = require('../utils/ai');
var result = await callAI([
{ role: 'system', content: PROMPTS.hpiEncounter + INJECTION_GUARD },
{ role: 'user', content: wrapUserText('transcript', transcript) }
], { model, maxTokens: 4000 });
// result = { content, model, usage }
Throws { code: 'model_not_permitted' } if the requested model isn't in the
active allowlist.
Prompts (src/utils/prompts.js)
Flat PROMPTS object. Any template can be overridden live by writing a
prompt.{name} row in app_settings. Admin Panel's Prompt Editor is the UX
for that.
Settings (src/utils/config.js)
var v = await config.get('feature.read_aloud', 'false'); // key, default
await config.set('registration_enabled', 'true');
2-minute in-memory cache. Writes invalidate immediately.
Logger (src/utils/logger.js)
logger.audit(userId, 'action', 'details', req, { category: 'auth' });
logger.apiCall(userId, endpoint, { model, tokensInput, tokensOutput, duration });
logger.access(userId, 'login', req, true);
logger.error('scope', err.message);
Writes go to the database (batched), the daily JSONL file, and Loki (if configured).
Frontend
No framework, no bundler, no build step. public/index.html is the only
document. Modules communicate through window.* globals and CustomEvent on
document.
Tab system
app.js owns activateTab(name):
- Fetch
/components/{name}.html(browser-cached for 1 h, bust by?v=BUILD_IDthat the server injects). - Inject into
.app-body. - Dispatch
CustomEvent('tabChanged', { detail: { tab: name } }). - Feature modules (
soap.js,encounters.js, etc.) listen for their own tab name and initialize DOM references inside the just-injected fragment.
Auth model (client)
auth.js branches on isNativeApp():
- Web — no localStorage token; relies on the
ped_authhttpOnly cookie set by the server./api/auth/meon boot verifies the session; failed verification shows the login screen. - Mobile — token lives in
capacitor-secure-storage-plugin(iOS Keychain / Android EncryptedSharedPreferences).getAuthHeaders()emitsAuthorization: Bearer <token>.
authFetch.js wraps window.fetch to catch 401 on authenticated /api/*
requests and force re-login. BroadcastChannel('pedscribe-auth') propagates
logout to sibling tabs.
Module load order
Fixed in index.html:
secureStorage → authFetch → auth → (feature modules)
All <script defer>. Dependencies enforced by declaration order.
Adding things
A new AI endpoint
-
Create
src/routes/myFeature.js:var express = require('express'); var router = express.Router(); var { callAI } = require('../utils/ai'); var { authMiddleware } = require('../middleware/auth'); var PROMPTS = require('../utils/prompts'); var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe'); var logger = require('../utils/logger'); router.post('/my-feature', authMiddleware, async (req, res) => { try { var { transcript, model } = req.body; var result = await callAI([ { role: 'system', content: PROMPTS.myFeature + INJECTION_GUARD }, { role: 'user', content: wrapUserText('transcript', transcript) } ], { model }); res.json({ success: true, text: result.content }); logger.audit(req.user.id, 'generate_my_feature', 'Generated', req, { category: 'clinical' }); } catch (err) { res.status(500).json({ error: 'Request failed' }); } }); module.exports = router; -
Mount in
server.js:app.use('/api', require('./src/routes/myFeature')); -
Add the template to
src/utils/prompts.js. -
Add a component under
public/components/myfeature.html. -
Add
public/js/myFeature.jswith atabChangedlistener. -
Register the tab button in
public/index.html.
A new table / column
New changes go in a migration file. See docs/migrations.md.
docker exec -w /app pediatric-ai-scribe npm run migrate:new -- add_my_table
# edit the generated file
A new setting
- Optional — add a default in the
defaultsarray insideinitDatabase()(only needed if the app should seed it on fresh installs). - Read at runtime:
await db.getSetting('my_key'). - Admin-editable automatically through
PUT /api/admin/configwhich accepts arbitrary keys.
Physician memory / correction tracker
- On note generation,
trackAIOutput(elementId, text)captures the original output in memory. - User edits the note in a contenteditable field.
- On Save,
saveCorrection(elementId, section)diffs current vs. original. - If changed by > 2 words or > 20 characters,
POST /api/memories/correctionstores the before/after inuser_memorieswith categorycorrection_{section}. - Next generation:
GET /api/memories/contextfetches the 10 most recent per category andsrc/utils/prompts.jsinjects them as[STYLE HINTS (low priority)]200-character snippets.
Tabs with correction capture: Live Encounter, SOAP, Dictation, Sick Visit, Well Visit (Hospital Course and Chart Review save corrections when available but don't always have a trackable single output element).
Maximum 20 corrections retained per category (oldest deleted).
Route reference
| File | Mount | Auth | Purpose |
|---|---|---|---|
auth.js |
/api/auth |
Public | Register, login, 2FA, email verify, password reset, backup codes |
oidc.js |
/api/auth |
Public | OIDC SSO (Authorization Code + PKCE) |
sessions.js |
/api/sessions |
Auth | Active sessions list + revoke |
hpi.js |
/api |
Auth | HPI from encounter or dictation |
soap.js |
/api |
Auth | SOAP generation |
chartReview.js |
/api |
Auth | Chart review / pre-charting |
hospitalCourse.js |
/api |
Auth | Hospital course |
wellVisit.js |
/api |
Auth | Well visit + SSHADESS |
sickVisit.js |
/api |
Auth | Sick visit |
milestones.js |
/api |
Auth | Developmental milestone narratives |
refine.js |
/api |
Auth | Refine / shorten / clarify |
transcribe.js |
/api |
Auth | STT (5 providers) |
tts.js |
/api |
Auth | TTS (3 providers) |
encounters.js |
/api |
Auth | Save / load / optimistic-lock encounters |
memories.js |
/api |
Auth | Templates + corrections |
audioBackups.js |
/api |
Auth | Encrypted audio retry store |
documents.js |
/api |
Auth | S3 documents (magic-byte checked) |
userPreferences.js |
/api |
Auth | Per-user STT/TTS choice |
nextcloud.js |
/api |
Auth | Encrypted WebDAV tokens |
billing.js |
/api |
Auth | ICD-10 / CPT code suggestion |
logs.js |
/api |
Auth | Usage + audit dump (admin-only endpoints gated further) |
admin.js |
/api/admin |
Admin | User management |
adminConfig.js |
/api/admin |
Admin | Settings, prompts, models, SMTP, OIDC |
adminMilestones.js |
/api/admin |
Admin | Milestone data management |
learningHub.js |
/api/learning |
Auth | Content delivery + quizzes |
learningAdmin.js |
/api/admin/learning |
Moderator | CMS CRUD |
learningAI.js |
/api/admin/learning |
Moderator | AI content gen, PPTX export |
Frontend JS module reference
| File | Purpose |
|---|---|
secureStorage.js |
Platform-branched token storage (Keychain / localStorage) |
authFetch.js |
Global fetch wrapper (401 → logout + reload) |
auth.js |
Login / register / SSO / session management / Turnstile / backup-code modal |
app.js |
Tab navigation, model selector, audio recorder, transcription orchestration |
liveEncounter.js |
Live recording UI + live preview |
soap.js, hpi.js (none — in liveEncounter), sickVisit.js, wellVisit.js, hospitalCourse.js, chartReview.js |
Clinical tabs |
milestones.js + milestonesData.js |
Milestones tab |
shadess.js |
SSHADESS adolescent assessment |
encounters.js |
Save / load / resume with optimistic lock |
memories.js |
Physician templates + corrections UI |
correctionTracker.js |
Captures AI-output edits |
browserWhisper.js |
In-browser WASM Whisper |
speechRecognition.js |
Web Speech API preview |
voicePreferences.js |
Per-user STT/TTS override |
audioBackup.js |
Server + IndexedDB backup retries |
nextcloud.js |
Connect / export |
documents.js |
S3 upload / download |
calculators.js |
Pediatric calculators (BP, BMI, growth, bilirubin, vitals, etc.) |
learningHub.js |
Content browser + CMS editor |
admin.js |
Admin panel (users, settings, prompts, models) |
Common tasks
Change default temperature
Edit the default in callAI() in src/utils/ai.js. Per-call options.temperature
overrides.
Override a prompt without deploying
Admin Panel → Prompts → pick the template → edit → save. Takes effect immediately; cache is invalidated on write.
Add a model to the dropdown
Admin Panel → Models → Add Custom Model. Exact provider ID, display name,
cost string, category (free / fast / smart / premium). Appears
immediately for all users.
Trace an AI call
docker compose logs -f pediatric-scribe | grep '\[AI\]'
Or:
SELECT endpoint, model_used, tokens_input, tokens_output, duration_ms, cost_estimate, error
FROM api_log
ORDER BY timestamp DESC
LIMIT 20;
Force a re-index
docker exec -w /app pediatric-ai-scribe npm run maint:reindex
Runs after any Postgres image upgrade if the startup drift check didn't catch it.
Local development
docker compose up -d postgres # just the DB
npm install
cp .env.example .env # set JWT_SECRET, DATA_ENCRYPTION_KEY, provider credentials
node server.js
App binds http://localhost:3000. Without APP_URL, production-mode guards
relax (open CORS, non-secure cookies) — never deploy like this.