Skip to Content
Patient GraphData Model

Patient Graph Data Model

The Patient Graph is Loop Health’s unified clinical data system, consolidating all patient health data into a single PostgreSQL schema with comprehensive audit trails and RBAC security.

Schema Location: packages/shared/src/db/patient-graph-schema.ts Database: health Supabase project ORM: Drizzle ORM with full TypeScript types


Architecture Overview

┌─────────────────────────────────────────────────────────────┐ │ PATIENT GRAPH SCHEMA │ │ (patient_graph.*) │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Core Identity │ │ ├─ customer_profiles (demographics, tier, conditions) │ │ └─ identity_mappings (Clerk, Rimo, BigCommerce) │ │ │ │ Clinical Data │ │ ├─ lab_results (parsed biomarkers) │ │ ├─ protocols (treatment protocols) │ │ ├─ patient_events (clinical timeline) │ │ └─ conversation_history (Luna AI logs) │ │ │ │ Treatment Lifecycle (Rimo Integration) │ │ ├─ patient_treatments (treatment orders) │ │ └─ patient_prescriptions (prescription fulfillment) │ │ │ │ Subscriptions & Billing │ │ └─ subscriptions (recurring products, auto-refill) │ │ │ │ Wearables & Biometrics │ │ ├─ patient_wearable_readings (raw sensor data) │ │ ├─ patient_wearable_daily_stats (aggregated stats) │ │ └─ wearable_data (DEPRECATED - use readings/stats) │ │ │ │ Security & Compliance │ │ └─ rbac_logs (HIPAA audit trail) │ │ │ └─────────────────────────────────────────────────────────────┘

Entity Relationships


Complete Enum Definitions

biological_sex

Biological sex for sex-specific biomarker reference ranges.

'male' | 'female' | 'other'

Usage: Customer profiles, biomarker range lookups


subscription_status

Product subscription lifecycle states.

'pending' // Initial state, awaiting approval 'approved' // Clinician approved (Loop Med only) 'rejected' // Clinician rejected (Loop Med only) 'active' // Subscription is active and billing 'paused' // Temporarily paused by customer 'cancelled' // Permanently cancelled 'completed' // Naturally completed (finite protocols)

New in v1.2.0: Added approved, rejected, completed for Loop Med treatment workflows.

State Machine:

pending → approved → active → [paused ⇄ active] → completed/cancelled pending → rejected (terminal)

subscription_interval

Recurring subscription intervals.

'weekly' | 'monthly' | 'quarterly' | 'biannual' | 'annual'

Default: monthly


protocol_status

Treatment protocol lifecycle states.

'active' // Currently running 'paused' // Temporarily on hold 'completed' // Successfully finished 'cancelled' // Terminated early

lab_status

Lab result processing states.

'pending' // Uploaded, not yet reviewed 'reviewed' // Clinician reviewed 'archived' // Archived for compliance

patient_event_type

Clinical timeline event types.

'lab_upload' // Lab report uploaded 'lab_parsed' // Biomarkers extracted 'protocol_created' // Protocol created 'protocol_started' // Protocol started 'protocol_paused' // Protocol paused 'protocol_completed' // Protocol completed 'protocol_cancelled' // Protocol cancelled 'protocol_check_in' // Patient check-in completed 'note_added' // Clinical note added 'profile_updated' // Profile updated 'consent_granted' // Consent given 'consent_revoked' // Consent revoked

New in v1.2.0: Added protocol_check_in for patient self-reporting.


rbac_role

Role-based access control roles.

'admin' // Full access, all resources 'staff' // Internal staff, most resources 'support' // Customer support, read-only patient data 'provider' // Clinicians, read patient data + write clinical notes 'customer' // Patients, own data only (ownOnly=true)

Permissions Matrix: See RBAC Documentation


rbac_action

RBAC action verbs.

'read' // View data 'write' // Create or update 'delete' // Soft or hard delete 'export' // Export data (compliance-restricted)

rbac_resource

Protected resources under RBAC.

'profile' // Customer profiles 'lab_results' // Lab reports and biomarkers 'protocols' // Treatment protocols 'events' // Patient events 'audit_log' // RBAC logs (admin only) 'conversation_history' // AI conversation logs 'treatment' // Rimo treatments (new in v1.2.0)

New in v1.2.0: Added treatment resource for Loop Med treatment management.


rbac_outcome

RBAC access decision results.

'allowed' | 'denied'

prescription_status

Prescription fulfillment lifecycle (Rimo integration).

