Skip to content

Data Model

Local schema (Drift/SQLite) and Supabase schema (Postgres) mirror each other. All tables carry id UUID, created_at, and updated_at for sync. Rows never hard-delete on the client — a deleted_at timestamp is set and reconciled with the remote.


profiles

One row per user.

columntypenotes
iduuid PKmatches auth.uid() when signed in
avg_cycle_lengthintdefault 28, range 21–45
avg_period_lengthintdefault 5, range 2–10
on_hormonal_contraceptionboolif true → flat programming mode
fitness_levelenumnew · regular · athlete
goalstext[]subset of strength, endurance, general, weight_management, mobility, recovery
equipmentenumbodyweight · dumbbells · gym
hr_maxint?for HR-zone rendering on export
lthrint?lactate threshold HR
ftpint?functional threshold power
timezonetextfor accurate day-boundaries
notifications_enabledbool

cycles

One row per menstrual cycle (period-start to next-period-start).

columntypenotes
iduuid PK
profile_iduuid FK
start_datedateday 1 of the cycle
end_datedate?null until the next cycle starts
period_length_daysint?computed on close
cycle_length_daysint?computed on close
notestext?

cycle_days

One row per day the user logs anything.

columntypenotes
iduuid PK
profile_iduuid FK
datedateunique per profile
flowenum?none · spot · light · medium · heavy
symptomstext[]e.g. cramps, bloating, headache, breast_tenderness, acne, mood_swings, fatigue
moodint?1–5
energyint?1–5 self-rating; feeds the recommender
sleep_hoursnumeric?
notestext?

Compound unique: (profile_id, date).


workouts

Catalog. Ships with seed rows from WORKOUT_LIBRARY.md. User-created custom workouts also live here with is_custom=true.

columntypenotes
iduuid PK
slugtextstable identifier like F1_full_body_strength
titletext
descriptiontext
phaseenummenstrual · follicular · ovulatory · luteal_early · luteal_late
min_levelenumnew · regular · athlete
equipmentenum[]
goalstext[]
duration_minint
intensity_ceiling_rpeint1–10
stepsjsonbsee WorkoutStep below
exportablebooltrue iff every step has a concrete duration + target
is_custombool
owner_profile_iduuid?non-null only when is_custom

WorkoutStep (embedded jsonb)

jsonc
{
  "order": 1,
  "kind": "work" | "rest" | "warmup" | "cooldown" | "repeat",
  "durationSeconds": 300,      // required for exportable=true
  "target": {                  // exactly one of the following:
    "rpe": 8,                  //   RPE 1..10
    "zone": 4,                 //   Zone 1..5
    "hr": { "min": 150, "max": 165 },
    "power": { "min": 220, "max": 260 }
  },
  "description": "5 reps goblet squat, 90s rest",
  "repeat": {                  // set only when kind == "repeat"
    "count": 6,
    "childSteps": [ /* nested WorkoutStep[] */ ]
  }
}

This shape is deliberately close to the Intervals.icu workout JSON and the Garmin FIT workout_step message so exporters are near-lossless.


workout_sessions

A completed instance of a workout.

columntypenotes
iduuid PK
profile_iduuid FK
workout_iduuid FK
datedate
phase_at_timeenumsnapshot; the user's phase may drift as cycle data updates, but this is what we recommended based on
day_of_cycle_at_timeint
completedbool
felt_scoreint?1–5 subjective post-workout feel; feeds the recommender
notestext?
external_idsjsonb?{ "strava": "12345", "garmin": "abc", "intervals_icu": "xyz" }

integrations

Per-provider OAuth state.

columntypenotes
iduuid PK
profile_iduuid FK
providerenumgarmin · strava · intervals_icu · apple_health · google_fit
enabledbool
access_token_encryptedbytea?never plaintext at rest
refresh_token_encryptedbytea?
expires_attimestamptz?
scopestext[]

Compound unique: (profile_id, provider).


Supabase RLS

Every table has RLS enabled. Policy on every user-owned table:

sql
create policy "own rows only"
on <table>
for all
using (profile_id = auth.uid())
with check (profile_id = auth.uid());

The workouts table adds a public-read exception for is_custom = false seed rows.


Sync strategy

  • Local writes are authoritative and offline-first. Each row gets updated_at = now().
  • Sync loop (when online and signed in): push local rows where updated_at > last_synced_at, then pull remote rows where updated_at > last_synced_at.
  • Conflict resolution: last-write-wins per row. This is fine for a single-user app synced across their own devices; if we later add sharing, we revisit.
  • Deletes are soft (deleted_at) so a stale device coming online can't resurrect deleted rows.

Extension points (kept in mind but not built)

  • basal_body_temp, cervical_mucus, lh_test_result columns on cycle_days — for fertility mode.
  • wearable_readings table — HRV, sleep score, resting HR — to make the recommender adjust for objective recovery, not just phase.
  • programs table — multi-week structured programs vs. day-by-day recommendations.

UNDA is a fitness and training support product. It is not a medical device.