Skip to content

Onset → LastVet Sync Contract

Purpose

Define the wire-level integration between Onset (iOS / Supabase) and LastVet (last.vet-api) that lets a veteran's protocol, adherence, and shared lab summary appear inside their LastVet record so LastVet can route it to their providers.

Roles

Role Identity Where it lives
Veteran (patient) Onset patient app user; LastVet veteran record iOS, onset-api (R940), LastVet Postgres
Onset client Registered OAuth client of LastVet, identified by client_id = "onset-ios" and a confidential client secret (or PKCE for the mobile half of the flow) LastVet oauth_clients table (new)
LastVet API last.vet-api Express server Resource server for /api/v1/me/sync

The veteran is the actor on every sync. There is no third-party provider, no Onset clinician, no admin in this loop.

Connect flow (one-time, per device)

The veteran taps "Connect to LastVet" in Onset Settings. We use OAuth 2.0 Authorization Code + PKCE with OIDC, with LastVet hosting the consent screen. The shipped redirect URI is the custom scheme onset://lastvet/callback (HTTPS Universal Link fallback on theonset.app remains registered).

Authoritative spec for the connect flow, token shapes, refresh rotation, threat model, and JWT claim formats: LASTVET_IDENTITY_BRIDGE.md. This document references that doc rather than duplicating it.

High-level shape:

Onset iOS                LastVet API / Web
   |                          |
   |-- ASWebAuthSession ----->|  GET /oauth/authorize
   |                          |     ?response_type=code
   |                          |     &client_id=onset-ios
   |                          |     &redirect_uri=onset://lastvet/callback
   |                          |     &scope=openid profile protocol.write adherence.write labs.write
   |                          |     &state=<random>&nonce=<random>
   |                          |     &code_challenge=<sha256(verifier)>&code_challenge_method=S256
   |                          |
   |                          |  veteran auths to LastVet, sees consent screen, taps Allow
   |                          |
   |<- 302 custom scheme -----|  onset://lastvet/callback?code=...&state=...
   |                          |
   |-- POST /oauth/token ---->|  authorization_code grant + code_verifier
   |                          |
   |<-- access_token (1h) ----|
   |    refresh_token (90d)   |
   |    id_token  (OIDC)      |  ← contains verified veteran identity claims
   |    scope                  |

After this, Onset: - Verifies the id_token (signature, iss, aud, nonce, exp). - Stores tokens and identity claims in iOS Keychain (WhenUnlockedThisDeviceOnly). No tokens are stored in Supabase. - Upserts patient_aggregator_connections in Onset Supabase: (patient_app_user_id, aggregator='lastvet') → status='connected', external_subject_id, scopes, connected_at.

The id_token's verified claims are the veteran identity Onset uses for display: sub (LastVet veteran UUID), name, https://last.vet/claims.veteran_verified, .branch, .rank. Onset never stores or computes a separate is_veteran flag — see LASTVET_IDENTITY_BRIDGE.md.

Sync cadence

Onset pushes to LastVet on these triggers:

Trigger Payload Notes
Connection established full snapshot of current protocol + last-90-day adherence Single batched POST.
Protocol changes (clinic update, custom edit, new assignment accepted) new protocol snapshot Diffs not enough — LastVet needs the full current state for the "Supplements" tab.
Daily, 22:00 local time yesterday's adherence + today's pending items Single best-effort job; failures retry next morning.
App foregrounded after >24h gap catch-up since last_synced_at Bounded: 30 days max; older data is dropped on the floor.
Manual "Sync now" tap full snapshot Settings → Connected health records → LastVet → Sync now.
Connection revoked none Onset stops pushing immediately and tells LastVet to invalidate the refresh token.

Idempotency: every item carries a stable source_key (see below). Duplicate syncs of the same item are no-ops on LastVet's side because of the (veteran_id, source, source_key) unique constraint that already exists in health_record_items.

Endpoint

POST  https://api.last.vet/api/v1/me/sync
Headers:
  Authorization: Bearer {access_token}
  Content-Type: application/json
  X-Onset-Client-Version: <Onset CFBundleShortVersionString>
  X-Idempotency-Key: <UUID per request>

LastVet today exposes this route at last.vet-api/src/routes/health-record.ts:39. The required change there is in LASTVET_API_CHANGES_REQUIRED.md.

Payload shape

