# ped-ai — Application Architecture (Deep Dive) > **Audience.** Engineers working in this repo (and Daniel six months from now). > Assumes familiarity with Node, Express, Postgres, and vanilla DOM. No > framework knowledge required — there isn't one. > > **Scope.** Top-to-bottom mechanical description of how the app is wired: > the IIFE frontend, the Express composition root, the schema, encryption > at rest, migrations, deployment, and the sacred zones you must not > casually edit. > > Companion docs: `docs/architecture.md` (one-page overview Daniel > maintains), `docs/database.md`, `docs/deployment.md`, > `docs/configuration.md`. This file is the long-form version that pulls > the threads together with file:line citations. --- ## 1. Overview ### What this is ped-ai (PedScribe / Pediatric AI Scribe) is a **self-hosted, single-tenant clinical documentation platform for pediatricians**. It runs as two Docker containers — an Express app and a PostgreSQL database — behind a user-supplied reverse proxy. The audience is one practice (often one clinician), not a multi-tenant SaaS. There is no per-tenant isolation because there is no concept of a tenant: every authenticated user shares the same Postgres schema with row-level `user_id` scoping. ### Top-level shape ``` ┌────────────────────────────────────────┐ │ Reverse proxy (Caddy / Nginx / etc.) │ │ TLS termination + host routing │ └────────────────────┬───────────────────┘ │ 127.0.0.1:3552 │ ┌────────────────────────────────────┴─────────────────────────────────┐ │ pediatric-ai-scribe (node:20-alpine, Express 4) │ │ ┌────────────────────────────────────────────────────────────────┐ │ │ │ static (public/) → vanilla JS SPA shell │ │ │ │ /api/* → 28+ Express routers │ │ │ │ /api/health → Docker healthcheck │ │ │ └────────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ TCP 5432 │ outbound HTTPS │ │ ▼ ▼ │ │ ┌──────────────┐ ┌────────────────────────┐ │ │ │ PostgreSQL │ │ AI provider │ │ │ │ pgvector │ │ Bedrock / Vertex / │ │ │ │ pg16 │ │ Azure / LiteLLM / │ │ │ │ │ │ OpenRouter │ │ │ └──────────────┘ └────────────────────────┘ │ │ │ │ Optional siblings: Loki (logs), Grafana (metrics), │ │ OpenBao (secrets), Nextcloud (WebDAV upload), S3 (documents) │ └──────────────────────────────────────────────────────────────────────┘ ``` ### Tech stack | Layer | Choice | | ---------------- | ------------------------------------------------------------ | | Runtime | Node.js 20 (alpine) | | HTTP | Express 4 + helmet + cors + cookie-parser + express-rate-limit | | DB | PostgreSQL 16 with `pgvector` extension | | DB driver | `pg` (pool, max 20) | | Migrations | `node-pg-migrate` (programmatic) | | Auth | JWT (HS256) signed locally + DB-backed `user_sessions` table | | Secrets | argon2id (current), bcrypt (legacy / rehash on next login) | | AI | Provider-agnostic via `src/utils/ai.js` → OpenAI SDK shape | | STT | OpenAI Whisper API / Google Gemini / AWS Transcribe / local whisper.cpp / browser Whisper (transformers.js) | | TTS | Google Cloud TTS / ElevenLabs / LiteLLM-routed | | Frontend | **Vanilla JS, no framework, no bundler.** Plain ` ``` Inside, it uses real `import`: ```js // public/js/bedside/index.js import './shared.js'; // side-effect: sets window._EM for back-compat import * as ageWeight from './age-weight.js'; import * as subNav from './sub-nav.js'; // ... ~18 modules [ ageWeight, subNav, ..., sutures ].forEach(function (m) { if (m && typeof m.init === 'function') m.init(); }); ``` This pocket exists because: - Bedside has 20 sibling files (one per pathway: airway, sepsis, NRP, etc.). Loading them all as IIFEs would mean 20 more ` ``` ### 5. Server route Create `src/routes/mytab.js`: ```js var express = require('express'); var router = express.Router(); var db = require('../db/database'); var { authMiddleware } = require('../middleware/auth'); var logger = require('../utils/logger'); router.use(authMiddleware); router.post('/mytab', async function (req, res) { try { var hello = (req.body || {}).hello; if (!hello) return res.status(400).json({ error: 'hello required' }); res.json({ success: true, echoed: hello, user: req.user.email }); logger.audit(req.user.id, 'mytab_call', 'mytab call: ' + hello, req, { category: 'general' }); } catch (e) { logger.error('POST /mytab', e.message); res.status(500).json({ error: 'Request failed' }); } }); module.exports = router; ``` ### 6. Mount the route in `server.js` Add to the authenticated feature router list near `server.js:279–303`: ```js app.use('/api', require('./src/routes/mytab')); ``` ### 7. Prompt entry (only if you're calling AI) If your route calls `callAI`, add the prompt template to `src/utils/prompts.js`: ```js PROMPTS.mytab = function (input) { return `You are a pediatric assistant. ${input}`; }; ``` Live-editable from the Admin Panel via `app_settings` key `prompt.mytab`. ### 8. Encounter API integration (only if it persists like an encounter) If your tab needs save/resume, **don't** create a new persistence table. Use `POST /api/encounters/saved` with a new `enc_type` value (e.g. `enc_type: 'mytab'`). The tab list at section 11 shows existing values. Add your new type to whichever client code lists encounter types (`public/js/encounters.js`'s rendering of saved encounters). **This one piece touches sacred code** — get per-change approval. ### 9. Lint check ```bash node scripts/lint-references.js ``` Should print `✓ All JS id references resolve to an HTML id.` and `✓ All asset paths resolve to a file on disk.` ### 10. Restart and test ```bash docker compose restart pediatric-scribe ``` Open the app, click the new tab, click the button, see the JSON echo. Look at `audit_log`: ```sql SELECT * FROM audit_log WHERE action = 'mytab_call' ORDER BY timestamp DESC LIMIT 5; ``` ### 11. Commit ``` feat(mytab): add demo tab end-to-end - Sidebar button + tab placeholder - public/components/mytab.html - public/js/mytab.js - src/routes/mytab.js mounted at /api/mytab - Audit category: general ``` If the new tab is admin/moderator-only, add a role check before the sidebar button toggle in `auth.js` (look for the existing pattern around `cms-tab-btn` and `admin-tab-btn`). --- ## Appendix A — File map (compressed) ``` server.js # composition root (388 lines) docker-entrypoint.sh, Dockerfile, docker-compose{,.local,.e2e,.monitoring}.yml src/ db/ database.js, migrate.js middleware/ auth.js, logging.js routes/ 32 Express routers — see section 9 table utils/ config, crypto, auditQueue, redact, logger, errors, fileType, platform, notify, sessions, passwords, promptSafe, prompts, models, ai, embeddings, transcribe*, tts* migrations/ # node-pg-migrate, timestamp-prefixed public/ index.html # ~38