Repository navigation
09 Family Profile Isolation
One HealthBook AI account (internal code name: BabyCare) can hold several family profiles: the user, a child, a parent or any other family member. Every document, medicine, vaccination, lab value, Daily Health entry and timeline item belongs to exactly one profile, and the API checks that ownership on every request. Records are never merged or shared between profiles. Another account can see a profile only through an explicit caregiver grant.
- Model each profile as one
patientsrow owned by one account (patients.user_id). - Resolve every profile-scoped or resource-scoped request through
own_patient()/own_resource(). - Return 404, not 403, for resources the caller has no relationship to.
- Reject citations of source documents from another profile.
- Grant other accounts
vieweroreditoraccess through single-use invitations, which only the owner can issue or revoke.
| Endpoint | Purpose | Ownership rule |
|---|---|---|
POST /api/patients |
Create a profile (name, DOB, tz, sex, relationship) | Owned by the caller |
GET /api/patients |
List profiles | Owned ones plus those with a caregiver_access row |
GET/PATCH /api/patients/{pid}/profile |
Profile metadata and age group | own_patient |
PUT /api/patients/{pid}/modules |
Show or hide modules for this profile | own_patient |
POST /api/patients/{pid}/invitations |
Issue an invitation code (viewer or editor, guardian_permission: true) |
Owner only (guardian) |
POST /api/invitations/accept |
Accept a code | Code hash, unaccepted, not expired, same email |
GET /api/patients/{pid}/family |
Owner flag and members | own_patient |
DELETE /api/patients/{pid}/family/{access_id} |
Revoke a caregiver | Owner only |
flowchart TD
ACC["Account demo@example.invalid"]
OTHER["Other account without a grant"]
CG["Caregiver account with grant on A"]
subgraph BOUNDARY["Authorization boundary - require_user then own_patient or own_resource on every request"]
subgraph PA["Family Profile A - Alex Example"]
A1["Reports and documents"]
A2["Medicines"]
A3["Vaccinations"]
A4["Labs"]
A5["Daily Health"]
A6["Timeline"]
end
subgraph PB["Family Profile B - isolated"]
B1["Reports and documents"]
B2["Medicines"]
B3["Vaccinations"]
B4["Labs"]
B5["Daily Health"]
B6["Timeline"]
end
end
ACC -->|"owns"| PA
ACC -->|"owns"| PB
A2 -.->|"source_document_id must be in A"| A1
B2 -.->|"source_document_id must be in B"| B1
A2 -.-x|"citing a B document returns 400"| B1
OTHER -.-x|"404 Not found"| BOUNDARY
CG -->|"viewer reads, editor writes"| PA
patient = db.get(Patient, patient_id)
if not patient: raise HTTPException(404, 'Not found')
if patient.user_id != user.id:
grant = db.query(CaregiverAccess).filter_by(patient_id=patient_id, user_id=user.id).first()
if not grant: raise HTTPException(404, 'Not found')
if user._write_access and grant.role != 'editor':
raise HTTPException(403, 'Read-only caregiver access')own_resource(db, user, Model, id) loads the row, reads its patient_id and runs the same check. Routes keyed by a record id rather than a profile id (/api/medicines/{id}, /api/vaccines/{id}, /api/documents/{id}/download, lifetime courses, care entries, cameras and so on) are therefore protected exactly like routes under /api/patients/{pid}/.... Every router (main, workflows, care_api, lifetime_api, history_api, export_api, share, camera_api, vitals_api, profiles_api) calls one of these two functions before touching data.
A foreign profile and a nonexistent profile both return 404 Not found. A 403 would confirm that profile or record id 42 exists and belongs to someone, which lets a caller enumerate ids across families. 403 is used only where the caller is already a legitimate member: a viewer caregiver trying to write, or a non-owner trying to manage sharing.
Structured records keep provenance to an original document. safe_doc(db, pid, doc_id, page) requires that the document exists and that document.patient_id == pid. Otherwise it returns 400 "Source document does not belong to this profile". It also returns 422 if the cited page is outside the document. care_api and lifetime_api apply the same rule to linked entities (for example an episode or course on another profile). This blocks the case where the caller owns both profiles but cites a document from the wrong one.
| Role | Read | Write |
/ask, /signed-access POST |
Manage sharing |
|---|---|---|---|---|
| Owner | yes | yes | yes | yes |
editor |
yes | yes | yes | no (403) |
viewer |
yes | no (403) | yes (treated as reads) | no (403) |
Invitations: the owner calls POST /invitations and receives a secrets.token_urlsafe(32) code. Only its SHA-256 is stored, it expires after 3 days and the owner hands it over personally. Accepting requires the signed-in account's email to match the invitation. The owner cannot accept for their own profile (409), and re-accepting updates the role. Revoking deletes the grant and cancels that caregiver's pending or retrying notifications for the profile. Access ends at once, so the next download returns 404.
api/profiles.py derives an age group (infant, toddler, child, teen, adult, senior) from the DOB and default modules per group. web/profiles.js uses profile.modules to hide navigation tabs and care_kinds to tailor Daily Health. Disabling a module never deletes data or moves it between profiles.
- PostgreSQL:
harden_database()enables row-level security on every table and revokes the hosted-database client roles (anon,authenticated), so only the API's owner connection can read rows. Profile isolation itself is enforced in the API layer. - Account deletion removes the account's own profiles and records and leaves other families untouched (
test_account_deletion_removes_owned_records_but_not_other_family). - Reminder fan-out and push delivery re-check that the recipient is still the owner or a grantee before sending.
Isolation also protects provenance. A medicine, lab value or antibiotic course can cite only an original document from the same profile, so a parent-entered or clinician-documented record never points at another person's evidence.
api/security.py (own_patient, own_resource), api/main.py (safe_doc, profile routes), api/workflows.py (guardian, invitations, family, revoke), api/profiles.py, api/profiles_api.py, api/models.py (Patient, CaregiverAccess, Invitation), api/cloud.py (harden_database), web/profiles.js. Tests: tests/test_api.py (test_authentication_and_family_isolation, test_cross_family_source_document_is_rejected), tests/test_profiles.py, tests/test_workflows.py (test_read_only_invitation_and_immediate_revocation, test_review_wrong_page_and_cross_family_are_rejected), tests/test_lifetime.py, tests/test_history_api.py, tests/test_import_history.py, tests/test_export.py, tests/test_review_confirm.py, tests/test_share.py, tests/test_delivery_security.py, tests/browser/family_workspace.spec.js.
Authentication and Session Security · Database and Data Model · Medical Document Ingestion · Share With Doctor Architecture · API Design · Security and Privacy Model
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