Skip to content

20 Notifications and Reminder Architecture

Anubha Parashar edited this page Oct 10, 2026 · 1 revision

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.

Responsibilities

  • 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.

Inputs and outputs

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).

What can create a reminder

The API enforces these gates before a source counts as active:

  • Medicine: POST /medicines/{id}/confirm requires doctor-written instructions, exact start and end dates when times are set, and a linked prescription document. Editing a medicine increments version and sets it back to draft, which stops its reminders until it is confirmed again.
  • Vaccine: cannot be confirmed without a real card or doctor document (India_UIP_reference rows are rejected) and a date. Any material edit increments version and clears confirmed. Recording given_date removes the reminders.
  • Visit: cannot be confirmed without a hospital-issued document. Status changes (attended, cancelled) increment version, and only planned visits 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.

Mechanism

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
Loading

Scheduling

  • Worker process: python -m api.worker runs tick() every 15 s. A file lock, plus a PostgreSQL advisory lock where available, keeps it to a single instance. It writes a worker_heartbeat row on each tick.
  • Scheduler endpoint: POST /api/internal/scheduler/tick runs one tick for hosts that sleep. An external scheduler calls it with X-Scheduler-Key, which is compared with hmac.compare_digest. The endpoint returns 404 until a key of at least 24 characters is configured, and 403 for a wrong key.

Reconcile and dedupe

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.

Delivery, retries and receipts (worker.deliver)

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.

Generic push text

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.)

Acknowledge and snooze

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.

Clinical safety rules

  • 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_date is 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.'

Implementation

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.

Related pages

Medicines and Administration Model · Vaccination Model · Web Application Architecture · Android Application Architecture · Family Profile Isolation · Deployment Architecture · Security and Privacy Model

Clone this wiki locally