Skip to content

Device Setup — EAS Build + OTA Updates

Superseded (2026-07): folded into Builds & Deployment. Kept for reference.

Status: Superseded

How to get TwoMore v2 running on a physical device with over-the-air (OTA) updates wired up.

One-time setup

1. Install deps

bash
yarn install

This installs eas-cli as a dev dep (already added to root package.json).

1b. Environment variables

bash
cp .env.example .env
ln -sf ../../.env apps/mobile/.env

Why the symlink? Expo CLI loads env vars from the project root (where app.json lives = apps/mobile/), not from the monorepo root. The symlink keeps a single source of truth so we don't have two .env files drifting apart. .gitignore already ignores .env anywhere in the tree.

Edit .env and fill in at minimum:

  • EXPO_PUBLIC_SUPABASE_URL — your Supabase project URL (separate from v1's)
  • EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY — Supabase anon key

Without these the app won't connect to the backend. Get them from Supabase Dashboard → Settings → API.

1c. Create a test user

In the Supabase Dashboard → Authentication → Users → Add user, create an email+password account. You'll use it to sign into the app on device. (Sign-up isn't in the app UI yet — Phase 8c.)

2. Log in to Expo

bash
npx eas login

(account: lbsky — the one used for v1)

3. Create the v2 EAS project

bash
yarn eas:init

This runs eas project:init inside apps/mobile/. It creates a new EAS project (separate from v1's) and inserts extra.eas.projectId into apps/mobile/app.json.

Then wire the OTA updates URL:

bash
cd apps/mobile && npx eas update:configure

This adds updates.url pointing at your new project's Expo updates server, using the projectId from the previous step.

Commit both changes to app.json.

4. Configure native credentials (iOS + Android)

bash
cd apps/mobile
npx eas credentials

Set up iOS push certs, Android keystore, etc. Only needed when you first build native.

5. First development build

bash
yarn build:dev

Produces a development client APK/IPA. Install it once per device — it contains the native code. After that, JS changes ship via OTA without rebuilding.

Daily workflow

Test changes locally

bash
yarn mobile

Expo dev server starts. Connect the dev client on device to reload.

Deploy JS changes via OTA

bash
yarn ota:preview

Runs eas update --branch preview --environment preview. Devices on the preview channel auto-fetch the update next launch.

Deploy a new native build (when you add a native module)

bash
yarn build:preview

APK/IPA for internal testing.

For store-ready builds, run from the mobile app directory so the production profile in apps/mobile/eas.json is used:

bash
cd apps/mobile && eas build --profile production

When to build native vs OTA

OTA (no rebuild):

  • JS/TS changes
  • Tamagui styling
  • Pure component work
  • Feature logic in packages/*
  • New Supabase queries

Native rebuild required:

  • Adding/removing native modules (e.g., @react-native-kakao/user, new payment SDK)
  • Changing app.json plugins array
  • Bumping expo SDK version
  • Changing bundle identifier / package name
  • Adding custom native code

Rule of thumb: if yarn install adds something that ships native code, rebuild. Otherwise OTA.

Native dep preflight (prevents cloud-build failures)

Yarn workspaces do not auto-install peer dependencies, and Expo SDKs pin specific versions of native modules (reanimated, gesture-handler, svg, worklets). A drift either produces a Gradle failure in the cloud ("Process 'command 'node'' finished with non-zero exit value 1") or a runtime crash.

Every yarn build:dev and yarn build:preview runs yarn preflight (= expo install --check) first. If it reports drift, run yarn expo install <pkg>@<expected-version> inside apps/mobile/ before retrying the build. Never bypass.

When bumping reanimated across a major version, also verify the babel plugin path. Reanimated v4 moved it from react-native-reanimated/pluginreact-native-worklets/plugin — see apps/mobile/babel.config.js.

Troubleshooting

"No EAS project configured": run yarn eas:init.

"Process 'command 'node'' finished with non-zero exit value 1" in :react-native-reanimated: the react-native-worklets peer dep is missing or mismatched. Run yarn preflight to diagnose.

"Fonts not loading": verify .otf files exist in assets/fonts/. Pretendard fonts are committed to the repo (per v1 convention).

"Styles are missing after OTA": Tamagui compiler output depends on your tokens.config — if tokens changed, a native rebuild is required since tamagui-web.css is baked at build time.

"Update shipped but device doesn't pick it up": close and reopen the app twice. expo-updates fetches on launch, applies on the next one. Or run npx expo update --clear-cache.

Channels

ChannelPurpose
developmentDev client builds, live reload from Metro
previewInternal testing builds, OTA updates for QA
productionStore builds, OTA updates for end users

Markdown remains the source of truth. Run yarn docs:check before handoff.