.st0{fill:#FFFFFF;}

How I Switch between Staging and Production URLs 

 November 29, 2011

by Jon Reid

16 COMMENTS

How do you switch your app between a staging URL (for development and testing) and a production URL (for real world use)? I’ve changed my mind about my approach, because one size doesn’t fit all.

In 9 Code Smells of Xcode Preprocessor Macros, I originally suggested that instead of using the preprocessor, it would be better to use a plist. Here’s the preprocessor code smell:

#if STAGING
static NSString *const fooServiceURL = @"https://dev.foo.com/services/fooservice";
#else
static NSString *const fooServiceURL = @"https://foo.com/services/fooservice";
#endif

I wrote, “Instead of defining these URLs in your code, treat them as resource definitions and place them in a plist, organized by type.”

<dict>
    <key>Staging</key>
    <dict>
        <key>fooServiceURL</key>
        <string>https://dev.foo.com/services/fooservice</string>
    </dict>
    <key>Production</key>
    <dict>
        <key>fooServiceURL</key>
        <string>https://foo.com/services/fooservice</string>
    </dict>
</dict>

I still think this is a good approach for a complicated app that talks to many services. Putting all the URLs in a plist makes them easy to find and manage.

But for a simple app, I’ve found this to be overkill. Instead, I create a class for the URLs, and access them through methods:

- (NSString *)fooURLString
{
    DebugSettings *debugSettings = [self debugSettings];
    if ([debugSettings usingStaging])
        return @"https://dev.foo.com/services/fooservice";
    else
        return @"https://foo.com/services/fooservice";
}

The plist approach also makes it harder to switch between staging and production on the fly, because all the URLs have to be reloaded. With the “define it in code” approach, all I have to do to switch URLs is change a property in DebugSettings.

Question: What are your approaches switching your app between a staging URL and a production URL? Leave a comment below.

Jon Reid

About the author

Programming was fun when I was a kid. But working in Silicon Valley, I saw poor code lead to fear, with real human costs. Looking for ways to make my life better, I learned about Extreme Programming, including unit testing, test-driven development (TDD), and refactoring. Programming became fun again! I've now been doing TDD in Apple environments for 20 years. I'm committed to software crafting as a discipline, hoping we can all reach greater effectiveness and joy. Now a coach with Industrial Logic!

  • Hello Jon,
    just stumbled across your Blog. Very nice work and thanks alot for sharing!
    Determining configuration data during compile time is always a pain. It’s like turning software to hardware. Though I have to admit, that I start a new project this way when things are very simple :)
    Regards
    Nick

    • Hi Nick,

      I’m glad you found your way here. :)

      Engineering is often the art of balancing trade-offs. I started one project with compile-time #if’s, and that was fine as long as I was the only one who was using the staging services. But when it became important for other people to be able to switch from production to staging, I converted to a runtime switch.

      One of the best parts about having a good suite of unit tests is the ability to change your mind and rewrite something!

  • This is an interesting read, but I have a question about security. In the scenario where the URLs are placed in a plist, wouldn’t this leave an unnecessary security risk by potentially exposing the location of the staging server, or any other item that would not be necessary in the final released version? Wouldn’t conditional build statements prevent unwanted data from being included in the final build?

    For example:

    #ifdef DEBUG
    URL = @"http://devserver.com";
    #elif STAGING
    URL = @"https://staging.com";
    #else
    URL = @"https://realserver.com";
    #endif

    • Mike, that’s a good point. In my work, all staging servers are hidden behind the corporate firewall — but not everyone has that.

      Also, I’ve found it helpful to allow switching between environments on the fly. Our testers don’t like the idea of qualifying one binary but releasing a different binary.

      But as far as hiding unwanted data, I wonder if there’s any way to have different build configurations load different plists. Otherwise, yeah, that’s a valid use of conditional build statements.

  • Jon,

    Thanks for the information. I have been working in Android for the past year and had a solution that worked really well for us. Now I am finding my way back to iOS/Mac again and I am really struggling with the best method for switching between environments.

    How do you set your “DebugSettings”? Hidden screen, secret back door, something else?

    Here are some of the ways I have been considering from what I consider best to worst.

    Option1
    ———-

    My current thought (not implemented yet) is to code the production environment into the code. Then look for a specially named file in the apps documents directory. If that file exists I load it and use it’s configuration instead.

    I will provide a command line tool to QA and dev that wraps this functionality and can switch to a given environment quickly. I will use one of the MobileDevice.framework wrappers to accomplish the file copy.

    Downside is the app will need to be restarted when I want to load the new configuration. Configuration is lost when app is uninstalled and reinstalled.

    Option2
    ———–
    Create another app that the production client looks for and calls into to get it’s configuration.

    Option3
    ———–
    Have the server move folks between environments.

    Have any recommendations?

  • As an update I found the solution that solves the problem for me. I am using https://github.com/xslim/mobileDeviceManager and a script that is checked in. The developer can create their custom configuration and copy it to the documents directory. Now we keep production checked in and have a runtime check for our custom configuration file.

    Here is an example of the tools usage:

    $ mobileDeviceManager -o copy -app “com.domain.MyApp” -from ~/.myAppConfig/app_override.plist

    This way the developer can keep their custom configuration in their home directory (out of source control) without fear of accidental checkin. We already use process like this for other desktop and android apps so this fits our process really well. This has the added benefit that if a testers device is failing we can point it at a custom debug server with extra logging to simplify the debug process and not need to deploy a new binary to that device during internal testing.

    I hope this can help someone else.

    • That works as long as you have different builds for staging and production. But that also makes it impossible for someone to take the shipping app and switch it to staging to diagnose a problem.

    • Well, it does hold in practice because my examples come from real-world apps. I do hear your request, though, for sample projects. I don’t think it would work for this example because everyone probably has a slightly different way of doing it. My main points for this post are:
      – Don’t overcomplicate something if you don’t have to.
      – Consider how switching services on the fly might be helpful for testing. I’ve seen it be helpful when a problem is reported on a shipping app, to take the actual app and switch it immediately to the staging URLs.

  • Xcode allows us to do some tricks, so it’s even possible to configure these stuff (URLs, keys, whatnot) with YAML files. I wrote a simple tool, just a proof of concept, and it seems to work well on our project.
    Here is some details, if you interested.

    • I like this a lot! The code generation alone is interesting, but I’m really curious about the way you integrated it with a custom build rule — I’ve never seen that. The other way I have seen is to use a script which scans for all files of a particular type.

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
    >