{
  "categories": ["medications", "supplements", "behavioral", "labs"],
  "syncedAt": "2026-05-20T22:00:00Z",
  "items": [
    {
      "sourceKey": "onset:protocol:<protocol_id>:item:<item_id>",
      "source": "onset",
      "category": "supplements",
      "eventType": "supplement_active",
      "title": "Magnesium Glycinate",
      "subtitle": "400 mg, evening, with food",
      "occurredAt": "2026-05-20T19:00:00Z",
      "code": null,
      "codeSystem": null,
      "valueText": "400 mg PO QHS",
      "raw": {
        "protocol_id": "<uuid>",
        "protocol_revision": 7,
        "protocol_name": "Recovery Protocol — Dr. Byrnes",
        "practitioner_name": "Dr. Byrnes",
        "clinic_protocol_id": "<uuid|null>",
        "schedule_summary": "every day, evening, 400 mg",
        "is_practitioner_authored": true,
        "active_from": "2026-05-01",
        "active_through": null
      }
    },
    {
      "sourceKey": "onset:adherence:<protocol_id>:<item_id>:2026-05-19",
      "source": "onset",
      "category": "behavioral",
      "eventType": "supplement_intake_logged",
      "title": "Magnesium Glycinate — taken",
      "subtitle": "Evening dose, on time",
      "occurredAt": "2026-05-19T19:14:00Z",
      "valueText": "completed",
      "raw": {
        "protocol_id": "<uuid>",
        "item_id": "<string>",
        "item_name": "Magnesium Glycinate",
        "scheduled_block": "evening",
        "scheduled_for": "2026-05-19T19:00:00Z",
        "completed_at": "2026-05-19T19:14:00Z",
        "skipped": false
      }
    },
    {
      "sourceKey": "onset:lab:<lab_result_id>:<biomarker>",
      "source": "onset",
      "category": "labs",
      "eventType": "lab_result",
      "title": "Vitamin D, 25-OH",
      "subtitle": "37 ng/mL (ref 30–100)",
      "occurredAt": "2026-05-02T00:00:00Z",
      "code": "1989-3",
      "codeSystem": "http://loinc.org",
      "valueText": "37 ng/mL",
      "raw": { /* full Onset LabValue */ }
    }
  ]
}

Field-by-field rules

  • source: always the literal string "onset" for items written by Onset. LastVet's /me/sync currently overwrites source to "va"; that needs to allow "onset" for Onset-issued tokens. See LastVet changes doc.
  • sourceKey: opaque to LastVet, stable per Onset entity. Prefix scheme onset:<entity>:<id...> is mandatory so the namespace can't collide with VA imports. Must be deterministic — re-syncing the same logical item produces the same sourceKey.
  • category: must be one of LastVet's CONSENT_CATEGORIES. v1 uses medications and supplements for protocol items, behavioral for adherence events, labs for lab summaries shared via Onset, self_reported for daily journal entries (phase-gated, behind a toggle).
  • eventType: free string up to 50 chars — used by LastVet to label the event in the timeline UI. Onset's vocabulary: supplement_active, supplement_discontinued, supplement_intake_logged, supplement_intake_skipped, lab_result, daily_checkin.
  • occurredAt: when the thing happened in real life, not when it was synced. For supplement_active, that's the protocol activation date. For supplement_intake_logged, the timestamp the user marked it complete.
  • raw: free-form JSON for Onset's own consumption later (e.g., re-rendering the protocol in a LastVet provider view). LastVet stores this verbatim. Do not put PHI here that wouldn't already be reconstructible from the other fields — raw is not separately scope-redacted.

What gets synced (share domains)

Domain Onset source LastVet consent key Payload hints
Protocol stack Active UserProtocolItem rows onset_protocol_stack raw.stack_type = "protocol_stack"
Supplement routine Same items, daily context onset_supplement_routine raw.routine_kind = "daily"
Fitness routine FitnessProtocol + weekly days onset_fitness_routine eventType: fitness_active, raw.stack_type = "fitness"
Adherence ProtocolCompletion (90-day window) protocol_adherence category: behavioral, fitness ids fitness:{id}:{weekday}
Labs (optional) LabResult when practitionerShareLabs labs_summary category: labs

Stable sourceKey prefixes include onset:stack:, onset:routine:, onset:fitness:, and onset:adherence:.

What gets synced (legacy four categories section)

medications and supplements

Every active item in every active StoredProtocol for the patient. One health_record_items row per (protocol, item). Discontinuation produces a supplement_discontinued event with occurredAt = discontinuation date and subtitle describing why ("clinic update", "patient removed", "protocol ended"). Items currently held in titration / cycle phases are still included; the schedule summary in subtitle reflects the current phase.

behavioral

One row per logged completion (or explicit skip) of a protocol item. Use this category — not a custom one — because LastVet's existing consent UI already exposes "Check-in History" under behavioral and providers know how to read it. We do not back-fill historical adherence on first sync; we backfill the last 90 days only.

labs

