Building standalone app update without publishing it

You have to publish before you build with Expo. To avoid publishing over your prod bundle while in a testing phase, you will likely want to use release channels (https://docs.expo.io/versions/latest/distribution/release-channels/).

This may or may not be helpful for your situation, but my team uses a system of including the minimum build number that the JS is compatible with in the release channel in order to prevent production builds from getting the JS update too early. I describe it here: How to check compatibility of OTA updates with custom native modules. This is useful if you don’t want anyone to get that JS until they’ve upgraded the binary.

If you just want to put out a new TestFlight build pointing to new JS (for testing purposes only) that you will later send out as an OTA update to everyone (and then you just let the build expire without ever pushing it live in the store), the above technique wouldn’t really work. You may be able to work something out with the “promote” feature of release channels (https://docs.expo.io/versions/latest/distribution/advanced-release-channels/#promoting-a-release-to-a-new-channel). Another option is to create a separate app that’s just for TestFlight testing and always points to a “test” or “staging” bundle (this is what we do). You just need to make sure this app has a different slug and bundle ID when compared to the production version (you could hot swap an alternate app.json to accomplish this). An advantage of the latter technique is that testers can have both the test and prod versions of your app installed side-by-side. This is actually a pretty common thing to do in native development even apart from Expo.