Switching App Environment at Runtime—A Counterargument

Sam Dods
Kin + Carta Created
7 min readMar 11, 2019

I first came across in-app environment switching about five years ago from someone I was interviewing. I thought, hey, that sounds cool, it saves having a separate build for each environment. So I went away after the interview and I experimented.

I quickly realised why I had been using the alternative approach.

Two approaches to accessing multiple environments

When building an app at scale, you will almost certainly access multiple environments, such as QA, UAT, Staging, Production.

You need a way of easily testing in each of these environments.

  1. One approach is to use a different Bundle ID for each environment. This means you can have a QA build running alongside a separate Staging build and a further separate Production build, for example, all on a single device. The choice of environment is made at build time using, say, different Xcode Schemes. Your CI pipeline can trigger and deploy separate builds for all environments, perhaps depending on the branch you’ve pushed to.
  2. Another approach is to allow switching environments at runtime. The developer or tester then only installs one app, but can choose which environment they’re testing against, by selecting from a menu only visible in non-App Store builds, or using a companion app to launch the target app against a specific environment.

Why I recommend against switching at runtime

Being able to switch environments at runtime might seem cool and there are some innovative ways to do so without introducing a hidden menu into the app. A companion app can be used to open the target app using a URL scheme with an argument to select the environment. There’s no debating that’s a clever approach!

But I strongly advise against any switching at runtime. And here is why:

1. API Keys for multiple environments in a single app

Generally speaking, I recommend against this for security reasons. Having production keys in your test builds, or vice versa, opens up your business to multiple vulnerabilities. This would undoubtedly be picked up in penetration testing.

But let’s say you would keep production builds separate from non-production builds. Even so, you will still have API keys, URLs and other settings for multiple environments (e.g. Dev, QA, UAT, Staging) embedded in a single build. This increases the complexity of accessing these settings when your app needs them. It means looking them up at runtime to “resolve” for the selected environment.

Furthermore, a non-production backend such as Staging might deal with production data. Now you have keys in your non-production app which can be used to modify production data.

🤓 Having a separate build for each environment means you can instead reference a setting (e.g. API key) through a static immutable property, which is undoubtedly more robust and requires significantly less code.

2) Lots of unnecessary complexity

Runtime switching would introduce lots of code that would never be used in the App Store build. Yet this code would still be present in the App Store build because you would be unable to wrap this code in #if DEBUG or any other compiler directives. Using compiler directives would require a different Xcode Build Configuration for the App Store build vs. the build for all other environments. That is highly ill-advised as it means your QA and UAT is carried out on a build with a configuration that doesn’t match the one used for your end users. Sounds dangerous to me!

You might laugh in the face of danger—ha, ha, ha—and use compiler directives, because after all, your second Xcode configuration would differ by just a single -DEBUG flag, right? (Yes, that’s right, until you modify your first configuration and forget to make the same changes to your second.) You’re still introducing otherwise-unnecessary code and complexity, which should be reason enough to steer clear of this approach.

🤓 Less code. Need I say more?

3) Complicated and confusing internal releases

Being able to switch environment at runtime may cause difficulties with internal releases. For instance, you may wish to release QA builds to your testing team and release UAT builds to your stakeholders. With the ability to switch environment in the app, which environment is connected by default? Presumably you’d choose UAT so that your stakeholders don’t need to switch environment. But then your internal testers have to switch to the QA environment after installing.

There may also be functionality that doesn’t work in one of the environments because, say, WIP backend services haven’t been propagated to the later environments yet. Releasing all environments in a single build will at best lead to confusion and at worst lead to senior stakeholders raising tickets for bugs that are not actually bugs.

🤓 Having a separate build per environment means stakeholders can be given access to the specific builds/environments they need and app deployments can be synchronised with backend environment deployments.

4) It’s less clear which environment you’re running against

When all environments are in a single build, it’s not immediately clear which environment you’re connected to when you open the app. You’d have to presumably call this out in a Help menu or something, leading to more unnecessary code.

Given that your Staging build is linked to Production data (as is typical), this could lead to mistakes being made on live data without knowing it!

🤓 With a separate app installed for each environment, the environment can be made clear from the display name, e.g. MyApp QA and MyApp Staging. You can even use an app icon overlay to further improve distinction.

5) Push notifications would be received for wrong environment

This is a real pain and can lead to confusion. Confusion can lead to wasted time with bugs raised for no reason and then investigating something that would never happen in reality. I’ll explain…

Push notifications are linked to a specific Bundle ID. If all environments are within a single app (and therefore single Bundle ID) then you could be running a QA build and receive a push notification related to your Staging account. Opening the push notification might try to deep-link to something that doesn’t exist in the current environment. It’s unclear for which environment the push notification was intended.

Having separate app builds makes it clear which push notifications are for which app (and therefore which environment). When opening a notification, it will take you directly to the dedicated app for the correct environment.

🤓 This removes any scope for mistakes. If you find a bug while testing, then you know it’s a bug—you don’t have to waste time figuring out whether or not you had the correct environment set.

6) You can’t run in different test environments at the same time

If your app doesn’t quite behave as expected and you wish to check against another environment, then you have to switch to the other environment in the same app, which means resetting all app state. You will lose your progress in the app: what you’d searched for, which tab you were on, etc. and you’d have to sign in again. Then you have to do it all again to switch back to the environment you were originally testing against.

You have to hard reset the whole app when switching environments, because you can’t risk state being leftover from the previous environment which would undoubtedly lead to bugs.

🤓 A separate app for each environment means you can easily switch to another app to test something in another environment without losing state in either app/environment.

7) Harder to maintain

The fact that environment switching requires you to “hard reset” the whole app when switching environments introduces more maintenance complexity. There are lots of bugs that could come from having corrupt data leftover in memory from a previous configuration, so all state must be cleared out.

As the project grows and new state is added here and there, there’s a greater risk that some state is not removed when switching environment. Any object that is storing state should have its state reset.

You may have third-party dependencies that are configured based on your environment, e.g. a different API key for your Analytics provider. All such third-party dependencies have to be reinitiated when switching environments.

Because of the added maintenance complexity, you would be expected to write more documentation to explain how the environment switching works and what must be done when adding new state, to ensure that state is cleared out upon switching environment.

🤓 Having a separate build per environment is simple for anyone to understand. It requires no additional documentation and introduces no additional maintenance complexity.

Conclusion

Being able to switch between several environments in a single app build will introduce a lot of unnecessary code. That is code that someone has to spend time writing and later maintaining. And for what? In my (albeit subjective) opinion, this extra effort would be for an experience that is worse for developers and worse for testers.

The only benefit I can see is that it’s less overhead for Apple Developer Account administrators who, in the case of a single build for multiple environments, would only have to create a single provisioning profile. For me, that’s not enough to outweigh all the disadvantages listed above.

I’m keen to hear others’ arguments though, so please leave a comment!

And if you’re interested in how we build robustness and reliability into our apps at TAB, why not introduce yourself?

--

--

Sam Dods
Kin + Carta Created

Tech Lead and Mobile Evangelist based in Edinburgh, Scotland