React Native mobile app built with Expo and expo-router.
From the repository root:
# Install dependencies (workspaces are wired up, so the root install covers mobile)
pnpm install
# Build and run on iOS simulator
pnpm --filter @posthog/mobile ios
# Start the development server (after initial build)
pnpm --filter @posthog/mobile startFirst-time iOS setup also requires the watchOS SDK to be installed in Xcode — see Prerequisites.
- Expo - Build tooling, native APIs, OTA updates
- expo-router - File-based routing
- NativeWind - Tailwind CSS for React Native
- React Query - Async data fetching and caching
- Zustand - Client state management (UI state, selections, local flags)
- Phosphor Icons - Icon library
Code is organized by feature in src/features/. Features own native components, one-source hooks, and view state. They do not own copies of cloud contracts, transport, orchestration, or presentation rules.
src/features/
├── auth/ # Authentication & user session
│ ├── hooks/
│ ├── lib/
│ ├── stores/
│ └── types.ts
├── chat/ # PostHog AI chat interface
│ ├── components/
│ ├── hooks/
│ ├── stores/
│ └── types.ts
├── inbox/ # Native inbox rendering and query hooks
│ ├── components/
│ ├── hooks/
│ └── stores/
└── tasks/ # Native cloud-task rendering and host adapters
├── components/
├── hooks/
├── services/
└── stores/
Mobile and desktop use the same cloud-task architecture. New work must preserve these ownership rules:
@posthog/sharedowns runtime contracts and Zod schemas.@posthog/api-clientowns authenticated PostHog HTTPS transport and its request/response types.@posthog/coreowns cloud-task orchestration and headless presentation decisions, including sessions, queues, permissions, models, repositories, inbox rules, and automation semantics.apps/mobileowns Expo lifecycle, React Native rendering, gestures, sheets, notifications, audio, secure storage, and small persisted view-state stores.@posthog/uiowns the DOM/Quill renderer and web view state.
Do not add a mobile API facade, duplicate a shared type, or re-export a core helper through a mobile file. Import the owning package directly. If desktop and mobile need different visuals, add a headless descriptor or decision function to core and keep two thin renderers.
Intentional host differences are limited to platform capabilities and view state. Mobile may persist native navigation state, cached picker snapshots, optimistic attachment echoes, and notification preferences; it must not implement retries, reconnection, transport parsing, task lifecycle, or cross-store decisions in those stores.
Routes for the screens are defined by the file structure in src/app/ using expo-router.
(tabs)/- Parentheses create a layout group (tab navigator)_layout.tsx- Configures the navigator for that directory[id].tsx- Square brackets define dynamic route parameters- Stacks and modals live outside tab group, configured in
_layout.tsx
src/app/
├── _layout.tsx # Root layout
├── index.tsx # Entry redirect
├── auth.tsx # Auth screen (unauthenticated)
├── (tabs)/ # Tabs group
│ ├── _layout.tsx # Layout for all tabs
│ ├── index.tsx # Home tab (Conversations)
│ ├── tasks.tsx # Tasks tab
│ └── settings.tsx # Settings tab
├── chat/ # Chat stack
│ ├── index.tsx # New chat
│ └── [id].tsx # Chat by ID (dynamic route)
└── task/ # Task stack
├── index.tsx # New task
└── [id].tsx # Task by ID (dynamic route)
src/
├── components/ # Reusable UI components (Text, etc.)
└── lib/
├── posthog.ts # Analytics setup
├── queryClient.ts # React Query client
├── theme.ts # Design tokens
└── logger.ts # Logger setup
- Node.js 22+
- pnpm 10.23.0
- Xcode (for iOS development)
- watchOS SDK (iOS builds embed the Apple Watch companion; without this SDK installed,
expo run:iosfails withwatchOS X.X must be installed in order to run the scheme)- Install via
xcodebuild -downloadPlatform watchOS, or in Xcode → Settings → Components → Platforms → download the latest watchOS
- Install via
- Android Studio (for Android development)
- EAS CLI is optional — all
eascommands below are invoked vianpx eas. Install globally withnpm install -g eas-clionly if you prefer the bare command.
All commands use pnpm --filter @posthog/mobile <script> to target this workspace package. (-F is a shorter alias for --filter.)
Development server:
pnpm --filter @posthog/mobile start # Start Expo dev server
pnpm --filter @posthog/mobile start:clear # Start with cleared Metro cacheBuild and run:
pnpm --filter @posthog/mobile ios # iOS simulator
pnpm --filter @posthog/mobile ios:device # iOS device (requires Apple Developer account)
pnpm --filter @posthog/mobile android # Android emulator/deviceNative code generation:
pnpm --filter @posthog/mobile prebuild # Generate ios/ and android/ folders
pnpm --filter @posthog/mobile prebuild:clean # Delete and regenerate (when adding native deps)EAS builds:
pnpm --filter @posthog/mobile build:dev # Development build (iOS, cloud)
pnpm --filter @posthog/mobile build:dev:local # Development build (iOS, local)
pnpm --filter @posthog/mobile build:preview # Preview build (iOS)
pnpm --filter @posthog/mobile build:production # Production build (iOS)TestFlight:
pnpm --filter @posthog/mobile testflight # Submit to TestFlightUtilities:
pnpm install # Installs all workspaces, including mobile
pnpm --filter @posthog/mobile lint # Run Biome check
pnpm --filter @posthog/mobile lint:fix # Run Biome check with auto-fix
pnpm --filter @posthog/mobile format # Run Biome formatcd apps/mobile
# Development server
pnpm start # alias for: expo start
pnpm start:clear # alias for: expo start --clear
# Build and run
pnpm ios # alias for: expo run:ios
pnpm ios:device # alias for: expo run:ios --device
pnpm android # alias for: expo run:android
# Generate native code
pnpm prebuild # alias for: expo prebuild
pnpm prebuild:clean # alias for: expo prebuild --clean
# EAS builds (iOS) — pnpm aliases exist for these
pnpm build:dev # eas build --profile development --platform ios
pnpm build:dev:local # eas build --profile development --platform ios --local
pnpm build:preview # eas build --profile preview --platform ios
pnpm build:production # eas build --profile production --platform ios
# EAS builds (Android) — no aliases, invoke directly
npx eas build --profile development --platform android
npx eas build --profile preview --platform android
npx eas build --profile production --platform android
# TestFlight
pnpm testflight # alias for: eas submit --platform ios
# Linting
pnpm lint
pnpm lint:fix
pnpm formatexpo prebuild generates the native ios/ and android/ folders from your Expo configuration.
When to run prebuild:
- First time setting up the project
- After adding/removing native dependencies (e.g.,
expo-camera,react-native-maps) - After changing
app.jsoniOS/Android configuration - After updating Expo SDK version
When to use --clean:
- Switching between Expo SDK versions
- Native build is failing and you want a fresh start
- You've made manual changes to native files that you want to discard
The --clean flag removes existing ios/ and android/ directories before regenerating.
The iOS app embeds the watchOS companion as part of its build, so the watchOS SDK must be installed in Xcode even if you're only running on an iPhone simulator. Without it,
expo run:iosfails before compilation withwatchOS X.X must be installed in order to run the scheme. Install viaxcodebuild -downloadPlatform watchOSor Xcode → Settings → Components → Platforms. This is a one-time setup.
The watchOS companion is a native SwiftUI target generated during Expo prebuild by the local config plugin at plugins/withWatchApp.js.
Canonical native source lives outside generated iOS output:
native/watch/— SwiftUI watch app source, Info.plists, and entitlementsnative/ios/— iPhone WatchConnectivity bridge
Generated output lives under ios/, including ios/watch/, ios/PostHog/WatchTaskControlModule.*, and PostHog.xcodeproj/project.pbxproj.
- iPhone remains the authenticated relay for the paired watch.
- Mobile derives compact task snapshots from task/session state and sends them through WatchConnectivity.
- Desktop-started local tasks work through the shared PostHog task run log/status backend, then iPhone relays to the watch.
- Watch actions send compact commands back to iPhone, which routes them through existing mobile commands (
permission_response,cancel, retry/resume, and handoff URLs). - Direct watch-to-Mac WatchConnectivity is not supported by Apple; Mac handoff uses
posthog-code://task/{taskId}/run/{taskRunId}.
cd apps/mobile
pnpm prebuild
# or, when regenerating native projects:
pnpm prebuild:cleanThe ./plugins/withWatchApp plugin copies native sources from native/, recreates the watch app/extension targets, and embeds them in the iOS app. If generated iOS files or Xcode targets drift, update native/ and rerun prebuild instead of editing generated project files manually.
- Open
ios/PostHog.xcworkspacein Xcode. - Select the iOS app scheme with a paired iPhone + Apple Watch simulator destination.
- Build/run the iOS app; Xcode should install the embedded watch app.
- Sign in on iPhone and open or start a PostHog task.
- Open the watch app and verify the mission overview, checklist, timeline, approvals, and blocker cards.
- Cloud task from phone/mac updates progress on watch.
- Desktop/local task shows a
Localbadge and receives progress through persisted task run logs. - Approval card actions reach the existing permission response path.
- Stop maps to the existing cancel command; retry maps to resume/retry from iPhone.
- Open on iPhone uses
posthog://task/{taskId}; Open on Mac usesposthog-code://task/{taskId}/run/{taskRunId}. - Haptics fire once for approval needed, completion, failure/stale blockers, and action acceptance — not on every polling update.
- Intermittent connectivity shows cached mission state rather than raw errors/logs.
Defined in eas.json:
| Profile | Purpose | Distribution |
|---|---|---|
development |
Dev client with debugging | Internal only |
preview |
Production-like for testing | Internal only |
production |
App Store / Play Store release | Public |
Local vs Cloud builds:
- Cloud (default): Runs on Expo's servers, no local Xcode needed
- Local (
--local): Runs on your machine, faster iteration, requires Xcode/Android SDK