'pending' // Created, awaiting transmission 'processing' // Transmission in progress 'transmitted' // Sent to pharmacy 'failed' // Transmission failed 'shipped' // Pharmacy shipped 'delivered' // Confirmed delivery 'cancelled' // Order cancelled

wearable_source

Wearable device providers (for legacy wearable_data table).

'oura' | 'whoop' | 'garmin' | 'fitbit' | 'apple_health' | 'dexcom' | 'libre' | 'manual'

⚠️ Deprecated: Use patient_wearable_readings with source field instead.


wearable_metric_type

Wearable metric categories (for legacy wearable_data table).

'sleep' | 'recovery' | 'hrv' | 'glucose' | 'strain' | 'activity' | 'readiness' | 'body'

⚠️ Deprecated: Use patient_wearable_readings with dataType field instead.


wearable_data_type

Individual wearable metrics (new granular system).

// Sleep metrics 'sleep_duration' | 'sleep_score' | 'rem_sleep' | 'deep_sleep' | 'light_sleep' | 'awakenings' // Recovery & HRV 'hrv' | 'resting_heart_rate' | 'recovery_score' // Glucose 'glucose' | 'glucose_variability' // Activity 'steps' | 'calories' | 'active_minutes' | 'strain_score' // Body composition 'weight' | 'body_fat' | 'muscle_mass'

identifier_type

Identity mapping types for cross-system resolution.

'clerk_user_id' // Clerk authentication ID 'email' // Email address 'bigcommerce_customer_id' // BigCommerce CRM 'stripe_customer_id' // Stripe billing 'hubspot_contact_id' // HubSpot CRM (legacy) 'phone' // Phone number 'fingerprint_visitor_id' // Fingerprint.js

Table Reference

customer_profiles

Patient demographic and clinical profiles.

ColumnTypeConstraintsDescription
iduuidPK, default randomProfile UUID
externalIdtextunique, not nullClerk user ID
emailtextunique, not nullEmail address
firstNametextFirst name
lastNametextLast name
dateOfBirthdateBirth date (for age calculations)
biologicalSexbiological_sexBiological sex (for reference ranges)
phoneNumbertextPhone number
timezonetextdefault ‘America/New_York’IANA timezone
subscriptionTiertextdefault ‘free’Subscription tier
conditionstext[]default []Medical conditions
medicationstext[]default []Current medications
allergiestext[]default []Known allergies
tagsjsonbdefault []User tags for segmentation
metadatajsonbdefault Extensible metadata
createdAttimestampnot null, default now()Creation timestamp
updatedAttimestampnot null, default now()Last update timestamp

Indexes:

  • customer_profiles_external_id_idx on externalId
  • customer_profiles_email_idx on email
  • customer_profiles_tier_idx on subscriptionTier

Example:

{ "id": "123e4567-e89b-12d3-a456-426614174000", "externalId": "user_2a1b3c4d5e6f", "email": "patient@example.com", "firstName": "Jane", "lastName": "Doe", "dateOfBirth": "1985-06-15", "biologicalSex": "female", "phoneNumber": "+12125551234", "timezone": "America/Los_Angeles", "subscriptionTier": "premium", "conditions": ["hypothyroidism", "pcos"], "medications": ["levothyroxine 100mcg"], "allergies": ["penicillin"], "tags": ["high-risk", "monthly-check-in"], "metadata": { "source": "referral", "referrer_id": "provider_123" } }

subscriptions

Product subscriptions for auto-refill and recurring orders.

Alias: patientTreatments (for treatment workflows)

ColumnTypeConstraintsDescription
iduuidPKSubscription ID
customerIduuidFK → customer_profiles, not nullPatient
productIdtextnot nullProduct SKU
productNametextnot nullProduct name
offeringIdtextRimo offering ID (Loop Med)
offeringNametextRimo offering name
clinicianIdtextAssigned clinician ID
clinicianNametextClinician name
monthlyPriceCentsnumberstored as textMonthly price (cents)
notestextClinical notes
bigcommerceProductIdtextBigCommerce product ID
rechargeSubscriptionIdtextRecharge subscription ID
rimoTreatmentIdtextRimo treatment ID
statussubscription_statusnot null, default ‘pending’Current status
approvedAttimestampClinician approval time
startedAttimestampSubscription start time
completedAttimestampCompletion time
cancelledAttimestampCancellation time
pausedAttimestampLast pause time
intervalsubscription_intervalnot null, default ‘monthly’Billing interval
intervalCounttextnot null, default ‘30’Days per interval
discountPercentagetextnot null, default ‘10’Discount %
nextOrderDatetimestampNext scheduled order
lastOrderDatetimestampLast order date
convertedFromReorderstextnot null, default ‘0’Migration flag
metadatajsonbnot null, default Additional data
createdAttimestampnot null, default now()
updatedAttimestampnot null, default now()

