Repository navigation
04 Backend Architecture
The HealthBook AI backend is a single FastAPI application (internal code name: BabyCare) that serves the JSON API under /api and, when the web/ directory is present, the static web client at /. Building api/main.py does all the startup work: it migrates the schema, locks down the database, prepares the private document store and installs the middleware. It then includes twelve feature routers. A separate worker process delivers reminders. This page covers how the pieces fit together; the domain rules live on the feature pages.
- Expose one HTTP API for the web app and the Android app. The mobile client sends a bearer token; the web client uses a session cookie plus a CSRF header.
- Enforce transport guardrails (origin check, rate limit, request size, security headers) before any handler runs.
- Authenticate every protected request, then check that the caller owns the profile or record, or has caregiver access to it.
- Keep clinical text encrypted at the application layer and original files encrypted in a private store.
- Bring the schema to the Alembic head on every start of the API and the worker.
| Input | Output | Notes |
|---|---|---|
HTTPS request to /api/...
|
JSON, PDF, ZIP, CSV or image bytes | All responses get Cache-Control: no-store
|
Authorization: Bearer <token> (mobile) |
Authenticated User
|
Token is looked up by SHA-256 hash |
babycare_session cookie + X-Requested-With: BabyCare (web) |
Authenticated User
|
CSRF header required on non-GET |
X-Scheduler-Key header |
One reminder worker tick |
/api/internal/* only |
X-Camera-Worker-Key header |
Camera config / event ingest | Home camera worker only |
| Multipart upload | Encrypted object + documents row |
12 MB, 35 pages, magic-byte type check |
api/main.py runs these steps in order when it is imported:
- Creates
FastAPI(title='HealthBook AI — local prototype'). - Calls
migrate.upgrade(), which takes a file lock and runs Alembic tohead. - Calls
cloud.harden_database(engine). On PostgreSQL this enables RLS on every table and revokes the Supabase client roles. On SQLite it does nothing. - Calls
cloud.prepare_storage(), which creates or verifies the private bucket when the Supabase backend is selected. - Calls
cloud.install_handlers(app), which mapsMissingObjectto 404 andStorageErrorto 503 without leaking object names. - Registers
guardrails.secure_headersas HTTP middleware. IfBABYCARE_CORS_ORIGINSis set, it also addsCORSMiddlewarewith an explicit origin list, credentials allowed, and the headersAuthorization,Content-Type,X-Requested-WithandX-Share-Pin. - Defines the core routes in
main.pyitself: auth, profiles, documents, medicines, visits, vaccines, observations, reminders, today, timeline, search, doctor summary and health. - Includes the routers in this order:
workflows,providers,history_api,lifetime_api,cloud,share,care_api,camera_api,export_api,vitals_api,profiles_api,password_reset. - Mounts
StaticFiles(web/, html=True)at/.
All routers use prefix='/api', except password_reset, which uses /api/auth/password-reset. In care_api, the generic /care/{kind} routes are registered last so that specific paths such as /care/day and /care/settings match first.
flowchart TD
C["Web or Android client"] -->|"HTTPS"| CORS["CORSMiddleware<br/>explicit origins only"]
CORS --> G["secure_headers middleware"]
G --> O{"Unsafe method with<br/>foreign Origin?"}
O -->|"yes"| R403["403 Origin not allowed"]
O -->|"no"| RL{"rate_allowed per IP<br/>auth 30 per min, api 600 per min"}
RL -->|"exceeded"| R429["429 with Retry-After 60"]
RL -->|"ok"| SZ{"Content-Length over 13 MiB?"}
SZ -->|"yes"| R413["413 Request too large"]
SZ -->|"no"| AUTH["require_user<br/>bearer or cookie plus CSRF header"]
AUTH -->|"invalid or expired"| R401["401"]
AUTH --> OWN["own_patient or own_resource<br/>owner or caregiver grant"]
OWN -->|"no grant"| R404["404 Not found"]
OWN -->|"viewer writing"| R403b["403 Read-only caregiver"]
OWN --> H["Route handler<br/>Pydantic validation"]
H --> DB[("SQLAlchemy session<br/>EncryptedText columns")]
H --> ST[("storage.py<br/>local dir or private bucket")]
H --> AU["audit row in same transaction"]
H --> RESP["Response plus security headers"]
| Control | Behaviour |
|---|---|
| Origin check | On /api/* requests that are not GET, HEAD or OPTIONS and carry an Origin, the origin must equal the API's own base URL or be listed in BABYCARE_CORS_ORIGINS. Otherwise the request gets 403. |
| Health bypass |
/api/health skips rate limiting, so it never touches the database. |
| Rate limit |
rate_allowed(key, limit, seconds) keys on auth:<ip> (30 per 60 s) for /api/auth/* and on api:<ip> (600 per 60 s) for everything else. The key is SHA-256 hashed before storage. The check runs in the threadpool so it cannot block the event loop. |
Store: db (default) |
One rate_windows row per key. A conditional UPDATE ... WHERE count < limit makes the increment atomic. A first-insert race is retried after IntegrityError. Counts persist across restarts and are shared across instances. |
Store: memory
|
BABYCARE_RATE_LIMIT_STORE=memory gives a fixed window per process under a lock, with no database write per request. It suits the single free instance. Restarts reset the counts, which only makes the limiter more lenient. The map is cleared once it passes 50,000 keys. |
| Retry-After | A 429 response carries Retry-After: 60 and a generic message. |
| Request size | A Content-Length over 13 MiB gets 413, and a non-numeric one gets 400. Upload handlers also cap the body they read at 12 MB. |
| Headers |
X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer, Cache-Control: no-store, X-Frame-Options: DENY, Permissions-Policy: camera=(self), microphone=(), geolocation=(), plus a CSP: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self'; frame-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'. HSTS (one year, includeSubDomains) is added only when BABYCARE_ENV=production. |
Other modules call the same rate_allowed for narrower limits. For example, a password reset request is limited to 3 per hour per email hash.
-
get_db()yields aSessionLocal()session withautoflush=Falseandexpire_on_commit=False. On PostgreSQL the pool is sized for the free pooler (default 5 + 5 overflow, recycled every 900 s, TLSsslmode=require, no server-side prepared statements). -
require_useraccepts a bearer token or thebabycare_sessioncookie. Cookie requests that are not GET, HEAD or OPTIONS must sendX-Requested-With: BabyCare, or they get 403. The token is matched by SHA-256 againstsessions.token_hashand its expiry is checked. The dependency also marksuser._write_access: true for unsafe methods, except paths ending in/askor/signed-access. -
own_patient(db, user, pid)returns the profile when the caller is its owner or holds acaregiver_accessgrant. A missing grant returns 404, so the API does not confirm that the profile exists. Aviewergrant that attempts a write returns 403. -
own_resource(db, user, Model, id)loads any row that has apatient_idand appliesown_patientto it. -
workflows.guardian()adds a primary-guardian check (owner only) for sharing, invitations, the audit log, export and AI vision consent.
See Family Profile Isolation and Authentication and Session Security.
upgrade() takes FileLock(DATA_DIR/migration.lock, timeout=60), so the API and the worker never migrate at the same time. On SQLite, if the database has patients and is not at head, it first writes an encrypted snapshot named before-migration-<UTC timestamp>.sqlite.enc to the data directory. The snapshot is serialized in memory and then Fernet-encrypted. Alembic then runs upgrade head. Downgrades in the migrations raise errors on purpose; recovery means restoring an encrypted backup. See Database and Data Model.
python -m api.worker migrates, takes reminder-worker.lock, and on PostgreSQL also takes pg_try_advisory_lock, so only one worker runs. It calls tick() every 15 s; with BABYCARE_WORKER_ONCE=1 it runs once and exits. A tick does three things:
- Reconciles confirmed plans into
remindersandnotificationsfor every profile. - Delivers up to 100 due push notifications through Expo or Web Push (VAPID), always with the same generic sentence.
- Updates
worker_heartbeat.
Retries back off exponentially, up to 6 attempts. Receipts are polled, so an accepted push is never sent twice. On free hosting, where the instance sleeps, an external scheduler calls POST /api/internal/scheduler/tick with X-Scheduler-Key to run the same tick(). A reminder is never proof that a dose was given. See Notifications and Reminder Architecture.
get_storage() picks LocalStorage (DATA_DIR/encrypted_files, files opened with O_EXCL and mode 0600) or SupabaseStorage (a private bucket reached only with the service-role key, with three retries on transport errors), according to BABYCARE_STORAGE. Callers use write_document, read_document, delete_document, document_exists and list_documents. Callers always pass bytes that are already encrypted, and object names are random <48 hex>.enc strings. In production, local storage is refused unless BABYCARE_ALLOW_LOCAL_STORAGE=true. See Encryption and Private Storage.
| Module | Responsibility |
|---|---|
__init__.py |
Package marker |
main.py |
App factory, startup sequence, core routes (auth, profiles, documents, medicines, vaccines, visits, observations, reminders, timeline, search) |
config.py |
Environment, DATA_DIR, DATABASE_URL normalisation, production refusals |
database.py |
Engine, pool options, SQLite FK pragma, SessionLocal, get_db
|
models.py |
Core ORM tables (users through record revisions) |
care_models.py |
Daily Care journal, stool photos and intake, camera tables |
lifetime_models.py |
Fever episodes, antibiotic courses and administrations, evidence, parent entries |
share_models.py |
Share packages and access log |
vitals_models.py |
Vitals journal rows |
security.py |
PBKDF2 password hashing, session issue, require_user, own_patient, own_resource
|
guardrails.py |
Origin check, rate limiting, size limit, security headers |
crypto.py |
Fernet key handling and the EncryptedText column type |
storage.py |
Local and private Supabase blob backends |
cloud.py |
RLS lock-down, bucket preparation, error handlers, scheduler tick, internal status, web push subscriptions |
migrate.py |
Locked Alembic upgrade with encrypted pre-migration snapshot |
worker.py |
Reminder reconciliation and push delivery loop |
reminders.py |
Confirmed plans to persisted reminders and notifications |
push_web.py |
VAPID Web Push with a fixed generic body |
mailer.py |
Transactional email over HTTPS providers |
password_reset.py |
Reset token table and /api/auth/password-reset/* routes |
workflows.py |
Review/confirm, signed file access, admissions, visit series, administrations, episodes, growth, family, notifications, devices, audit, export, account deletion |
review_validation.py |
Field-level validation for "Confirm and save to history" |
extract.py |
Local OCR and medication candidates |
extraction.py |
Page-linked structured candidates and document classification |
vaccination.py |
Reference-only schedule charts and card date parsing |
providers.py |
Opt-in external vision transcription with per-document consent |
history.py |
remember(), which writes the previous values to record_revisions
|
history_api.py |
Dashboard, history, medication, vaccination and lab series, thumbnails, document back-links |
intelligence.py |
Extractive "Ask" answers grounded in authorized records |
reporting.py |
Source-labelled timeline additions and the text summary |
lifetime.py, lifetime_api.py
|
Lifetime fever and antibiotic analysis and its API |
care.py, care_api.py
|
Daily Care engine and API |
care_vision.py |
Descriptive stool-image observations |
stool_intake.py |
Capture-time detection for stool photos |
vitals.py, vitals_api.py
|
Vitals validation, summaries and API |
profiles.py, profiles_api.py
|
Age groups, per-profile modules, profile API |
share.py |
Share with Doctor scopes, ZIP and PDF, doctor links |
export_model.py, export_pdf.py, export_api.py
|
Record export model, PDF renderer, API |
camera.py, camera_api.py, camera_worker.py
|
Camera helpers, API, local worker |
datasync.py |
Dialect-neutral raw table copy for migration and backup |
backup.py |
Encrypted SQLite backup and restore CLI |
- Every handler that reads profile data calls
own_patientorown_resourcebefore it touches records. - The source-document checks (
safe_doc) reject a document that belongs to another profile (400) or a page outside the original (422). -
as_jsonnever serialisespassword_salt,password_hash,token_hash,extracted_cipher,storage_nameorsha256.
api/main.py, api/guardrails.py, api/security.py, api/database.py, api/migrate.py, api/worker.py, api/storage.py, api/cloud.py
System Architecture · API Design · Database and Data Model · Security and Privacy Model · Deployment Architecture
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