Skip to main content

Architecture

info

This page covers the internal architecture of Flow Control including data flow, tech stack, and key architectural decisions.

Flow Control series

  1. Flow Control
  2. Architecture - You are here
  3. Cursor sync daemon
  4. Music sync daemon
  5. Health sync daemon (disabled)
  6. HealthSync iOS app
  7. Deployment

Data flow overview

Goals page with exercise goals

Tech stack

ComponentTechnology
FrameworkNext.js 15 (App Router)
DatabasePostgreSQL 16
ORMPrisma 7 with @prisma/adapter-pg
AuthenticationAuth.js v5 + OIDC provider
ChartsRecharts
UITailwind CSS, Radix UI
DeploymentKubernetes + Flux GitOps

Database schema

The dashboard uses several key tables:

CursorEvent

Stores individual Cursor AI usage events.

ColumnTypeDescription
eventDateDateTimeTimestamp of request
kindStringRequest type (Included, On-Demand)
modelStringAI model used
inputTokensIntInput tokens
outputTokensIntOutput tokens
costDecimalUSD cost

DailyStat

Pre-computed daily statistics for fast rendering.

ColumnTypeDescription
dateDatePrimary key
cursorRequestsIntDaily request count
cursorCostDecimalDaily cost
pipelineTotalIntTotal pipelines
firstActivityTimeFirst activity time
lastActivityTimeLast activity time

DailySleep

Sleep records from Apple Health.

ColumnTypeDescription
dateDateTimeSleep date (wake-up date convention, matching Oura)
sleepStartStringSleep start time (HH:MM)
sleepEndStringSleep end time (HH:MM)
timeAsleepMinsIntActual sleep minutes
efficiencyIntSleep efficiency (0-100)
remMinsIntREM stage duration
coreMinsIntLight/Core stage duration
deepMinsIntDeep stage duration
importedAtDateTimeWhen the record was first created
rollingDebtMinsInt?Unused -- snapshotting was tried and removed (see decisions below)

Workout

Workout sessions from Apple Health (via HealthSync iOS). Includes distance (Decimal, km) from HKWorkout.totalDistance for running, cycling, walking.

MusicEvent

Music listening events from the music-sync daemon.

ColumnTypeDescription
trackNameStringTrack title
artistStringTrack artist
albumStringAlbum name
genreString?Genre tag
durationFloat?Track duration in seconds
startedAtDateTimeWhen playback started
endedAtDateTime?When playback ended/changed

HealthSettings

Configuration and cached AI insights.

ColumnTypeDescription
sleepDebtThresholdIntDaily sleep target in minutes (default 450 min / 7.5h). Configurable from the iOS app's Sleep Preferences.
sleepDebtWindowIntRolling window (default 14 days)
cachedInsightJson?Cached AI health insight
cachedInsightAtDateTime?When insight was cached

Key architectural decisions

Production-only environment

Decision: Single production environment, no dev/staging.

Why:

  1. Simplicity reduces maintenance overhead
  2. Personal dashboard does not need multiple environments
  3. GitOps makes rollbacks trivial if needed

Local daemons for locked data

Decision: Run daemons locally on Mac rather than in Kubernetes.

Why:

  1. Cursor stores auth token in local SQLite (cannot access from cluster)
  2. Health data flows through local Google Drive sync
  3. Daemons have minimal resource requirements

Auto-discovery of GitLab projects

Decision: Dynamically discover projects from GitLab API instead of hardcoded list.

Why:

  1. New projects automatically tracked without code changes
  2. No manual configuration when adding repositories
  3. Projects refreshed on each import cycle

Gap-aware active hours

Decision: Calculate active hours by detecting gaps greater than 30 minutes.

Why:

  1. Span hours (first to last activity) overstate work time
  2. Breaks for meals, meetings, and sleep should not count
  3. More accurate representation of actual work

AI insight caching

Decision: Cache AI-generated health insights until new data arrives.

Why:

  1. Health data only updates once per day
  2. Reduces AI API costs significantly
  3. Faster page loads (cached response is instant)

Billing cycle alignment

Decision: Align all cost displays to Cursor's actual billing cycle.

Why:

  1. Dashboard costs must match Cursor's billing page
  2. Billing anchor time discovered via API analysis (not always midnight)
  3. Users can navigate between billing periods

Sleep debt always computed live

Decision: Compute rolling sleep debt from raw records on every request rather than storing snapshots.

Why:

  1. rollingDebtMins snapshots were tried (column exists in DailySleep) but produced wildly incorrect values during bulk imports (e.g., 57 mins stored vs 700 mins computed live)
  2. With a single trusted source (Oura via HealthKit), retroactive chart changes from data revisions are minimal
  3. Live computation from 28 days of records is fast enough for every page load
  4. Eliminates a category of bugs where snapshots and reality diverge

Single-source sleep data

Decision: The iOS app filters HealthKit sleep queries to a single selected tracker (Oura, Apple Watch, iPhone, or AutoSleep).

Why:

  1. Multiple sleep sources in HealthKit produce overlapping/duplicate data
  2. Combining sources inflated sleep durations by up to 2x, corrupting sleep debt calculations
  3. Matching Oura's convention (wake-up date attribution) requires knowing which source is authoritative

Wake-up date convention

Decision: Sleep sessions are attributed to the date you wake up, not the date you fell asleep.

Why:

  1. Matches Oura's date convention (the authoritative sleep data source)
  2. "Last night's sleep" for Feb 15 is the sleep session ending on Feb 15, even if bedtime was Feb 14
  3. Session-based grouping in HealthKitManager.swift uses groupIntoSessions() to cluster samples by a 2-hour gap threshold, then assigns the wake-up date

Minute truncation fix (Feb 15, 2026)

Decision: Accumulate total seconds across all merged sleep intervals per stage, then round to minutes once at the end.

Why:

  1. The original mergedMinutes() function floor-truncated (Int()) on each individual interval
  2. With 5-20 intervals per stage, each losing ~0.5 min on average, this lost ~13 min/night
  3. Over 14 nights, the truncation error accumulated to 3 hours of phantom sleep debt
  4. Discovered by comparing Oura native values against the database: consistent ~13 min/night shortfall

API endpoints

EndpointMethodDescription
/api/healthGETHealth check for K8s probes
/api/sourcesGETData source status and health
/api/insightsGETDecision support insights
/api/stats/summaryGETAggregated stats for MCP tools
/api/stats/activity-trendGETBilling-cycle activity data
/api/stats/forecastGETUsage forecasting
/api/health/summaryGETHealth metrics summary
/api/health/insightsGETAI-powered health recommendations
/api/import/cursor-eventsPOSTReceive events from daemon
/api/import/music-eventsPOSTReceive music events from daemon
/api/import/health-appPOSTReceive health data from HealthSync iOS app
/api/import/healthPOSTReceive health data from daemon (disabled)
/api/import/gitlabPOSTTrigger GitLab import
/api/music/timelineGETMusic events for a time range
/api/music/top-flowGETTop songs during flow-building periods

Security considerations

ControlImplementation
API key authenticationBearer token for daemon endpoints
Input validationZod schemas for all incoming data
Rate limiting120 requests/minute per client
Secure token handlingCursor token read-only, never logged
HTTPS enforcementWarning logged for non-HTTPS

Next: Cursor sync daemon covers the local daemon that fetches Cursor AI usage data. See also Music sync daemon for Apple Music integration.