Skip to content

Schema

Protocol uses three PostgreSQL schemas:

  • person - user identity, health profile, preferences (one row per user)
  • supplements - shared inventory plus a per-user journal and context
  • public - OAuth token storage only (not user-facing data)

All migrations live in supabase/migrations/ and are applied by the deploy workflow.

Per-user demographics and profile data. Every table here is user-scoped with RLS enabled.

The canonical identity record. One row per human.

ColumnTypeNotes
idtext PKShort slug (john, jane). Not a UUID - intentionally human-readable
google_emailtext UNIQUEMust match the Google account used at OAuth sign-in
display_nametextShown in LLM responses
sextext'm' or 'f' (enforced by CHECK) - drives sex-specific lab reference ranges
date_of_birthdateUsed for age-gated recommendations

One row per user (user_id is both PK and FK to person.users with ON DELETE CASCADE). All list-shaped fields are jsonb arrays validated by CHECK constraints and Pydantic on read.

ColumnTypePurpose
conditionsjsonb[]Current medical conditions [{name, status, notes}]
family_historyjsonb[]Hereditary risks [{condition, relative}]
substancesjsonb[]Caffeine / alcohol / other [{name, frequency, notes}]
diet_notestextFree-form eating patterns
activity_notestextFree-form exercise notes
safety_checksjsonb[]Topics the LLM must verify before recommending
methodology_notestextPreferred health framework
health_prioritiesjsonb[]Ranked goals

One row per user - locale, units, and communication style.

ColumnDefaultConstraint
language'en'ISO 639-1 (two lowercase letters)
units'metric'metric or imperial
currency'USD'ISO 4217 (three uppercase letters)
date_format'YYYY-MM-DD'One of seven allowed patterns
location-Free-form text - city, region, country
occupation-Free-form text - user's job or profession
communication-Free-form text - communication style

Shared across all users. A single household keeps one catalog so that two family members on the same "Jamieson Vitamin D3 1000 IU" can reference the same inventory_id. No RLS - reads are broadly allowed; writes go through MCP tools.

ColumnTypeNotes
idinteger PKGenerated identity
nametext-
brandtext-
categorytexte.g. vitamin, mineral, herb
formtexte.g. capsule, softgel, powder
dosage_per_unittexte.g. "500 mg", "1000 IU"
featurestext[]Optional tags, e.g. {timed release,micronized}. Default {}
urltextOptional product URL

UNIQUE (name, brand) prevents duplicate catalog entries.

The core protocol history. Every regimen change closes a row (sets ended_at) and opens a new one linking back via replaces_id. This is what lets you reconstruct the exact protocol on any past date.

ColumnTypeNotes
idinteger PKGenerated identity
user_idtextFK to person.users(id) ON DELETE CASCADE
inventory_idintegerFK to supplements.inventory(id)
time_blockstext[]Non-empty subset of {morning, lunch, evening, any}
dosagetextFree-form, e.g. "500 mg"
frequencytextFree-form, e.g. "twice daily"
started_atdateDefaults to CURRENT_DATE
replaces_idintegerFK to supplements.journal(id). NULL for first entries
replacement_reasontextRequired when replaces_id is set
ended_atdateNULL means active
end_reasontextOptional, requires ended_at

Invariants enforced by CHECK constraints and triggers:

  • ended_at >= started_at if present
  • end_reason requires ended_at
  • replaces_id and replacement_reason are both-or-neither
  • Unique active entry per user+supplement - partial unique index (user_id, inventory_id) WHERE ended_at IS NULL prevents two concurrent rows for the same thing
  • Closed rows are immutable - immutable_closed trigger rejects any UPDATE on a row where ended_at IS NOT NULL
  • Replacement chain integrity - require_chain trigger enforces that replaces_id points to a closed row for the same inventory_id, and that you cannot insert a second "first" entry for a supplement that already has history

One row per (user_id, inventory_id) - the purpose the user takes this supplement for. Separate from journal because purpose rarely changes while dosage and timing do.

ColumnTypeNotes
idinteger PKGenerated identity
user_idtextFK to person.users(id) ON DELETE CASCADE
inventory_idintegerFK to supplements.inventory(id)
purposetext[]Non-empty array, e.g. {bone health,immune support}

UNIQUE (user_id, inventory_id) - one context entry per user per supplement.

Backing store for FastMCP's OAuth flow (access tokens, refresh tokens, PKCE state). RLS is enabled but no policies are defined - only the postgres superuser (the MCP server itself) should ever read this table. anon and authenticated roles are explicitly revoked in the migration.

ColumnTypeNotes
collectiontextPart of composite PK - groups token records by type
keytextPart of composite PK - identifies the specific token
valuejsonbToken payload
ttldouble precisionTime-to-live in seconds
created_attimestamptz-
expires_attimestamptzIndexed for expiry lookups