Table: moving an Expo app's builds from EAS to aipps.dev. eas build becomes aipps build, eas.json build profiles become aipps.json (aipps init), eas credentials becomes aipps credentials set, eas submit becomes aipps build --submit. 60 iOS + 60 Android builds a month: $154 on EAS Starter, $3 on aipps.dev, 51x less

You have an Expo app, an eas.json, and a monthly EAS bill that grows with every preview build. This guide moves the builds to aipps.dev without touching the app itself: same bundle ID and package name, same signing key, same store listings, and EAS Update can stay where it is. It takes about an hour, most of it waiting for the first two builds. At the end there is a cost table for five team sizes, worked from both price lists.

Disclosure: aipps.dev is our product. Expo's prices are from expo.dev/pricing and ours from aipps.dev/pricing, both read on 28 October 2026; the side-by-side of the two services is in Expo EAS Build vs aipps.dev in 2026.

What moves and what stays

text
moves to aipps.dev                    stays as it is
eas build (all profiles)              app code, app.json / app.config.js
Android upload keystore (a copy)      bundle ID, package name, store listings
eas submit (Play tracks, TestFlight)  EAS Update, if you use it
build numbers (aipps sets them)       your Expo account (nothing to delete)

Nothing is cancelled on the Expo side until you decide to. A sensible order is to run both for a week: keep production releases on EAS, send preview builds through aipps.dev, then move the store builds once the first TestFlight and Play uploads have gone through.

Step 1: install the command line and run aipps init

You need Node.js 20 or newer. Make an API key in the aipps.dev console under Keys, then in the project folder:

bash
npm i -g aipps
aipps login            # paste the API key
cd my-expo-app
aipps init             # reads app.json, package.json and eas.json, writes aipps.json, adds the app

aipps init sees expo in package.json and saves the app as an Expo project, takes the bundle ID and package name from app.json, and copies your eas.json build profiles. Commit aipps.json next to eas.json.

The aipps.dev docs: Quick start with npm i -g aipps, aipps login, aipps init and aipps build -p android, then an example aipps.json with app, framework, android package, iOS bundle identifier and development, preview and production profiles, and the note that profiles from eas.json are carried over
Quick start and aipps.json in the aipps.dev docs, 28 October 2026.

What happens to eas.json

Here is a typical eas.json and the aipps.json that aipps init writes from it. From each profile it keeps distribution, developmentClient, env and channel; the development, preview and production profiles always exist.

json
// eas.json
{
  "build": {
    "development": { "developmentClient": true, "distribution": "internal" },
    "preview": { "distribution": "internal", "channel": "preview",
                 "env": { "EXPO_PUBLIC_API_URL": "https://staging.example.com" } },
    "production": { "autoIncrement": true, "channel": "production",
                    "env": { "EXPO_PUBLIC_API_URL": "https://api.example.com" } }
  }
}

// aipps.json (written by aipps init)
{
  "app": "my-expo-app",
  "name": "My Expo App",
  "framework": "expo",
  "ios": { "bundleIdentifier": "com.example.myapp" },
  "android": { "package": "com.example.myapp" },
  "profiles": {
    "development": { "developmentClient": true, "distribution": "internal" },
    "preview": { "distribution": "internal", "channel": "preview",
                 "env": { "EXPO_PUBLIC_API_URL": "https://staging.example.com" } },
    "production": { "distribution": "store", "channel": "production",
                    "env": { "EXPO_PUBLIC_API_URL": "https://api.example.com" } }
  }
}

The eas.json settings that work differently

text
eas.json                     on aipps.dev
autoIncrement, appVersion    not needed: every build gets a build number past the
  Source remote              highest one the stores already have
android.buildType            production -> .aab for Play; other profiles -> APK
ios.simulator                with an App Store Connect key, preview is an ad hoc .ipa
                             and production an App Store .ipa; without one, simulator
resourceClass, image, node   not used: Android in a fresh container (JDK 17, Node 22,
                             SDK 35/36, NDK 27), iPhone on our Mac mini (Xcode 26+)
extends                      not followed: copy the fields into each profile
env, EAS environment vars    copied into aipps.json but not set during the build yet;
                             use app.config.js (below)
channel                      copied but not written into the build; set the update
                             channel in app config (below)