New in v1.2.0: Added treatment management fields (offeringId, clinicianId, approvedAt, startedAt, completedAt, cancelledAt, pausedAt, monthlyPriceCents, notes) for Loop Med telehealth workflows.

Indexes:

  • subscriptions_customer_id_idx on customerId
  • subscriptions_status_idx on status
  • subscriptions_product_id_idx on productId
  • subscriptions_recharge_subscription_id_idx on rechargeSubscriptionId

Example (Loop Med Treatment):

{ "id": "sub_123", "customerId": "cust_456", "productId": "semaglutide-0.25mg", "productName": "Semaglutide 0.25mg", "offeringId": "rimo_offering_789", "offeringName": "GLP-1 Weight Management", "clinicianId": "dr_smith_123", "clinicianName": "Dr. Sarah Smith, MD", "monthlyPriceCents": 29900, "notes": "Starting dose, titrate after 4 weeks", "status": "approved", "approvedAt": "2026-03-20T10:00:00Z", "startedAt": "2026-03-21T00:00:00Z", "interval": "monthly", "nextOrderDate": "2026-04-21T00:00:00Z" }

patient_prescriptions

Prescription orders fulfilled through Rimo Health pharmacy.

ColumnTypeConstraintsDescription
iduuidPKPrescription ID
customerIduuidFK → customer_profiles, not nullPatient
treatmentIduuidFK → subscriptions, not nullAssociated treatment
rimoOrderIdtextunique, not nullRimo order ID
medicationNametextnot nullMedication name
dosagetextnot nullDosage (e.g., “0.25mg”)
quantitynumberstored as jsonbQuantity dispensed
refillsnumberstored as jsonbRefills authorized
prescribedDatetimestampPrescription written date
nextRefillDatetimestampNext refill due date
statusprescription_statusnot null, default ‘pending’Fulfillment status
pharmacyIdtextFulfilling pharmacy ID
pharmacyNametextPharmacy name
trackingNumbertextShipment tracking
transmittedAttimestampSent to pharmacy at
shippedAttimestampShipped at
deliveredAttimestampDelivered at
cancelledAttimestampCancelled at
metadatajsonbnot null, default Additional data
createdAttimestampnot null, default now()
updatedAttimestampnot null, default now()

New in v1.2.0: Added prescribedDate and nextRefillDate for prescription tracking and refill automation.

Indexes:

  • patient_prescriptions_customer_id_idx on customerId
  • patient_prescriptions_treatment_id_idx on treatmentId
  • patient_prescriptions_rimo_order_id_idx on rimoOrderId
  • patient_prescriptions_status_idx on status

patient_wearable_readings

Raw wearable device sensor readings (granular, time-series data).

ColumnTypeConstraintsDescription
iduuidPKReading ID
customerIduuidFK → customer_profiles, not nullPatient
dataTypewearable_data_typenot nullMetric type
valuenumericnot nullMeasured value
unittextnot nullUnit of measurement
deviceIdtextDevice identifier
deviceTypetextDevice type (oura, whoop, etc.)
sourcetextnot nullData source
recordedAttimestampnot nullMeasurement time
metadatajsonbnot null, default Additional context
createdAttimestampnot null, default now()

Indexes:

  • patient_wearable_readings_customer_id_idx on customerId
  • patient_wearable_readings_data_type_idx on dataType
  • patient_wearable_readings_recorded_at_idx on recordedAt

Example:

{ "id": "reading_123", "customerId": "cust_456", "dataType": "glucose", "value": 95.5, "unit": "mg/dL", "deviceId": "dexcom_789", "deviceType": "dexcom", "source": "dexcom_api", "recordedAt": "2026-03-20T14:30:00Z", "metadata": { "trend": "stable", "alert_triggered": false } }

patient_wearable_daily_stats

Aggregated daily statistics from wearable devices (cached for performance).

