●ACQUISITION — Rork makes its first acquisition, buying Paperline, a macOS app that generates native Swift apps with AI●FUNDING — The $15M seed led by Left Lane Capital backs Rork's push to redefine how mobile apps are built and monetized●GROWTH — Rork Max reportedly hit $1.5M ARR within three days of launch and doubled annual revenue in two weeks●ENGINE — Rork Max runs on Claude Code and Claude Opus 4.6, the first web Swift builder aiming to replace Xcode●SPLIT — Standard Rork uses React Native (Expo); Rork Max generates native Swift across the whole Apple ecosystem●PRICING — Start free; paid plans begin at $25/month, with Rork Max at $200/month●ACQUISITION — Rork makes its first acquisition, buying Paperline, a macOS app that generates native Swift apps with AI●FUNDING — The $15M seed led by Left Lane Capital backs Rork's push to redefine how mobile apps are built and monetized●GROWTH — Rork Max reportedly hit $1.5M ARR within three days of launch and doubled annual revenue in two weeks●ENGINE — Rork Max runs on Claude Code and Claude Opus 4.6, the first web Swift builder aiming to replace Xcode●SPLIT — Standard Rork uses React Native (Expo); Rork Max generates native Swift across the whole Apple ecosystem●PRICING — Start free; paid plans begin at $25/month, with Rork Max at $200/month
Three Builds on One iPhone: Environment Separation for Rork (Expo) Apps
Split a Rork-generated Expo app into dev, staging, and production builds that live side by side on one device. A hands-on walkthrough of dynamic app.config.ts, eas.json profiles, and isolating notifications, analytics, and billing per environment.
One morning I tried a pre-release fix by installing a test build on my own iPhone, and the icon for the app I use every day quietly disappeared. The bundle ID was identical, so the test build overwrote my production install. Re-login, restoring purchases, re-granting notification permission. I lost maybe ten minutes, but the constraint that followed — "I can't casually test on my daily-driver phone" — kept shaving away at my development speed.
As an indie developer running several apps in parallel, that friction maps directly onto productivity. This article lays out an environment-separation setup that splits a Rork-generated Expo app into dev, staging, and production builds that coexist on the same device, based on the exact configuration I run across my own wallpaper apps at Dolice.
The setup at a glance
Before the concrete config, it helps to see that there are only three things to do.
Make `app.config.ts` dynamic via `APP_VARIANT`, giving each environment its own bundle ID and app name
Add development / preview / production profiles to `eas.json`, pinning `APP_VARIANT` in each
Swap external service credentials — notifications, analytics, billing — to a separate set per environment
With those three in place, the same source code yields three distinct app identities that coexist on one device.
Why a single bundle ID eventually traps you
The code Rork first emits usually has just one bundle ID (bundleIdentifier on iOS, package on Android). That is fine for a prototype, but after launch it breaks in three predictable ways.
You can't keep a production app and a test app on the same device, because the OS identifies installs by bundle ID and overwrites matching ones. EAS Update (OTA) channels get crossed, so a JS bundle you meant for testing can reach production users. And your analytics, billing, and notification data get contaminated, mixing your debugging into real DAU and conversion numbers until they're unreadable.
The fix is simple in principle: split into dev (local development), staging (pre-release verification), and production, and give each its own bundle ID, app name, and service credentials.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Switching app.config.ts dynamically with APP_VARIANT to give each environment its own bundle ID and name
✦Organizing eas.json into development / preview / production profiles so all three coexist on one device
✦Isolating push tokens, analytics, and RevenueCat per environment so testing never pollutes production data
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Keep slug identical across all three environments. slug is the EAS project identifier, so splitting it makes EAS treat each variant as a separate project, fracturing your build history and secrets. What you split is the bundle ID and the display name — not the project itself.
Icons and names you can tell apart on the home screen
Three identical-looking icons on one device invite mistakes. I start from the production icon and overlay a red band for dev and a yellow band for staging, and append (Dev) and (Stg) to the names. That alone makes them instantly distinguishable on the home screen and in Spotlight.
It's a small thing, but that "tell at a glance" state all but eliminates the human error of operating dev while thinking it's production, or the reverse.
Organizing eas.json into three build profiles
Feeding APP_VARIANT at build time is eas.json's job. Pin the environment variable per profile.
Here is how the three environments divide the work.
Environment
Purpose
Bundle ID
Distribution
Update channel
development
Local dev / dev client
...myapp.dev
internal
(no OTA)
preview
On-device check / TestFlight
...myapp.stg
internal
preview
production
Store release
...myapp
store
production
You build by naming a profile, e.g. eas build --profile preview. There's no risk of getting APP_VARIANT wrong because CI pins it to the profile.
Isolating notifications, analytics, and billing per environment
Even with split bundle IDs, data contamination continues if the app points at the same service keys as production. Use extra.variant as the anchor for one small map that swaps service config per environment.
From experience, I strongly recommend separating RevenueCat per environment. Reuse the same API key and your sandbox purchases from debugging bleed into production conversion and LTV, inflating your measured conversion rate by a few % and skewing decisions. Split the project (or at least the App), and set sendAnalytics: false for dev so your own taps never land in the numbers — your future self analyzing the data will thank you.
The same goes for AdMob: always use test ad units in dev and staging. Hitting production units during device testing risks being flagged as invalid traffic and having serving suspended.
Aligning EAS Update channels with environments
OTA accidents happen when channels and profiles drift apart. When you push an update, always name the same channel you built with.
# OTA to stagingeas update --branch preview --message "checkout fix"# OTA to productioneas update --branch production --message "checkout fix"
Because each profile pins a channel in eas.json, a preview build only receives preview-branch updates and never production ones. The worst-case accident — "a test update flowed to production" — is prevented simply by keeping this correspondence aligned.
Pitfalls you tend to hit in practice
Once it's running, a few stumbles are common.
On Google Play, each bundle ID (package name) is a separate app, so accidentally uploading the staging package to the production track gets rejected for a signing-key mismatch. Build a habit of confirming you built with APP_VARIANT=production before uploading.
Push tokens and ATT state are also managed per environment. A notification permission you granted in dev does not carry over to staging. That's by design, but before you panic that "notifications aren't arriving," recall which build you granted it in.
Finally, extra from expo-constants may not be replaced with new values via an OTA update. For values you want to change through updates — like a service endpoint — it's safer to derive them from the expo-updates channel or from server-side remote config rather than extra.
Splitting environments is unglamorous, and the initial setup takes about half a day. But once you can test without fear of breaking production on your everyday phone, pre-release checks become remarkably light. Indie development is partly a contest over how much you can reduce the friction of verification, and this is an investment I always recoup first. Thank you for reading.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.