Skip to content

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

ProfileData sourceUse case
developmentMetro (hot reload)Active coding
e2e-testSupabase (no credentials)Maestro E2E test runs
previewReal SupabaseIntegration testing
productionReal SupabaseApp Store / final release

Daily Development (PC on, Metro running)

bash
# Local Supabase (must be running: npx supabase start)
npx expo start --dev-client --tunnel

# Browser (no phone needed)
npx expo start --web

Hot reload is instant. No rebuild needed for JS/TS changes.


Standalone Builds (PC can be off)

Preview build — integration testing

bash
eas build --platform android --profile preview

Requires Supabase + Kakao credentials to be set as EAS Secrets first (see below).

E2E test build — Maestro automation

bash
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:

  1. Confirm the change is OTA-safe: no native module, native config, app version, runtime version, plugin, permission, icon/splash, or EAS profile change.
  2. Run the required verification, usually yarn check.
  3. Commit the exact source that should ship.
  4. Push the commit.
  5. Confirm the OTA source/native compatibility guard passes:
    bash
    node scripts/guard-ota-release.mjs preview
  6. Publish to preview:
    bash
    yarn ota:preview
  7. 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:

bash
yarn build:preview

For 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:

bash
yarn check:r1-release-readiness:eas

For handoff while blockers are still open, use the non-failing report:

bash
yarn report:r1-release-readiness

It 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:

bash
git log -1 --pretty=%s

Keep the commit subject human-readable because it becomes the EAS Update label.

Commands

bash
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:canary

EAS 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)

bash
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

bash
eas env:list                       # view project environment variables
eas env:pull                       # pull remote env for inspection

Current 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:

  1. Run migrations against the target instance:
    bash
    npx supabase db push --db-url "postgres://..."
  2. 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
  3. Run yarn check:supabase-types when the required Supabase env is available.
  4. Build and test with eas build --profile preview.

Build Cheat Sheet

bash
# 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 val

Troubleshooting

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.

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