# geniusrun database schema Generated from the live schema via `go run ./cmd/dumpschema` -- do not hand-edit. The source of truth is `backend/internal/store/schema.sql`; regenerate this file after changing it. ## Tables - [`users`](#users) - [`profile`](#profile) - [`workout_kinds`](#workout_kinds) - [`workout_type_paces`](#workout_type_paces) - [`activities`](#activities) - [`laps`](#laps) - [`activity_samples`](#activity_samples) - [`kind_assignments`](#kind_assignments) - [`sync_state`](#sync_state) - [`sync_runs`](#sync_runs) ## `users` ```sql CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, oidc_sub TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); ``` ## `profile` ```sql CREATE TABLE profile ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL DEFAULT 'Default', garmin_email TEXT NOT NULL DEFAULT '', garmin_password TEXT NOT NULL DEFAULT '', -- Set once, the first time this user successfully authenticates with -- Garmin (handleAuthLogin/handleAuthMFA). Null means never connected -- -- the login gate uses this (not the in-memory auth status, which -- resets on restart) to decide whether a returning user must -- reconnect before entering the app. garmin_connected_at TEXT, rolling_window_days INTEGER NOT NULL DEFAULT 90, -- BackfillHorizonDays bounds how far back "Sync now" reaches when -- walking backward from today; read fresh on every sync (not cached at -- server startup), so changing it here takes effect on the next click. backfill_horizon_days INTEGER NOT NULL DEFAULT 1095, max_heart_rate REAL, resting_heart_rate REAL, hr_zone1_min_pct REAL NOT NULL DEFAULT 50, hr_zone1_max_pct REAL NOT NULL DEFAULT 60, hr_zone2_min_pct REAL NOT NULL DEFAULT 60, hr_zone2_max_pct REAL NOT NULL DEFAULT 70, hr_zone3_min_pct REAL NOT NULL DEFAULT 70, hr_zone3_max_pct REAL NOT NULL DEFAULT 80, hr_zone4_min_pct REAL NOT NULL DEFAULT 80, hr_zone4_max_pct REAL NOT NULL DEFAULT 90, hr_zone5_min_pct REAL NOT NULL DEFAULT 90, hr_zone5_max_pct REAL NOT NULL DEFAULT 100, -- Applies uniformly to every fixed-duration workout type's phase -- detection (Easy, Long, Tempo, Threshold 30'/60', MAS Test). Interval -- workouts detect phases from lap data directly and don't use these. warmup_minutes REAL NOT NULL DEFAULT 10, cooldown_minutes REAL NOT NULL DEFAULT 5, -- Review Queue pace-chart artifact filter: a stretch of samples slower -- than min_representative_pace_sec_per_km is dropped unless it persists -- for at least min_representative_time_seconds, in which case it's a -- real stop/walk break rather than GPS/motion noise at recording start. min_representative_pace_sec_per_km REAL NOT NULL DEFAULT 720, min_representative_time_seconds REAL NOT NULL DEFAULT 3, -- Chart colors: pace_color/heart_rate_color are each chart's "main -- line" color; warmup/effort/recovery/cooldown_color are the "effort -- kind" colors the frontend derives fills from (see -- ExpectedVsActualChart). pace_color TEXT NOT NULL DEFAULT '#3b82f6', heart_rate_color TEXT NOT NULL DEFAULT '#ef4444', warmup_color TEXT NOT NULL DEFAULT '#c2410c', effort_color TEXT NOT NULL DEFAULT '#7c3aed', recovery_color TEXT NOT NULL DEFAULT '#15803d', cooldown_color TEXT NOT NULL DEFAULT '#fb923c', main_line_tint_pct REAL NOT NULL DEFAULT 20, background_darken_pct REAL NOT NULL DEFAULT 35, target_brighten_pct REAL NOT NULL DEFAULT 20, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(user_id) ); ``` ## `workout_kinds` ```sql CREATE TABLE workout_kinds ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', color TEXT NOT NULL DEFAULT '', rule_json TEXT NOT NULL, priority INTEGER NOT NULL DEFAULT 0, is_active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(user_id, name) ); ``` ## `workout_type_paces` ```sql CREATE TABLE workout_type_paces ( workout_kind_id INTEGER PRIMARY KEY REFERENCES workout_kinds(id) ON DELETE CASCADE, pace_min_sec_per_km REAL, pace_max_sec_per_km REAL, hr_min_pct_hrr REAL, hr_max_pct_hrr REAL ); ``` ## `activities` ```sql CREATE TABLE activities ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, garmin_activity_id INTEGER NOT NULL, -- Derived at sync time from Garmin's own eventType.typeKey=="race" -- -- a hard fact, not a rule the user tunes (see internal/classify's -- is_race metric and handleReclassifyAll's Race special-case). event_type_key TEXT NOT NULL DEFAULT '', -- Set when the activity was recorded from a structured Garmin workout; -- used to resolve each lap's expected pace/HR band (see -- internal/sync/mapping.go's alignWorkoutTargets). workout_id INTEGER, start_time_utc TEXT NOT NULL, duration_seconds REAL NOT NULL, distance_meters REAL NOT NULL, avg_hr REAL, max_hr REAL, avg_speed_mps REAL, elevation_gain_m REAL, aerobic_training_effect REAL, anaerobic_training_effect REAL, vo2max_value REAL, raw_json TEXT NOT NULL, details_fetched_at TEXT, details_raw_json TEXT, splits_fetched_at TEXT, -- Genuine raw get_workout_by_id() response, the source used to compute -- alignWorkoutTargets. Null when the activity has no workout_id. workout_raw_json TEXT, -- Set when get_workout_by_id returned a definitive HTTP 404 (the -- workout was deleted on Garmin's side after being linked to this -- activity) -- distinct from workout_raw_json staying null for "not yet -- fetched": this activity is excluded from ActivitiesMissingWorkout so -- it stops being retried forever (see -- docs/superpowers/specs/2026-07-27-workout-not-found-design.md). -- workout_raw_json itself is never fabricated; it just stays null. workout_not_found_at TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(user_id, garmin_activity_id) ); ``` Indexes: ```sql CREATE INDEX idx_activities_start_time ON activities(start_time_utc); ``` ## `laps` ```sql CREATE TABLE laps ( id INTEGER PRIMARY KEY AUTOINCREMENT, activity_id INTEGER NOT NULL REFERENCES activities(id) ON DELETE CASCADE, lap_index INTEGER NOT NULL, avg_speed_mps REAL, intensity_type TEXT NOT NULL DEFAULT '', hr_drift_bpm_per_min REAL, hr_recovery_bpm_per_min REAL, -- Expected band for this lap, resolved from the activity's structured -- Garmin workout when its steps line up 1:1 with the recorded laps. -- Null when there's no structured workout or the counts don't match. target_pace_low_mps REAL, target_pace_high_mps REAL, target_hr_low_bpm REAL, target_hr_high_bpm REAL, raw_json TEXT NOT NULL, UNIQUE(activity_id, lap_index) ); ``` ## `activity_samples` ```sql CREATE TABLE activity_samples ( id INTEGER PRIMARY KEY AUTOINCREMENT, activity_id INTEGER NOT NULL REFERENCES activities(id) ON DELETE CASCADE, elapsed_seconds REAL NOT NULL, timestamp_ms INTEGER NOT NULL, heart_rate REAL, speed_mps REAL, distance_m REAL, elevation_m REAL ); ``` Indexes: ```sql CREATE INDEX idx_activity_samples_activity ON activity_samples(activity_id, elapsed_seconds); ``` ## `kind_assignments` ```sql CREATE TABLE kind_assignments ( id INTEGER PRIMARY KEY AUTOINCREMENT, activity_id INTEGER NOT NULL REFERENCES activities(id) ON DELETE CASCADE, workout_kind_id INTEGER REFERENCES workout_kinds(id), assignment_source TEXT NOT NULL CHECK(assignment_source IN ('rule_engine','manual')), status TEXT NOT NULL CHECK(status IN ('assigned','needs_review')), confidence REAL, candidate_kinds_json TEXT NOT NULL DEFAULT '[]', created_at TEXT NOT NULL DEFAULT (datetime('now')) ); ``` Indexes: ```sql CREATE INDEX idx_kind_assignments_activity ON kind_assignments(activity_id); CREATE INDEX idx_kind_assignments_status ON kind_assignments(status); ``` ## `sync_state` ```sql CREATE TABLE sync_state ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, earliest_synced_date TEXT, backfill_complete INTEGER NOT NULL DEFAULT 0, UNIQUE(user_id) ); ``` ## `sync_runs` ```sql CREATE TABLE sync_runs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, kind TEXT NOT NULL CHECK(kind IN ('backfill','incremental','full')), started_at TEXT NOT NULL, finished_at TEXT, activities_fetched INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL CHECK(status IN ('running','success','error')), error_message TEXT ); ``` ## Views ### `current_kind_assignment` ```sql CREATE VIEW current_kind_assignment AS SELECT a.* FROM kind_assignments a JOIN ( SELECT activity_id, MAX(id) AS max_id FROM kind_assignments GROUP BY activity_id ) latest ON a.activity_id = latest.activity_id AND a.id = latest.max_id; ```