Last Updated: 2026-03-24 Last Verified: 2026-03-24
Before making architectural statements, verify by reading actual code:
# 1. AI Provider (OpenAI via Supabase Edge Function proxy)
cat mobile/src/utils/aiClient.ts | head -30
# 2. TypeScript Usage (mobile uses TypeScript)
cat mobile/tsconfig.json
# 3. Database Schema
cat mobile/src/types/schema.json | head -100
# 4. Edge Functions (list all sync functions + orchestrators)
ls -1 supabase/functions/sync-*
# Expected: sync-all-devices, sync-cgm-devices, sync-fitbit, sync-libre, sync-oura, sync-whoop
# 5. Dependencies (check actual versions)
cat mobile/package.json | grep -A 5 '"dependencies"'
cat web/package.json | grep -A 5 '"dependencies"'Source of Truth: See ARCHITECTURE.md for comprehensive system design, tech stack, and deployment details.
- "Mobile uses JavaScript only" → ✅ Mobile uses TypeScript (see
mobile/tsconfig.json) - "Web uses React 18" → ✅ Web was upgraded to React 19 and reintegrated into workspace
- Mobile Language: TypeScript + JSX
- AI Provider: OpenAI (GPT-4o-mini, GPT-4o, gpt-4o-mini-transcribe)
- Backend: Supabase PostgreSQL + Deno Edge Functions
- Web: React Router v7 (integrated into workspace)
- Insight Engine: Pattern Spotter v2 + data-driven metric discovery (Deno Edge Functions)
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
HealthDecoder (formerly DashMobileApp) is a monorepo containing a mobile app (React Native/Expo), web application (React Router), and shared packages for health event tracking. The app allows users to log health-related events (food, glucose, insulin, activity, supplements, etc.) using voice, text, or camera input, with AI-powered parsing to extract structured data.
HealthDecoder/
├── package.json # Root workspace configuration
├── packages/
│ └── shared/ # @healthdecoder/shared - Shared utilities and types
│ ├── src/
│ │ ├── database/ # Supabase client initialization
│ │ ├── health/ # Nutrient calculations
│ │ └── types/ # Database types and schemas
│ └── dist/ # Built output (CommonJS + ESM)
├── mobile/ # React Native (Expo) mobile application
├── web/ # React Router web application (temporarily excluded from workspace)
├── supabase/ # Database migrations and seed scripts
└── .husky/ # Git hooks for pre-commit tests
NEVER create files at the repository root. The only files permitted at root level are:
CLAUDE.md— AI assistant guidanceARCHITECTURE.md— System design referenceKNOWN_ISSUES.md— Active known issues trackerpackage.json,tsconfig.json,.gitignore, and other tooling config files
All documentation and generated files MUST be placed in the correct directory. No exceptions.
| File type | Directory | Naming convention |
|---|---|---|
| Feature plans, architecture proposals, migration plans | docs/planning/ |
<topic>.md |
| Implementation guides, setup docs, completion reports | docs/implementation/ |
<topic>.md |
| Code review reports | docs/reviews/ |
YYYY-MM-DD-<scope>.md |
| Feature documentation | docs/features/ |
<feature-name>.md |
| Security documentation | docs/security/ |
<topic>.md |
| Test plans and strategies | docs/testing/ |
<topic>.md |
| Completed/historical docs | docs/archive/<category>/ |
<topic>.md |
| SQL scripts and queries | scripts/ or supabase/ |
context-dependent |
- NEVER place
.mdfiles at the repository root (except the three listed above) - NEVER place
.sqlfiles at the repository root — usescripts/orsupabase/ - ALWAYS check
docs/README.mdfor the current directory index before creating docs - ALWAYS update
docs/README.mdwhen adding a new document - When asked to create a plan, put it in
docs/planning/, not root - When asked to create a review, put it in
docs/reviews/with date prefixYYYY-MM-DD-<scope>.md - If unsure where a file belongs, ask — do not default to root
The project uses npm workspaces to manage shared code between mobile and web applications.
Root package.json workspaces:
packages/*- Shared packages (currently@healthdecoder/shared)mobile- React Native appweb- React Router web application
Common Commands (from root):
npm install # Install all workspace dependencies
npm run test:mobile # Run mobile tests
npm run build:shared # Build shared package
npm run test:shared # Run shared package testsThe @healthdecoder/shared package contains platform-agnostic code used by both mobile and web.
- Database:
initializeSupabase(),getSupabase(),isSupabaseInitialized() - Types:
Database,Tables,TablesInsert,TablesUpdate,Json - Health:
calculateConsumedNutrients()
npm run build:shared # Build to dist/ (CJS + ESM + types)import {
initializeSupabase,
calculateConsumedNutrients,
} from "@healthdecoder/shared";- Language: TypeScript + JSX (see
mobile/tsconfig.json) - Framework: Expo 54 with React Native 0.81
- Routing: Expo Router (file-based routing)
- State Management: Zustand for auth state
- Data Fetching: @tanstack/react-query
- Backend: Supabase (authentication and database)
- Testing: Jest with @testing-library/react-native
- AI Processing: OpenAI API (GPT-4o-mini for text, GPT-4o for vision, gpt-4o-mini-transcribe for transcription)
- Shared Code:
@healthdecoder/sharedworkspace package
mobile/
├── src/
│ ├── app/ # File-based routes (Expo Router)
│ │ ├── (tabs)/ # Tab navigation routes
│ │ │ ├── discover.tsx # Main discovery feed
│ │ │ ├── lab.tsx # My Lab (experiments)
│ │ │ ├── my-body.tsx # My Body (health metrics)
│ │ │ ├── playbook.tsx # Playbook (recommendations)
│ │ │ ├── profile.tsx # User profile (hidden tab)
│ │ │ ├── insights.tsx # Metabolic insights (hidden tab)
│ │ │ └── home.tsx # Legacy event logging (hidden tab)
│ │ ├── (auth)/ # Auth routes (login, signup, verify)
│ │ ├── (onboarding)/ # Onboarding flow
│ │ ├── discovery/ # Discovery detail screens
│ │ ├── event/ # Event detail screens
│ │ └── _layout.tsx # Root layout with providers
│ ├── components/ # Reusable UI components
│ ├── hooks/ # React hooks (glucose, experiments, etc.)
│ ├── types/ # TypeScript types (synced with shared package)
│ └── utils/ # Utilities and hooks
│ ├── auth/ # Authentication logic (Zustand store, hooks)
│ ├── experiments/ # Experiment framework (A/B testing engine)
│ ├── fitnessTrackers/ # Device connection/sync hooks
│ ├── aiClient.ts # AI API abstraction layer (OpenAI via proxy)
│ ├── eventParser.ts # AI event parsing with structured output
│ ├── timezoneRegistration.ts # Auto-detect + push device IANA timezone
│ ├── pushTokenRegistration.ts # Push notification token management
│ ├── logger.ts # Structured logging to app_logs table
│ └── supabaseClient.ts # Mobile-specific Supabase initialization
├── __tests__/ # Test files
├── jest.setup.js # Jest configuration with mocks
├── metro.config.js # Metro bundler config (includes workspace support)
└── app.json # Expo configuration
Development:
cd mobile
npm run android # Run on Android
npm run ios # Run on iOSTesting:
cd mobile
npm test # Run all tests
npm run test:watch # Watch mode
npm run test:coverage # Generate coverage report
npm run test:ci # CI mode (used in pre-commit hook)Supabase Client:
- Mobile initializes the shared Supabase client in
src/utils/supabaseClient.js - Uses
initializeSupabase()from@healthdecoder/shared - Auth handled separately with Expo SecureStore (not managed by Supabase client)
Authentication:
- Zustand store in
src/utils/auth/store.jsmanages auth state - Token stored in Expo SecureStore
useAuthhook insrc/utils/auth/useAuth.jsprovides auth methods
Event Processing Flow:
- User inputs via voice/text/camera on home screen
- Voice input →
voiceRecording.js→ audio file - Audio →
aiClient.js(transcription) → text - Text →
eventParser.js→ AI API (structured output) → parsed event - Events saved to Supabase
user_health_eventstable - History screen fetches events with React Query
Environment Variables:
Required in mobile/.env:
EXPO_PUBLIC_SUPABASE_URL- Supabase project URLEXPO_PUBLIC_SUPABASE_KEY- Supabase anon keyEXPO_PUBLIC_OPENAI_TEXT_MODEL- OpenAI model for text parsing (e.g.,gpt-4o-mini)EXPO_PUBLIC_OPENAI_VISION_MODEL- OpenAI model for image analysis (e.g.,gpt-4o-mini)EXPO_PUBLIC_OPENAI_TRANSCRIPTION_MODEL- OpenAI model for audio transcription (e.g.,gpt-4o-mini-transcribe)
Note: The OpenAI API key is stored server-side in Supabase Edge Function secrets, not in the mobile app. All AI calls are proxied through Edge Functions (openai-proxy-chat, openai-proxy-vision, openai-proxy-transcribe) using JWT authentication.
Path Aliases:
@/*maps tosrc/*(configured in tsconfig.json)@healthdecoder/sharedresolves via npm workspace
Metro Configuration:
metro.config.jsincludeswatchFoldersfor the shared packagenodeModulesPathsincludes both mobile and root node_modules
Testing:
- Tests use Jest with jest-expo preset
- Extensive mocking in
jest.setup.jsfor Expo modules - Tests located alongside source files in
__tests__/directories - Pre-commit hook runs full test suite
The app supports multiple event types defined in mobile/src/utils/eventParser.ts:
food- Nutrition tracking (calories, macros)glucose- Blood glucose readingsinsulin- Insulin administrationactivity- Exercise/movementsupplement- Supplement intakesauna- Sauna sessionsmedication- Medication trackingsymptom- Symptom tracking
Each event type has required and optional fields validated during parsing.
Note: Reintegrated into npm workspaces after upgrading to React 19 (matching mobile).
- Framework: React Router v7 with SSR
- UI: Chakra UI, Tailwind CSS
- Backend: Hono API routes (file-based in
src/app/api/) - Database: To be migrated to Supabase (currently Neon PostgreSQL)
- State Management: Zustand
- Data Fetching: @tanstack/react-query
web/
├── src/
│ └── app/ # React Router routes
├── plugins/ # Vite plugins for custom functionality
├── __create/ # Route building utilities
└── react-router.config.ts
Development:
cd web
npm run dev # Start dev server with SSR
npm run typecheck # Type checkingPath Aliases:
@/*maps to./src/*
Mobile:
- Uses Metro bundler with workspace support
- Expo prebuild for native projects (iOS/Android)
- Supports web via
expo-web-browser
Shared Package:
- Uses tsup for building
- Outputs both CommonJS and ESM formats
- Generates TypeScript declaration files
Web:
- Vite bundler
- Custom plugins in
plugins/directory - SSR enabled by default
Pre-commit Hook:
- Validates schema.json is in sync with Supabase
- Runs mobile test suite
- Builds shared package (if it exists)
- Configured in
.husky/pre-commit - Tests must pass for commit to succeed
Committing Code: Run tests from the repository root or the pre-commit hook will handle it automatically.
Mobile App Data Flow:
- Input Layer: Home screen captures voice/text/camera input
- Processing Layer: Voice/text sent to AI API for parsing (with structured output)
- Validation Layer:
eventParser.jsvalidates against event schemas - Storage Layer: Supabase stores events in
user_health_eventstable - Display Layer: React Query fetches and caches events for history screen
Device Sync (Dual-Cron Architecture):
- Fitness trackers (Whoop, Fitbit, Oura): synced hourly via
sync-all-devices(0 * * * *) - CGM devices (Libre, Dexcom): synced every 5 minutes via
sync-cgm-devices(*/5 * * * *) - Both orchestrators use vault RPC for auth, trace ID propagation, and rate-limited batching
ingestion_logprovider:cron-orchestrator(fitness) vscgm-orchestrator(CGM)
Authentication Flow:
- User initiates OAuth via
GoogleAuthWebViewcomponent - Supabase handles OAuth redirect
- JWT token stored in SecureStore
- Zustand store updates auth state globally
- Protected routes check auth state from store
When modifying event types:
- Update schemas in
mobile/src/utils/eventParser.ts(OpenAI strict mode format) - Update prompts in event parsing utilities (
mobile/src/utils/eventParser.ts,voiceEventParser.js) - Add tests for new event type validation
- Update Supabase table schema if needed
When adding shared utilities:
- Add code to
packages/shared/src/ - Export from
packages/shared/src/index.ts - Run
npm run build:sharedto rebuild - Import in mobile/web using
@healthdecoder/shared
When adding new screens:
- Mobile: Add files to
mobile/src/app/following Expo Router conventions - Web: Add files to
web/src/app/following React Router conventions - Both use file-based routing with automatic route generation
Testing best practices:
- Mock Expo modules are configured in
jest.setup.js - Use
@testing-library/react-nativefor component testing - Keep tests close to source files in
__tests__/directories - Run tests before committing (enforced by pre-commit hook)
All code in the mobile/ directory must use React Native-compatible APIs only.
WARNING: Jest runs in Node.js, NOT React Native. Tests will NOT catch React Native runtime-specific failures. Code that passes all tests can still crash on device. Always verify API compatibility before using any web-standard API in mobile code.
UI Constraints:
- accessibilityRole: Only use valid React Native values:
none,button,link,search,image,keyboardkey,text,adjustable,imagebutton,header,summary,alert,checkbox,combobox,menu,menubar,menuitem,progressbar,radio,radiogroup,scrollbar,spinbutton,switch,tab,tablist,timer,toolbar. Do NOT use web-only values likedialog,navigation,main,article, etc. - Styling: Use
StyleSheet.create()or inline styles, not CSS classes or web-only style properties - Components: Use React Native components (
View,Text,TouchableOpacity, etc.), not HTML elements - APIs: Use React Native / Expo APIs, not browser APIs (
window,document,localStorage, etc.) - Modals: Use
accessibilityViewIsModal={true}on Modal components instead ofaccessibilityRole="dialog"
Network & File API Constraints:
- File uploads MUST use XMLHttpRequest: React Native has three constraints that eliminate all other approaches:
fetch()does NOT supportFormData.append('file', {uri, name, type})— throws "Unsupported FormDataPart implementation"Blobdoes NOT supportArrayBufferorUint8Array— throws "Creating blobs from ArrayBuffer not supported"XMLHttpRequestDOES support{uri, name, type}in FormData natively — this is the ONLY working approach
- Never use
fetch()for file uploads in React Native. UseXMLHttpRequestwith the{uri, name, type}FormData pattern. All three constraints above pass silently in Jest/Node.js, making them invisible to tests.
Module & Import Constraints:
- No dynamic
require()for already-imported modules: Never userequire('@/utils/foo')inside a function if the module is already imported at the top of the file viaimport. Dynamicrequire()can return incomplete module objects in Hermes production bundles, causing "undefined is not a function" errors that Jest cannot catch. - Prefer top-level
importstatements: For lazy loading, useReact.lazy()or dynamicimport()(notrequire()). - Hermes vs Node.js: The production JS engine (Hermes) has different module evaluation behavior than Node.js (Jest). Circular dependencies and partial module evaluation affect them differently.
The bottom tab bar uses a custom label renderer to prevent label truncation on Android. Do NOT modify this without understanding the root cause:
Problem: React Navigation's default Label component (@react-navigation/elements) has numberOfLines={1} hardcoded, which truncates labels to "Ho...", "Hist...", "Prof..." on Android devices.
Solution: We use tabBarLabel: renderTabLabel with a custom Text component that:
- Has NO
numberOfLinesconstraint - Has explicit
width: 80to ensure labels have enough horizontal space - Uses
allowFontScaling={false}to prevent accessibility settings from breaking layout
Rules:
- NEVER remove or replace the custom
tabBarLabelfunction inmobile/src/app/(tabs)/_layout.jsx - NEVER add
tabBarLabelStyle- it's handled by the custom renderer - NEVER add
numberOfLinesto the custom label Text component - ALWAYS keep explicit
widthon the label style (minimum 70px) - Tests in
mobile/__tests__/components/TabBar.test.jsxverify this - they MUST pass
All AI calls are routed through Supabase Edge Functions for security:
- Proxy Functions:
openai-proxy-chat,openai-proxy-vision,openai-proxy-transcribe - Authentication: User's JWT token (from Supabase Auth), not a client-side API key
- API Key Storage: Server-side only (Supabase Edge Function secrets)
- Rate Limiting: Per-user rate limits enforced by proxy
- Usage Logging: All calls logged to
openai_usage_logstable
Client-side model configuration (for logging/metadata only):
EXPO_PUBLIC_OPENAI_TEXT_MODEL- Text parsing model nameEXPO_PUBLIC_OPENAI_VISION_MODEL- Vision model nameEXPO_PUBLIC_OPENAI_TRANSCRIPTION_MODEL- Transcription model name
Implementation:
- All AI calls go through
mobile/src/utils/aiClient.tsabstraction layer - OpenAI strict mode requires: lowercase types,
additionalProperties: false, all fields inrequiredarray,anyOffor nullable fields
The pattern-spotting pipeline runs server-side as a Deno Edge Function, analyzing 60-90 days of wearable data to find statistically significant behavioral patterns.
Pipeline: FETCH → CLEANSE → AGGREGATE → MERGE → DISCOVER → SCAN → CORRECT → RANK → FILTER → NARRATE → PERSIST
Key files:
supabase/functions/ai-engine/engines/pattern-spotter.ts— main pipeline orchestratorsupabase/functions/_shared/daily-aggregation.ts— aggregates raw data into daily metricssupabase/functions/_shared/local-time.ts— timezone-aware date/hour extraction (closure-basedLocalTimeExtractorsfactory)supabase/functions/_shared/metric-registry.ts— canonical 62-metric registry (SERVER_METRIC_REGISTRY)supabase/functions/_shared/metric-discovery.ts— data-driven metric + segment discoverysupabase/functions/_shared/statistical-tests.ts— Mann-Whitney U, Cohen's d, BH correctionsupabase/functions/spot-patterns-cron/index.ts— cron trigger
Admin dashboard: web/src/app/routes/admin/insights.tsx (listing) + insights.$runId.tsx (run detail with 11-step pipeline visualization)
Web imports from supabase shared: The web app imports SERVER_METRIC_REGISTRY from @supabase-shared/metric-registry via a Vite path alias (web/vite.config.ts + web/tsconfig.json). This avoids hardcoding the metric registry in the admin dashboard.
All data aggregation is timezone-aware. The system auto-detects the user's IANA timezone and uses it for all date/hour extraction in the insight engine pipeline.
How timezone is populated (no user action required):
- Email signup: device timezone passed as user metadata →
handle_new_user()trigger writes it touser_profiles - Every app launch:
timezoneRegistration.tscalls.update()onuser_profileswithIntl.DateTimeFormat().resolvedOptions().timeZone - Fitbit sync: fetches IANA timezone from Fitbit profile API
- Oura sync: fetches from Personal Info API
- WHOOP sync: preserves
timezone_offsetinvendor_metadata, computes localactivity_date - Libre sync: derives offset from
display_time - glucose_timestamp
Aggregation layer: Functions in daily-aggregation.ts accept an optional LocalTimeExtractors parameter (defaults to UTC). The pattern spotter creates extractors once per pipeline run via createLocalTimeExtractors(timezone) — a closure-based factory that captures 2 pre-built Intl.DateTimeFormat instances for the entire run (~54ms vs ~1.8s naive per-call construction).
Glucose special case: Glucose data uses display_time (already user-local wall-clock time) for date grouping and overnight/daytime classification. No IANA conversion needed.
Design doc: docs/planning/timezone-strategy.md
All three fitness tracker sync functions (Fitbit, WHOOP, Oura) use a shared token refresh utility with retry and error classification:
Key files:
supabase/functions/_shared/token-refresh.ts—TokenRefreshError(withpermanentflag),classifyTokenError(),refreshWithRetry(),fetchTokenEndpoint()- Each client (
fitbit-client.ts,oura-client.ts,whoop-client.ts) callsrefreshWithRetry(fetchTokenEndpoint(...))for token refresh
Error classification: HTTP 400 invalid_grant/invalid_token/invalid_client → permanent (set needs_reauth). HTTP 5xx, 429, network errors → transient (do NOT set needs_reauth, return 503, retry next sync). Handles both standard OAuth format ({ error: "invalid_grant" }) and Fitbit's nested format ({ errors: [{ errorType: "invalid_grant" }] }).
Sync function catch blocks: TokenRefreshError.permanent === true → set needs_reauth=true, log as failure, return 401. TokenRefreshError.permanent === false → do NOT set needs_reauth, log as warning, return 503.
The health snapshot tab uses a three-layer scoring system instead of static population ranges:
Layer 1 — Personal Baseline: P10/P50/P90 computed from 90-day trailing window of user's own data (30-day minimum). Computed by compute-baselines Edge Function, triggered after each sync via sync-all-devices.
Layer 2 — Cycle-Aware Baselines: For women who opt in, separate follicular/luteal baselines. Cycle data collected in-context on the health snapshot tab (not during onboarding) via CycleInsightCard after 30 days of data.
Layer 3 — Trend Direction: Cycle-aware trend computation compares like-to-like phases (luteal vs luteal). Uses last 14 days vs prior 14 days.
Key files:
mobile/src/utils/experiments/cyclePhase.ts— phase estimation pure functionmobile/src/utils/experiments/personalBaseline.ts— direction-aware status scoringmobile/src/utils/experiments/trendComputation.ts— cycle-aware trendsmobile/src/utils/experiments/computeWins.ts— personal bests, back-in-range, baselines-readymobile/src/utils/experiments/buildMetricScorecard.ts— enriches scorecard with personal datamobile/src/hooks/usePersonalBaselines.ts— fetches fromuser_personal_baselinestablemobile/src/hooks/useCyclePhase.ts— combines profile data with phase estimationsupabase/functions/compute-baselines/index.ts— server-side baseline computationmobile/src/components/Experiments/— HeroSummary, WinsSection, ExpertContext, CycleInsightCard, CycleConfirmationPrompt
Database tables: user_personal_baselines (P10/P50/P90 per metric per phase), user_cycle_reports (period confirmations for cycle length refinement)
Color logic: Direction-aware — outside_good (e.g., HRV above P90) = green, outside_bad (e.g., RHR above P90) = amber. Never red.
Privacy: Cycle data used solely for range personalization. Never sold, shared, or sent to external APIs. Fully deletable via Profile.
Design doc: docs/planning/personalized-health-snapshot.md
- Platform-agnostic utilities should be added to
@healthdecoder/shared - Mobile-specific code (Expo modules, React Native APIs) stays in
mobile/src/utils/ - Web-specific code stays in
web/src/ - Types and schemas should be defined once in the shared package
- Update ARCHITECTURE.md first (source of truth)
- Update CLAUDE.md (Claude Code guidance)
- Update mobile/.env and web/.env (if environment variables change)
- Update package.json scripts (if workflow changes)
DO:
- ✅ Read actual code files when making architectural statements
- ✅ Use verification checklist (top of this file)
- ✅ Reference ARCHITECTURE.md for comprehensive details
- ✅ Update "Last Verified" timestamp when reviewing
DON'T:
- ❌ Make assumptions about tech stack without verification
- ❌ Trust outdated documentation
- ❌ Rely on variable/file naming alone
- ❌ State absolute facts without reading source code
# What AI provider is actually used? (OpenAI via proxy)
grep -r "openai-proxy" mobile/src/utils/aiClient.ts | head -5
# What language is mobile written in?
test -f mobile/tsconfig.json && echo "TypeScript" || echo "JavaScript"
# What integrations exist?
ls -1 supabase/functions/sync-* | sed 's/.*sync-//'
# Expected: all-devices, cgm-devices, fitbit, libre, oura, whoop
# What's in the shared package?
cat packages/shared/src/index.ts | grep "^export"For comprehensive architecture details, see ARCHITECTURE.md