Lab biomarker summaries already present in Onset that the patient has chosen to share with their practitioner (preferences.practitionerShareLabs). We do not auto-share Onset-imported labs that the patient has not flagged for sharing — labs require an extra explicit opt-in inside the LastVet connect flow, mirrored in Onset Settings.

self_reported (phase-gated)

Daily journal entries — sleep, energy, mood, pain. Off by default. Available as a toggle once the basic protocol/adherence sync is stable in production. Mentioned here for completeness; not part of v1 wave.

Auth on every sync

Each POST /me/sync carries a Bearer access token. The access token includes (as JWT claims): - iss = "https://api.last.vet" - aud = "lastvet-resource-server" - sub = veteran_id (LastVet internal UUID) - client_id = "onset-ios" - scopes = ["protocol.write", "adherence.write", "labs.write"] - exp = now + 1h - jti = ...

LastVet's authMiddleware will need a JWT verification path that runs before the existing header-based bootstrap path. New x-user-type value "system_onset" (or simply: trust the JWT's client_id and bind req.veteranId = sub). See LastVet changes doc.

Refresh and revocation

  • Access tokens TTL 1 hour. Refresh tokens rotate on use, TTL 90 days.
  • A successful sync that returns a 401 triggers Onset to attempt one refresh; on failure, Onset surfaces "LastVet connection expired — reconnect" in Settings.
  • Patient-initiated revoke from Onset: Onset calls DELETE /api/v1/oauth/token with the refresh token, sets local connection_status = revoked, stops scheduling syncs. The LastVet side invalidates the token immediately and emits a connection_revoked audit event.
  • Patient-initiated revoke from inside LastVet (via the existing "Connected apps" surface or a new one): LastVet invalidates the token. Onset's next sync gets a 401 on access and an invalid_grant on refresh; Onset transitions to revoked_remotely in Settings and stops trying.

Failure handling

Status Onset behavior
200 record last_synced_at, count synced items
400 (validation) log full error to internal telemetry, do not surface to patient unless three consecutive failures (then: "There was a problem syncing — we're looking into it.")
401 (token expired/invalid) refresh once, retry once, else surface "Reconnect"
403 (scope/consent) mark connection as needing reauthorization
409 (idempotency replay) accept as success
429 exponential backoff, max 1h
5xx retry next scheduled sync; do not surface

Audit

Every sync attempt — success or failure — writes one row to Onset's data_share_events table (see PROVIDER_AUDIT_AND_PATIENT_ACCESS_HISTORY.md). LastVet writes its own event to its existing audit log. The patient sees both surfaces: - In Onset Settings → Connected health records → LastVet → "Activity": last 90 days of Onset-side sync attempts and outcomes. - In LastVet's existing "Where my data went" view: aggregated cross-source.

Veteran-facing display (LastVet iOS)

Synced Onset data is stored in health_record_items with source = 'onset'. Veterans read their own copy via:

GET /api/veteran/me/onset-summary
Authorization: veteran session (same as other `/api/veteran/me/*` routes)

Response buckets mirror the provider OnsetIntegrationCard: protocolStack, supplementRoutine, fitnessRoutine, adherenceSeries, labs, plus itemCount, syncedAt, and completion rates.

LastVet iOS Your Record placement:

UI surface Purpose
Onset category pill (top row) Dedicated read-only view with sub-tabs: Protocol stack, Supplement routine, Fitness routine, Adherence, Labs
Onset source filter (same row as All / VA / LastVet / Provider) Filters Timeline to Onset-sourced adherence entries
Onset status chip Shows item count + last sync time (separate from the LastVet self-reported chip)

Onset data is not mixed into the LastVet (platform) source filter — that filter remains for veteran-entered self-reported and social entries only.

Providers continue to use GET /api/v1/veterans/:id/onset-summary (consent-gated) and the provider portal Onset card.

What this contract does NOT do

  • It does not let LastVet write back into Onset. The provider portal in LastVet can leave clinical notes, but those stay in LastVet — Onset doesn't read LastVet.
  • It does not let providers register on Onset. Providers continue to use LastVet's existing portal.
  • It does not export the patient's full Onset data set. Only the four categories listed above.
  • It does not replace the existing practitioner→patient adherence push (shared_adherence table on Onset's clinic side). That continues to operate independently.

Open questions tracked elsewhere

  • iOS implementation details: LASTVET_INTEGRATION_PLAN.md.
  • LastVet-side migrations and code changes: LASTVET_API_CHANGES_REQUIRED.md.
  • Patient-facing UI copy: LASTVET_CONNECT_AND_REVOKE.md.
  • Audit table schema: PROVIDER_AUDIT_AND_PATIENT_ACCESS_HISTORY.md.