Intro
Expo configuration mistakes can break builds, cause runtime crashes, or leak secrets into source control. This field guide connects real-world mistakes to practical examples, expected outputs, failure signals, and recovery steps. The focus is on operational safety: observe before changing, limit the blast radius, use placeholders instead of secrets, verify the result, and document how to recover if the expected state is not reached.
This guide is for developers, DevOps consultants, and technical startup teams using Expo SDK 49 or higher, EAS CLI 3.x, and Node 18 LTS. It covers Expo configuration, Expo config mistakes, Expo validation, Expo rollback, and Expo troubleshooting. Related areas such as React Native, Android, and GitLab CI/CD appear only when they affect prerequisites, compatibility, security, observability, or recovery.
Every example uses explicit placeholders like <project-root>, <bundle-identifier>, or <your-slug>. Never paste real credentials, tokens, private keys, or production identifiers into an article or a configuration file.
Version and Environment Inventory
Before touching a configuration file, capture the current state. Run read-only commands from the official CLI or API. Define the expected result and failure signal before making any change.
Prerequisites
- Expo SDK 49 or higher installed (
npx expo --versionreturns49.0.0or newer). - EAS CLI 3.x (
npm install -g eas-cli@3). - Node 18 LTS or newer (
node --versionreturnsv18.0.0or newer). - Git repository with clean working tree (
git status --porcelainreturns nothing).
Read-only inventory commands
Run these commands from <project-root> and record their output with timestamps:
# 1. Verify Expo version
npx expo --version
# Expected: 49.0.0 (or your SDK version)
# 2. Verify EAS CLI version
eas --version
# Expected: eas-cli/3.0.0 linux-x64 node-v18.0.0 (or similar)
# 3. Show resolved config without modifying files
npx expo config --type public
# Expected: JSON object with expo.name, expo.slug, expo.version, etc.
# 4. List all configuration files tracked by git
git ls-files | grep -E 'app\.(json|config\.(js|ts))|eas\.json|metro\.config\.js|babel\.config\.js'
# Expected: relative paths like app.json, eas.json
Blast radius and recovery path
Any change to app.json or app.config.js can affect both development and production builds. Before editing, identify the affected environment:
# Show EAS build profiles without running a build
eas build:list --platform all --limit 5 --json
# Expected: array of past builds with profile names (e.g., "preview", "production")
To limit blast radius, make changes in a branch and test with a preview build before merging to main. Recovery path: commit the old config before changing it, then run git diff to see exactly what changed. If a build fails, revert the commit with git revert <commit-hash> and rerun the build.
git checkout -b fix/expo-config-timestamp
# Make the scoped change, then compare
git diff app.json
# If the change is larger than intended, reset the file:
git checkout -- app.json
Safe Configuration Path
This section walks through a common mistake: putting a secret directly into app.json. We then show the safe path using environment variables and app.config.js.
Mistake: hardcoded secret in app.json
Many developers add an API key directly to extra in app.json:
{
"expo": {
"name": "MyApp",
"slug": "myapp",
"extra": {
"apiKey": "sk_live_1234567890abcdef"
}
}
}
This secret is committed to git and visible to anyone with repository access. It may also appear in the generated native projects or EAS build logs.
Safe path: app.config.js with environment variable
Replace app.json with app.config.js. Keep app.json only for static metadata, or delete it entirely if app.config.js is complete.
Step 1: Remove the secret from app.json
{
"expo": {
"name": "MyApp",
"slug": "myapp"
}
}
Step 2: Create app.config.js in the project root
module.exports = ({ config }) => {
const apiKey = process.env.MYAPP_API_KEY;
if (!apiKey) {
console.warn('MYAPP_API_KEY is not set; using placeholder');
}
return {
...config,
extra: {
...config.extra,
apiKey: apiKey || 'placeholder-key-for-development-only',
},
};
};
Step 3: Add the environment variable to your shell or CI/CD secrets
Never store the real key in a file. For local development, use a .env file that is gitignored:
# .env (DO NOT COMMIT)
MYAPP_API_KEY=sk_live_1234567890abcdef
Then load it with dotenv or your shell. For EAS Build, set the secret in the EAS dashboard or using eas secret:create:
eas secret:create --name MYAPP_API_KEY --value "sk_live_xxx" --scope project
# Expected: Secret MYAPP_API_KEY created for project.
Step 4: Verify the resolved config contains the placeholder, not the real secret
Run the config resolution without the environment variable set:
MYAPP_API_KEY= npx expo config --type public
# Expected output includes extra.apiKey = "placeholder-key-for-development-only"
Then run it with the real secret in a local shell (but do not print the whole config if it contains sensitive data; use jq or grep to check only the key):
MYAPP_API_KEY=sk_live_test npx expo config --json | jq -r '.extra.apiKey'
# Expected: sk_live_test
Step 5: Commit and verify no secrets are in git history
git add app.config.js app.json .gitignore
git commit -m "Move API key to environment variable"
git grep 'sk_live_' $(git rev-list --all) || true
# Expected: no output (secret not found)
Blast radius and recovery: Changing from app.json to app.config.js affects config resolution for all commands (expo start, eas build, eas update). If a build fails after the change, revert the commit as described earlier and check that MYAPP_API_KEY is set in the build environment.
Verification and Diagnostics
After any configuration change, verify the app still works in development and that the build succeeds. Diagnostics should be read-only and version-appropriate.
Diagnostic commands after config change
# 1. Start the dev server with logging
expo start --clear
# Expected: QR code appears; press 'a' to open on Android, 'i' for iOS
# 2. Run Expo doctor to find dependency and config issues
npx expo-doctor
# Expected: "Didn't find any issues with the project!" or list of warnings/errors
# 3. Check for config schema errors by prebuilding native projects in a temp directory
npx expo prebuild --no-install --platform android
# Expected: android/ directory generated without errors; do not commit if you use CNG
# 4. Validate EAS build profile and compute credentials
eas build --platform android --profile preview --non-interactive --no-wait
# Expected: Build queued. Check status with `eas build:list`
Interpreting failure signals
If npx expo-doctor reports mismatched versions, update packages using npx expo install --fix and rerun. If expo prebuild fails, check that your app.config.js returns a valid config object and that all referenced files exist.
Example failure: missing icon path
# app.json includes "icon": "./assets/missing-icon.png"
npx expo config --type public
# Warning: Could not find file: ./assets/missing-icon.png
Fix: either add the file or remove the icon field. Then re-run verification.
Expected outputs summary
| Command | Expected Exit Code | Key Output |
|---|---|---|
npx expo --version | 0 | Version number |
npx expo config --type public | 0 | JSON with expo.name, expo.slug |
npx expo-doctor | 0 if no issues | "Didn't find any issues" |
npx expo prebuild --no-install | 0 if success | Generated native directories |
eas build --profile preview --no-wait | 0 if queued | Build URL |
Failure Modes and Recovery
This section covers the most common Expo configuration failure modes, how to detect them, and step-by-step recovery.
Failure mode 1: EAS Build fails due to invalid app.json
Symptom: eas build fails immediately with a JSON parse error or schema validation error.
Detection:
eas build --platform android --profile preview
# Output: "Error: Invalid app.json: ..." or "app.json: Unexpected token"
Recovery:
- Validate JSON syntax:
node -e "JSON.parse(require('fs').readFileSync('app.json','utf8')); console.log('Valid JSON')"
# Expected: Valid JSON
- Check schema using
npx expo config:
npx expo config --type public
# If error, fix the field mentioned.
- Re-run the build. If using
app.config.js, add atry-catchto log errors.
Failure mode 2: Missing environment variable in EAS Build
Symptom: Build succeeds but the app shows placeholder text or API calls fail with 401.
Detection: Inspect the build logs for the warning printed by app.config.js (MYAPP_API_KEY is not set).
Recovery:
eas secret:list --scope project
# Check if MYAPP_API_KEY is present
eas secret:create --name MYAPP_API_KEY --value "correct-key" --scope project
# Rebuild:
eas build --platform android --profile production --no-wait
Verify in the running app that the correct key is used. For non-production environments, use a placeholder.
Failure mode 3: Rollback of a broken update
You released an OTA update (via eas update) that crashes on startup. You need to roll back to a previous known-good update.
Detection: Users report crashes; you can check the update history:
eas update:list --branch production --limit 10
# Note the update ID of the last known-good version
Recovery: Republish the known-good commit to the same branch:
git checkout <known-good-commit-hash>
eas update --branch production --message "Rollback to stable"
# Expected: Update published to branch production
Or point the branch to a previous update using the dashboard. After rollback, tell users to force-close and reopen the app.
Failure mode 4: Config drift between environments
Your eas.json has different extra for preview and production, and a value is missing in production.
Detection: Compare resolved configs for different profiles:
eas config --profile preview --json | jq '.extra'
eas config --profile production --json | jq '.extra'
Recovery: Define environment-specific values in eas.json or in CI/CD variables. Update the missing value and re-run the production build.
Operations Checklist
Use this checklist before and after changing any Expo configuration. Replace placeholders with your actual values.
Pre-change
- [ ] Capture current state: run
npx expo config --type public > config-before.jsonand timestamp it. - [ ] Record Expo SDK and EAS CLI versions:
npx expo --versionandeas --version. - [ ] Ensure working tree clean:
git status --porcelainreturns empty. - [ ] Create a feature branch:
git checkout -b config-change-<date>. - [ ] Identify blast radius: list EAS profiles that will be affected (
eas build:listor inspecteas.json). - [ ] Define expected success and failure signals (e.g., build queued, config resolves without warnings).
During change
- [ ] Make one scoped change at a time (e.g., update slug, add extra, change icon).
- [ ] Use placeholders instead of secrets in files; set real values via environment or EAS secrets.
- [ ] Run
npx expo config --type publicafter the change and diff againstconfig-before.jsonto confirm only intended changes. - [ ] If using
app.config.js, add logging for missing environment variables (as shown earlier).
Post-change verification
- [ ] Run
npx expo-doctorand resolve all errors. - [ ] Start dev server and load app on one device (
expo start --clear). - [ ] Trigger a preview build:
eas build --platform android --profile preview --no-wait. - [ ] Check build logs for warnings about missing files or invalid fields.
- [ ] If build succeeds, install on a test device and verify the changed behavior.
Rollback preparation
- [ ] Know the last known-good commit hash:
git log -1 --format=%Hbefore change. - [ ] Document the exact revert command:
git revert <commit-hash>. - [ ] For production updates, note the last stable update ID using
eas update:list --branch production. - [ ] Test rollback in a staging environment before an incident forces it.
Example filled checklist entry
- Pre-change snapshot:
config-before-2025-03-10T18-30.jsonsaved by Priya Shah, Engineering Lead. - Change: moved
apiKeyfromapp.jsontoapp.config.jsusingMYAPP_API_KEYenvironment variable. - Verification:
npx expo config --json | jq -r '.extra.apiKey'returnsplaceholder-key-for-development-onlywhen env var unset; returns actual value when set locally. - Build verification:
eas build --platform android --profile previewsucceeded, build ID12345abc-6789-def0-1234-56789abcdef0. - Rollback:
git revert 9f8e7d6c5b4a3210and rebuild if necessary.
Conclusion
Expo configuration mistakes are avoidable when every recommendation is version-scoped, observable, and reversible. Copying a command without checking prerequisites and expected output is not an operations procedure. The examples in this guide use explicit placeholders, read-only observations, and minimal interventions with tested recovery paths.
As a next step, choose one low-risk verification for your Expo configuration: run npx expo config --type public, compare the output with your expected state, and run npx expo-doctor. Review dependencies such as React Native version compatibility, Android build tools, and CI/CD variables.
A reliable technical workflow makes failure visible, protects sensitive values, limits changes to the intended resource, and defines recovery verification before an incident forces the decision.