Skip to main content

Data models

Scoped to Stage 1 (foundation for personal coach – running data).
I'll extend this when I add Apple HealthKit, predictive analytics, and holistic health data later.


Conventions

  • IDs: id is an auto-increment int for internal joins; external IDs stored with unique constraints (e.g., stravaId).
  • Timestamps: createdAt, updatedAt (UTC).
  • Soft delete: use an audit entry; don’t hard-delete activities.
  • Units: SI (metres, seconds). Format in UI only.
  • Privacy: store references to images, not blobs, in primary tables.

Activity

Core activity data with performance optimisations.

The canonical record for a run imported from Strava (running only in stage 1).

fieldtypenotes
idBigInt (pk)Strava activity id
nameStringActivity title
sportTypeString"Run","Ride","Walk","Workout" (indexed)
dateDateTimeActivity date (indexed)
distanceKmFloatDistance in kilometers (indexed)
movingTimeSecIntMoving time in seconds
elapsedTimeSecIntTotal elapsed time
elevationGainMFloatElevation gain in meters
avgHrInt?Average heart rate (nullable)
maxHrInt?Maximum heart rate (nullable)
avgCadenceFloat?Average cadence (nullable)
maxCadenceFloat?Maximum cadence (nullable)
avgPaceSecPerKmInt?Average pace per km (indexed)
fastestPaceSecPerKmInt?Best 1km pace (indexed)
caloriesInt?Calories burned (nullable)
avgPowerFloat?Average watts (nullable)
maxPowerFloat?Maximum watts (nullable)
temperatureFloat?Temperature in celsius (nullable)
descriptionString?Activity description (nullable)
gearNameString?Gear used (nullable)
achievementCountInt?Kudos, achievements (nullable)
photoCountInt?Number of photos (nullable)
elevationLowMFloat?Lowest elevation (nullable)
elevationHighMFloat?Highest elevation (nullable)
thumbPngBytes?Thumbnail (excluded from API responses)
isPrivateBooleanPrivacy flag
rawJson?Full Strava JSON (excluded from API responses)
createdAtDateTimeRecord creation timestamp

Indexes

  • unique(stravaId).
  • idx(startTime).
  • idx(athleteId, startTime).

Split

Enhanced per-kilometer splits with normalized data.

Normalised lap/split storage. No fabrication.

fieldtypenotes
activityIdBigIntForeign key to Activity
kmIntSplit number (1,2,3,4...)
distanceMFloat?Actual distance in meters (1000, 983, 83)
movingTimeSecInt?Actual duration for this split
elapsedTimeSecInt?Total elapsed time including stops
avgSpeedMsFloat?Average speed in m/s from Strava
paceSecInt?Pace per km (legacy field)
elevGainMFloat?Elevation gain for this split
hrAvgInt?Average heart rate for this split

Indexes

  • @@id([activityId, km]).
  • @@index([activityId]).

Rules

  • Last lap may be partial; store exactly as reported by Strava.
  • Normalized storage from Strava's raw data for accuracy.

Best efforts

Precomputed tables for faster UI (400m → 50k).

fieldtypenotes
idint (pk)
activityIdint (fk)→ activity.id
distanceMint400,800,1000,1609,5000,10000,21100,42200,50000
durationSintbest time observed
windowenumall, 2y, 1y, 3m
rankint1..N

Indexes

  • idx(distanceM, window, durationS).
  • idx(activityId).

Strava token

Database-first authentication with audit trail.

Strava OAuth tokens with enterprise-grade audit trail.

fieldtypenotes
idInt (pk)Auto-increment primary key
athleteIdBigIntStrava athlete ID (allows multiple records per athlete)
accessTokenStringEncrypted at rest
refreshTokenStringEncrypted at rest
expiresAtDateTimeToken expiry time
scopeString?OAuth scopes granted (nullable)
isActiveBooleanAudit trail flag (default: true)
createdAtDateTimeRecord creation timestamp
updatedAtDateTimeLast update timestamp
lastUsedAtDateTimeTracks token usage
revokedAtDateTime?When token was deactivated/disconnected
athleteNameString?Athlete profile info (nullable)
profileUrlString?Profile URL (nullable)

Indexes

  • @@index([athleteId, isActive]) for efficient queries.

Rules

  • Refresh proactively (e.g., 5 min before expiry).
  • On failure → status='degraded' and surface reconnect UI.

Audit

One row per significant event.

fieldtypenotes
idint (pk)
entityenumactivity, split, token, job, admin.
entityIdinttarget id
actionenumcreate, update, skip, delete, error.
messagestringhuman-readable
metajsonerror codes, request ids
createdAtdatetime

Job

Background work queue (webhook fetch, thumbnails, analytics).

fieldtypenotes
idint (pk)
typeenumfetch, thumbnail, analytics.
payloadjsonminimal
statusenumqueued, running, done, error, retry.
attemptsint0..N
lastErrorstring?nullable
createdAtdatetime
updatedAtdatetime

Rules

  • Exponential backoff; cap attempts; move to error with audit.

Thumbnail

Reference to generated images.

fieldtypenotes
idint (pk)
activityIdint (fk)→ activity.id
urlstringrelative/absolute
widthintpx
heightintpx
createdAtdatetime

Principle: API returns metadata/URLs; UI fetches images directly.


User

Placeholder for stage 2+.

For stage 1 this can be a single admin user or omitted entirely.

fieldtypenotes
idint (pk)
emailstringunique
roleenumadmin, user.
createdAtdatetime

Enhanced models

Stage 1 foundation complete.

Caching strategy

  • Thumbnails: public, max-age=86400, stale-while-revalidate=604800 (24h cache + 7-day stale).
  • Activity details: public, max-age=3600, stale-while-revalidate=86400 (1h cache + 24h stale).
  • Activity lists: public, max-age=300, stale-while-revalidate=1800 (5min cache + 30min stale).
  • Maps & splits: public, max-age=3600, stale-while-revalidate=86400 (1h cache + 24h stale).

Performance protection

  • Large field exclusion: raw (MB of JSON) and thumbPng (base64 images) automatically excluded from API responses.
  • Field selection: Lists APIs use explicit field selection to avoid performance issues.
  • Database optimisation: Proper indexing on sportType, date, fastestPaceSecPerKm, distanceKm.

ER sketch

Stage 1 complete.

Activity (1) — (N) Split
Activity (1) — (N) ThumbnailJob
Activity (1) — (N) ActivityStream
Activity (1) — (N) BestEffort

StravaToken — standalone with isActive audit trail
ActivityFetchJob — webhook-triggered job queue
WebhookSubscription — app-wide subscription cache

Real-time webhook architecture with enterprise-grade audit trails and performance optimisation.