Builds & Deployment
Status: Reference
Status: ActiveLast reviewed: 2026-06-28
Layer 2 guide. Read when building APKs, pushing OTA updates, or setting up production credentials. The canonical release/versioning workflow lives in Release Versioning And OTA Workflow.
Build Profiles
| Profile | Data source | Use case |
|---|---|---|
development | Metro (hot reload) | Active coding |
e2e-test | Supabase (no credentials) | Maestro E2E test runs |
preview | Real Supabase | Integration testing |
production | Real Supabase | App Store / final release |
Daily Development (PC on, Metro running)
# Local Supabase (must be running: npx supabase start)
npx expo start --dev-client --tunnel
# Browser (no phone needed)
npx expo start --webHot reload is instant. No rebuild needed for JS/TS changes.
Standalone Builds (PC can be off)
Preview build — integration testing
eas build --platform android --profile previewRequires Supabase + Kakao credentials to be set as EAS Secrets first (see below).
E2E test build — Maestro automation
eas build --platform android --profile e2e-test- APK without credentials — used by EAS Workflows for Maestro E2E test runs.
OTA Updates (push JS changes without rebuilding)
OTA is configured through expo-updates in apps/mobile/app.json and build channels in apps/mobile/eas.json. Current runtime policy is appVersion; app version 0.6.1 maps to runtime 0.6.1.
Current pre-production policy
Until R4 Launch Readiness Final Gate is accepted, the only normal OTA target is preview.
Production OTA commands remain in package.json so the release path is ready, but they are mechanically gated by scripts/guard-ota-release.mjs. They fail unless TWOMORE_ALLOW_PRODUCTION_OTA=1 is set after launch readiness is explicitly accepted.
Version-control flow
OTA publishes the local JS bundle, so never publish from a local-only source state. The root ota:* scripts run scripts/guard-ota-release.mjs, which refuses dirty worktrees, branches without upstreams, unpushed commits, local branches behind upstream after a fetch, and preview OTA attempts after native-sensitive files changed beyond the last compatible preview build/source baseline in apps/mobile/ota-compatibility-baseline.json. That makes the EAS artifact traceable to fetchable source and a compatible installed binary before the update command runs.
Use the Release Versioning And OTA Workflow decision gate to classify significant changes before publishing. Significant means "commit + push + evidence"; it does not automatically mean OTA-safe.
Use this order for pre-production JS-only changes:
- Confirm the change is OTA-safe: no native module, native config, app version, runtime version, plugin, permission, icon/splash, or EAS profile change.
- Run the required verification, usually
yarn check. - Commit the exact source that should ship.
- Push the commit.
- Confirm the OTA source/native compatibility guard passes:bash
node scripts/guard-ota-release.mjs preview - Publish to preview:bash
yarn ota:preview - Record the EAS update group, runtime version, source commit, upstream-sync status, smoke evidence, and rollback pointer in Release Evidence Ledger when the slice needs release evidence.
If the change is not OTA-safe, build a new preview binary instead:
yarn build:previewFor R1 acceptance, do not infer readiness from a green yarn check or a successful build alone. Run the aggregate gate after credentials, live association files, and candidate preview builds are in place:
yarn check:r1-release-readiness:easFor handoff while blockers are still open, use the non-failing report:
yarn report:r1-release-readinessIt reports static R1 contracts, live app-link proof, Kakao credential proof, preview OTA/native compatibility, EAS preview env-name presence, and latest Android/iOS preview build compatibility without printing secret values.
The update message comes from the latest commit subject:
git log -1 --pretty=%sKeep the commit subject human-readable because it becomes the EAS Update label.
Commands
yarn ota:preview
# Launch-gated, not for normal pre-production work:
TWOMORE_ALLOW_PRODUCTION_OTA=1 yarn ota:production
TWOMORE_ALLOW_PRODUCTION_OTA=1 yarn ota:production:canaryEAS Secrets (required for preview / production)
Credentials are stored encrypted in EAS — never commit them to eas.json or .env.
One-time setup (when you have production credentials)
eas env:create --scope project --name EXPO_PUBLIC_SUPABASE_URL \
--value "https://xxxx.supabase.co"
eas env:create --scope project --name EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY \
--value "sb_publishable_..."
# Optional fallback for environments that still use the legacy anon JWT:
eas env:create --scope project --name EXPO_PUBLIC_SUPABASE_ANON_KEY \
--value "eyJhbGci..."
eas env:create --scope project --name EXPO_PUBLIC_KAKAO_APP_KEY \
--value "your-kakao-key"
# Preferred R1 Kakao Share build-time alias. Use the same Kakao Native App Key
# as EXPO_PUBLIC_KAKAO_APP_KEY; the app config accepts either name.
eas env:create --scope project --name TWOMORE_KAKAO_NATIVE_APP_KEY \
--value "your-kakao-key"
# R1 KakaoTalk Share custom-template IDs. These are plaintext build-time IDs,
# not secrets; use the approved IDs from Kakao Developers message templates.
eas env:create --scope project --name TWOMORE_KAKAO_TEMPLATE_SESSION_INVITE \
--value "template-id"
eas env:create --scope project --name TWOMORE_KAKAO_TEMPLATE_CLUB_INVITE \
--value "template-id"
eas env:create --scope project --name TWOMORE_KAKAO_TEMPLATE_MATCH_RESULT \
--value "template-id"
eas env:create --scope project --name TWOMORE_KAKAO_TEMPLATE_TIER_PROMOTION \
--value "template-id"
eas env:create --scope project --name TWOMORE_KAKAO_TEMPLATE_STAT_CARD \
--value "template-id"
eas env:create --scope project --name TWOMORE_KAKAO_TEMPLATE_LEADERBOARD \
--value "template-id"Run once. EAS injects these automatically into every build across all profiles.
View / update secrets
eas env:list # view project environment variables
eas env:pull # pull remote env for inspectionCurrent status
Required secrets must exist in the target EAS environment. Development and preview profiles carry only dev-account conveniences in eas.json; production credentials must be verified in EAS before a release build or production OTA. Before R4 launch readiness is accepted, publish JS updates only to preview.
Use eas env:list, eas env:pull, and the EAS dashboard to verify the current remote state. Do not infer production readiness from local .env files.
Hosted Supabase Setup / Type Drift
When applying schema changes to a linked or hosted Supabase project:
- Run migrations against the target instance:bash
npx supabase db push --db-url "postgres://..." - Generate and format types from the live schema:bash
npx supabase gen types typescript --project-id xxxx \ | npx prettier --config .prettierrc.json --parser typescript \ > packages/app/src/adapters/supabase/generated.types.ts - Run
yarn check:supabase-typeswhen the required Supabase env is available. - Build and test with
eas build --profile preview.
Build Cheat Sheet
# Start dev server (tunneled — daily use)
npx expo start --dev-client --tunnel
# Build standalone APKs
eas build --platform android --profile preview # integration testing
eas build --platform android --profile e2e-test # Maestro E2E
# Push OTA JS updates to installed builds
yarn ota:preview
# Production OTA is launch-gated:
TWOMORE_ALLOW_PRODUCTION_OTA=1 yarn ota:production
# Manage credentials
eas env:list
eas env:create --scope project --name KEY --value valTroubleshooting
Build concurrency limit reached
Free EAS tier allows 1 concurrent build. Cancel any running build at expo.dev/accounts/lbsky/builds then retry.
Network request failed in preview build
Supabase credentials aren't set. Set credentials via eas env:create, verify with eas env:list or eas env:pull, and rebuild.