Architecture
This page covers the internal architecture of Flow Control including data flow, tech stack, and key architectural decisions.
Flow Control series
- Flow Control
- Architecture - You are here
- Cursor sync daemon
- Music sync daemon
- Health sync daemon (disabled)
- HealthSync iOS app
- Deployment
Data flow overview
Tech stack
| Component | Technology |
|---|---|
| Framework | Next.js 15 (App Router) |
| Database | PostgreSQL 16 |
| ORM | Prisma 7 with @prisma/adapter-pg |
| Authentication | Auth.js v5 + OIDC provider |
| Charts | Recharts |
| UI | Tailwind CSS, Radix UI |
| Deployment | Kubernetes + Flux GitOps |
Database schema
The dashboard uses several key tables:
CursorEvent
Stores individual Cursor AI usage events.
| Column | Type | Description |
|---|---|---|
| eventDate | DateTime | Timestamp of request |
| kind | String | Request type (Included, On-Demand) |
| model | String | AI model used |
| inputTokens | Int | Input tokens |
| outputTokens | Int | Output tokens |
| cost | Decimal | USD cost |
DailyStat
Pre-computed daily statistics for fast rendering.
| Column | Type | Description |
|---|---|---|
| date | Date | Primary key |
| cursorRequests | Int | Daily request count |
| cursorCost | Decimal | Daily cost |
| pipelineTotal | Int | Total pipelines |
| firstActivity | Time | First activity time |
| lastActivity | Time | Last activity time |
DailySleep
Sleep records from Apple Health.
| Column | Type | Description |
|---|---|---|
| date | DateTime | Sleep date (wake-up date convention, matching Oura) |
| sleepStart | String | Sleep start time (HH:MM) |
| sleepEnd | String | Sleep end time (HH:MM) |
| timeAsleepMins | Int | Actual sleep minutes |
| efficiency | Int | Sleep efficiency (0-100) |
| remMins | Int | REM stage duration |
| coreMins | Int | Light/Core stage duration |
| deepMins | Int | Deep stage duration |
| importedAt | DateTime | When the record was first created |
| rollingDebtMins | Int? | 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.
| Column | Type | Description |
|---|---|---|
| trackName | String | Track title |
| artist | String | Track artist |
| album | String | Album name |
| genre | String? | Genre tag |
| duration | Float? | Track duration in seconds |
| startedAt | DateTime | When playback started |
| endedAt | DateTime? | When playback ended/changed |
HealthSettings
Configuration and cached AI insights.
| Column | Type | Description |
|---|---|---|
| sleepDebtThreshold | Int | Daily sleep target in minutes (default 450 min / 7.5h). Configurable from the iOS app's Sleep Preferences. |
| sleepDebtWindow | Int | Rolling window (default 14 days) |
| cachedInsight | Json? | Cached AI health insight |
| cachedInsightAt | DateTime? | When insight was cached |
Key architectural decisions
Production-only environment
Decision: Single production environment, no dev/staging.
Why:
- Simplicity reduces maintenance overhead
- Personal dashboard does not need multiple environments
- GitOps makes rollbacks trivial if needed
Local daemons for locked data
Decision: Run daemons locally on Mac rather than in Kubernetes.
Why:
- Cursor stores auth token in local SQLite (cannot access from cluster)
- Health data flows through local Google Drive sync
- Daemons have minimal resource requirements
Auto-discovery of GitLab projects
Decision: Dynamically discover projects from GitLab API instead of hardcoded list.
Why:
- New projects automatically tracked without code changes
- No manual configuration when adding repositories
- Projects refreshed on each import cycle
Gap-aware active hours
Decision: Calculate active hours by detecting gaps greater than 30 minutes.
Why:
- Span hours (first to last activity) overstate work time
- Breaks for meals, meetings, and sleep should not count
- More accurate representation of actual work
AI insight caching
Decision: Cache AI-generated health insights until new data arrives.
Why:
- Health data only updates once per day
- Reduces AI API costs significantly
- Faster page loads (cached response is instant)
Billing cycle alignment
Decision: Align all cost displays to Cursor's actual billing cycle.
Why:
- Dashboard costs must match Cursor's billing page
- Billing anchor time discovered via API analysis (not always midnight)
- 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:
rollingDebtMinssnapshots were tried (column exists inDailySleep) but produced wildly incorrect values during bulk imports (e.g., 57 mins stored vs 700 mins computed live)- With a single trusted source (Oura via HealthKit), retroactive chart changes from data revisions are minimal
- Live computation from 28 days of records is fast enough for every page load
- 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:
- Multiple sleep sources in HealthKit produce overlapping/duplicate data
- Combining sources inflated sleep durations by up to 2x, corrupting sleep debt calculations
- 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:
- Matches Oura's date convention (the authoritative sleep data source)
- "Last night's sleep" for Feb 15 is the sleep session ending on Feb 15, even if bedtime was Feb 14
- Session-based grouping in
HealthKitManager.swiftusesgroupIntoSessions()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:
- The original
mergedMinutes()function floor-truncated (Int()) on each individual interval - With 5-20 intervals per stage, each losing ~0.5 min on average, this lost ~13 min/night
- Over 14 nights, the truncation error accumulated to 3 hours of phantom sleep debt
- Discovered by comparing Oura native values against the database: consistent ~13 min/night shortfall
API endpoints
| Endpoint | Method | Description |
|---|---|---|
/api/health | GET | Health check for K8s probes |
/api/sources | GET | Data source status and health |
/api/insights | GET | Decision support insights |
/api/stats/summary | GET | Aggregated stats for MCP tools |
/api/stats/activity-trend | GET | Billing-cycle activity data |
/api/stats/forecast | GET | Usage forecasting |
/api/health/summary | GET | Health metrics summary |
/api/health/insights | GET | AI-powered health recommendations |
/api/import/cursor-events | POST | Receive events from daemon |
/api/import/music-events | POST | Receive music events from daemon |
/api/import/health-app | POST | Receive health data from HealthSync iOS app |
/api/import/health | POST | Receive health data from daemon (disabled) |
/api/import/gitlab | POST | Trigger GitLab import |
/api/music/timeline | GET | Music events for a time range |
/api/music/top-flow | GET | Top songs during flow-building periods |
Security considerations
| Control | Implementation |
|---|---|
| API key authentication | Bearer token for daemon endpoints |
| Input validation | Zod schemas for all incoming data |
| Rate limiting | 120 requests/minute per client |
| Secure token handling | Cursor token read-only, never logged |
| HTTPS enforcement | Warning 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.