submit section               replaced by --submit and your store keys

Two rules decide what kind of build you get. The profile named production is the store build (.aab for Google Play, App Store .ipa); every other profile, including your own names such as staging, makes an installable test build. And development builds use the Debug configuration, as they do on EAS.

Environment values need one change. aipps.dev never uploads .env files (nor keystores or certificates), and it doesn't set the env block of a profile during the build yet. What it does set is PROFILE, the name of the profile being built, so an app.config.js can pick per-profile values itself:

javascript
// app.config.js
const API_URL = {
  preview: 'https://staging.example.com',
  production: 'https://api.example.com',
};
const profile = process.env.PROFILE || process.env.EAS_BUILD_PROFILE || 'development';

export default ({ config }) => ({
  ...config,
  extra: { ...config.extra, apiUrl: API_URL[profile] ?? API_URL.preview },
  updates: {
    ...config.updates,
    // EAS Build writes the channel into the app; builds made elsewhere send it as a header
    requestHeaders: { 'expo-channel-name': profile === 'production' ? 'production' : 'preview' },
  },
});

In the app, read the value with Constants.expoConfig.extra.apiUrl from expo-constants. Reading EAS_BUILD_PROFILE as well keeps the same file working on EAS while you run both. Only values that are public anyway belong here: anything in a mobile build can be read out of it, on EAS too. If you use EAS Update, the updates.requestHeaders line is the part that matters: it is how an app that wasn't built by EAS Build tells the update server which channel it follows. Keep runtimeVersion and updates.url as eas update:configure set them.

Step 2: bring your signing keys

Android: take your keystore from EAS

This is the one step not to skip. Google Play knows your app by its upload key; a build signed with a different key is refused. If EAS made the keystore for you, download it: run eas credentials -p android, pick the production profile, then the keystore, and choose the download option. You get the .jks file and EAS shows the key alias and both passwords. Then store it on aipps.dev:

bash
aipps credentials set android upload.jks --alias <key alias>
# asks for the keystore and key passwords (or reads AIPPS_KEYSTORE_PASSWORD and AIPPS_KEY_PASSWORD)
aipps credentials          # lists what is stored, never the secrets

Keep your own copy of the .jks somewhere safe as well. aipps credentials keystore -o upload.jks downloads it again from aipps.dev, so you are not locked in the other way either. A new app with no keystore gets one on its first Android release build; download that one too.

iPhone: an App Store Connect API key

aipps.dev signs iPhone builds with an App Store Connect API key you make, never with your Apple ID password. In App Store Connect, open Users and Access, then Integrations, App Store Connect API, and make a team key with the Admin role (it registers test phones and makes certificates and profiles). Download the AuthKey_XXXX.p8 file (Apple lets you download it once) and note the Issuer ID and your Team ID. If you already gave EAS an API key for submissions and it has that role, you can reuse it.

bash
aipps credentials set ios AuthKey_ABC123DEF4.p8 --issuer-id <issuer id> --team-id <team id>

Signing certificates and provisioning profiles that EAS made stay in your Apple account; aipps.dev makes its own through the key and doesn't need them. Test phones you registered with EAS are already in your Apple team. To add one, aipps devices invite prints a link the tester opens in Safari on the iPhone, and the next build includes it.

Google Play uploads: reuse your EAS Submit service account

If you used eas submit for Android, you already have a Google service account JSON key with release rights in the Play Console. The same file works here:

bash
aipps credentials set play service-account.json

Step 3: the first builds

bash
aipps build -p android                         # preview profile: an APK with an install page and QR code
aipps build -p ios                             # preview: ad hoc .ipa for registered phones
aipps build -p android --profile production    # .aab signed with your upload key
aipps builds                                   # this app's recent builds
aipps logs <id> -f                             # follow a build log

The project is uploaded much as EAS does it: what git tracks, plus untracked files that aren't ignored. The log streams to your terminal and the build ends with a link to its install page. For a sense of timing, the median Android preview build on aipps.dev from 23 to 28 October 2026 took 14.9 minutes and the median Android production .aab 17.0 minutes, start to finish, for small to mid-size Expo and React Native apps.

