Roadmap β
Where UNDA is, what's next, and what we're not building yet. Updated: 2026-08-31.
Legend β
- β Shipped
- π§ In progress
- π Next up
- π Planned
- βΈοΈ Deferred / out of scope for now
Where we are β
UNDA is a cycle-aware workout tracker for iOS (Android after v1). Local-first by default, opt-in cloud sync (Phase 7), evidence-based phase-aware training recommendations, and privacy-first architecture.
Stack: Flutter (Dart), SQLite via sqflite_sqlcipher (AES-256 at rest), Riverpod, Cloudflare Pages for the OAuth redirect + this docs site.
Progress by phase β
Phase 0 β Design + prototype scaffold β β
- Product brief, UNDA visual system, 5-phase cycle model.
- Flutter skeleton, phase-prediction engine, recommender, workout library with sport-specific programming (road cycling, bouldering, running).
- Activity-import layer (
ActivityHistoryImporter) +ProfileInferenceServicethat proposes a personalised profile from imported activities.
Phase 1 β UNDA visual system β β
- Dark teal theme, Inter type via
google_fonts, outlined-button-first. - Today / Calendar / Plan / Insights / Settings screens.
- CustomPainter waveform driven by per-day phase energy.
- 3-step onboarding β welcome, age gate, health-consent, cycle length, last period.
Phase 2 β Persistence + Insights β β
- Schema v1:
profiles,cycle_days,cycles,workout_sessions. - Insights tab: symptoms per phase, sessions per phase, average felt score, recent sessions.
Phase 3 β Legal doc scaffolding β β
- Full
/docs/legal,/docs/privacy,/docs/security,/docs/evidence,/docs/producttree per Legal Rules Β§21.
Phase 4 β Compliance blockers B1βB4 β β
- B1 Delete all my data (Β§12.1).
- B2 Provenance tagging on sensitive rows (Β§9.3).
- B3 Uncertainty rendering β ovulation window, confidence chip, Β± days.
- B4 Algorithm-version + factor breakdown persisted per session (Β§9.7, Β§9.5).
Phase 5 β Compliance blockers B5βB10 β β
- B5 Versioned consent architecture (
consentstable, no bundling). - B6 18+ age gate with audit timestamp.
- B7 Recommender: phase becomes a weight, not a filter (Β§9.1).
- B8 Export ZIP β
UNDA-export-*.zipwith README, profile.json, cycle.csv, sessions.csv, consents.csv, integrations.json (Β§12.2). - B9 Privacy Center β Export / Manage consents / Delete account.
- B10 SQLCipher AES-256 encryption at rest with key in Keychain / EncryptedSharedPreferences.
All 10 Pre-Beta Gate blockers closed.
Phase 6 β Integrations β β
.icsand.fitexporters β structured workout steps render to spec-compliant Garmin FIT files and RFC 5545 calendar entries.- Apple Health / Health Connect β reads menstruation + workouts, writes completed sessions back (opt-in, separate WRITE permission).
- Strava OAuth β
--dart-defineclient credentials, HTTPS redirect via Cloudflare Pages, tokens in Keychain, activity fetch. - Intervals.icu β API-key auth, activity fetch, planned-workouts read for Today's suggestion override, push UNDA workouts to your Intervals calendar with add/remove (Add to Intervals plan on any workout detail; deduped by workout-slug Γ date).
- Cross-provider dedup β same ride from Garmin β Strava β Intervals collapses to one row at read time (Β±5 min startedAt, Β±30 s duration, priority: intervals_icu > strava > apple_health).
- Sync service β 30-min throttle, refresh on app foreground, pull-to- refresh on Insights.
- In-app workout runner β full-screen countdown per step, up-next preview, pause / prev / skip, felt-score sheet on finish, auto-logs the session. Enabled only for indoor workouts (bodyweight / dumbbells / gym); outdoor endurance shows "Push to Garmin" as the primary action instead.
- Today's suggestion priority β already-trained today > Intervals-planned for today > UNDA recommender > no match.
- 2-week block planner β Plan tab has "Plan next 2 weeks" that layers cycle phases + your typical weekly volume + rest cadence from Intervals history + workout library filters. Fully reviewable day-by-day (swap / mark rest / regenerate) before you bulk-push everything to your Intervals.icu calendar. Deduped against previously-pushed workouts so re-pushes are idempotent.
Phase 6f.3 β Today readings snapshot + PMC load block β (2026-08-10) β
- New "Today's readings" card on Today that shows the raw signals the ReadinessEngine is looking at β sleep hours, steps, HRV (with 30-day baseline comparison), resting HR (with baseline), hardest activity from the last 48h (TSS / avg HR / duration), recent felt-score strip, and echoes of the self-report (mood / energy / symptom count).
- Every row is only rendered when we have data for it β no dashes for missing fields.
- Tones color-code the readings: red for outliers (sleep <6h, HRV β20%, RHR +7bpm, TSS β₯100), yellow for soft warnings, green for good signals, neutral otherwise.
- Under the readings, a new PMC-style load block with 6 metrics laid out Intervals-style:
Total(this-week hours:minutes),Fitness(CTL),Form(TSB),Load(acute-week TSS),Fatigue(ATL),Ramp(7d CTL delta). All computed from the last 42 days of imported activities via standard exponential-decay: Ξ±_CTL = 2/(42+1), Ξ±_ATL = 2/(7+1). Missing TSS values fall back to duration-based estimate (60min β 50 TSS) so activities without recorded stress still count. - New
recentImportsForLoadProvider(FutureProvider.autoDispose) holds the 42-day window so the block re-computes on every sync bump.
Phase 6f.2 β Today check-in + self-report readiness β (2026-08-10) β
- New "How are you feeling today?" card on Today between the energy dots and the section divider. Quick-taps for mood 1-5 and energy 1-5, plus pill buttons for Flow (5 chips) and Symptoms (full grouped multi-select sheet).
- Every tap persists via CycleDayRepository and bumps todayCheckInBumpProvider; the readiness card below auto-refreshes so logging low energy immediately updates the recommendation.
- ReadinessEngine now weights self-reported energy (+2.0 at β€2/5) and mood (+1.2 at β€2/5) β heavier than most objective health signals because the user is explicitly telling us how they feel.
- Verdict provider no longer gated on the readiness Health toggle: it runs on symptoms + mood + energy + recent imports even when Health scopes aren't granted. Health data is additive, not required.
Phase 6f.1 β Readiness reads recent effort β (2026-08-10) β
- ReadinessEngine now also looks at the last 48h of imported activities from Strava/Intervals/Health. Adds signals for high TSS (β₯100), high average HR (β₯90% of max), and long endurance sessions (90+ min) when today's planned workout is itself demanding.
- Skips stacking β one "recent effort" factor max per verdict, so a single big ride yesterday nudges once, not five times.
Phase 6f β Readiness-aware suggestions β (2026-08-10) β
- New HealthKit scope (opt-in): sleep, steps, HRV SDNN, resting HR. Requested only when "Use readiness signals" toggle is on in Personalize.
- ReadinessEngine β rules-based, null-tolerant, bounded. Signals stack additively; low/med/high thresholds decide silent / soft downshift / hard recovery.
- Today card shows a color-coded readiness card only when a nudge applies. Lists the specific factors (e.g. "Sleep: 5.2h β a bit short", "HRV: 18% below your 30-day average") and offers an alternative workout with Keep-planned / Swap buttons.
- Swap flow β updates recency for the recommender AND, when Intervals.icu is connected, deletes today's planned events and pushes the alternative to the user's calendar so their Garmin gets the swap.
- Readiness values are never persisted β computed in memory, discarded after render. Only Apple Health stores them.
Phase 6e β Cycle-tracker completeness β (2026-08-10) β
- Log period start / end β outlined actions on the Today card. "Start" re-anchors phase estimates, auto-closes the prior open cycle (writes
end_date+cycle_length_days), opens a new one. "End today" writesperiod_length_dayson the open cycle. - Per-day log sheet β tap any day on Calendar β full sheet with: flow intensity (spot/light/medium/heavy), 20-symptom catalog grouped by physical / mood / other, mood 1-5, energy 1-5. Retro-logs any past day. Persists via extended
CycleDayRepository.save/load. - Past cycles view β Insights β "Your cycles" section shows the last 6 cycles with lengths, average, and range. Auto-refreshes when a new period is logged (via
cyclesBumpProvider). - Apple Health seed β on connect,
AppleHealthImporter.fetchMenstruationDatesreads existing HealthKit menstruation entries, compresses to cycle starts, and seedscyclesrows (deduped by start_date). Users get instant history if they've been tracking in Health already.
Phase 7 β Apple sign-in + first-sync + red-team CI β (2026-08-10) β
- Sign in with Apple native flow β sign_in_with_apple package,
com.apple.developer.applesigninentitlement, PKCE nonce (raw generated on-device, SHA-256 hashed for Apple, raw sent to Supabase for verification). Wired to Supabase.auth.signInWithIdToken. - First-sync prompt β bottom sheet asks "Use this device / Use my cloud data" when both sides have non-trivial data on first sign-in. Silent auto-sync when only one side has data.
wipeLocalOnly()helper preserves the auth session for the download path. - Red-team RLS test β supabase/tests/rls_isolation.sql simulates two users via JWT-claim spoofing, asserts B cannot SELECT / UPDATE / DELETE A's rows. Wired into GitHub Actions workflow that runs against a separate test project on any PR touching supabase/.
- Apple Developer setup runbook β docs/product/apple-sign-in-setup.md covers App ID, Services ID, key, Supabase provider config.
Phase 7 β Account UX rework β (2026-08-10) β
- Sign-in screen now framed as "Your UNDA account", not "sign in to sync". Copy leads with the user's win: save your progress across devices. Two paths: Continue with Apple (placeholder, prominent) and Continue with email (magic link on tap).
- Optional account step at end of onboarding β pushed after
finish()succeeds. Skip button in the app bar + a "Not now" text button at the bottom. Never blocks entering the app. - Settings account card at the top, replacing the dev-flavoured "Sign in to sync" row. Signed out: hero with a "Create your account" CTA. Signed in: teal-bordered card with email + Sync now + Sign out.
Phase 7 β Supabase sync + magic-link auth β (2026-08-10) β
- Supabase project created in EU (Frankfurt). Full schema mirror of sqflite v5 with RLS on every user-owned table, keyed on
auth.uid(). Consents table is append-only. Cascading FKs + adelete_my_account()RPC for one-shot server-side deletion. - AuthService wraps supabase_flutter.auth: magic-link email (
signInWithOtp), Sign in with Apple stub (needs Apple Developer key), sign-out, delete-account-remotely. - CloudSyncService: bidirectional round-trip β upload local rows with
updated_at > remote, download remote rows withupdated_at > local. Table order respects FKs (profile first). Consents append-only. Local profile id rewritten toauth.uid()on first sign-in. - Sign-in screen with magic-link form + privacy blurb explaining EU hosting + RLS + delete-wipes-remote.
- Settings row: "Sign in to sync" when signed out; email + Sync now + Sign out when signed in.
- Delete-account propagates to Supabase via
delete_my_account()RPC when signed in. Β§12.1 compliance. - Local-first preserved β app runs identically without sync.
Everything in the earlier "still open in Phase 7" list (Apple native flow, deep-link handler, first-sync prompt, red-team CI) shipped in the follow-up entries above.
Phase 7f β Workout i18n + sport coverage β (2026-08-18) β
- Workout title / description i18n β slug-keyed translation table (
workout_i18n.dart) covers all 64 workouts across es / de / fr / ru / kk. Descriptions translated for the 6 menstrual-phase entries; the remainder fall back to canonical English until sport-scientist review per rules doc Β§14.Workout.titleL10n(lang)/descriptionL10n(lang)are the callsite API. Exports (FIT / ICS / Intervals push) intentionally keep the English canonical so cross-app calendars stay predictable. - MTB + CrossFit coverage bump β both were single-entry sports. Added
F_mtb_skills_ride_60,LE_mtb_endurance_90,F_crossfit_strength_wod,LE_crossfit_moderate_wod. Beta users of those sports now have real phase coverage.
Phase 8 β Landing v3 + transactional email + deploy pipeline β (2026-08-19) β
Full day of pre-launch infrastructure. Nothing in the Flutter app changed; everything sits on the public surface (undaflow.app + email).
Landing v3 β tide-staging redesign
- Applied
design_handoff_unda_landing_v3: full-viewport hero stage with three parallax swell layers + floating phone mockup, floating glass pill bar with menu overlay + backdrop-blur, sticky-phone scroller (IntersectionObserver swaps four screens: Today, Connections, Plan, Privacy), the-five-phases explainer, athlete voices (3 placeholder quotes to swap before public launch), about, waitlist form, footer. - H1 iterated: 'Train with your cycle, not against it' β 'Every cycle is a signal' β 'Move with the tide' β 'Move with your cycle' (final). Competitor-collision on the first, tide-metaphor felt oceanography in translation, cycle in the H1 is the clearest.
- FAQ rewrite β 5 differentiation-first questions replacing the earlier generic set: 'Why not a training app + a cycle tracker separately?', 'Who is UNDA for?' (non-gatekeeping), 'What if my cycle is irregular?', 'Who can see my data?', 'Which apps and devices?'. Phrased so it doesn't mirror competitor FAQ patterns.
- Real app-icon PNG in the brand pill (v3 handoff shipped an inline SVG placeholder).
/termspage live (bare Terms of Use, 14 sections) β closes the footer link that was 404. Fine for closed beta; final legal- reviewed version blocks public launch, not beta.
i18n
- Client-side language switch (EN / ES, RU dropped). Preference persisted in localStorage, seedable via
?lang=URL param, auto-detected fromnavigator.languageon first visit. Hidden below 640px so mobile doesn't crowd. All ~95 UI strings translated; Spanish reviewed by hand ('Fluye con tu ciclo' instead of literal 'MuΓ©vete con la marea' β sounds native rather than oceanography).
Waitlist backend hardening
- Honeypot field (name=
website, off-screen, aria-hidden). Non-empty β server returns 200 OK + silent drop so bots can't A/B around it. - Pitch-phrase deny-list on the
trainingfield ('open to a quick chat', 'our services', 'dedicated mobile app', 'schedule a call', etc.). Silent 200 drop. Motivated by a real cold-outreach signup spotted in KV. - Gmail dot normalisation:
p.r.a.n@gmail.comandpran@gmail.comnow dedupe to the same KV key. Applies only to gmail.com / googlemail.com; other providers stored as typed. - Rate limit stays at 5/hour per IP.
- Waitlist CSV export gitignored so accidental commits don't leak emails.
Transactional email β Resend end-to-end
- Waitlist confirmation email via Resend on first signup only. Fire- and-forget via
context.waitUntilso the response never waits on the mail API. Custom UNDA-styled HTML template (no<img>β inline wordmark, dark teal, accent-green CTA). Text fallback matches. Only sends on first signup, dedup enforced by KV probe. - Missing env vars β graceful no-op (dev / preview environments keep working).
confirmation_sent_atpersisted on the record for audit. - Sending domain
undaflow.appverified in Resend (SPF + DKIM in Cloudflare DNS).hello@undaflow.appreceiving via Cloudflare Email Routing (free tier) forwarding to a personal inbox. - Supabase custom SMTP wired through the same Resend account for magic-link + confirm-signup emails. Paste-ready UNDA templates in
docs/product/resend-setup.md. Rotated API key mid-day after the first key was revoked; both waitlist + Supabase now use one key namedunda-supabase-smtp.
Deploy pipeline
- GitHub Actions workflow
.github/workflows/deploy-landing.ymlβ Cloudflare Pages' git integration had silently stopped auto- deploying 8 days back (last auto-build was commit05e99ce; a week of landing changes never went live until this workflow shipped). Now on every push touchingweb/landing/**or the workflow file, Actions callswrangler pages deploywith--branch=main. First run succeeded 2026-08-19 11:39 UTC, subsequent deploys ~30-60s.
Assets prepared, deferred
og.svg+og.htmlβ source templates for the 1200Γ630 social- share card. PNG production deferred (needs a browser screenshot orrsvg-convert); until it lands, sharing undaflow.app shows a broken image thumbnail. Not a beta blocker; a should-do before any visible launch post.
Phase 9 β TodayContext reconciliation + Home unification β (2026-08-25) β
Three surfaces (Home "Today's suggestion", check-in verdict, Intervals plan card) were computing overlapping views of the same question and disagreeing. Reconciled into one provider.
TodayContextβ single provider (todayContextProvider) resolves "what to do today" across every surface. Anchor order: Intervals plan β algorithmic pick β phase-default β none.ReadinessEngineevaluates against the same anchor the UI shows.PlannedWorkoutAdapterβ infers RPE from Intervals zone tags (Z1..Z6) + English/Spanish keywords so the engine can score a raw Intervals event.- Home unification β Coach card + Already-trained card were saying the same thing twice on rest days.
_CoachCardfalls silent on already-trained + no-anchor states._AlreadyTrainedCardrewritten with three states (single completed β recovery framing, multiple β substantial-training framing) reading total minutes from a newtrainedTodaySummaryProvider. - UX polish batch β
UndaPageRoute(fade+parallax) replacing everyMaterialPageRoute(22 sites); Hero on workout card title β detail AppBar;AnimatedMetriccount-up on the cycle-day number;UndaSkeletonshimmer replacing allCircularProgressIndicatorin metric-detail screens; tab bar migrated to Phosphor viaUndaIconswith outlineβfill on select; haptics on log-period, confirm-cycle, save day entry, mark workout completed, tab switch.
Phase 10 β UNDA Coach β (2026-08-26 β 2026-08-30) β
Cycle-aware training assistant, shipped in three passes.
Phase 10a β On-device scaffold + Daily Read card
UndaAssistantinterface +LocalAssistant(deterministic, offline, no cycle data leaves device). Recognises a small intent set (medical decline, causal-hormone decline, can-I-train-hard, why-easier, phase context). Every branch grounds its claim onTodayContext.factors.- Plan Assistant bottom sheet (
showModalBottomSheetover Plan, ~92px top inset so the week grid stays visible). Suggested prompts derived from the visible plan; envelope rendered as ordered structured parts (prose / grounding / session proposal / impact / follow-ups). Never raw markdown. - Daily Read card at top of Plan β two states (Hold / Swap), gated by
shouldProposeSwap(RPE β₯ 7 OR duration β₯ 60min precondition- β₯ 2 signals disagree). Swap state shows original struck through above replacement; buttons are equal weight (Accept / Train anyway).
dailyReadDecisionProvidercaps one decision per day.
- β₯ 2 signals disagree). Swap state shows original struck through above replacement; buttons are equal weight (Accept / Train anyway).
Phase 10b β Cloudflare backend + consent gate
web/landing/functions/api/coach.jsβ Pages Function on the landing site'sundaflow.appproject. Proxies Claude with a server-side SYSTEM prompt carrying every Β§17d safety rule verbatim (medical decline, no causal hormone claims, hedge on estimated data, no performance promises, use user data first, scope: training/recovery/cycle only, ground every claim). Rate limit 30 req/hr per IP via the existingWAITLISTKV.RemoteCoachclient implementingUndaAssistant. Consent gate:remoteCoachEnabledProvider(default off) β off returns theLocalAssistant, on returnsRemoteCoachpointing athttps://undaflow.app/api/coach. Endpoint override viaremoteCoachEndpointProviderfor dev.- Settings β Coach section with the toggle.
docs/backend/README.mddocuments Anthropic key setup,wrangler pages secret put, rotation, cost model (~$0.005/turn on Haiku 4.5).
Phase 10c β Rich chat UX (Β§12-Β§15 QA)
- Animated three-dot typing indicator (replaces the linear progress bar), empty state collapses after first turn, header simplified.
CoachHistoryRepositoryβ 10 last user turns persisted toSharedPreferences, de-dupes verbatim repeats. Empty-state "RECENT" section with Today / Yesterday / date labels + Clear button._DataUsedChipβ every coach turn carries a snapshot of theTodayContextfields the coach saw at answer time (phase, cycle_day, readiness, signals, today_session, upcoming_sessions, already_trained_today). Snapshot persisted per-turn so old answers keep showing the fields the coach actually saw.AskCoachButtonβ reusable contextual entry point. Deployed on Activity Details ("How was my ride onβ¦"), Calendar Day view ("Tell me about Tuesdayβ¦"), HRV / Sleep / Steps / Load detail screens ("Why did my HRV drop this week?"). Each pre-fills a natural-language question so the coach answers on open with zero typing.
Phase 11 β Plan + Calendar + Share overhaul β (2026-08-26 β 2026-08-27) β
Design handoff drops 3β6 landed as one continuous rework of the three tabs most visible to users.
Plan tab (design Β§5a, Β§12)
- Two-week grid at the top: 7Γ2 day cards with load bars tinted by each day's phase, phase-week headers ("FOLLICULAR β OVULATORY"). Selected-day session card below, phase legend, hormone markers ExpansionTile at bottom (was above sessions).
- Actionable session rows with intensity bars derived from RPE ceiling (not faked from duration), 44Γ44 trailing add button, Hero on title into workout detail.
- Controls row above the grid: Regenerate, Edit (opens the existing
PlanBuilderScreenfor per-day swap/rest/push), Push to Intervals pill (only visible when connected; routes into the builder for the working push flow). - Intervals overlay:
twoWeekPlanProviderfetches the athlete's Intervals calendar for the same 14 days and overrides UNDA- generated days that have an Intervals workout, adapting viaPlannedWorkoutAdapter. Overridden days marked with a 5px blue dot next to the number. - New
IntervalsWorkoutParserrecovers structuredWorkoutStep[]from Intervals descriptions (endurance macro syntax like "3x8min @ Z4 recovery 3min" and strength lists like "Hip thrust 3x12"). Returns null when there's no confidence signal β_TextOnlyFallbackin the runner shows the description + a "Log as done" primary that runs the normal completion path. - Test coverage: 7 parser unit tests, all passing.
Calendar tab (design Β§14, Β§19, QA Β§4-Β§5)
CycleEnergyStripabove the month grid: single cubic curve through all cycle days, gradient stepped through phase colours at midpoints, dashed vertical rule + ring at today. Replaces the six per-week wave paths that used to cross day numbers.- Tide grid (Β§19): 22Γ12 S-curve
TideGlyphabove each numeral, amplitude = phase energy, stroked in the day's phase-stroke colour (new dark-theme variants added toUndaPhaseColors). - Logged period runs render as one continuous accent capsule across consecutive days via run-aware corner radii (16 outer, 0 interior). Predicted runs use
_RunEdgeDashPainterβ top/ bottom continuous, cap arcs on outer cells only. Ovulation is a 16Γ1.5px surge underline; today wraps the whole cell column in a radius-32 capsule (glyph + numeral together). - Day/Month/Year segmented range: MONTH default, DAY opens the new
DayViewScreen(cycle context + activities + symptoms + recovery signals β each section renders only when it has data), YEAR opensYearViewScreen(12 rows, summary card of cycles / mean length / flagged count, per-cycle segments coloured by plausibility). - Cycle-energy wording corrected:
CYCLE ENERGY / Using average curveβESTIMATED CYCLE PATTERN / Based on your cycle estimate. Log how you feel across several cycles to build your personal pattern.Switches toYOUR PERSONAL PATTERN / Based on your logged cyclesonceuserDataReady.
Share editor (design Β§20, QA addendum)
_ComposedShareCardβ export branches on photo presence: photo added = composited photo + fixed scrim + card typography; no photo = transparent PNG. Preview shows a dark neutral for legibility but never reaches export.- Metric editor:
_availableStatsFor(ImportedActivity)picks chips in the design's per-type priority order (ride/hike promote elevation, strength offers no distance chip). Inline numbered chip picker (1/2/3 badges) β first chip becomes the card's 42px hero. Duplicate guard filters any stat matching the headline. - Elevation profile band for ride/hike/MTB/trail-run with > 50m gain: 44px band, 1.8px stroke over 28%-opacity fill.
- Matched Save/Share pair: 52px height, 14px radius, w600. Share is filled primary, Save is matching outlined. Dropped 'to Photos' suffix.
- New
ActivityRouteMapon Activity Details β full-width 16:10 card rendering the decoded polyline. Accent stroke with soft shadow, filled start dot with white core, distance chip bottom-right. No basemap tile (spec + tile-licence + photo- fight reasons).
Recent sessions (QA Β§3)
WorkoutLibrary.bySlug()resolves session slugs to human titles. Imported activities with raw-ID names (intervals:117914320, 6+ digit numerics) fall back to the activity-type label. Internal IDs never surface as user-facing.dedupeSessions()in the aggregator collapses cross-provider duplicates by day + sport family + 50% duration overlap. Wired intounifiedSessionsProvider.
Insights (QA Β§1-Β§2)
- "Your cycles" row:
actualβToday+Currentbadge. - Symptoms-by-phase / Sessions-completed-by-phase / Felt-averages sections hidden this pass (widgets retained for when the interpretation layer lands).
In progress / next up β
π UX iteration from beta feedback (v0.2) π next β
See the section below for the full list. Highest-value, all code-only, tractable in one commit:
- Home overcrowding fix (move Load block to Insights, collapse Readings, keep only cycle-day header / phase tag / energy row / period actions / Today's session / plan CTA).
- Session-card stale-on-cycle-day-boundary invalidation.
- Text-size accessibility setting.
- Dedicated Profile section.
π Daily Insight suite (v0.2 flagship UX) β
Slice 1 (pre-workout check-in) and the readiness engine already shipped. Remaining slices ranked by tractability:
- Slice 4 β Exercise analytics + cycle overlay. Per-exercise history, performance charts with subtle phase bands as background (never causal), tappable points showing cycle-day + symptoms alongside power/RPE/sleep. All local data β no research pipeline needed.
- Slice 5 β Personal cycle-performance patterns. The roadmap's strongest differentiator, longitudinal user-data only, gated by minimum-data rules we already ship (β₯3 valid cycles, confidence labels scale with sample size). Bigger scope.
- Slice 3 β Daily Insight stories. Immersive vertical flow from a compact Home card. Content pipeline (JSON in-repo β CMS later) + claims-register check per SCI-001 before anything user-facing ships. Editorial work, not code.
- Slice 2 β Daily metrics grid. Mostly already shipped via the Home daily-metrics section.
π Garmin Connect integration β
- Requires Garmin developer partner approval β slow, needs a business entity + a written use case.
- Once approved: OAuth 2.0 flow similar to Strava, activity fetch, planned- workout push.
π Google Health Connect (Android) β
- Companion to Apple Health once we ship Android.
- Same read (menstruation + workouts) + write (completed sessions) pattern.
- Runtime permissions per category, incremental grants.
π Recommender v1.0.0 β
- Feed real signals into the recommender: today's subjective energy, recent felt-scores per phase, imported HRV / RHR / sleep from Health.
- "Recent felt-score in this phase" as a factor so historically-bad phases auto-downshift.
- Explainability panel: show per-factor contribution history over the last N recommendations so users understand how UNDA is learning.
π Daily Insight + check-in analytics suite (v0.2 flagship UX) β
Full spec: docs/product/daily-insights-spec.md. One coherent experience across three information layers β what's happening today, what the user has actually experienced historically, and evidence-based educational context. Core principle: personal data first, population-level cycle research second. Cycle phase never independently moves a score; it is context alongside the user's own state and history.
Build slices, each shippable alone:
- Pre-workout check-in (~10β20s flow) β five screens: energy, sleep, soreness (0β4), menstrual symptoms with severity, motivation. Result: readiness score + explained recommendation ("soreness high β threshold intervals cut from 4Γ8 to 3Γ8"). Extends the existing Today check-in card (
_TodayCheckIn) and ReadinessEngine; soreness + motivation are new inputs. Post-workout variant: felt vs expected / completed-modified- stopped / optional RPE. - Daily metrics grid β modular 2-column "Your day" cards (cycle, sleep, training load, recovery, symptoms, hydration), user-configurable via Edit metrics. Mostly re-surfaces data we already have: readings snapshot, PMC load block, cycle model.
- Daily Insight stories β immersive vertical flow opened from a compact Home card ("Day 6 Β· Today's cycle insight Β· Evidence: Moderate"). Categories: physiology / training / recovery / nutrition / symptoms / mind / skin / sexual wellbeing (opt-in). Each page: category, cycle context, UNDA-wave illustration, β€130-word explanation, optional personal-context block, evidence badge, collapsible sources accordion with "why we use this paper". No astrology aesthetics β water/wave/depth visual language only. Content pipeline must follow the spec's core content rule: CLAIM β SOURCE β EVIDENCE QUALITY β LIMITATIONS β ALLOWED WORDING β PERSONALISATION RULE.
- Exercise analytics + cycle overlay β per-exercise history (personal best, selectable metric graphs), performance charts with phase bands as a subtle background layer that must not imply causation, tappable points showing cycle day + symptoms alongside power/RPE/sleep, summary cards + history table linking to original sessions.
- Personal cycle-performance patterns β "Your patterns" panel driven by longitudinal user data only, gated by the minimum-data rule (no pattern claims before ~3 cycles; confidence labels scale with sample size, variance, missing data). This is the scientifically defensible answer to generic "cycle syncing" and UNDA's strongest differentiator.
Dependencies / notes:
- Check-in slice feeds Recommender v1.0.0 directly (subjective energy, soreness, felt-scores as factors).
- Estimated vs ovulation-supported vs hormone-confirmed labelling reuses the Β§9 provenance work already shipped in v5 schema + calendar states.
- Evidence badge system needs an editorial content source (structured JSON in-repo first; CMS later) and a claims-register check per SCI-001 before anything user-facing ships.
- Sexual wellbeing category is opt-in only and inherits the partner-sharing threat-model review below.
π Exercise demo videos (v0.2) β
5β15s looping demo per exercise, shown above the set/rep block on the run screen. Silent, no talking, one clip per exercise in the annotated subset (~100 highest-use, not all 873). Tap the loop β exercise detail screen with the fuller instructions already in the imported DB.
Content options, roughly costed:
- Commission a coach + videographer for a 2-3 day shoot. ~$8-12k, UNDA owns the footage outright.
- Commission consistent 3D animations. ~$10-15k, longer turnaround, but every clip has identical lighting / camera / body proportions.
Do NOT embed random YouTube exercise videos β no distribution rights, no consistency, and hostile to a commercial product.
Engineering side is small (~1 week) once the clips exist: wire a video player into the runner step card, decide bundled-with-app vs R2-CDN hosting, ship. The gate is the content shoot, not the code.
π UX + content β
- Rest-day interstitial explaining recovery is training.
- Copy pass across all screens β soften remaining "peak strength" language per the claims register.
- Notification scheduling (workout reminder, period countdown).
- Sport-scientist review of the full workout library (v0.2 β 64 workouts currently canonical-English; ARB translations for the remaining 58 descriptions land in the same review pass per rules doc Β§14).
π FTP builder β polarized-training path for cyclists (v0.2 feature, post-beta) β
Focused improvement path for road cyclists who want to move FTP up. Existing recommender is phase-aware and history-aware; this adds the missing pieces to actually drive FTP adaptation instead of just matching what the user already does.
The training science we'd wire in:
- Polarized ~80/20 distribution (Z2 vs threshold+VO2). Best-supported model for time-crunched cyclists.
- Sweet-spot progression: 2Γ20 β 3Γ15 β 2Γ25 β 2Γ30 across 4-8 weeks.
- VO2 intervals: 4Γ4 min @ ~115% FTP; 6Γ3 min. Raises the ceiling.
- Long zone-2 rides for aerobic base.
- Progressive overload without simultaneous duration + intensity bumps.
- Recovery week every 3-4 weeks (30-40% volume drop).
- FTP retest every 6-8 weeks.
UNDA's differentiator vs generic FTP tools (TrainerRoad, etc.):
- Sweet-spot progression scheduled in follicular (best window for hard work).
- Long Z2 pushed to luteal-early (higher core temp is fine for base).
- VO2 intervals avoided in luteal-late (rising fatigue + injury risk).
- Recovery week aligned to menstrual where possible (natural energy dip).
- FTP retest at follicular peak (most likely to hit true max).
- No other cycling platform overlays cycle phase on this β it's a real differentiator, not a marketing line.
What needs to be built (three tiers, cumulative):
Tier 1 (~4h):
- New library entries: 2Γ20 threshold, 4Γ4 VO2, 3Γ15 SST progression, ramp test, 20-min FTP test. All phase-tagged.
profile.ftpWattsoptional field (self-report or read from Intervals.icu athlete profile).
Tier 2 (~1 day, builds on Tier 1):
TrainingGoalenum:improveFtp / maintain / event / weightLoss.- Weekly intensity ratio computed from imports (Z2 minutes vs threshold+ minutes; or Z2 TSS vs above-Z2 TSS).
- Recommender factor
polarized_pull: negative when the past 7 days are already >30% high-intensity, positive when we're below 15%. - Rationale text: "This week's already 40% high-intensity β pulling toward a Z2 ride to protect the ratio."
Tier 3 (~2-3 days, flagship):
- Recovery week scheduling: every 4th cycle (or configurable), the planner drops volume ~35% and skips high-intensity days.
- FTP retest reminders:
lastFtpTestAton profile; 6-8 weeks after last test, planner reserves a follicular-peak day for the test workout and notifies the user beforehand. - Ramp visualization: show projected FTP curve if the user stays on the plan (transparent estimate, not a promise).
Data model gaps:
profile.ftpWattsβ nullable int.profile.trainingGoalβ enum.profile.lastFtpTestAtβ nullable DateTime.- Import zone estimation: without power data, use HR zones. Fallback for users without a power meter.
Not a v1 feature because:
- Needs sports-medicine review β training prescriptions to menstruators cross into a heavily regulated area.
- Cycle-phase-aware training research is still developing; guidance needs to be framed carefully (soft "typical for this phase" language, not prescriptive).
- Real user data from beta is needed to tune the polarized pull weight.
π Target events β race + expedition goals (v0.2) β
TrainingGoal.eventBuild already exists as a generic flag but the app does nothing with it. Users training for a specific event carry two pieces of information the recommender needs and doesn't have: what they're training for (which changes weekly-volume shape and taper timing) and when it is (which anchors the ramp / peak / taper windows to a real date). This lands both.
Why it matters:
- The recommender's ramp curves are context-free without a date on the calendar. A runner 14 weeks out from a marathon and a runner 3 weeks out both get the same suggestions today, which is the wrong answer in both cases.
- Cycle-phase timing shifts once there's a date. A marathon in luteal- late is a real cost the user should see coming (higher perceived effort + higher injury risk in that window); one in ovulatory is a window most users would want to know about. UNDA can name it.
- Sports outside of pure endurance (multi-day trekking, alpine summits, expedition-style climbs) have completely different training shapes β long back-to-back Z2 days, load-carry sessions, altitude prep β and no other cycle-aware app addresses them.
Event categories worth typing separately (the recommender's shape differs across these, not just the duration):
| Category | Examples | Recommender shape |
|---|---|---|
| Road running race | 5K Β· 10K Β· half Β· marathon Β· ultra | Progressive long runs + quality workouts, 2β3 week taper |
| Cycling event | Gran fondo Β· sportive Β· stage race | Polarized volume + peak-power blocks, 10β14 day taper |
| Triathlon | Sprint Β· Olympic Β· 70.3 Β· Ironman | Multi-sport periodization + brick sessions |
| Trail / mountain run | Skyrace Β· vertical km Β· trail ultra | Vert-loading, downhill legs, hike-run intervals |
| Trekking trip | Multi-day hike (Camino Β· GR20 Β· long-distance route) | Back-to-back load-carry days, pack familiarization |
| Mountain summit | Mont Blanc Β· Kilimanjaro Β· Aconcagua Β· Denali | Load carries + altitude acclim prep + step-ups |
| High-altitude trek | Everest base camp Β· Annapurna circuit Β· Torres del Paine | Aerobic base + trekking pole familiarity + hypoxic tolerance if kit available |
| Climbing objective | Multi-pitch grade goal Β· big-wall link-up | Endurance + power-endurance + antagonist work |
| Custom | User-named event with a date + duration/effort estimate | Generic taper + progressive ramp |
What needs to be built:
Tier 1 (~1 day):
TargetEventdata class:type,date, optionalcustomName, optionalnotes.profile.targetEventβ nullable single-slot field. One event at a time keeps the UI honest; multi-event plans belong in a Tier 3 scheduling pass.- v12 sqflite migration + Supabase mirror (three columns:
target_event_type,target_event_date,target_event_name). - Settings row: "Target event" β picker (category chips β date + optional name β save). Editing clears when the event date is in the past.
- Profile card: "COUNTDOWN" tile β X weeks / Y days until the event, category glyph, dashed border once the date is inside the taper window.
Tier 2 (~2 days, builds on Tier 1):
- Recommender factor
event_taper_pull: negative from taper-start to event-day (drops high-intensity, cuts volume ~40 % week over week). Taper length varies by category (marathon 3 wk, gran fondo 2 wk, expedition 1 wk pre-departure). - Recommender factor
event_ramp_pull: from event-date backward, scales weekly volume up along a category-appropriate curve. Cyclist polarized-pull (Β§FTP builder) composes with this cleanly. - Calendar marker on the event date + phase-check callout at plan time ("Your marathon lands in luteal-late β expect ~5 % higher perceived effort in the last 6 miles").
- Plan-header banner: "Marathon on 12 Oct Β· 8 weeks to go Β· taper begins 21 Sep".
Tier 3 (~3β5 days, flagship):
- Category-specific plan templates seeded from the workout library, spanning 8 / 12 / 16 weeks depending on the objective. Mont Blanc and EBC pull load-carry + step-up sessions from a new expedition block; marathons pull long runs + tempo + intervals from the existing running library plus the four workouts added on 2026-09-02 (progression run Β· fartlek Β· hill strides Β· day-one shakeout).
- Multi-day event support: a trekking trip is one row with
dateas start anddurationDays, generating a scheduled taper before and a low-load resume after. - Post-event debrief screen: felt-vs-expected + injury/illness fields, feeding the recommender's next planning cycle.
Data model gaps:
TargetEventTypeenum (~14 values from the table above, pluscustom).TargetEventmodel +profile.targetEventfield.profile.eventHistoryβ completed events, for the recommender to learn per-user taper length and typical peak week. Deferred to Tier 3.
Not a v1 feature because:
- Category-specific ramp curves need a sports-medicine review pass before shipping β training prescriptions to menstruators around a named event cross into a regulated area.
- Expedition-specific workouts (altitude prep, load carries) are a new slice of the workout library β 15β25 additional entries, reviewed against real objectives.
- Beta signals are needed on which categories users actually pick. A gran-fondo template that nobody uses shouldn't ship.
π Nutrition guidance β cycle-aware, non-prescriptive (v0.2) β
The app tracks training load and cycle phase; the biggest missing third leg is fuelling. Users training hard through follicular / peak windows or trying to recover in menstrual reach for nutrition suggestions the app is currently silent on. This entry defines what a first-pass nutrition surface could ship without crossing into regulated advice territory.
The line UNDA does not cross:
- No prescriptive macros ("eat 130 g carbs today"). Prescription requires a dietitian; UNDA is not one.
- No supplement suggestions naming brands, dosages, or timing. Supplements are consumables with medication-like risks and vary by jurisdiction.
- No caloric targets, no weight-management plans, no fasting protocols. These carry disordered-eating risk on any population and are triple-regulated when framed around a menstrual cycle.
- No causal cycle-phase claims about food ("your body needs X in luteal"). Same rule as Β§17d for the coach β cycle context, not hormone claims.
- No medical framing (deficiency, insufficiency, "your body is low in") without lab data the app cannot ingest.
What the surface can honestly say (sports-nutrition consensus, population-level, non-prescriptive):
| Signal | Suggestion shape | Evidence base |
|---|---|---|
| Long session planned (>90 min) | "Sessions past ~90 min usually benefit from carbs during the effort." | Widely accepted in sports nutrition; ACSM position stand |
| Hard session in follicular / ovulatory | "Higher-intensity days generally read easier with a fuelled start." | General endurance-training practice |
| Menstrual phase logged | "Iron loss during the period is normal β food sources include leafy greens, legumes, red meat." | Nutrition education, not prescription |
| Session logged, no fuelling | "Refuelling within ~1β2 h of a hard session supports recovery." | Recovery-window consensus |
| Water intake trend | "You've logged X ml today Β· typical training days are ~2β3 L for most adults." | General hydration guidance |
Copy pattern (matching the coach's Β§17d voice):
- Every card ends in "Guidance, not medical advice. Ask a dietitian for anything specific to your body."
- Statements are population-level ("typical", "generally", "most adults"), never personal ("you should").
- Estimated cycle days trigger the same hedge as the coach.
What needs to be built:
Tier 1 (~1 day, information-only, no personalization):
- Nutrition tab or nutrition slot inside the existing Home daily-metrics grid.
- One
NutritionInsightCardper relevant signal (see table above), static text authored under the claims-register process (SCI-001). - Persistent muted footer per card, same pattern as the coach's disclaimer.
- No new logging: cards derive from data the app already has (planned session length, phase, session-completion state).
Tier 2 (~2 days, opt-in food logging):
- Optional quick-log surface: liquids Β· fuel-taken-during-session Β· post-session refuel Β· yes/no per session. Not a macro tracker; three boolean-shaped chips per session.
- Signal into ReadinessEngine: sessions logged without refuel contribute a small "recovery lag" flag on the next day's readiness card, framed neutrally.
- Weekly pattern surface in Insights: "Sessions with a logged post-refuel had a felt-score ~0.4 higher on average" β the same cycle-of-evidence pattern insights already uses.
Tier 3 (~1 week, coach integration + Apple Health import):
RemoteCoachpicks up hydration + fuelling context alongside training load. The prompt config gains explicit "do not prescribe macros, brands, or dosages" rules to match the client posture.- Apple Health import for dietary energy + hydration where the user has granted read permission. Never a coaching input on weight metrics.
- Refuel-window notifications, opt-in per session type. Muted by default; user turns them on per sport.
Data model gaps:
NutritionLog(nullable per-session):preSessionFuel: bool?,midSessionFuel: bool?,postSessionRefuel: bool?,hydrationMl: int?.- Nutrition Apple Health categories require new
NSHealthShareUsagestrings and a fresh consent line β bundled with Β§12 privacy review. - No supplement, brand, or product schema. Deliberate β that's the domain that pulls the whole feature into medical-device territory.
Not a v1 feature because:
- Claims register review is mandatory before any user-facing string ships (SCI-001). The five copy patterns above are candidates for that review, not a completed pass.
- Sports-dietitian review for the Tier 1 copy β same gate the cycle-training prescriptions carry. Cheaper here because the advice is population-level, not personal.
- DPA + EU regulation check: dietary suggestions to menstruators around training load may or may not clear MDR Class I in the EU. Legal review before the feature toggles on for external users.
- Disordered-eating risk is real and material. Copy needs an eating-disorder specialist review pass; safeguards (rate-limit on refuel prompts, no calorie displays, no body-composition anywhere near this surface) locked before it ships.
β AI training coach chat β see Phase 10 above β
Shipped 2026-08-30. Kept the entry here so search still finds it, but the current state is documented in Phase 10 above. What actually shipped diverged from the original design in two ways worth noting:
- Backend: Cloudflare Pages Function on the landing project, not Supabase Edge Function. Same account as the landing site, zero new infra, safety-prompt config lives in
functions/api/coach.jsas theSYSTEM_PROMPTconstant. Rate limit 30 req/hr per IP via the existingWAITLISTKV. Cost model unchanged β Haiku 4.5 default (~$0.005/turn), Sonnet override available viaCOACH_MODELsecret. - Consent gate is a hard on/off, not "always on with disclosure". Default off; the user opts in from Settings β Coach. Off returns the on-device
LocalAssistant, on returnsRemoteCoachand the compactTodayContextsummary leaves the device. Every turn's data snapshot is visible via the_DataUsedChipin the sheet.
The original ambitions below (safety non-negotiables in the system prompt, no diagnosis, hedged on estimated data, no fasting/ restrictive-diet claims, sports-medicine review before opening externally) remain the operating rules. The SYSTEM_PROMPT in functions/api/coach.js carries them verbatim. External sports- medicine review is still open before opening the toggle to non- internal users.
Original design notes below preserved for reference:
Not in v1 β deliberately deferred. Needs safety review before public users see it. See Β§10 Rule (fitness guidance, not diagnosis) + Β§11 (third-party sharing requires review).
Architecture (target):
- App β Supabase Edge Function (EU) β Anthropic Claude API.
- User context assembled server-side from Supabase (phase, HistorySummary, recent activities, symptoms, readiness). Client never handles the Anthropic key.
- Streaming responses back through the Edge Function.
- Rate limit per user (30 msgs/day tentative) via Postgres or KV.
- On-device conversation history only β server does NOT persist chat transcripts (privacy stance).
Model:
- Default: Claude Sonnet 4.6 (best safety training for medical-adjacent topics, strong reasoning on user's specific context).
- Cost optimization: Haiku 4.5 for follow-up turns after Sonnet has established context. Cuts ~40% of bill.
- Prompt caching: 2000-token system prompt (cycle-training research + hard safety rules) cached across every turn β 10Γ cheaper input on the cached portion.
Estimated cost (Sonnet 4.6, with caching, ~$0.011/turn):
| Users | Turns/user/week | Monthly |
|---|---|---|
| 50 (beta) | 10 | ~$22 |
| 500 | 8 | ~$175 |
| 5,000 | 6 | ~$1,320 |
Safety non-negotiables in the system prompt:
- No diagnosis (PCOS, endometriosis, etc.) β redirect to clinician.
- No fasting / restrictive diets tied to cycle phases.
- No encouragement to train through severe pain, heavy bleeding, chest pain, or unexplained fatigue >2 weeks β always redirect to clinician.
- Trailing disclaimer on every medical-adjacent turn: "UNDA is a fitness tool, not a medical device. If X persists, please talk to a healthcare provider."
- Neutral language for non-heteronormative users (some are trans/ non-binary and menstruate).
- Acknowledge research uncertainty ("some users findβ¦", not "you shouldβ¦").
Effort estimate: ~2-3 days build + ~1 week external review by a sports medicine specialist + a menstrual health researcher + a lawyer before opening beyond internal testers.
Blockers before this ships:
- Legal entity registered (data controller of record).
- Privacy Policy updated with third-party LLM processor disclosure.
- Signed DPA with Anthropic (available on their commercial plans).
- Safety review as above.
- Consent flow inside the app: "Your cycle + activity context is sent to our AI provider (Anthropic, US-hosted). You can turn this off at any time from Settings."
π Partner / support-person sharing (v0.2 feature, post-beta) β
User can share a read-only view of her current phase + upcoming period window with a chosen support person (partner, friend, coach). Purpose: support conversations, meal/date planning, gym scheduling. NOT for fertility awareness or contraception (see below).
Why deferred (not v1): Menstrual data is one of the most sensitive personal-data categories under GDPR Article 9. Sharing it β even with a partner β is a well-documented vector for coercive control and stalkerware misuse. Before we ship this we need a threat model, a safety review, and UX patterns that assume the sharer may be pressured to enable it.
Target design:
- View-only, always. Partner cannot log, edit, delete, export.
- User picks what to share per invite: {phase name only} / {phase + next-period estimate} / {phase + symptoms}. Default = smallest.
- Server-side share tokens; RLS enforces the read-only slice.
- Partner options (choose one at rollout, probably start with A): A. Web link β partner opens a URL in any browser, no account needed. Simplest. Downside: URL can be forwarded. B. Partner-side app account β partner installs UNDA in "partner mode", accepts invite, sees data in-app. More friction, more control (revoke instantly, per-device audit).
- Revocation:
- User can revoke instantly from Settings β Sharing.
- No notification to the partner when revoked (per safety review).
- Optional: silent expiry after 90 days, user must renew.
- Hide-the-feature toggle: user can turn off the sharing menu from Settings so it never appears (helpful if the sharer is being pressured to check for it β feature just doesn't exist for them).
Safety non-negotiables:
- One-tap revoke. No confirmation dialog beyond "Stop sharing".
- No revoke notification to the partner (they should not know when).
- No location, no calendar exposure β only the specific phase/period fields the user opts in to.
- Explicit warning at share time: "This isn't for pregnancy prevention β periods can shift. Don't use this data to plan around fertility."
- Not shareable with more than 3 people at once (guard against accidental broadcast).
- Sharing UI must not appear in the app switcher / notification previews when locked.
Data model:
sharestable (user_id, granted_to_email OR granted_to_token, granted_scope enum, granted_at, revoked_at, last_viewed_at).granted_scope: {phase_only, phase_and_next, phase_symptoms}.- Never persist the partner's viewing history beyond
last_viewed_at. - Delete-my-account cascades: shares are wiped, existing view URLs return 404 within the same request.
Legal blockers before this ships:
- GDPR Art. 9 fresh explicit consent flow at each share, versioned.
- Privacy policy update: "Sharing (optional)" section.
- Terms of use clause: partner viewer has no right to redistribute.
- Region check β some jurisdictions (e.g. some US states post-2022) add legal exposure to holding menstrual data at all; sharing might amplify that. Legal review before enabling in-app.
Effort estimate: ~2 weeks engineering + safety review + legal review. Non-trivial because of the safety UX iteration required.
π Cycle modes: trying to conceive, following partner, pregnancy (v0.2) β
Single "Cycle mode" picker shipped in v1 with 4 options: tracking (default), trying to conceive, following partner, pregnancy. Only tracking and pregnancy are fully wired in v1 β the other two save the selection (so users can express intent, and we get signal on demand) but need additional UX + medical review before they light up.
Trying to conceive (v0.2)
- Highlight fertility window on Calendar with distinctive treatment (not just an outline).
- Add basal body temp logging in the per-day sheet.
- Prompt for ovulation-test result during the fertility window.
- Gentler workout suggestions during the actual ovulation day (already partially there via ligament-laxity note).
- Explicit disclaimer: not for contraception or fertility diagnosis; redirect to a fertility specialist for medical guidance.
- Effort: ~3-5 days engineering + fertility-tracking specialist review.
Following partner (v0.2)
- Read-only mode for non-menstruators supporting someone tracking with UNDA.
- Two flows: a) Invite-based β partner shares a token, follower installs UNDA and sees the shared subset (phase + next period estimate + optional symptoms). b) Manual β follower enters partner's cycle length + last period start and gets local-only estimates. No sync between accounts.
- Safety: see the sharing entry below β same non-negotiables (revoke instantly, no revoke notification to partner, etc.).
- Effort: ~1-2 weeks engineering + safety review. Ties into the "Partner / support-person sharing" entry below.
Pregnancy β full version (v0.3+)
- v1 pause is graceful but does nothing helpful for pregnant users beyond preserving their history. Full pregnancy mode is a genuine product build:
- Trimester tracking, week-by-week milestones.
- OB-GYN-reviewed pregnancy-safe exercise library.
- Weight gain tracking.
- Pregnancy-specific symptoms (nausea, back pain, swelling, kicks).
- Kick counter after ~28 weeks.
- Warning system: unusual symptoms β prompt to contact provider.
- Blocker: needs OB-GYN + prenatal-specialist consultant. This is medical liability territory, no shortcuts.
- Effort: 4-6 weeks engineering + multi-week medical review.
π UX iteration from beta feedback (v0.2) β
Captured 2026-08-14 after first internal-TF beta report. All shipped Tier-1 fixes went out in the follow-up build; these are the deeper UX pieces that need design work first, not just code.
- Text density on Today card is too high. User said "too much information displayed across the tabs, the UI could be simplified and given more breathing room." Consider: collapsible Today Readings section, hide Load block behind a tap-to-expand, smaller default set of readings.
- Tab animations still feel not smooth enough. Current pill has stretch + BackdropFilter blur + inside-top highlight. Compare against the actual iOS 26 tab morph β may need spring physics instead of cubic ease.
- Text sizes overall too small. Type scale bump already happened (bodyLarge 15β17). If beta users still report small, consider a 16-point default and a text-size accessibility setting.
- Dedicated Profile section. Currently account state is inside Settings. Consider a top-of-Settings profile card OR a separate tab showing: account status, connected services, activity persona (cyclist / climber / hiker / mixed), simple stats (weekly volume, cycles logged, sessions completed).
- "Today's session" showing stale info. Root cause was date-key cache from providers not invalidating on app resume. Tier-1 fix landed (invalidate today-scoped providers on lifecycle resume). Beta users still seeing it β next lift is invalidating on cycle-day boundary crossing too, not just app resume.
- Main page overcrowding. User wants "only the most important information and actions" on Home. Candidate consolidation:
- Move Load block to Insights.
- Move Readings snapshot to a collapsible.
- Keep: cycle-day header, phase tag, energy row, period actions, Today's session card, plan CTA.
π Beta operations (pre-demo-launch) β
Everything the app needs so we can actually run a real beta without losing user trust the first time something breaks. All small on their own; missing any of them makes a bad first impression.
- TestFlight setup β internal testing group first (up to 100), then external testing group (up to 10k, requires Apple beta review, ~24h). Two-tier invitation flow: waitlist candidates get a code + link via the Resend confirmation email once they're pulled from KV.
- Error tracking β Sentry (or free tier equivalent). Currently zero crash / non-fatal error reporting; a silent NPE in prod is invisible. Wire in
app/lib/main.dartrunZonedGuarded+ FlutteronErrorhook. Also send SQLCipher / migration failures as breadcrumbs. - In-app feedback channel β shake-to-report on iOS routes to a Supabase Edge Function β forwards to a dedicated inbox. Include auto-attached anonymised context: build number, phase, connected providers, last 5 recommender rationales. Never attach cycle data.
- Force-update mechanism β minimum-supported-version check on app launch against a Supabase
app_configrow. If a critical fix ships, we can force old builds to update instead of leaving them broken. - Feature-flag / kill switch β server-side toggles for recommender phase-weighting, Intervals push, readiness engine, and the workout runner. If a rule fires badly in beta, disable remotely without a new build.
- Backup + restore rehearsal β restore a Supabase snapshot to a staging project once, document the exact commands, verify RLS policies survive the restore. Do this before beta, not after an incident.
- Support inbox β
support@undaflow.approuted to a real inbox (Cloudflare Email Routing already set up forhello@; add a second rule). Response-time expectation stated in Terms. - β
Deploy pipeline β GitHub Actions workflow deploys the landing on every push to
maintouchingweb/landing/**(shipped 2026-08-19; supersedes Cloudflare Pages' git integration, which had silently stopped firing). Similar workflow needed for the Flutter app CI once tests exist.
π Growth + content (pre-demo-launch, non-code) β
Content production has weeks-of-calendar-time lead, so it needs to be tracked here even though most of it doesn't touch the repo.
- App Store listing β long description, short description, keywords (ASO research), category (Health & Fitness, secondary Sports), age rating (17+ given medical-adjacent claims boundary), contact info, privacy policy URL, support URL.
- Screenshots β 6 required per device size (iPhone 6.7", 6.5", 5.5" minimum). Localised into EN + ES. Show: Today card, Calendar, Plan builder, Insights, Workout runner mid-set, Sign-in. Framed Figma templates > hand-crafted.
- App Store preview video β optional but doubles conversion on Health & Fitness. 15-30s, no voiceover, product only.
- Press kit β one downloadable ZIP with logo variants (SVG, PNG 1024, favicon), product screenshots, founder headshot, boilerplate paragraph, tagline, contact.
- Launch content β 3 pieces before public reveal: founder note on LinkedIn (drafted), a blog post on the phase-as-signal-not-prescription stance (SCI-001 in plain language), a short piece on the local-first privacy design.
/og.pngfor the landing β 1200Γ630, matches hero. Currently the meta tag points at a missing asset; every social share shows a broken image./termspage β footer links to it, 404 today. Bare Terms of Use is enough for closed beta; final legal-reviewed version blocks public launch, not beta.
π Monetization + payments (v0.2, post-beta) β
Not built. Beta is free. Paid features unlock at public launch.
Tier model (proposed, subject to beta signal):
| Tier | What's included | Price to test |
|---|---|---|
| Free | Cycle tracking Β· phase-aware workout suggestions from the built-in library Β· symptom + energy check-ins Β· local export Β· basic Health / Strava integration (one-way import, no adaptive engine) Β· local charts | β¬0 |
| UNDA Plus | Daily read (Push / Steady / Hold cards from Β§18) Β· personal cycle-performance patterns (Daily Insight slice 5) Β· HRV / sleep / load analysis fed into the readiness engine Β· adaptive plan (2-week block planner with recommender feedback) Β· advanced insights (symptom Γ phase matrix, felt-vs-expected trends) Β· full two-way integrations (Intervals push, Garmin, Apple Health write-back) Β· FIT / ICS exports | β¬8.99β11.99 / mo |
| UNDA Plus Annual | Everything in Plus | β¬69β89 / yr (25β35 % annual discount to push the yearly plan) |
| Coach (later, B2B) | Coach dashboard covering multiple athletes at once, each with their cycle + readiness context, weekly load view, per-athlete comment thread on the plan | B2B pricing set at rollout β likely β¬30β60 / coach / mo, tiered by athlete count |
- 7-day free trial on Plus, no card required.
- Founding-member lifetime pricing for beta users: β¬120 one-time, offered only to users who joined by public launch date.
Where the line falls β free vs. Plus:
- Basic Health / Strava import is free. Reading a workout from Strava is not a paid feature; the user recorded it, they should see it. The Plus value is what UNDA does with those signals β adaptive planning, readiness scoring, phase-aware suggestions driven by HRV / sleep / load, not the import itself.
- Adaptive planning + daily read are Plus. Both are the flagship differentiator against a plain cycle tracker + activity feed; neither ships in v1 yet (Β§17 assistant and Β§18 daily read are gated on the server prompt config).
- Personal patterns are Plus. Requires β₯3 valid cycles of user data (Β§9d) before the surface renders, so it's a natural cross-sell moment once the user has stuck around.
- Full two-way integrations are Plus β Intervals push, FIT exports, Garmin write-back. Read-only "here's what I did" stays free; writing back into the user's training ecosystem is where the recommender's work lands.
Coach / B2B β deferred (later, not v0.2):
Not a v1 or v0.2 tier. Requires:
- Multi-tenant data model (coach has read access to N athletes; each athlete has explicit consent per coach β never organisational default-share). Distinct threat model from Β§Partner sharing above; coach relationship is professional but the safety rules from Β§21 still apply (one-tap revoke, no notification on revoke, silent expiry option).
- Athlete-consent revocation surfaces both in the coach dashboard (informational) and in the athlete's app (actionable) β never the same widget on both sides.
- A separate coach subscription with a distinct pricing model β billed to the coach, not the athlete. Athletes remain free / Plus on their own subscription independently.
- Sports-medicine review before the coach dashboard exposes cycle-phase-labelled data β the same "guidance, not prescription" frame that carries the individual coach sheet needs to hold when a professional is on the other side of the screen.
- Effort estimate for Coach v1: ~4β6 weeks after Plus ships, contingent on the multi-tenant model landing cleanly and the legal review of professional-context data sharing.
Not sold, ever:
- Data. Not summarised, not aggregated, not to "training partners" or affiliate integrations.
- Reminder frequency (dark pattern of paying to reduce nagging).
- Basic cycle tracking or on-device features β everything that runs locally stays free.
Take-rate reality on the tested prices β the β¬8.99β11.99 sticker is gross. Apple / Google take 30 % the first year and 15 % after year 1 on auto-renewing subscriptions (Small Business Program brings year-1 down to 15 % if company revenue is < $1 M / yr, which UNDA will qualify for at launch). Concretely at β¬11.99 monthly under Small Business: gross β¬11.99 β net ~β¬10.19, minus EU VAT collected by the store on the user's behalf. At β¬89 annual: gross β net ~β¬75.65. Plan cash flow on the net numbers. Web-side checkout (Stripe / RevenueCat Web) is an option for the annual plan only β DMA in the EU permits it β but adds VAT-OSS registration and cross-store account-linking work; not in scope for the first paid launch.
Blockers before this ships:
- Legal entity registered (data controller of record + payment processor beneficiary).
- Apple paid apps agreement signed + Apple Small Business Program enrolment (needs to happen every year, brings year-1 take from 30 % β 15 %).
- Google Play Merchant account (once Android ships).
- Terms of Service updated with pricing + auto-renewal + refund policy. Auto-renewal disclosure required in every region we sell.
- Privacy Policy updated to disclose Apple / Google / Stripe as payment processors (US-hosted).
- Server-side receipt validation via a Supabase Edge Function listening to App Store Server Notifications v2.
- StoreKit 2 integration on iOS; Google Billing on Android.
- Feature-gating implemented across every Plus feature.
- Restore Purchases flow tested end-to-end (Apple requirement).
- Kill switch on the paywall so we can disable purchasing if there's a receipt-validation regression.
- Optional but sane: RevenueCat as middleware (~free below 10k MTR, ~$99/mo above). Abstracts Apple/Google differences, provides churn analytics, saves ~1 week of engineering.
Effort estimate: ~2 weeks engineering (once legal + Apple contract in place) + ~1 week integration testing + weeks of legal / Apple review running in parallel.
Not before beta. Charging during beta is wrong β beta users are giving UNDA free QA labour and cycle-feel data. Free beta β paid launch is the standard sequence.
Engineering breakdown β Apple StoreKit path β
Where we are (2026-09-09):
- β Paywall / Compare-plans / Manage-plan screens shipped (Β§30d, build 62). Reachable from Profile β Subscription row.
- β
SubscriptionRepositoryinterface +EntitlementTierenum + RiverpodentitlementProvider+subscriptionProductsProviderinapp/lib/features/subscription/subscription_state.dart. - β
Fixture repo returns free entitlement + hardcoded β¬9.99 / β¬79 products with 250 ms simulated round-trip;
purchase()always succeeds after 600 ms so button state renders. - π Everything below is unbuilt.
Slice A β Tester override (do first, unblocks beta demos):
- [ ] Email allowlist in
SubscriptionRepository.currentEntitlement()keyed on the Supabase-signed-in user; returnsplusAnnualfor listed emails,freeotherwise. Deployed via a single hardcoded list in the repo β no server round-trip, revocable by shipping an update. - [ ] Hidden 5-tap gesture on Profile β app version row that flips local entitlement to
plusAnnualfor a single session (for App Store reviewers who don't have a tester account). - [ ]
docs/product/testers.mdlisting who has demo access + why. Manage from that file, not from a spreadsheet. - [ ] TestFlight release note documenting the override behaviour so testers know they're not being charged.
- Effort: ~2 hours. No Apple contract needed. Ships next build.
Slice B β App Store Connect setup (parallel with C; blocks DβH):
- [ ] Legal entity registered (Manex UG or equivalent) β data controller + Apple contract signatory.
- [ ] Apple Developer Program enrolment upgraded to Organisation (individual account can't sell subscriptions in EU).
- [ ] Apple Paid Applications Agreement signed in App Store Connect (Agreements β Paid Apps).
- [ ] Banking + tax forms filled in per market (W-8BEN-E for US tax treaty, EU VAT ID, DE tax residency certificate).
- [ ] Apple Small Business Program application submitted (year-1 take 30 % β 15 %; must reapply annually).
- [ ] Products created in App Store Connect: -
com.unda.plus.monthlyβ auto-renewable subscription, 1 month, groupunda_plus, β¬9.99 EU base tier. -com.unda.plus.annualβ auto-renewable, 1 year, same group, β¬79 EU base tier. 7-day free-trial introductory offer. - [ ] Localisations for both products (EN, DE, FR, ES, IT, NL, PT at minimum β matches current app locales).
- [ ] Subscription-group display name + benefit list per locale (visible in the Apple Subscriptions management screen).
- Effort: ~1β2 weeks calendar, ~1 day of actual work; the rest is Apple review latency on legal docs.
Slice C β StoreKit 2 wiring (Dart-side):
- [ ] Pick middleware: RevenueCat (recommended β abstracts Apple/Google, ships restore + receipt validation + churn analytics, free under 10k MTR) vs raw
in_app_purchaseFlutter plugin. Decision needed before starting. - [ ] Real
SubscriptionRepositoryimplementation swapped in behind the existing interface. No UI changes required. - [ ]
resolveProducts()βPurchases.getOfferings()mapped toSubscriptionProduct(localised price + period + saving badge pulled fromStoreProduct, never hardcoded). - [ ]
purchase()βPurchases.purchaseStoreProduct()with properPurchaseResultFailuremapping for user-cancel, network fail, already-owned, payment-not-allowed. - [ ]
restore()βPurchases.restorePurchases(). Required by Apple guideline 3.1.1; missing = rejection. - [ ]
Transaction.updates(or RevenueCataddCustomerInfoUpdate Listener) wired intoentitlementProviderso a renewal on another device flips the local tier without an app restart. - [ ] Persist last-known entitlement to secure storage β app opens offline with the correct tier (spec: State table row 1). Cache TTL: never expires locally, only cleared on explicit sign-out.
- Effort: ~3β5 days with RevenueCat, ~2 weeks raw.
Slice D β Server-side receipt validation:
- [ ] Supabase Edge Function
subscriptions/webhookβ receives App Store Server Notifications v2 (JWS-signed), verifies Apple's certificate chain, extractstransactionInfo+renewalInfo. - [ ]
subscriptionstable (Supabase) β one row per user Γ product, storesoriginalTransactionId,productId,expiresDate,autoRenewStatus,environment,lastNotificationType. RLS policy: user reads own row, service role writes. - [ ] Notification types to handle:
SUBSCRIBED,DID_RENEW,DID_FAIL_TO_RENEW,EXPIRED,REFUND,GRACE_PERIOD_EXPIRED,REVOKE. Log unknown types, don't crash. - [ ] Server-side entitlement resolved from
subscriptionstable, not the client's claim. Client hint β server verifies. - [ ] Sandbox + production notification URLs both configured in App Store Connect. Sandbox URL is a separate function deployment.
- [ ] If RevenueCat: skip this whole slice, wire their webhook to a much simpler Edge Function that just mirrors their entitlement into
subscriptionsfor RLS gating. - Effort: ~4β7 days raw, ~1 day via RevenueCat.
Slice E β Feature gates (audit every Plus surface):
- [ ] Single
useEntitlement()hook β returnsEntitlementTierfrom the reconciled provider (local cache OR server truth, whichever is fresher). - [ ] Locked-row pattern (Β§30d) applied to: daily-read card, adaptive-plan buttons, insights matrix screens, Garmin connect row, FIT export button, Intervals push toggle. Dim + single "Unlock with Plus" affordance, never blocks logging.
- [ ] Server-side gate for anything that hits an Edge Function (coach chat, cloud sync of Plus-only fields). Client gate alone is defeated by a jailbroken build.
- [ ] Analytics event
paywall_shownwithoriginfield so we can measure which surface drives conversion. - Effort: ~2β3 days.
Slice F β Compliance + policy copy:
- [ ] Terms of Service updated with pricing + auto-renewal language + refund policy (Apple mandates specific text in every locale).
- [ ] Privacy Policy discloses Apple + RevenueCat (if used) as payment / entitlement processors, US-hosted.
- [ ] Paywall footer links to Terms + Privacy (currently placeholder β wire the URLs).
- [ ] Auto-renewal disclosure text on paywall matches Apple's required boilerplate word-for-word.
- Effort: ~1 day + legal review turnaround.
Slice G β QA + submission:
- [ ] Sandbox tester accounts created in App Store Connect (one per locale we sell in β sandbox is region-locked).
- [ ] TestFlight full purchase flow verified β sandbox renewals compress 1 year β 1 hour so a full renewal cycle can be tested in an afternoon.
- [ ] Restore flow tested on a fresh device with a signed-in Apple ID that has a prior sandbox purchase (Apple review checks this specifically).
- [ ] Refund handling β sandbox
Report a Problemβ confirmREFUNDnotification flips entitlement to free within one minute. - [ ] Kill switch: remote flag (Supabase config table) that hides the paywall CTA and disables new purchases without an app update, in case a receipt-validation bug ships.
- [ ] App Store Reviewer Note in submission β includes a demo account with the 5-tap override enabled, and step-by-step instructions to reach the paywall + gated features.
- Effort: ~2β3 days.
Total post-legal engineering effort: ~2β3 weeks with RevenueCat, ~4β5 weeks raw StoreKit 2. Legal + Apple review runs in parallel and is the schedule-critical path.
Order of operations: A (tester override, ships now) β B (start legal / Apple paperwork in parallel with everything below) β decide RevenueCat vs raw (start of C) β C + D in parallel β E (gate audit) β F (policy) β G (submit).
π Security audit + leak review (pre-beta) β
Per-rule audit against docs/security/security-rules.md (18 rules, launch-blocking). β
= verified today; β οΈ = finding to fix; π = open verification still needed.
Rule 1 β EU data hosting
- β
Supabase project
gkrthhbqyyyfohtjhasucreated in EU (Frankfurt) region at Phase 7. Add a periodic check that no data-plane resource drifts out of EU (e.g. new Storage bucket accidentally created in US).
Rule 2 β Row Level Security
- β RLS enabled on every user-owned table (Phase 7 initial migration).
- β
Cross-user isolation test lives at
supabase/tests/rls_isolation.sqland runs in.github/workflows/supabase-rls-test.ymlon any PR touchingsupabase/**. - π Extend the RLS test to cover new tables as they land (e.g. any future
shares,feedback,subscriptiontables). Nothing to add today; process check for the future.
Rule 3 β Never expose privileged keys
- β
Repo audit clean: no
service_role, noSUPABASE_SERVICE_ROLE_KEYin the Flutter source. - β
.envand.env.*gitignored;.env.local+.env.exampleexist locally but are NOT tracked in git (verified viagit ls-files). - β
Supabase project ref / anon key passed to the app via
--dart-define(seeapp/lib/core/services/integrations/supabase_config.dart), never hardcoded. - π Rotate the
RESEND_API_KEYdocumented indocs/product/resend-setup.mdafter any team change (already rotated once when a token was revoked mid-day 2026-08-19).
Rule 4 β Separate identity from sensitive data
- β
Health/cycle tables (
cycle_days,cycles,workout_sessions,consents) reference the user byauth.uid()(UUID), never by email or name. - β
Profile row stores email only where auth requires it; symptom / flow / mood / energy live on
cycle_dayskeyed by UUID. - π Extend
docs/DATA_MODEL.mdwith an explicit note stating this design so future contributors don't accidentally denormalise email onto a health table.
Rule 5 β Minimise cloud data
- β Local-first design already respects this β cycle logs, symptoms, workouts all stored in local SQLCipher DB by default; nothing leaves the device unless the user opts into sync.
- π Audit
notesfields (free-text symptom notes, workout notes). Currently synced when sync is on. Consider keeping notes local-only in v0.2 per rule 5's "highly sensitive free-text notes locally where possible."
Rule 6 β No sensitive data in logs β οΈ
- β οΈ
notifications_service.dart:141logs the scheduled fire timestamp for a period reminder β timestamp implies a period date. Even debug-only, iOS device logs can persist. Fix: replace with count or generic message before beta. File indocs/security/logs-audit.md. - β
No
print()ordebugPrint()includes cycle dates, symptom sets, raw HRV values, emails, or tokens. Only two otherdebugPrintcalls inapple_health_importer.dart(logs error type only) andintervals_cycle_sync.dart(logs count, not dates). - π Add a lint rule / CI grep to fail the build if
print(/debugPrint(appears alongsidesymptom,email,token,hrv, orcycle_dayin the same expression.
Rule 7 β No health data in URLs
- β
Repo audit clean: no
Uri.queryParametersconstruction with symptom / cycle / period / HRV / flow fields. - β
Deep links use
unda://auth-callbackand don't carry any user-generated data. - π Verify universal links (
undaflow.app/...) once configured don't accept health-flavoured query params.
Rule 8 β Resend
- β Waitlist confirmation email contains zero cycle / health info β generic "You're on the list" message, product overview, thank-you.
- β Supabase magic-link email carries only the sign-in link + expiry disclaimer; no phase / cycle data.
- π If we ever add a "your period countdown" or "your workout is today" push notification, it must go via APNs / device-local notifications, never Resend. Document the boundary in
docs/product/notifications.mdwhen notifications land.
Rule 9 β Cloudflare
- β Landing site is static; no authenticated user data crosses Cloudflare's edge cache.
- β
Waitlist Function returns
{ok: true}β cacheable but leaks nothing. - π If we ever front Supabase auth or health data with a Cloudflare Worker, add explicit
Cache-Control: private, no-storeon those responses before shipping.
Rule 10 β Analytics
- β Zero product analytics in v1 (roadmap "What we're deliberately NOT doing"). No SDKs, no event tracking.
- π If added later, enforce an event allowlist review as part of every PR that adds an analytics call (Β§22 checklist gate).
Rule 11 β Admin security
- π User confirm: MFA on your Supabase dashboard login.
- π User confirm: MFA on your Cloudflare dashboard login.
- π User confirm: MFA on your GitHub account (repo owns CI secrets; compromise β prod access).
- π Document least-privilege: no designer / contractor should have Supabase project role above "Read-only".
Rule 12 β Storage
- β Not yet using Supabase Storage. When we do (avatar uploads, waveform snapshots, PDF exports), private bucket + per-user RLS policies from day 1.
Rule 13 β Authentication
- β No passwords stored by us β magic link + Sign in with Apple only.
- β
Supabase Auth handles rate limiting by default; the app-level rate limit on
/api/waitlistis separate (5/hour/IP). - π Confirm Supabase Auth β Settings β Rate Limits: email sends per hour, magic-link expiry (default 1h is fine), password reset attempts (n/a for us). Screenshot the current values into
docs/security/auth-limits.md.
Rule 14 β Encryption
- β HTTPS everywhere: Cloudflare edge β TLS 1.3; Supabase enforces TLS on all connections; Resend SMTP over TLS on port 465.
- β SQLCipher AES-256 for local DB at rest (Phase 5 B10). Key stored in iOS Keychain / Android EncryptedSharedPreferences.
- π Application-level encryption for future sensitive fields (e.g. free-text notes if we ever cloud-sync them) β no work needed today, design decision when notes go remote.
Rule 15 β Deletion
- β
delete_my_account()RPC exists insupabase/migrations/20260810_initial.sql, cascades across user-owned tables. - β
Client wired:
AuthService.deleteAccount()calls the RPC then wipes local DB viadeleteAllMyData(). Privacy Center surfaces this to the user. - π Test end-to-end with a real account, not fixtures. Create a test user, populate real cycle + session rows, run delete-account, verify Supabase state is empty AND local DB is wiped.
- π Document backup retention. Supabase backs up automatically; what's the retention window, and does delete-account propagate to backups? Ask Supabase support in writing, save the answer at
docs/security/backup-retention.md.
Rule 16 β Third-party processors
- π Maintain
docs/legal/processors.md(create if it doesn't exist) listing every service that receives user data: Supabase, Resend, Cloudflare (Pages + Email Routing + KV), Apple (App Store / HealthKit / Sign in with Apple). - π DPA signed with each β see
docs/product/legal-checklist.mdPhase 3. - π For each, note the exact fields sent. e.g. Resend: email address only. Cloudflare KV: email + free-text 'how do you train' + IP + UA + referrer.
Rule 17 β Security testing (pre-launch)
- β Cross-user access + RLS: automated in CI.
- π API authorization: manual pentest scenarios β try to hit
/auth/v1/adminendpoints from a normal user token; confirm 401. - π Storage permissions: n/a until Storage is used.
- π Edge Functions: n/a until we ship any.
- π Authentication abuse: simulate credential-stuffing against magic-link endpoint, confirm Supabase rate-limits kick in.
- π Exposed secrets: TruffleHog / gitleaks scan on the whole repo history before beta invitation. Add to
.github/workflows/.
Rule 18 β Core security principle (design tenet, not a checklist)
- Encoded in the roadmap's "In progress / next up" architecture: local-first means a Supabase breach exposes only sync-opted data. Encryption at rest limits device-loss impact. This principle guides every future design decision.
Not on the list but worth adding
- π Content Security Policy header for the landing (blocks script injection at the browser layer). Add to
web/landing/_headersor via Cloudflare transform rules. - π HSTS preload for
undaflow.apponce we're confident about TLS config (~1 week of clean traffic first). - π Dependency vulnerability scanning β
npm auditon landing,flutter pub deps --style=listweekly, Dependabot enabled on the GitHub repo.
π Testing β
- Widget tests for onboarding + delete-all-my-data flows.
- Golden tests for Today / Calendar / Plan screens.
- Repository unit tests for schema migrations and dedup.
- CI (GitHub Actions): analyzer + tests on every push.
Pre-launch gates β
Pre-Beta Gate (Β§24) β
The 10 compliance engineering blockers (B1βB10) are all closed. What's still needed before a first invite goes out to a real user beyond you:
Security audit β every rule in docs/security/security-rules.md must be either β
or knowingly deferred. Full per-rule status: see the π Security audit + leak review section above.
Legal + governance
- Legal entity registered (needed for Stripe, Apple paid contract, DPA counterparty, VAT). Everything downstream sits on this β do it first, calendar time ~2-4 weeks depending on jurisdiction.
- Named security officer + real
security@mailbox routed (Cloudflare Email Routing add a second rule besidehello@). - Signed DPA with Supabase (available on their commercial tier).
- Signed DPA with Resend (available in dashboard).
- DPIA completed (Data Protection Impact Assessment) or formal applicability decision documented.
- Incident-response tabletop exercise run once. One hour with a written scenario ("Supabase breach exposes hashed emails") and documented decisions.
- Trademark search for "UNDA" / "Undaflow" β EUIPO + national office in your primary jurisdiction. Not clearance (that's for public launch), just search to know if there's a conflict lurking.
Product + technical
- Error tracking wired (Sentry) β see Beta ops section above.
- In-app feedback channel working.
- Force-update mechanism live.
- Feature-flag kill switches for recommender + integrations.
- Backup + restore rehearsal documented.
- Deep link
unda://auth-callbackverified end-to-end on TestFlight. - Deep link
undaflow.appuniversal link entitlement configured. - Delete-my-account tested to actually cascade to Supabase (Β§12.1).
- Export ZIP tested with a real user's data (not just fixtures).
- App version pinning in place (
app_config.min_version).
Content + onboarding polish
- Onboarding copy pass β every screen reviewed for hedged language (SCI-001) and clarity.
- 3-cycle disclaimer clearly surfaced in onboarding (predictions are low-confidence until 3+ cycles logged).
- Empty states written for every screen (Calendar with no data, Insights with no sessions, etc.).
- β
/termspage live on landing (bare version shipped 2026-08-19). /og.pngβ SVG + HTML render source live; PNG production still needed (broken share thumbnails until then).- Landing
#instagram/#strava/#cookieslinks replaced with real destinations or hidden.
Beta ops
- TestFlight internal group set up with 5-10 invited testers.
- Waitlist β TestFlight code delivery flow tested end-to-end via the Resend confirmation email.
- Support inbox
support@undaflow.appmonitored. - One-page runbook: "beta user hits X, do Y" for the top-5 likely issues (sync fails, magic link doesn't arrive, phase estimate feels wrong, cycle length was wrong at onboarding, wants data deleted).
Pre-Public-Launch Gate (Β§25) β
Beyond the beta gate:
- Final Privacy Policy, Terms of Service, Spanish Legal Notice (LSSI).
- Cookie/tracker consent for the web app.
- Trademark clearance for "UNDA".
- App Store privacy declarations completed.
- Google Play Health Apps declaration completed.
- MDR medical-device boundary reviewed by counsel.
- CRA applicability + reporting process reviewed.
- AI Act assessment if any AI features have shipped.
- Retention automation active (rolling deletion jobs).
- End-to-end user export + deletion verified.
What we're deliberately NOT doing β
- βΈοΈ Fertility / pregnancy tracking β out of scope for v1. Different regulatory posture; would need a separate assessment before any code.
- βΈοΈ AI coach / LLM features β no in-app AI in v1. If added later, needs Β§11 review (no health data to third-party LLMs without approval).
- βΈοΈ Ads / analytics β no product analytics wired at all today. When we add any, the event allowlist is enforced by review.
- βΈοΈ Social / sharing β no follows, no cycle sharing between users. Not on the roadmap.
- βΈοΈ Wearable direct BLE β we integrate via HealthKit / Health Connect, not directly with device Bluetooth.
- βΈοΈ Under-18 version β UNDA v1 is 18+. A minors' product needs a separate legal, privacy, and safeguarding assessment first.
Non-negotiable engineering rules β
From /legal/UNDA_LEGAL_ENGINEERING_RULES:
- No sensitive health data in generic analytics.
- No sensitive health data in generic logs.
- No production health data in development.
- Explicit provenance on every data point.
- Predictions are not facts.
- Fitness guidance, not diagnosis.
- Every recommendation is versioned.
- Data must be deletable.
- Data must have a purpose.
- Third-party data sharing requires review.
- Least privilege.
- Consent is versioned.
Every PR touching personal-data handling runs through the Β§22 checklist.