Repository navigation
25 Security and Privacy Model
HealthBook AI (internal code name: BabyCare) holds a family's medical documents and daily care records, so its security model assumes the hosting platform, the database dashboard or a stored file could be exposed, and limits what each layer reveals. This page brings the controls the code implements into one model: transport and headers, authentication, profile ownership, application-layer encryption, database lock-down, private object storage, and the privacy rules for sharing, push, AI and the home camera. Each control names the module that implements it.
- Authenticate every account-scoped request and authorize it against the specific family profile.
- Keep clinical content unreadable at rest without the application key, and keep the storage and database tiers closed to client-side keys.
- Minimise disclosure: push payloads, logs, share access logs and the internal status endpoint carry no medical detail.
- Make every disclosure outside the family explicit, scoped, time-limited and revocable: doctor links, export, external AI.
- Leave an audit trail of writes, exports, shares and consents.
flowchart TD
subgraph clients["Clients"]
W["Web app<br/>cookie plus CSRF header"]
A["Android app<br/>bearer token"]
D["Doctor browser<br/>share token plus PIN"]
K["Home camera worker<br/>worker key"]
end
W -->|"HTTPS"| GW["FastAPI guardrails<br/>origin, rate limit, size, CSP, HSTS"]
A -->|"HTTPS"| GW
D -->|"HTTPS"| GW
K -->|"HTTPS"| GW
GW --> AUTH["Authentication<br/>require_user, hashed session lookup"]
GW --> SHARE["Share token check<br/>hash, expiry, revoke, PIN"]
GW --> CAMK["Camera key check<br/>hash of worker key"]
AUTH --> OWN["Profile ownership check<br/>own_patient and own_resource"]
SHARE --> SCOPE["Package scope only<br/>selected record ids"]
OWN --> APP["Handlers<br/>validation and audit"]
SCOPE --> APP
CAMK --> APP
APP --> ENC["Application-layer encryption<br/>Fernet EncryptedText"]
ENC --> PG[("PostgreSQL<br/>RLS on every table<br/>client roles revoked")]
APP --> ENCF["Fernet encrypt originals"]
ENCF --> OBJ[("Private Supabase object storage<br/>service-role key on server only")]
The API connects to PostgreSQL as the table owner, which RLS does not restrict. harden_database() enables row level security on every public table and revokes all table, sequence, function and schema privileges from the Supabase anon and authenticated roles. Anyone holding a client key for the hosted REST or GraphQL endpoints can therefore read nothing. In production, a hardening failure stops the server from starting. GET /api/internal/status reports any tables without RLS and any remaining grants.
| Threat | Control | Module |
|---|---|---|
| Cross-site script injection or clickjacking | CSP default-src 'self', script-src 'self', object-src 'none', frame-ancestors 'none'; X-Frame-Options: DENY; nosniff; Referrer-Policy: no-referrer
|
api/guardrails.py |
| Downgrade or sniffing on the network | HTTPS hosting; HSTS for one year in production; Secure session cookie in production |
api/guardrails.py, api/main.py
|
| CSRF against the cookie session |
SameSite=Strict cookie; non-GET cookie requests need X-Requested-With: BabyCare; unsafe requests from foreign origins get 403; CORS limited to explicit origins |
api/security.py, api/guardrails.py
|
| Credential stuffing or API flooding | 30 auth requests per minute and 600 API requests per minute per IP; 429 with Retry-After: 60; database or memory store |
api/guardrails.py |
| Stolen database rows reveal sessions | Session token token_urlsafe(36), only its SHA-256 stored; 14-day expiry; logout deletes the row |
api/security.py |
| Password database theft | PBKDF2-HMAC-SHA256, 390,000 iterations, random 16-byte salt per user; minimum length 12; constant-time comparison; one error message for unknown email and wrong password |
api/security.py, api/main.py
|
| Account enumeration or abuse through password reset | Always 202 with the same message; email sent in the background; 3 requests per hour per email hash; token is 32 random bytes, stored only as SHA-256, valid 30 minutes, single use, newest link wins; carried in the URL fragment; confirmation signs out every session | api/password_reset.py |
| Another family reading a profile |
own_patient and own_resource on every route; 404 for foreign ids; safe_doc rejects a source document from another profile |
api/security.py, api/main.py
|
| A read-only caregiver changing records | Writes need an editor grant; sharing, invitations, audit, export, camera keys and AI consent are primary-guardian only; revoking a caregiver cancels their pending notifications |
api/security.py, api/workflows.py
|
| Database or bucket dump | Fernet encryption of clinical text and originals; the key is never stored alongside the data; startup refuses to generate a new key when encrypted data exists |
api/crypto.py, api/storage.py
|
| Supabase client keys used directly | RLS on every table and grants revoked; the bucket must be private, or startup fails |
api/cloud.py, api/storage.py
|
| Long-lived file URLs leaking | Fernet-sealed tickets valid for 120 s and bound to a live session; no public object URLs | api/workflows.py |
| Doctor link guessed, forwarded or kept | 256-bit token, only its hash stored; optional 4 to 12 digit PIN with salted PBKDF2; 8 failed PINs in 15 minutes gives 429; expiry of 1 h, 24 h, 3 d or 7 d; revocation (410); view-only and download switches; noindex and no-store headers; documents addressed by index inside the package only |
api/share.py, api/share_models.py
|
| Share access log leaking content | The log stores the event, time, a SHA-256 of the IP, and a short label; never clinical content | api/share.py |
| Camera credentials or video leaving home | Credentials embedded in the stream URL are moved into encrypted columns; private-network hosts by default; worker key from token_urlsafe(32) is shown once and stored as a hash; only a key holder can fetch the stream config; the worker sends derived events only, never raw video or audio |
api/camera.py, api/camera_api.py, api/camera_worker.py
|
| Lock screen exposing medical detail | Expo and Web Push send one fixed sentence ("You have a confirmed care reminder. Open HealthBook AI to review it."); registration requires consent_generic_push: true
|
api/worker.py, api/push_web.py, api/cloud.py
|
| Silent transfer to an external AI | Off unless configured; per-document consent flag plus a matching provider hostname; HTTPS only; guardian only; at most 10 pages; the consent is audited before transmission; the result stays needs_review
|
api/providers.py |
| Scheduler endpoint abuse | Returns 404 unless BABYCARE_SCHEDULER_KEY has at least 24 characters; header compared in constant time; returns counts only |
api/cloud.py |
| Spreadsheet formula injection in exports | CSV cells starting with = + - @ or a tab or CR get a leading quote |
api/workflows.py |
| Logs containing PHI | The middleware never logs bodies; the worker, mailer and storage log error classes only; email bodies and addresses are not logged |
api/guardrails.py, api/worker.py, api/mailer.py
|
-
Data export.
GET /api/patients/{pid}/export(guardian only) returns a ZIP withrecords.json, the decrypted originals, transcriptions and review payloads. The selective PDF and ZIP exports inapi/export_api.pyandapi/share.pyare built in memory and audited. -
Deletion.
POST /api/account/deleterequires the password and the exact phraseDELETE MY ACCOUNT AND OWNED CHILD RECORDS. Owned profiles cascade, the encrypted blobs are deleted, and other families' profiles and records are untouched. The response reminds the operator that offline backups expire only under their own retention policy. -
Audit trail. The
audittable records user, profile, action, entity and id for every create, correction, confirmation, export, share, consent and password reset. Guardians read the latest 250 entries throughGET /api/patients/{pid}/audit. Previous values are kept inrecord_revisions.
A notification is never proof that a dose was given. OCR or AI output is extracted, not clinically verified, until a caregiver confirms it against the original. Stool-image interpretation is observational only. Share and export labels keep prescribed and administered information apart, and keep parent-reported entries apart from clinician-documented ones.
- There is no multi-factor authentication or account lockout beyond the per-IP rate limit.
- Dates, statuses and ids are not application-encrypted.
- The camera worker key is compared with a plain hash equality, not
hmac.compare_digest. - In-memory rate limiting is per process.
- Key rotation is not implemented.
See Limitations.
api/guardrails.py, api/security.py, api/crypto.py, api/storage.py, api/cloud.py, api/password_reset.py, api/share.py, api/providers.py, api/push_web.py, api/worker.py, api/camera.py, api/camera_api.py, api/workflows.py
Authentication and Session Security · Family Profile Isolation · Encryption and Private Storage · Share With Doctor Architecture · Home Camera Architecture · Notifications and Reminder 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