Before moving the store builds, check one thing: the production .aab must be signed with the upload key Play expects. Play Console shows that key's SHA-1 fingerprint under Test and release, App integrity; compare it with keytool -list -v -keystore upload.jks. If they match, Play accepts the bundle in the next step.

Step 4: store releases

bash
aipps build -p android --submit            # Play internal track (or: --submit alpha | beta | production)
aipps build -p ios --submit                # App Store build, uploaded to TestFlight

--submit implies the production profile and uses the app's own store keys from step 2. Build numbers go past whatever the store already has, so the first aipps.dev upload after a run of EAS builds doesn't collide with them, and you can drop autoIncrement. From TestFlight or the Play track, the release goes on exactly as before: phased release, review, staged rollout.

Step 5: CI

If eas build runs in your CI, swap the command and the token. The aipps command line reads AIPPS_API_KEY from the environment, so there is no login step. A GitHub Actions job that sends every merge to main to the Play internal track and TestFlight:

yaml
on:
  push:
    branches: [main]
jobs:
  release:
    runs-on: ubuntu-latest
    env:
      AIPPS_API_KEY: ${{ secrets.AIPPS_API_KEY }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npm i -g aipps
      - run: aipps build -p android --submit internal
      - run: aipps build -p ios --submit

The CI job only uploads the project and follows the build, so the cheapest Linux runner is enough even for the iPhone build; add --no-wait to return at once and let the build finish on its own.

What it costs: EAS against aipps.dev

Builds split evenly between iPhone and Android, medium EAS workers ($2 an iOS build, $1 an Android build, the plan's credit taken off), 22 working days a month, and builds that count as one on aipps.dev (up to 20 minutes and 150 MB). aipps.dev gives 3 builds a day free and each $3 pack adds 10 a day, so the number of packs depends on your busiest day, not your average. The table shows both: packs for the average day and packs for a day twice as busy.

text
builds a month    EAS (Starter or    per working   aipps.dev       aipps.dev, a day   how many
(iOS + Android)   Production)        day           average day     twice as busy      times less
15 + 15           $0 (Free)          1.4           $0              $0                 both free
30 + 30           $64                2.7           $0              $3 (1 pack)        21x or more
60 + 60           $154               5.5           $3 (1 pack)     $3 (1 pack)        51x
150 + 150         $424               13.6          $6 (2 packs)    $9 (3 packs)       47-71x
300 + 300         $874               27.3          $9 (3 packs)    $18 (6 packs)      49-97x

EAS:   Starter $19 incl. $45 credit: 30 + 30 = $19 + (30 x $2 + 30 x $1) - $45 = $64
       150 + 150 = $19 + ($300 + $150) - $45 = $424 (Production: $199 + $450 - $225, the same)
aipps: 3 free a day + 10 per $3 pack: 1 pack = 13 a day, 2 = 23, 3 = 33, 6 = 63

So a team that moves its builds pays between 21 and 97 times less, and the gap widens with volume because the EAS bill grows with every build while ours grows in steps of 10 a day. Longer builds narrow it: a build of 20 to 60 minutes, or with an app file over 150 MB, counts as three, and builds over 60 minutes need the Large add-on ($5 a month per app, up to 120 minutes and 2 GB). If your builds take 30 minutes, the 60 + 60 team needs about 16 build counts a day, 2 packs ($6) instead of 1, which is still 26 times less than $154. An app's first build on each platform counts as one at most, because it fills empty caches.

What you give up

Be clear about this before cancelling anything. On EAS Production, ten iOS builds run at once; aipps.dev runs one build at a time, two from the second pack, so a burst of builds waits in line. EAS Workflows has no counterpart; you run aipps build from the CI you already have. EAS can sign in to your Apple account and create everything for you; aipps.dev needs the API key from step 2. And per-profile environment values go through app.config.js until aipps.dev sets a profile's env during the build. If those matter more than the bill, keep EAS for the store builds and move only the preview builds, which are usually most of the count.

Move your Expo app's builds: aipps init reads your eas.json, and every build gets an install page with a QR code.

Open the console

Sources: expo.dev/pricing (plans, build credit, per-build prices, concurrency), read 28 October 2026; aipps.dev/pricing and aipps.dev/docs, same day; the aipps command line (npm package aipps) for how aipps init carries eas.json over; build times from the aipps.dev build records, 23 to 28 October 2026.