ColumnTypeConstraintsDescription
iduuidPKStat ID
customerIduuidFK → customer_profiles, not nullPatient
datedatenot nullDate (YYYY-MM-DD)
dataTypewearable_data_typenot nullMetric type
avgValuenumericDaily average
minValuenumericDaily minimum
maxValuenumericDaily maximum
sumValuenumericDaily sum (for cumulative metrics)
countintegerNumber of readings
unittextnot nullUnit of measurement
metadatajsonbnot null, default Additional aggregations
createdAttimestampnot null, default now()
updatedAttimestampnot null, default now()

Unique Constraint: (customerId, date, dataType) - one stat per customer/date/metric

Indexes:

  • patient_wearable_daily_stats_customer_id_idx on customerId
  • patient_wearable_daily_stats_date_idx on date
  • patient_wearable_daily_stats_data_type_idx on dataType

Caching Strategy:

  • 5-minute TTL in memory cache
  • Background aggregation via Trigger.dev
  • Upsert on sync to prevent duplicates

Migration History

VersionDateChanges
v1.0.02026-02-21Initial schema
v1.1.02026-03-10Add wearable readings/daily stats tables
v1.2.02026-03-20Add treatment management fields (Loop Med)

v1.2.0 Changes (March 20, 2026)

Subscriptions table:

  • Added offeringId, offeringName (Rimo offering tracking)
  • Added clinicianId, clinicianName (provider assignment)
  • Added monthlyPriceCents (pricing)
  • Added notes (clinical notes)
  • Added approvedAt, startedAt, completedAt, cancelledAt, pausedAt (lifecycle timestamps)
  • Added approved, rejected, completed to subscription_status enum

Prescriptions table:

  • Added prescribedDate (prescription written date)
  • Added nextRefillDate (refill automation)

Events table:

  • Added protocol_check_in to patient_event_type enum

RBAC:

  • Added treatment to rbac_resource enum

Common Query Patterns

Get patient with full clinical context

import { createRepositories } from '@loop/patient-graph'; import { db } from './db'; const repos = createRepositories(db); const profile = await repos.profiles.findByExternalId('clerk_user_123'); const labs = await repos.labs.list({ customerId: profile.data.id, limit: 10 }); const protocols = await repos.protocols.list({ customerId: profile.data.id }); const events = await repos.events.list({ customerId: profile.data.id, limit: 50 });

Aggregate wearable data

const glucoseStats = await repos.wearableData.getDailyStatsForDateRange( customerId, 'glucose', '2026-03-01', '2026-03-20' ); // Returns array of daily aggregations with avg/min/max

Treatment workflow (Loop Med)

// 1. Create treatment (pending approval) const treatment = await repos.treatments.create({ customerId: 'cust_123', rimoTreatmentId: 'rimo_456', offeringId: 'offering_789', offeringName: 'GLP-1 Weight Management', status: 'pending', }); // 2. Clinician approves await repos.treatments.update(treatment.data.id, { status: 'approved', clinicianId: 'dr_smith', clinicianName: 'Dr. Sarah Smith, MD', approvedAt: new Date().toISOString(), notes: 'Starting dose, titrate after 4 weeks', }); // 3. Create prescription const rx = await repos.prescriptions.create({ customerId: 'cust_123', treatmentId: treatment.data.id, rimoOrderId: 'rimo_order_999', medicationName: 'Semaglutide', dosage: '0.25mg', quantity: 4, refills: 3, prescribedDate: new Date().toISOString(), nextRefillDate: addMonths(new Date(), 1).toISOString(), });

Best Practices

Always use repositories, never raw SQL

Do:

const repos = createRepositories(db); const result = await repos.profiles.findById(id);

Don’t:

const result = await db.query('SELECT * FROM customer_profiles WHERE id = $1', [id]);

Handle Result<T> properly

All repository methods return Result<T> with ok and error:

const result = await repos.profiles.findById(id); if (!result.ok) { console.error('Failed to fetch profile:', result.error); return; } const profile = result.data; // Type-safe access

Use transactions for multi-step operations

await db.transaction(async (tx) => { const repos = createRepositories(tx); const treatment = await repos.treatments.create({...}); const event = await repos.events.create({ type: 'treatment_approved', customerId: treatment.data.customerId, }); });

Always log RBAC access

await repos.rbacLogs.create({ actorId: user.id, actorRole: 'provider', customerId: patientId, resource: 'lab_results', action: 'read', outcome: 'allowed', });

Schema Visualization Tools

View Live Schema:

cd packages/shared pnpm drizzle-kit studio # Opens Drizzle Studio at http://localhost:4983

Generate ER Diagram:

cd packages/shared pnpm drizzle-kit introspect --out=./docs/schema.png