Shipping iOS apps without a Mac
How I deployed VITAL to TestFlight using only Expo, EAS Build, and a Chromebook. The exact stack, exact config, exact gotchas.
I shipped a full iOS app to TestFlight last month. I do not own a Mac.
The whole pipeline ran from a $300 Chromebook, in a browser tab, with no Xcode, no simulator, no .xcworkspace file. The user-facing result was an iOS app indistinguishable from one built in Xcode by an Apple developer.
Here's exactly how.
The stack
- Expo (React Native) for the app itself
- EAS Build (Expo's hosted builder) for the actual
.ipacompilation - EAS Submit to upload the build to App Store Connect / TestFlight
- An Apple Developer account ($99/year)
That's it. No local build tools. No fastlane. No Mac.
The 6-step pipeline
# 1. Init the project (or open an existing one)
npx create-expo-app vital
cd vital
# 2. Log in to Expo
npx expo login
# 3. Configure EAS
npx eas build:configure
# This creates eas.json with build profiles
# 4. Run a development build (lives on your phone, hot-reloads from your laptop)
npx eas build --platform ios --profile development
# 5. Run a production build (a real, signed .ipa for TestFlight)
npx eas build --platform ios --profile production
# 6. Submit it
npx eas submit --platform ios --latest
Each eas build command queues your build on Expo's macOS workers. You watch logs in the browser. ~15 minutes later, you have a signed .ipa. EAS handles all the certificate/provisioning-profile dance for you.
The eas.json that worked for VITAL
{
"cli": { "version": ">= 5.0.0" },
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": { "simulator": false }
},
"preview": {
"distribution": "internal",
"ios": { "resourceClass": "m-medium" }
},
"production": {
"ios": { "resourceClass": "m-medium" },
"autoIncrement": true
}
},
"submit": {
"production": {
"ios": {
"appleId": "you@example.com",
"ascAppId": "1234567890",
"appleTeamId": "ABCDEFG123"
}
}
}
}
The ascAppId you get after you create the app shell in App Store Connect (the only mandatory web-only step). Everything else gets generated by EAS the first time you run a build.
What surprised me
1. EAS handles certificates better than I would have.
When you run a production build for the first time, EAS asks if it can manage your Apple certificates. Say yes. It logs into your Apple Developer account, generates the right certs, stores them encrypted, and rotates them when they expire without you doing anything. I have not thought about a .p12 file in 18 months.
2. The simulator is kind of optional.
I used to think you needed a simulator to develop iOS apps. You don't. Expo Go on a real iPhone gives you hot-reload from your dev machine over Wi-Fi. The "simulator" was always just a Mac thing — real-device development is faster anyway.
3. App Store Connect is the slowest part.
Apple's review queue ranges from 12 hours to 5 days. Your code being ready isn't the bottleneck — getting Apple to approve a TestFlight build is. Submit early. Even a working "Hello World" build that gets approved early means subsequent builds are fast-tracked through review.
Gotchas I hit
Gotcha #1: Push notifications. APNs (Apple Push Notification service) requires a .p8 key from your Apple Developer account. You upload it to Expo. Expo handles the rest. But you cannot test push notifications in Expo Go — you need an actual development build (step 4 above) for that.
Gotcha #2: Native modules. If your app needs a native iOS module that isn't already in the Expo SDK, you need to write a config plugin. This is the one place where not having a Mac hurts — debugging native modules without local builds is slow. For VITAL, I avoided this by sticking to Expo SDK modules and by using expo-camera instead of a custom react-native-vision-camera config.
Gotcha #3: Bundle identifier collisions. Your bundle ID has to be globally unique on Apple's side. The first time I set up VITAL, I tried com.yiplabs.vital and Apple said "taken." (Probably by an old test of mine.) I switched to com.yiplabs.vitalapp and moved on. Just pick something boring and unique on attempt one.
Gotcha #4: The first build is slower. EAS caches your dependencies and certs after the first run. Build #1 is ~25 minutes. Build #2 onward is ~12 minutes. Don't panic on the first one.
Cost reality check
- Expo EAS: Free tier gets you 30 builds/month. If you're an active solo dev shipping multiple builds a day, you'll need the $19/month plan.
- Apple Developer: $99/year, mandatory.
- App Store Connect: free.
- Total to ship a real iOS app: $99 + maybe $19/mo of EAS = ~$300 first year.
For comparison, a M2 MacBook Air is €1,200. The Chromebook I built VITAL on cost €280. The math made itself.
Should you actually do this?
If you're a solo dev who only ships an iOS app every six months, yes. The Mac was bottlenecking you on a single task.
If you build iOS apps full-time and you also need to write SwiftUI components, no, get a Mac. You'd hit the limits of Expo + EAS pretty fast.
If you're a JS dev who's been "thinking about getting into mobile" but kept waiting for the right time — there's no right time. There's just an npx create-expo-app and a TestFlight build 90 minutes later.
I built VITAL solo, on a Chromebook, in 4 weeks. If I'd waited until I owned a Mac, I'd still be waiting.
✦ Keep reading
More in this category →Got an idea you want to build?
Hire me →