Onset Sharing Audit and Patient Activity History¶
Goal¶
Every outbound sync from Onset to LastVet — success or failure — is recorded immutably and is visible to the patient in plain language. v1 covers only the LastVet sync. The schema is general enough that a second aggregator (or the deferred civilian snapshot-share path) can plug in without a refactor.
This doc replaces the earlier PROVIDER_AUDIT_AND_PATIENT_ACCESS_HISTORY.md, which assumed a third-party-provider read API. There is no such API in v1 (see ADR-002), so the audit table is now patient-share-events, not provider-access-events.
Audit table¶
data_share_events¶
Lives in Onset Supabase. One row per sync attempt. Append-only. Never updated, never deleted.
CREATE TABLE data_share_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
patient_app_user_id UUID NOT NULL REFERENCES auth.users(id),
channel TEXT NOT NULL CHECK (channel IN ('aggregator_push')), -- v1 only value
aggregator TEXT NOT NULL CHECK (aggregator IN ('lastvet')), -- v1 only value
external_subject_id TEXT NOT NULL, -- LastVet veteran_id
event_type TEXT NOT NULL, -- see catalog below
trigger TEXT NOT NULL, -- 'connect_initial' | 'protocol_changed' | 'scheduled_daily' | 'foreground_catchup' | 'manual' | 'revoke'
request_id TEXT NOT NULL, -- correlation id; matches X-Idempotency-Key
http_status INT, -- nullable for client-side-only events
result_status TEXT NOT NULL CHECK (result_status IN
('success','failed_validation','failed_auth','failed_consent',
'failed_rate_limited','failed_server','failed_network','revoked')),
categories_pushed TEXT[], -- e.g. ARRAY['supplements','behavioral']
item_count INT, -- number of items in this push (success only)
latency_ms INT,
error_message TEXT, -- redacted; no PHI
client_version TEXT, -- Onset CFBundleShortVersionString
ip_hash TEXT -- SHA-256 of source IP, never raw
);
CREATE INDEX idx_data_share_events_patient_recent
ON data_share_events (patient_app_user_id, occurred_at DESC);
CREATE INDEX idx_data_share_events_request
ON data_share_events (request_id);
event_type catalog (v1)¶
| event_type | When | result_status values |
|---|---|---|
sync_attempted |
Every POST to /me/sync, before result is known |
always paired with a follow-up sync_completed |
sync_completed |
POST to /me/sync returned (any status) |
success, failed_* |
connection_established |
OAuth dance succeeded; first sync queued | success |
connection_revoked |
Patient disconnected from Onset Settings | revoked |
connection_revoked_remotely |
LastVet returned invalid_grant on refresh |
revoked |
token_refreshed |
Refresh token exchange succeeded | success |
token_refresh_failed |
Refresh exchange returned 4xx | failed_auth |
A successful sync produces two rows: sync_attempted then sync_completed. They share request_id. We pair them so a sync that hangs past timeout still leaves a forensic trail.
Write rules¶
- Append-only. Trigger / RLS policy denies UPDATE and DELETE for all roles except a sealed
ops_audit_adminrole used only for retention enforcement. - One row per attempt, regardless of outcome. Failures are logged in detail so the patient can see "we tried and it didn't work" — silent failures are not acceptable.
error_messageis sanitized server-side: known categories ("network unavailable", "LastVet rate limit", "consent revoked at LastVet") get fixed strings; raw stack traces / response bodies are not stored.ip_hashis SHA-256 with a per-environment salt. Raw IPs never land in this table.
Patient-facing UI¶
Onset Settings → Connected health records → LastVet → "Activity"¶
Reverse-chronological list, last 90 days, grouped by day. Each row:
─── May 20, 2026 ──────────────────────────────────────────
● Synced 14 items · Supplements, Adherence · 10:00 PM
● Synced 2 items · Lab results · 3:14 PM
─── May 19, 2026 ──────────────────────────────────────────
● Synced 12 items · Supplements, Adherence · 10:01 PM
─── May 18, 2026 ──────────────────────────────────────────
⚠ Sync failed · We'll try again tomorrow · 10:00 PM
Tapping a row expands to show: - Trigger ("Daily background sync" / "You tapped Sync now" / "After protocol change") - Categories pushed - Outcome with a plain-language explanation if failed - Timestamp - Onset client version
We do NOT show: request IDs, IP hashes, raw HTTP statuses, or LastVet internal IDs. Those live in the table for engineering forensics; they don't belong in patient UX.
"Where my data went" cross-link¶
The activity list links to LastVet's existing per-grant access view (the one providers' reads land in). Copy: "See providers who have viewed this data on LastVet →". Deep links into LastVet iOS via Universal Link.
Retention¶
- Online retention in
data_share_events: 24 months. - Cold archive: 6 years (matches HIPAA retention for accounting-of-disclosures-equivalent records). Cold archive is encrypted, write-once, separate-credential-only access.
- Retention enforcement runs nightly by the
ops_audit_adminrole; it moves rows older than 24 months to cold storage and removes them from the hot table.
Monitoring (engineering, not patient-facing)¶
Alerts:
- Repeated failed_consent for the same patient_app_user_id within 24h → potential bug in revoke handling.
- Spike in failed_server across many patients → LastVet outage; surface in our internal status.
- Spike in failed_validation after a release → schema drift, likely a payload-shape regression.
- sync_attempted with no matching sync_completed after 60s → hang detection.
- Daily integrity check that connection_established rows have a successful initial-sync row within 60s.
Cross-side reconciliation¶
LastVet writes its own audit row on its side for every /me/sync it processes. Both sides should be reconcilable by:
- request_id (Onset's X-Idempotency-Key is logged on LastVet's side).
- Time window + external_subject_id.
A nightly job (LastVet side, optional) emits a count-mismatch alert if the two logs drift. Not a v1 must-have but cheap to add.
What this doc explicitly does NOT cover¶
- LastVet-side audit (separate, lives in LastVet's existing
audit_logtable). - Provider read access on Onset — there is none in v1.
- Civilian sharing — tabled, see ADR-002.
What this doc replaces¶
The earlier PROVIDER_AUDIT_AND_PATIENT_ACCESS_HISTORY.md, which assumed Onset would log inbound provider reads. We don't have inbound provider reads, so we log outbound patient pushes instead. Same intent (transparency, immutability, retention), narrower surface.