App Development

Setup

cd fieldnotes-ai-app
npm install
cp .env.example .env   # fill in values — see Environment Configuration below
npx expo start
npx expo start --tunnel  # use on restricted networks (e.g. university WiFi)

Platforms:

npx expo start --ios       # macOS + Xcode required
npx expo start --android   # Android Studio + emulator required
npx expo start --web

Environment Configuration

.env lives in ./fieldnotes-ai-app/

# Supabase
# set EXPO_PUBLIC_AUTH_PROVIDER=mock for local login without Supabase 
# (practitioner@test.com / password123, admin@test.com / admin123)
EXPO_PUBLIC_AUTH_PROVIDER=supabase
EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
EXPO_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

[!IMPORTANT] Azure API Keys (OpenAI, Speech) are no longer required in the client-side .env file. These services are now proxied through Supabase Edge Functions to ensure API keys never ship inside the Expo bundle.

Project Structure

fieldnotes-ai-app/
├── app/                        # Expo Router — every .tsx file is a route
│   ├── _layout.tsx             # Root layout + auth guard
│   ├── index.tsx               # Entry point redirect
│   ├── login.tsx
│   ├── (admin-tabs)/           # Admin screens
│   ├── (practitioner-tabs)/    # Practitioner screens
│   └── (web)/                  # Web-only views
├── src/
│   ├── auth/                   # authProviderFactory.ts + providers/ (IAuthProvider, Supabase, Mock)
│   ├── components/
│   │   ├── screens/            # ProtectedRoute, WebLayout, AdminWebLayout
│   │   └── ui/                 # Reusable primitives (Button, Card, Input, etc.)
│   ├── constants/               # theme.ts, ui.ts, routes.ts, filter configs
│   ├── contexts/               # AuthContext
│   ├── hooks/                  # useRecordingAutoSync, use-color-scheme, etc.
│   ├── lib/                    # azureOpenAI.ts, azureSpeech.ts, supabaseClient.ts
│   ├── services/               # All backend communication (no direct supabase calls in screens)
│   ├── types/                  # Shared TypeScript interfaces
│   └── utils/                  # formatters.ts, logger.ts, fileHelpers.ts
├── scripts/                    # Utility scripts (accessibility, rtm-generator, etc.)
├── supabase/
│   ├── functions/              # Edge Functions (azure-openai, azure-speech, admin-create-user)
│   ├── policies/               # RLS SQL (cases_rls.sql, clients_rls.sql)
│   └── migrations/             # Database schema migrations
└── __tests__/
    ├── acceptance/
    ├── integration/
    └── utils/testUtils.tsx

Routing & Security

Routing via ProtectedRoute

Navigation is controlled by src/components/screens/ProtectedRoute.tsx. It uses the AuthContext (driven by Supabase Auth) to: - Redirect unauthenticated users to /login. - Redirect authenticated users to their respective home screens based on user.role (Admin vs. Practitioner). - Prevent practitioners from accessing web-only routes. - Prevent admins/practitioners from bypassing guards via deep links or browser history.

API Security (RLS)

We use Row Level Security (RLS) in Supabase to secure data at the database level. - Even if a user has the anon key, they can only see data they are explicitly permitted to see based on their role and ID. - Policies are stored in supabase/policies/.

Edge Function Proxying

For sensitive third-party services like Azure OpenAI and Azure Speech: 1. The app calls a Supabase Edge Function using supabase.functions.invoke(). 2. The Edge Function (running on Supabase servers) injects the private API keys. 3. The response is returned to the app.

This pattern keeps our secrets safe from reverse-engineering of the mobile app bundle.

Coding Rules/Conventions:

  • Screens live in app/ only.
    • No exceptions
  • No inline styles or StyleSheet.create() in screens.
    • Use NativeWind className. Extract repeated layouts into src/components/ui/
  • No direct supabase or Azure SDK calls in screens.
    • All backend calls go through src/services/
  • Dont use any.
    • TypeScript strict mode is on

Creating a Screen

// app/(practitioner-tabs)/my-screen.tsx
import { Screen } from '@/src/components/ui/Screen';
import { Text } from '@/src/components/ui/Text';

export default function MyScreen() {
  return (
    <Screen>
      <Text className="text-xl font-bold">Title</Text>
    </Screen>
  );
}

Creating a UI Component

// src/components/ui/MyComponent.tsx
import { View } from 'react-native';
import { Text } from './Text';

interface MyComponentProps {
  title: string;
  subtitle?: string;
}

export function MyComponent({ title, subtitle }: MyComponentProps) {
  return (
    <View className="p-4 bg-white rounded-lg">
      <Text className="text-lg font-bold">{title}</Text>
      {subtitle && <Text className="text-sm text-gray-500">{subtitle}</Text>}
    </View>
  );
}

Creating a Service

// src/services/myService.ts
import { supabase } from '@/src/lib/supabaseClient';
import type { MyType } from '@/src/types';

export async function getItems(): Promise<MyType[]> {
  const { data, error } = await supabase
    .from('my_table')
    .select('*')
    .order('created_at', { ascending: false });

  if (error) {
    console.error('[myService] getItems:', error.message);
    throw error;
  }
  return data ?? [];
}
import { router } from 'expo-router';

router.push('/cases');
router.push({ pathname: '/case-overview', params: { id: caseId } });
router.replace('/(practitioner-tabs)/home');
router.back();
import { useLocalSearchParams } from 'expo-router';

const { id } = useLocalSearchParams<{ id: string }>();

Auth Context

import { useAuth } from '@/src/contexts/AuthContext';

const { user, signOut } = useAuth();

Auth Provider System

Controlled by EXPO_PUBLIC_AUTH_PROVIDER:

  • supabaseSupabaseAuthProvider (prod)
  • mockMockAuthProvider (for tests/offline dev)

In both, src/auth/providers/IAuthProvider.ts implements IAuthProvider

Async State Pattern

useEffect(() => {
  let cancelled = false;
  getCases()
    .then((data) => { if (!cancelled) setCases(data); })
    .catch((err) => { if (!cancelled) setError(err.message); })
    .finally(() => { if (!cancelled) setLoading(false); });
  return () => { cancelled = true; };
}, []);

Common Commands

npx expo start -c        # clear Metro cache
npm run lint
npm run lint:fix
npx tsc --noEmit
npm test
npm run test:coverage
npx expo export --platform web

Makefile

A Makefile exists at project root to to help with CI commands

  • Usage: make <target>
  • Commands:
make help        # Show available commands

make test        # Full CI pipeline (lint, typecheck, tests, builds)
make test-fix    # Attempt auto-fix (clean, install, lint-fix, typecheck)

make lint        # Run ESLint
make lint-fix    # Auto-fix ESLint
make typecheck   # TypeScript check
make test-unit   # Run Jest tests with coverage

make build-ios      # Verify iOS export
make build-android  # Verify Android export
make build-web      # Verify web export

make up          # Clean + install
make clean       # Clear cache/artifacts
make reset       # Regenerate node_modules + lockfile

Troubleshooting

Problem Fix
Install failures make reset
Dependency / cache issues make clean && make install
Lint errors make lint-fix, auto fix: make lint-fix
TypeScript errors make typecheck
Test failures make test-unit
Full CI failing locally make test
Port 8081 in use lsof -ti:8081 \| xargs kill -9
Module not found make clean and verify @/ path ali
Expo SDK mismatch npx expo install --fix