How to Use EAS Build Profiles Correctly

EAS Build profiles explained: development, preview, production. The env var story, the submit story, and the mistakes I stopped making.

Learn/How to Use EAS Build Profiles Correctly
intermediate45 minutes

How to Use EAS Build Profiles Correctly

EAS Build profiles explained end-to-end: dev, preview, production, env vars, autoIncrement, submit config, and the mistakes I stopped making.

Prerequisites

Expo projectEAS account

Why profiles matter

If you only ever run eas build with no profile, you will eventually ship a dev build to TestFlight or wonder why your production env vars are not set. Profiles are the config layer that makes this safe.

The three profiles I always have

  • development: For running on device with expo-dev-client. Debug JS, dev menu, hot reload.
  • preview: For internal testing via TestFlight and Play internal. Production binary, staging env vars.
  • production: For store release. Production binary, production env vars, bumped versions.

eas.json I actually ship

{
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "env": { "EXPO_PUBLIC_API_URL": "http://localhost:3000" }
    },
    "preview": {
      "distribution": "internal",
      "env": { "EXPO_PUBLIC_API_URL": "https://staging.api.yourapp.com" },
      "channel": "preview"
    },
    "production": {
      "autoIncrement": true,
      "env": { "EXPO_PUBLIC_API_URL": "https://api.yourapp.com" },
      "channel": "production"
    }
  }
}

autoIncrement on production is worth the toggle

Set autoIncrement: true on the production profile. EAS bumps buildNumber (iOS) and versionCode (Android) automatically every build. If you forget to bump manually, App Store Connect rejects the upload. This is the single line that prevents that.

Env vars that work at build time vs runtime

  • EXPO_PUBLIC_* env vars are baked into the JS bundle at build time. Change them in eas.json.
  • Runtime env vars need to come from a server fetch. Do not try to read process.env in production for anything except EXPO_PUBLIC_*.

Submit config so you do not paste JSON into App Store Connect

Add a submit block to eas.json:

{
  "submit": {
    "production": {
      "ios": {
        "appleId": "you@example.com",
        "ascAppId": "1234567890",
        "appleTeamId": "XXXXXXXXXX"
      },
      "android": {
        "serviceAccountKeyPath": "./google-service-account.json",
        "track": "internal"
      }
    }
  }
}

Now eas submit -p ios --profile production does the upload. No manual drag into Transporter.

Mistakes I stopped making

  • Forgetting channel: Without it, OTA updates from expo-updates go to a default channel and mix preview with production. Set the channel explicitly on every profile.
  • Same bundle id across profiles: If dev and prod share a bundle id, installing one deletes the other. Use a suffix on development (com.yourapp.dev).
  • Secrets in eas.json: Never commit secrets. Use eas secret:create for anything sensitive.

What the boilerplate gives you

AI App Factory ships eas.json with all three profiles, separated bundle ids for dev, channel config, and a pre-wired submit block. You edit domain and team id.

See pricing

Or let AI App Factory handle this for you.

Everything in this guide is already pre-configured in AI App Factory. 11 AI agents automate the rest.

AI App FactoryLearnHow to Use EAS Build Profiles Correctly