NewiPhone Duo, live in your browser. Try it →
Vibeview
Pricing
Cloud Builds · Expo

Build an Expo app without EAS

Rokas Kašinskas — Co-founder, VibeViewPublished 2026-10-07Updated 2026-10-07

EAS Build is Expo’s own hosted build service and the path Expo’s documentation describes, and for many teams it is the right choice. But nothing about a managed Expo project requires it. Prebuild generates ordinary Xcode and Gradle projects, and any machine with the toolchain can build them. This guide shows the same project built on VibeView cloud builds instead: a simulator build you can open on a cloud iPhone or Android emulator, then signed iOS and Android builds sent to both stores.

For a side-by-side of the two services, plans and credential handling included, each claim sourced from Expo’s own pages, see EAS Build alternative: VibeView vs Expo EAS. The rest of this page is the how-to.

What you need

  • A VibeView account and the CLI: npm install -g vibeview && vibeview login
  • A managed Expo project, with ios/ and android/ in .gitignore (prebuild recreates them on every build)
  • For the stores: an Apple Developer account and a Google Play Console account

Every plan includes cloud builds each month; builds beyond your plan’s allowance draw from usage credit. The pricing page has the numbers. A complete, working example of everything below is Kitlist, a small Expo app that was built and shipped entirely through cloud builds.

Step 1: simulator builds, with prebuild in the command

Run a cloud build from the project root:

vibeview build --cloud --platform ios

For a managed project the CLI suggests a command that installs dependencies, runs npx expo prebuild, then drives xcodebuild (iOS) or Gradle (Android) directly. It reads the workspace and scheme name from the name in app.json. If your project uses a dynamic app.config.js or app.config.ts, it can’t read that name without running your code, so it asks you to write the command instead. Accept the suggestion and vibeview.json looks like this:

{
  "defaultPlatform": "ios",
  "platforms": {
    "ios": {
      "build": {
        "command": "npm ci && npx expo prebuild --platform ios && xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build CODE_SIGNING_ALLOWED=NO",
        "artifact": "ios/build/Build/Products/Debug-iphonesimulator/*.app"
      }
    },
    "android": {
      "build": {
        "command": "npm ci && npx expo prebuild --platform android --no-install && cd android && ./gradlew assembleDebug",
        "artifact": "android/app/build/outputs/apk/debug/*.apk"
      }
    }
  }
}

Don’t use npx expo run:ios or npx expo run:android as a build command. They build and install onto a local simulator or connected device, so on a build machine they fail before compiling anything.

vibeview build --cloud --platform all builds both. The first succeeded build creates the app in VibeView from the binary (name, bundle id and icon included) and saves its appId to vibeview.json. Choose Run this build on the build’s page to open it on a cloud simulator, or run vibeview dev to work against it with Metro and Fast Refresh. A debug build that embeds its JavaScript bundle, which is what you get without expo-dev-client, still loads from your Metro whenever Metro is running.

Environment values work as you’d expect: plain ones such as APP_VARIANT go in a build.env map in vibeview.json, and tokens or other private values go in build variables and secrets, never in the command.

Step 2: signing a project that has no native folders

A managed project has no android/app/build.gradle or Xcode project to hold signing settings, so both go in config plugins that patch the generated files during prebuild.

Android. Use withAppBuildGradle from expo/config-plugins to append VibeView’s guarded signingConfigs.release block (it’s in App Signing) to the generated Gradle file. The block reads four properties VibeView sets only during a production build, so every other build ignores it. Then get a keystore:

vibeview credentials generate --platform android --name release

iOS. Store an App Store Connect API key once, then generate the certificate and profile through Apple’s API. No Apple ID password, no Keychain:

vibeview credentials asc-key add --key-id <KEY_ID> --issuer-id <ISSUER_ID> --file AuthKey_<KEY_ID>.p8
vibeview credentials generate --platform ios --name release --app <app-id>

Expo’s iOS project uses CocoaPods, and a provisioning profile passed on the xcodebuild command line applies to every Pod target and fails the archive. So the second plugin sets manual signing, the team and the profile on the app target only, and the archive command passes nothing but the signing identity. Kitlist’s plugins/ folder has both plugins, ready to copy.

Step 3: production builds with automatic build numbers

Add the release commands and turn on autoIncrement for each platform:

