Skip to content

UNDA — Data Flow

Status: Draft. Reflects the current prototype: local-first, sqflite on device, no network calls in production paths.

High-level flow

             ┌─────────────────────────────────────────────────────────┐
             │                   User's device                         │
             │                                                         │
   user ──►  │   UI (Flutter widgets) ──► Riverpod state ──► sqflite   │
             │                                     │            │       │
             │                                     └── read ──► │       │
             │                                                  ▼       │
             │                                             unda.db      │
             │                                          (unencrypted    │
             │                                           in v0.1 — B10) │
             └─────────────────────────────────────────────────────────┘

   (planned) Personalize → MockImporter → ProfileInferenceService → profileProvider
   (planned) Settings → sync toggle ON → Supabase (EU region), row-level security
   (planned) WorkoutDetail → .fit export → user shares file (never uploaded by UNDA)

Actual current flows

F1. Onboarding → Profile persistence

  1. User picks cycle length + last period date.
  2. OnboardingController.finish()profileProvider.state = new Profile(...).
  3. PersistedProfileNotifier writes to profiles table with sync_enabled=false, onboarding_complete=true.
  4. Nothing leaves the device.

F2. Symptom logging

  1. User taps a chip on Calendar.
  2. SymptomLog.toggle(date, symptom) → sqflite cycle_days upsert.
  3. Nothing leaves the device.

F3. Mark session completed

  1. User taps "Mark completed", picks felt-score 1–5.
  2. SessionLog.logCompleted(...) → sqflite workout_sessions insert.
  3. Nothing leaves the device.

F4. Personalize demo import (MockImporter)

  1. User taps a demo scenario.
  2. MockImporter.fetchRecent(...) returns fixture activities (no network).
  3. ProfileInferenceService.infer(...) produces an InferredProfile.
  4. User confirms → profile updated in sqflite.
  5. Nothing leaves the device.

Planned flows (not yet wired)

FP1. Cloud sync (opt-in)

  • Supabase EU region.
  • Row-level security keyed on auth.uid() — user only sees their own rows.
  • Sync direction: local ↔ remote, last-write-wins per row (updated_at).
  • Transfer basis: within EU/EEA; no international transfer in the default configuration.
  • Consent event required: health_data_processing and cloud_sync (see consent-register.md).

FP2. Strava / Intervals.icu / Garmin import (opt-in per provider)

  • OAuth in the user's browser.
  • Tokens go to secure storage (iOS Keychain / Android EncryptedSharedPreferences) via flutter_secure_storage.
  • Fetch: recent activities (default 90 days backfill, then incremental).
  • Storage: only fields listed in data-inventory.md under imported_activity. Raw JSON blobs are not persisted.
  • Per-provider consent event: strava / intervals_icu / garmin.
  • Disconnect: stops future sync and deletes previously imported activity rows (documented per §8.4).

F5. Export (shipped)

  1. User taps "Export my data" in Privacy Center.
  2. DataExporter.buildZip() writes UNDA-export-YYYYMMDD-HHmmss.zip to getTemporaryDirectory() with: README.txt, profile.json, cycle.csv, sessions.csv, consents.csv, integrations.json.
  3. share_plus invokes the OS share sheet. User chooses destination (Files, iCloud, AirDrop, mail, etc.).
  4. Nothing leaves the device via UNDA-controlled channels — the user picks the recipient.

Data leaves the device only when...

  • Cloud sync is explicitly ON.
  • An integration OAuth is explicitly authorized.
  • The user chooses to share/export.

Every one of these paths must be gated by a versioned consent record (§4).

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