Complete guide for building beautiful apps with Expo Router. Covers fundamentals, styling, components, navigation, animations, patterns, and native tabs.
日本語の概要は準備中です。原文の説明を表示しています。
Build a NowStack Mobile iOS app and upload it to TestFlight. Always check OS first: macOS uses local `eas build --local`; Windows/Linux must run ns-setup-expo and use EAS cloud.
インストールする前に、エージェントに与えられる指示の中身を確認できます。
Take a working NowStack Mobile app from local dev to an installable TestFlight build. Run phases A-D for setup only (stop before the build, report readiness); run all phases end to end to build and upload. This is the iOS beta surface; for App Store review use ns-ios-distribute.
Default build mode on macOS is a local Mac build with eas build --local, so it does not consume Expo/EAS cloud build credits. On Windows or Linux, do not attempt a local iOS build; load/follow ns-setup-expo, verify the Expo/EAS account and project link, then use the EAS cloud build path.
The proven flow (battle-tested on a real app built from this boilerplate):
easProjectId into site-config.ts.eas.json production env at the prod Convex URLs.mobile-app/scripts/asc-api.mjs — no browser, no Apple ID 2FA.eas build --local, credentialsSource: "local", and an explicit --output /tmp/{slug}.ipa..ipa to TestFlight with asc publish testflight into an internal beta group.asc validate testflight until the build is valid for beta distribution, then verify every expected tester is invited and has access to the uploaded build.The key insight: interactive eas build credential setup requires Apple ID 2FA and cannot be automated reliably. The ASC API key path avoids 2FA entirely. On macOS, local eas build --local avoids Expo cloud build credits while still using the same local signing files; on Windows/Linux, EAS cloud is the only supported iOS build path.
</objective>
AuthKey_<KEY_ID>.p8 file, its key ID, and the team issuer ID. If unknown, run the ns-find-asc-credentials skill, or point the user to App Store Connect > Users and Access > Integrations > App Store Connect API (key must have Admin or App Manager role).npx eas-cli@latest whoami, a real easProjectId in site-config.ts, and expo-doctor clean. If missing, load/follow ns-setup-expo before building.asc CLI installed (brew install asc) — used only for the final upload; everything else goes through scripts/asc-api.mjs.xcodebuild -version, command -v pod). On Windows/Linux, skip these checks and use EAS cloud after ns-setup-expo.
</prerequisites>
<state_variables>
| Variable | Source |
|---|---|
{bundle_id} | SiteConfig.bundleId |
{apple_team_id} | SiteConfig.appleTeamId |
{eas_project_id} | output of eas project:init, then written to site-config.ts |
{asc_key_id} / {asc_issuer_id} / {asc_p8_path} | from the user / ns-find-asc-credentials. Never commit, never print. |
{convex_prod_url} / {convex_prod_site_url} | from npx convex deploy output / Convex dashboard |
{asc_app_id} | numeric app ID resolved from the bundle ID (Phase D) |
{build_mode} | local only on macOS unless --expo is forced; expo on Windows/Linux |
{ipa_path} | /tmp/{slug}.ipa from local build, or downloaded cloud artifact |
{build_id} | buildId from asc publish testflight --output json |
{beta_group_id} | Internal beta group ID |
{what_to_test} | Build-specific TestFlight notes, derived from the current change or asked once if unknown |
{review_contact_*} | TestFlight beta review contact fields from project/release config, App Store Connect, or the user |
{expected_tester_emails} | User-requested tester emails plus existing internal group testers when the goal is "invite all" |
| </state_variables> |
<platform_build_routing> Run this before Phase A build checks:
uname -s || node -p "process.platform"
Darwin / darwin: local iOS production builds are allowed. Continue with local toolchain checks and use Phase E unless the user explicitly passed --expo.Linux, win32, or Windows PowerShell: local iOS production builds are not allowed. Load/follow .agents/skills/ns-setup-expo/SKILL.md; verify eas-cli, eas whoami, real easProjectId, and cd mobile-app && npx expo-doctor. Then set {build_mode}=expo and use Phase E-expo.ns-setup-expo; do not continue to signing/build/upload until Expo account linking is green.
</platform_build_routing><critical_safety>
.p8 keys, .p12 files, p12 passwords, provisioning profiles, or credentials.json. The repo .gitignore already excludes them — verify before any commit.scripts/asc-api.mjs; do not write them into files inside the repo.--expo cloud EAS builds (consumes build credits), and the TestFlight upload.appVersionSource is remote.eas.json points at the PROD Convex deployment before building, or testers ship with a dev backend.
</critical_safety>npm run check-setup # must be free of errors (easProjectId warning is expected pre-init)
cd mobile-app && npx tsc --noEmit && npm run lint
uname -s || node -p "process.platform"
npx eas-cli@latest whoami # Expo account logged in?
command -v asc && asc version
xcodebuild -version && command -v pod # required only when OS is macOS/Darwin and build_mode=local
Read site-config.ts and confirm with the user: title, bundleId, appleTeamId, payment product IDs. The bundle ID is permanent once the app record exists — it must be final now.
Export the ASC credentials for the rest of the session:
export ASC_KEY_ID="<10-char key id>"
export ASC_ISSUER_ID="<issuer uuid>"
export ASC_P8_PATH="/path/to/AuthKey_<KEY_ID>.p8"
</phase>
<phase n="B" title="EAS project init">
Skip if `easProjectId` in `site-config.ts` is already a real UUID.
cd mobile-app
npx eas-cli@latest project:init --non-interactive --force
npx eas-cli@latest project:info
Write the printed project ID into site-config.ts > easProjectId. Re-run npm run check-setup — the easProjectId warning must be gone.
</phase>
npx convex deploy -y
# prod env starts EMPTY — set every required var:
npx convex env set --prod SITE_URL "https://<the-app-domain>"
npx convex env set --prod BETTER_AUTH_SECRET "$(openssl rand -hex 32)" # fresh, never reuse dev
npx convex env set --prod RESEND_API_KEY "$(npx convex env get RESEND_API_KEY)"
npx convex env set --prod EMAIL_FROM "$(npx convex env get EMAIL_FROM)"
# if Apple Sign In is enabled: APPLE_CLIENT_ID = {bundle_id}, APPLE_CLIENT_SECRET copied from dev
# if Google Sign In is enabled: GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET copied from dev
npx convex env list --prod
Then put the prod URLs into mobile-app/eas.json > build.production:
"production": {
"autoIncrement": true,
"credentialsSource": "local",
"env": {
"EXPO_PUBLIC_CONVEX_URL": "https://<prod-deployment>.convex.cloud",
"EXPO_PUBLIC_CONVEX_SITE_URL": "https://<prod-deployment>.convex.site"
}
}
credentialsSource: "local" is what makes the build fully non-interactive (Phase E).
</phase>
1. Resolve the app record (hard gate):
node mobile-app/scripts/asc-api.mjs GET "/v1/apps?filter[bundleId]={bundle_id}"
Empty data → STOP. Tell the user to create the app record at appstoreconnect.apple.com (My Apps > + > New App) with this exact bundle ID, then resume. Otherwise save {asc_app_id} = data[0].id.
2. Bundle ID registration + capabilities:
node mobile-app/scripts/asc-api.mjs GET "/v1/bundleIds?filter[identifier]={bundle_id}"
# if missing, register it:
# POST /v1/bundleIds {"data":{"type":"bundleIds","attributes":{"identifier":"{bundle_id}","name":"{title}","platform":"IOS"}}}
node mobile-app/scripts/asc-api.mjs GET "/v1/bundleIds/<BUNDLE_DB_ID>/bundleIdCapabilities"
# enable missing capabilities the app uses (IN_APP_PURCHASE, APPLE_ID_AUTH, PUSH_NOTIFICATIONS):
# POST /v1/bundleIdCapabilities {"data":{"type":"bundleIdCapabilities","attributes":{"capabilityType":"APPLE_ID_AUTH"},"relationships":{"bundleId":{"data":{"type":"bundleIds","id":"<BUNDLE_DB_ID>"}}}}}
3. Distribution certificate. First check for an existing one (Apple caps distribution certs at ~2-3 per account):
node mobile-app/scripts/asc-api.mjs GET "/v1/certificates?filter[certificateType]=DISTRIBUTION"
Reuse only if the user has its private key/.p12 locally. Otherwise create a new one (with user confirmation):
openssl req -new -newkey rsa:2048 -nodes -keyout dist.key -out dist.csr \
-subj "/emailAddress=<user-email>/CN={title} Distribution/C=US"
node -e "const fs=require('fs');fs.writeFileSync('cert-req.json',JSON.stringify({data:{type:'certificates',attributes:{certificateType:'DISTRIBUTION',csrContent:fs.readFileSync('dist.csr','utf8')}}}))"
node mobile-app/scripts/asc-api.mjs POST /v1/certificates cert-req.json
# save body.data.id (cert id) and body.data.attributes.certificateContent (base64) -> dist.cer
4. App Store provisioning profile:
node -e "const fs=require('fs');fs.writeFileSync('profile-req.json',JSON.stringify({data:{type:'profiles',attributes:{name:'{title} AppStore',profileType:'IOS_APP_STORE'},relationships:{bundleId:{data:{type:'bundleIds',id:'<BUNDLE_DB_ID>'}},certificates:{data:[{type:'certificates',id:'<CERT_ID>'}]},devices:{data:[]}}}}))"
node mobile-app/scripts/asc-api.mjs POST /v1/profiles profile-req.json
# save body.data.attributes.profileContent (base64) -> profile.mobileprovision
5. Build the .p12 (the -legacy flag is mandatory — Apple tooling rejects OpenSSL 3.x default ciphers):
echo "<certificateContent-b64>" | base64 -d > dist.cer
P12PASS=$(openssl rand -hex 12)
openssl x509 -inform der -in dist.cer -out dist.pem 2>/dev/null || cp dist.cer dist.pem
openssl pkcs12 -export -in dist.pem -inkey dist.key -out dist.p12 \
-name "{title} Distribution" -passout pass:"$P12PASS" -legacy
echo "<profileContent-b64>" | base64 -d > appstore.mobileprovision
6. Wire into the project (gitignored paths only):
mkdir -p mobile-app/credentials
cp dist.p12 mobile-app/credentials/dist.p12
cp appstore.mobileprovision mobile-app/credentials/
node -e "const fs=require('fs');fs.writeFileSync('mobile-app/credentials.json',JSON.stringify({ios:{provisioningProfilePath:'credentials/appstore.mobileprovision',distributionCertificate:{path:'credentials/dist.p12',password:process.argv[1]}}},null,2))" "$P12PASS"
git check-ignore mobile-app/credentials.json mobile-app/credentials/dist.p12 # MUST print both paths
Setup-only mode STOPS here — report readiness (app record, cert, profile, credentials.json, eas.json) and the next command. </phase>
<phase n="E" title="Local iOS build (macOS default)"> Default path on macOS only. This uses the local Mac/Xcode toolchain and does **not** consume Expo cloud build credits. It still uses EAS CLI for config/versioning and local credentials for signing.cd mobile-app
npx eas-cli@latest build --platform ios --profile production \
--local --non-interactive --output /tmp/{slug}.ipa
If the build fails in expo doctor due to patch-version mismatches, inspect the exact error. Do not blindly upgrade dependencies during a release unless the build is blocked; if blocked, run npx expo install --check, apply the minimal Expo SDK patch updates, rerun npx tsc --noEmit && npm run lint, then rebuild.
Local autoIncrement can skip build numbers if a canceled cloud build already reserved one. That is acceptable for App Store Connect.
</phase>
cd mobile-app
npx eas-cli@latest build --platform ios --profile production --non-interactive --no-wait
Poll until terminal state (FINISHED / ERRORED):
npx eas-cli@latest build:list --platform ios --limit 1 --json --non-interactive
# or: npx eas-cli@latest build:view <BUILD_ID> --json
On FINISHED, download the artifact:
curl -sL -o /tmp/{slug}.ipa "<artifacts.buildUrl>"
</phase>
<phase n="F" title="TestFlight upload">
**1. Internal beta group** (TestFlight needs a group; `asc publish testflight` fails without `--group`):
node -e "const fs=require('fs');fs.writeFileSync('group-req.json',JSON.stringify({data:{type:'betaGroups',attributes:{name:'Internal',isInternalGroup:true,hasAccessToAllBuilds:true},relationships:{app:{data:{type:'apps',id:'{asc_app_id}'}}}}}))"
node mobile-app/scripts/asc-api.mjs POST /v1/betaGroups group-req.json
# 409 "already exists" is fine — fetch the id: GET "/v1/apps/{asc_app_id}/betaGroups"
2. Upload and wait for processing (asc must be authenticated — asc auth status --validate, else asc auth login with the same ASC key):
asc publish testflight --app "{asc_app_id}" --ipa /tmp/{slug}.ipa \
--group "<BETA_GROUP_ID>" --wait --timeout 45m --output json --pretty \
| tee "/tmp/{slug}-testflight-upload.json"
node -e "const r=require('/tmp/{slug}-testflight-upload.json'); console.log(r.buildId)"
Save the printed value as {build_id}. Do not continue if processingState is not VALID; keep waiting/polling with asc status --app "{asc_app_id}" --output table until the build is valid, or report the exact Apple processing failure.
3. Complete TestFlight metadata and validate readiness. A successful binary upload is not enough. The skill must make asc validate testflight pass before reporting success.
First inspect the current gaps:
asc validate testflight --app "{asc_app_id}" --build "{build_id}" --output table
If beta review contact fields are missing, fetch the review detail ID and edit the missing fields. Use existing project/release contact values when available; otherwise ask the user once for the missing contact fields and resume the workflow.
asc testflight review view --app "{asc_app_id}" --output json --pretty \
| tee "/tmp/{slug}-testflight-review.json"
# Extract the betaAppReviewDetail id from the JSON output, then:
asc testflight review edit --id "{review_detail_id}" \
--contact-first-name "{review_contact_first_name}" \
--contact-last-name "{review_contact_last_name}" \
--contact-email "{review_contact_email}" \
--contact-phone "{review_contact_phone}" \
--demo-account-required false \
--notes "{review_notes}"
If "What to Test" is missing, create or update the build localization for en-US. The notes must be specific to the uploaded build; do not use an empty placeholder.
asc build-localizations list --build "{build_id}" --locale en-US --output json --pretty \
| tee "/tmp/{slug}-build-localizations.json"
# If no en-US localization exists:
asc build-localizations create --build "{build_id}" --locale en-US \
--whats-new "{what_to_test}"
# If one exists:
asc build-localizations update --id "{build_localization_id}" \
--whats-new "{what_to_test}"
Rerun validation after every metadata change and do not move to tester verification until it is clean:
asc validate testflight --app "{asc_app_id}" --build "{build_id}" --output table
4. Add testers and attach the uploaded build. Internal testers must be App Store Connect team members; external emails work via groups. For "invite all", treat all existing internal group testers plus user-provided emails as {expected_tester_emails}. Do not assume group access refreshed invite links.
node -e "const fs=require('fs');fs.writeFileSync('tester-req.json',JSON.stringify({data:{type:'betaTesters',attributes:{email:'<tester-email>'},relationships:{betaGroups:{data:[{type:'betaGroups',id:'<BETA_GROUP_ID>'}]}}}}))"
node mobile-app/scripts/asc-api.mjs POST /v1/betaTesters tester-req.json
For each expected tester, verify/create the tester row, add them to the internal group, attach the uploaded build, and resend a fresh invite:
asc testflight testers list --app "{asc_app_id}" --output json --pretty
asc testflight testers invite --app "{asc_app_id}" --email "<tester-email>" --group "{beta_group_id}"
asc testflight testers add-groups --id "{tester_id}" --group "{beta_group_id}"
asc testflight testers add-builds --id "{tester_id}" --build-id "{build_id}"
asc testflight testers invite --app "{asc_app_id}" --email "<tester-email>" --group "{beta_group_id}"
5. Verify installability and invitations. Finish only after all checks below are true:
asc status --app "{asc_app_id}" --output table
asc validate testflight --app "{asc_app_id}" --build "{build_id}" --output table
asc testflight groups links view --group-id "{beta_group_id}" --type betaTesters --paginate --output json --pretty
asc testflight groups links view --group-id "{beta_group_id}" --type builds --paginate --output json --pretty
asc testflight testers builds list --tester-id "{tester_id}" --output table
asc testflight testers groups list --tester-id "{tester_id}" --output table
Report the build number, build ID, validation result, beta group, and verified tester emails. If any tester cannot be invited because of App Store Connect account visibility or team membership, fix the visibility when possible; otherwise report that tester as the only remaining blocker with the exact ASC error. </phase>
<failure_modes>
.p12 rejected during signing → it was exported without -legacy. Re-export: openssl pkcs12 -in dist.p12 -nodes -out tmp.pem -passin pass:"$P12PASS" && openssl pkcs12 -export -in tmp.pem -out dist.p12 -passout pass:"$P12PASS" -legacy.DELETE /v1/certificates/<id>) or whether they have its .p12 to reuse. Never revoke without confirmation — it breaks other apps signed with it.--local. Load/follow ns-setup-expo, verify eas whoami + linked easProjectId, then use Phase E-expo.xcodebuild -version, xcode-select -p, command -v pod. If unavailable and the user accepts cloud quota usage, rerun with EAS cloud.credentialsSource: "local" is missing in eas.json, or credentials.json paths are wrong (they are relative to mobile-app/).--expo cloud build fails asking for credentials → same credential checks as local, then rerun cloud build.asc publish testflight fails with "--group is required" → create the beta group first (Phase F step 1).asc validate testflight fails after upload → fix the reported metadata immediately. Missing review contact fields are fixed with asc testflight review edit; missing "What to Test" is fixed with asc build-localizations create/update. Do not report TestFlight success until validation passes.{build_id}, then resend the invite after build attachment with asc testflight testers invite --app "{asc_app_id}" --email "<email>" --group "{beta_group_id}"."ITSAppUsesNonExemptEncryption": false to infoPlist in mobile-app/app.config.ts so future builds skip it.eas.json had prod URLs, or prod Convex env vars are missing (npx convex env list --prod). Rebuild after fixing — env is baked at build time.--non-interactive. Run npx eas-cli@latest credentials in a real terminal with the user present, then build. Do not attempt to script the 2FA prompt.
</failure_modes><success_metrics>
npm run check-setup passes with a real easProjectId./tmp/{slug}.ipa without any interactive prompt; Windows/Linux EAS cloud builds reach FINISHED after ns-setup-expo verifies Expo account linking.asc validate testflight --app "{asc_app_id}" --build "{build_id}" --output table passes after metadata fixes.git status shows no credential files staged; nothing secret printed in the transcript.
</success_metrics>まだレビューはありません。使ってみた感想をお寄せください。
概要と使いどころ
Complete guide for building beautiful apps with Expo Router. Covers fundamentals, styling, components, navigation, animations, patterns, and native tabs.
日本語の概要は準備中です。原文の説明を表示しています。
Routes general Convex requests to the right project skill. Use when the user asks which Convex skill to use or gives an underspecified Convex app task.
日本語の概要は準備中です。原文の説明を表示しています。
Use when optimizing Convex database read bandwidth, documents-read cost, query indexes, full-table scans, function-level usage dashboard findings, or code that uses .filter(), unbounded .collect(), N+1 reads, or expensive reactive Convex queries.
日本語の概要は準備中です。原文の説明を表示しています。
Build reusable Convex components with isolated tables and app-facing wrappers. Use for defineComponent, app.use, ComponentApi, or shared backend modules.
日本語の概要は準備中です。原文の説明を表示しています。
Plan safe Convex schema and data migrations. Use for widen-migrate-narrow rollouts, backfills, breaking field changes, table splits, or failed schema deploys.
日本語の概要は準備中です。原文の説明を表示しています。
Audit Convex performance for hot reads, write contention, subscriptions, function limits, OCC conflicts, latency, read amplification, and expensive reactive queries.
日本語の概要は準備中です。原文の説明を表示しています。