{
  "platforms": {
    "ios": {
      "appId": "my-app-abc123",
      "build": {
        "command": "...",
        "artifact": "...",
        "productionCommand": "npm ci && npx expo prebuild --platform ios && xcodebuild archive -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Release -archivePath ios/build/MyApp.xcarchive ${VIBEVIEW_CODE_SIGN_IDENTITY:+\"CODE_SIGN_IDENTITY=$VIBEVIEW_CODE_SIGN_IDENTITY\"} && xcodebuild -exportArchive -archivePath ios/build/MyApp.xcarchive -exportPath ios/build -exportOptionsPlist \"$VIBEVIEW_EXPORT_OPTIONS_PLIST\"",
        "productionArtifact": "ios/build/*.ipa",
        "autoIncrement": true
      }
    },
    "android": {
      "appId": "my-app-def456",
      "build": {
        "command": "...",
        "artifact": "...",
        "productionCommand": "npm ci && npx expo prebuild --platform android --no-install && cd android && ./gradlew bundleRelease",
        "productionArtifact": "android/app/build/outputs/bundle/release/*.aab",
        "autoIncrement": true
      }
    }
  }
}

With autoIncrement on, every production build gets the next number from a counter VibeView keeps per app and platform, and receives it as the VIBEVIEW_BUILD_NUMBER environment variable. Add an app.config.js (it can extend your existing app.json) and map that variable to ios.buildNumber, and the same value parsed as an integer to android.versionCode. The Cloud Builds docs have the two-line snippet. When an App Store Connect key is stored, the iOS counter starts above the builds already in App Store Connect; for an app already on Play, seed the Android counter once with vibeview versions set --app <app-id> --platform android <number>.

Note that adding an app.config.js means the CLI can no longer read your app name for its suggested commands. That only matters for new platforms; the commands already in vibeview.json keep working.

Then build both:

vibeview build --cloud --production --platform all

The signed .ipa and .aab appear on the Builds page with a Download action. They can’t run on a simulator, so test with the Step 1 builds.

Step 4: submit to TestFlight and Google Play

Create the app record once in each store: App Store Connect (My Apps → + → New App) and Play Console (All apps → Create app). For Play, also store a service account key with vibeview credentials play-key add --file service-account.json (Store Submission lists the console steps). Check both stores before you build, then submit:

vibeview submit --check --platform ios
vibeview submit --check --platform android
vibeview submit --platform ios --latest
vibeview submit --platform android --latest --track internal

Inside a project whose vibeview.json links the apps, --check looks up the bundle id and package name on its own. The iOS build reaches TestFlight after Apple’s processing, usually 10 to 15 minutes; the Android release is live on the internal track when the command returns. Both uploads are the binary only: listings, screenshots and review stay in the store consoles.

Moving an existing EAS project

You can keep EAS and VibeView side by side while you try this, since cloud builds don’t read eas.json. Things to carry over by hand:

  • Build profiles. An EAS profile’s environment variables become build.env entries (plain values) or build variables and secrets (private ones).
  • Credentials. EAS-managed credentials stay where they are. Either generate new ones in VibeView, or upload copies of the files you already have with vibeview credentials add. For an app already on Play, upload the same upload key your listing already trusts rather than generating a new one.
  • Build numbers. Seed VibeView’s counter above your last store build with vibeview versions set, unless an App Store Connect key will find it for you on iOS.

Frequently asked questions

Frequently asked questions

Can you build an Expo app without EAS?

Yes. npx expo prebuild turns a managed project into standard Xcode and Gradle projects, which any macOS or Android build environment can compile. VibeView runs prebuild as part of the cloud build command, so nothing native has to live in your repository.

Do I need a Mac?

No. iOS builds run on VibeView’s macOS builders and the TestFlight upload runs server-side, so Windows and Linux work for the whole flow.

Does this work with expo-dev-client?

Yes. A development build with expo-dev-client is a normal debug build: build it in the cloud, run it on a cloud simulator, and connect with vibeview dev. Projects without expo-dev-client work too.

Is VibeView an EAS Build replacement?

For the build and store-submission steps, yes. VibeView also runs the result on cloud iPhone, Android, Apple TV and Android TV devices. The EAS Build alternative page compares the two in detail, including where EAS is the better pick.

Next steps

Try it yourself

Open a live iOS simulator or Android emulator in your browser.

Start free