Repository navigation
20 Notifications and Reminder Architecture
HealthBook AI (internal code name: BabyCare) creates reminders only from care events a person has explicitly confirmed against a source document. Reminders are generated server-side and stored. A worker delivers them as in-app notifications and as generic remote pushes through Expo (Android) or Web Push (browser). A reminder is a scheduling aid only: delivering, acknowledging or snoozing it never records that a medicine or vaccine was given.
- Turn verified prescriptions, confirmed vaccine due dates and confirmed planned visits into time-zone-correct reminder instants.
- Store reminders and per-recipient, per-channel notifications with idempotent keys.
- Cancel reminders whose source has been revised, confirmed again, stopped or completed.
- Deliver remote pushes with fixed generic text, retries and provider receipt tracking.
- Let users acknowledge or snooze a notification without creating any administration record.
| Input | Output |
|---|---|
Medicine with status='verified', start and end dates, schedule_times
|
med-{id}-v{version}-{date}-{HH:MM} reminders |
Vaccine with confirmed, due_date, a real source_document_id, no given_date, not a reference row |
vaccine-{id}-v{version}-{due}-{lead}d for each lead in reminder_days (default 7,1,0), at 09:00 local |
HospitalVisit with confirmed, status='planned'
|
visit-{id}-v{version}-{planned_at} plus ...-day-before at 09:00 |
Registered PushDevice rows |
notifications rows (channel in_app or expo) |
POST /api/notifications/{nid}/action |
acknowledge or snooze (1–1440 minutes, default 15) |
Read endpoints: GET /api/patients/{pid}/reminders?days= (0–120, computed live) and GET /api/patients/{pid}/notifications (the caller's own notifications, with worker_running and push_configured flags).
The API enforces these gates before a source counts as active:
-
Medicine:
POST /medicines/{id}/confirmrequires doctor-written instructions, exact start and end dates when times are set, and a linked prescription document. Editing a medicine incrementsversionand sets it back todraft, which stops its reminders until it is confirmed again. -
Vaccine: cannot be confirmed without a real card or doctor document (
India_UIP_referencerows are rejected) and a date. Any material edit incrementsversionand clearsconfirmed. Recordinggiven_dateremoves the reminders. -
Visit: cannot be confirmed without a hospital-issued document. Status changes (attended, cancelled) increment
version, and onlyplannedvisits remind. - Timezone change on a profile un-verifies medicines and un-confirms open vaccines and visits, so stored clock times cannot silently change meaning.
- OCR output and imported reference schedules never create reminders.
local_time() rejects nonexistent or ambiguous DST wall times rather than guessing.
flowchart TD
EVT["Confirmed care event<br/>verified prescription, confirmed vaccine date,<br/>confirmed planned visit"] --> RULE["Reminder rule<br/>reminder_items builds versioned keys<br/>vaccine leads 7, 1, 0 days"]
RULE --> SCHED["Scheduler<br/>worker loop every 15 s or<br/>POST internal scheduler tick"]
SCHED --> REC["reconcile per profile<br/>insert new keys, cancel stale versions,<br/>expire pending older than 1 day"]
REC --> FAN["Fan-out to owner and caregivers<br/>dedupe_key reminder, user, device, cycle"]
FAN --> INAPP["In-app notification<br/>status available"]
FAN --> PUSH["Remote push<br/>Expo or Web Push, generic text"]
PUSH --> RCPT["Receipt check<br/>accepted then provider_received or failed"]
INAPP --> NOTE["Notification"]
RCPT --> NOTE
NOTE --> ACK["Acknowledge<br/>notification status only"]
NOTE --> SNOOZE["Snooze 1 to 1440 min<br/>new cycle at snoozed_until"]
SNOOZE --> FAN
DISCLAIM["Notification is not proof a dose was given"]
NOTE -.- DISCLAIM
ACK -.- DISCLAIM
-
Worker process:
python -m api.workerrunstick()every 15 s. A file lock, plus a PostgreSQL advisory lock where available, keeps it to a single instance. It writes aworker_heartbeatrow on each tick. -
Scheduler endpoint:
POST /api/internal/scheduler/tickruns one tick for hosts that sleep. An external scheduler calls it withX-Scheduler-Key, which is compared withhmac.compare_digest. The endpoint returns 404 until a key of at least 24 characters is configured, and 403 for a wrong key.
reconcile(db, profile) computes reminders for the next 35 days (with 1 day of lookback). It inserts any key not already present; reminders.key is unique, so repeated ticks are idempotent. For each existing pending, notified or snoozed reminder that is no longer planned and whose source_active() check fails, it sets the reminder to cancelled and cancels its pending or retry notifications. source_active() requires that the key still starts with the source's current v{version}. A revision therefore cancels every reminder that carries the old version, including outstanding retries. Pending reminders more than a day overdue become expired, so stale medication instructions are not delivered after an outage.
Each due reminder fans out to the owner and every caregiver grantee, with one notification per channel (in_app, plus expo for each enabled device). The dedupe key is {reminder_id}:{user_id}:{device_id}:{cycle}, where cycle is initial or the snooze timestamp. It is unique in notifications, so a reminder is notified once per cycle per device.
Before sending, the worker re-checks that the recipient still has access, that the device is enabled, that the reminder is not cancelled or acknowledged, that the source is still active, and that the reminder is less than a day stale.
| Situation | Result |
|---|---|
Expo ticket ok
|
accepted, receipt id stored, receipt polled after 15 min |
Receipt ok
|
provider_received |
| Receipt missing | poll again in 15 min. After 23 h: receipt_unknown
|
DeviceNotRegistered |
failed, device disabled |
MessageRateExceeded |
retry in 5 min |
| Transport error before acceptance |
retry, backoff min(3600, 30·2^attempts) s, failed after 6 attempts |
| Transport error during receipt lookup | stays accepted, so an accepted push is never re-sent |
Push disabled (BABYCARE_PUSH_ENABLED not true) |
push_not_configured, checked again in 5 min |
| Web Push 404/410 |
failed, subscription disabled. 429/5xx: retry with the same backoff |
Web Push devices are stored as PushDevice rows with a webpush: token prefix and are sent through push_web.send() using VAPID. Expo tokens are registered through POST /api/devices, and browser subscriptions through POST /api/push/web/subscribe. Both require consent_generic_push: true. A device registered to another account returns 409.
Remote payloads always have the title "HealthBook AI" and the body "You have a confirmed care reminder. Open HealthBook AI to review it.", plus notification_id. No name, medicine, diagnosis or appointment detail leaves the server through Expo, Apple, Google or browser push services. The details are shown only inside the signed-in app. (The web app's optional in-tab notification, used while the app is open and permission is granted, is built locally from the signed-in reminder list.)
notification_action() loads the caller's own notification and checks the reminder's profile with own_resource. Acknowledge sets acknowledged_at and the status, and cancels that user's other pending pushes for the same reminder. Snooze returns 409 if the underlying schedule has changed. Otherwise it cancels pending pushes and sets snoozed_until, and the next tick fans out a new cycle. Both write an audit action ending in _notification_only. Neither creates a MedicationEvent, a vaccine given_date or a visit status change.
- A notification or reminder is never proof that a dose was given. Prescribed ≠ Scheduled ≠ Given/Administered.
- A vaccination due date is not a vaccination administered. Reminders stop only when a real
given_dateis recorded. - An attended appointment is not a treatment administered. Marking a visit attended only stops visit reminders.
- Every reminder item carries
safety: 'Confirmed appointment or prescription reminder only; delivery does not record administration.'
api/reminders.py (reminder_items, reconcile, source_active, local_time), api/worker.py (tick, deliver, deliver_web_push), api/push_web.py, api/cloud.py (scheduler tick, status, web push subscribe), api/workflows.py (notifications, actions, devices, revoke cancelling), api/main.py (confirmation gates, /reminders), api/models.py (Reminder, Notification, PushDevice, WorkerHeartbeat), web/sw.js, web/app.js (enableWebPush), mobile/App.tsx (syncNotifications). Tests: tests/test_api.py, tests/test_cloud.py, tests/test_delivery_security.py.
Medicines and Administration Model · Vaccination Model · Web Application Architecture · Android Application Architecture · Family Profile Isolation · Deployment Architecture · 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