Repository navigation
05 Web Application Architecture
The HealthBook AI web app (internal code name: BabyCare) is a no-build, framework-free progressive web app in web/. Plain JavaScript files load in a fixed order from index.html, share one global state object, and build on each other by wrapping a small set of global functions. There is no bundler, no transpiler and no third-party browser SDK; the files the browser runs are the files in the repository.
- Render every signed-in view (Today, Records, Medicines, Hospital, Vaccinations, Timeline, Lab results, Daily Health, Care Calendar, Lifetime Health, Share, Export, Vitals, profiles) from data returned by the API.
- Talk to the API through one
request()helper that handles both authentication modes, the CSRF header, errors and cancellation. - Provide an installable offline shell through a service worker. The worker never caches API responses or patient records.
- Receive generic Web Push reminders.
- Serve the separate, login-free doctor page (
doctor.html) used by Share With Doctor.
| Input | Output |
|---|---|
window.BABYCARE_CONFIG.apiBase from config.js
|
API origin, and from it the auth mode (cookie or bearer) |
JSON from /api/...
|
HTML strings written into #app
|
User clicks (data-action, data-tab) and form submits |
execute(action, id, button) / submitForm(form) calls |
| Push events from the browser push service | A notification with fixed generic text |
URL fragment #t= on doctor.html, #reset-password= on /
|
Share package view, password reset flow |
index.html loads config.js synchronously, then these deferred scripts in this order: app.js, features.js, history.js, lifetime.js, charts.js, analytics.js, share.js, care.js, exports.js, profiles.js, password.js. Deferred scripts run in document order, so each file can rely on the globals defined by the files before it.
app.js defines the core: the state object S, NAV and ICONS, small helpers (esc, fmt, el), request(), load(), reload(), render(), layout(), modalHtml(), execute() and submitForm(), plus the document-level click, submit and change listeners that dispatch to them. A later module keeps a reference to the previous function and puts its own in its place:
const dcBaseRender = render;
render = function () { /* handle care/calendar/camera tabs */ dcBaseRender(); };A module handles the actions, tabs, forms and modals it owns and passes everything else down the chain. Two files work differently. charts.js defines the pure chart library PC (functions that return SVG/HTML strings) and wraps nothing. analytics.js replaces view functions (todayView, resultsView, vaccinesView, episodesView, growthView) rather than the dispatch functions.
flowchart TD
CFG["config.js<br/>BABYCARE_CONFIG.apiBase"] --> APP["app.js<br/>S, request, load, reload,<br/>render, execute, submitForm, modalHtml"]
APP --> FEAT["features.js<br/>wraps reload, layout, render, modalHtml, execute, submitForm"]
FEAT --> HIST["history.js<br/>wraps reload, modalHtml, execute, submitForm"]
HIST --> LIFE["lifetime.js<br/>wraps modalHtml, execute, submitForm, render, reload"]
LIFE --> CHARTS["charts.js<br/>defines PC chart library, wraps nothing"]
CHARTS --> ANA["analytics.js<br/>replaces todayView, resultsView,<br/>vaccinesView, episodesView, growthView"]
ANA --> SHARE["share.js<br/>wraps render, reload, execute"]
SHARE --> CARE["care.js<br/>wraps modalHtml, render, reload, execute, submitForm"]
CARE --> EXP["exports.js<br/>wraps render, execute"]
EXP --> PROF["profiles.js<br/>wraps render, execute, submitForm"]
PROF --> PW["password.js<br/>wraps render, execute, submitForm"]
PW --> DISPATCH["Document listeners in app.js call<br/>the outermost render, execute, submitForm"]
Because the listeners call the global names when an event fires, they always reach the outermost wrapper (password.js), and the call then works its way down to app.js.
S holds the session marker (token), user, patients, the selected profile pid, the active tab, the open modal, and the per-profile collections fetched by reload() (docs, meds, visits, vaccines, observations, reminders, today). Feature modules attach their own namespaces, for example S.dc (Daily Health), S.pv (Vitals), S.pwReset and S.pwForgot. reload() remembers the pid it started with and throws away its results if the user switched profiles while the requests were in flight.
| Concern | Behaviour |
|---|---|
| Base URL |
API_BASE + '/api' + path. API_BASE comes from config.js
|
| Auth mode |
TOKEN_MODE is true when API_BASE is non-empty. Cross-origin (static site plus a separate API): Authorization: Bearer <token>, token kept in sessionStorage, credentials: 'omit'. Same-origin: an httpOnly cookie, credentials: 'same-origin', and only a babycare_signed_in flag in sessionStorage
|
| CSRF | Every request sends X-Requested-With: BabyCare. The API requires this header on cookie-authenticated writes |
| Caching | cache: 'no-store' |
| 401 | On any non-/auth/ path: clear the token and flags, re-render the sign-in view, throw "Session expired" |
| Errors | Throws an Error with .detail, .status and .retryAfter (parsed from Retry-After) |
| Cancellation | Accepts signal. An AbortError is re-thrown unchanged so callers can ignore it |
In token mode, <img src="/api/..."> cannot carry the bearer header. A MutationObserver (protectImages) fetches those images with the header and swaps in blob: URLs.
care.js (Daily Health, Care Calendar, Today's care, Home camera) uses an explicit request lifecycle held in DCL.
flowchart TD
NAV["Explicit event<br/>tab click, date change, profile change,<br/>mutation, manual retry"] --> ENS["dcEnsure<br/>decide which resources the current view needs"]
ENS --> LOAD["dcLoad key, path, apply"]
LOAD --> PIDCHK{"DCL.pid is not equal to S.pid?"}
PIDCHK -->|"yes"| RESET["dcReset<br/>abort controllers, clear cache, gen plus 1"]
PIDCHK -->|"no"| CACHE{"Cached?"}
RESET --> CACHE
CACHE -->|"yes"| APPLY["apply cached data to S.dc"]
CACHE -->|"no"| INFL{"Already in flight?"}
INFL -->|"yes"| SHARED["return the same promise"]
INFL -->|"no"| COOL{"429 cooldown active?"}
COOL -->|"yes"| TIMER["single retry timer, no request"]
COOL -->|"no"| FETCH["request with AbortController signal"]
FETCH -->|"ok and gen and pid unchanged"| STORE["cache.set, apply to S.dc"]
FETCH -->|"429"| SETCOOL["cooldownUntil = Retry-After or 30 s"]
FETCH -->|"stale or aborted"| DROP["discard response"]
STORE --> RENDER["render<br/>pure: reads S, writes HTML"]
APPLY --> RENDER
SETCOOL --> RENDER
- Cache keys are scoped by profile, resource and parameter (for example
day:<pid>:<date>,month:...,analytics:...). -
dcInvalidate(prefixes)runs after mutations (dcRefresh) and onreload(). Nothing else empties the cache. -
dcReset()runs on logout, on a profile change and when a render finds no token. It aborts every outstandingAbortController, incrementsgenso late responses are dropped, and clears the cache, the in-flight map, the error and the cooldown timer. - When a 429 arrives,
dcStatus()shows a quiet card ("The server is busy. Retrying automatically in N s.") with a manual Retry button. One timer callsdcEnsure()again. Nothing retries in a loop.
In care.js, render() and every view function (careView, calendarView, cameraView and the rest) only read S and return HTML. They never start a request. Loads are triggered by navigation (a data-tab click listener calls dcEnsure() after app.js has rendered), by actions, by form submits and by reload().
An earlier version did start requests inside views when state looked stale. If a load failed, the state stayed stale, so every re-render triggered another fetch. Under a persistent 429 this became a render, fetch, fail, render loop running at network speed. The loop kept the per-IP rate limit window full and locked the whole client out. The explicit lifecycle above replaced it, and tests/browser/request_storm.spec.js covers it (dashboard, Daily Health, mutation, date change, profile change, logout and mocked 429). Render also snapshots and restores any open dialog's inputs (dcSnapModal / dcRestoreModal), so a background load does not erase text the user has typed.
- On
installit precaches the shell (/, the CSS and JS files,/icon.svg) under a versioned cache name. Onactivateit deletes older caches. - On
fetchit ignores non-GET requests and any path under/api/, so those go straight to the network. Other GETs are network-first, with the cache as a fallback only when offline. - On
pushit showstitleandbodyfrom the payload, falling back to the generic sentence "You have a confirmed care reminder. Open HealthBook AI to review it." It usestag: 'babycare-reminder'and keeps onlynotification_idas data.notificationclickfocuses or opens the app.
enableWebPush() in app.js reads /api/push/web/config, subscribes with the VAPID public key and posts the subscription with consent_generic_push: true. See Notifications and Reminder Architecture.
config.js is generated by scripts/configure_deployment.py. It sets apiBase (for example https://<api>.onrender.com) only when the page is served from the static hosting hostnames (https://<project>.web.app). A local server keeps same-origin /api and cookie mode.
doctor.html and doctor.js form a separate, login-free page. It reads the share token from the URL fragment (#t=...), which the browser does not send in the page request. It calls /api/share/public/{token}, asks for a PIN on 401 and sends it as X-Share-Pin, and shows clear messages for 404/410 (expired or revoked) and 429 (too many PIN attempts). It loads only config.js, analytics.css and share.css, not the app modules, and sets noindex and no-referrer.
- The service worker never caches
/api/, and the API sendsCache-Control: no-storeon every response. - The CSP (
script-src 'self') blocks inline and third-party scripts. All HTML built from data goes throughesc(). - Tokens are kept in
sessionStorage, neverlocalStorage. In cookie mode, JavaScript cannot read the session cookie.
The web app only displays what the API returns. charts.js draws only values the API returned and infers nothing on the client. Labels keep "Prescribed ≠ Scheduled ≠ Given", "OCR extracted ≠ clinically verified" and "appointment attended ≠ treatment administered" visible in the UI. A browser notification is never proof that a dose was given.
web/index.html, web/config.js, web/app.js, web/features.js, web/history.js, web/lifetime.js, web/charts.js, web/analytics.js, web/share.js, web/care.js, web/exports.js, web/profiles.js, web/password.js, web/sw.js, web/doctor.html, web/doctor.js, tests/browser/request_storm.spec.js.
System Architecture · Authentication and Session Security · Daily Health and Care Calendar · Share With Doctor Architecture · Notifications and Reminder Architecture · API Design · Testing and Validation
Overview
Platform
Identity and access
Medical records
- 10 Document Ingestion
- 11 OCR and Human Verification
- 13 Medicines and Administration
- 14 Vaccination Model
- 15 Labs, Growth, Longitudinal
- 16 Illness and Lifetime Health
Daily care
Output and sharing
Security
Engineering
- 26 Testing and Validation
- 27 Deployment and CI/CD
- 28 Technology Stack
- 29 Design Decisions
- 30 Limitations
- 31 Roadmap