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
.envfile. 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 intosrc/components/ui/
- Use NativeWind
- No direct
supabaseor Azure SDK calls in screens.- All backend calls go through
src/services/
- All backend calls go through
- 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 ?? [];
}
Navigation
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:
supabase→SupabaseAuthProvider(prod)mock→MockAuthProvider(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 |