Skip to main content
ANVISoftware Solutions
Lesson 11 of 11Advanced20 min

Store Release

By the end of this lesson

Prepare, submit and update an application in the app stores.

Shipping a web application is one action with one outcome: you deploy, and the next request serves the new code. Shipping a mobile application is a sequence with several parties in it, at least one of which can say no, and the last step belongs to your users rather than to you.

Three things stand between a finished build and an engineer using it. The store listing has to be complete and accurate. A reviewer has to approve the build, which takes time and sometimes ends in a rejection. And then each user has to install the update, on their own schedule, which for a meaningful fraction of them is never.

Everything difficult about mobile releases comes from the last point, and it deserves stating plainly at the start: you cannot recall a release. Once a build has reached users, those users have that build. You can stop the rollout to people who have not updated yet, and you can submit a fix and wait, but you cannot take a bad version off the devices that already have it. The consequences of that are architectural, and they are the subject of the second half of this lesson.

Terms used precisely below. The first two are often confused, and confusing them wastes a submission:

Version name and build number
The version name is what users see, such as 4.2.0. The build number is an internal counter that must increase with every upload, including uploads that are rejected or never released. Both platforms refuse a build whose number they have seen before, and discovering that at submission time is an avoidable delay.
Store metadata
The description, screenshots, category, age rating, support contact, release notes and privacy disclosure. It is part of the release, not paperwork that follows it: the build cannot be reviewed without it, and it is what a person reads when deciding whether to install your app.
Privacy disclosure
A structured declaration of what data your app collects, why, and who it is shared with, including data collected by third-party libraries you added. It has to match what the app actually does. An inaccurate declaration is both a review problem and a trust problem, and the libraries are where teams get it wrong.
Review
A check by the platform before your build reaches users. Turnaround is usually measured in hours to a few days and it is not a number you can promise to anybody. It can end in a rejection with a reason to address, which starts the clock again.
Phased or staged rollout
Releasing an approved build to a growing percentage of users over several days rather than all at once. It is the main way to limit the damage from a bad build, because it caps how many people receive it before you notice.
Remote configuration
A small document your app fetches from your own server at launch, holding feature switches and a minimum supported version. It is how you change the behaviour of an already-installed app without a release, and it is the lever that makes a mobile incident survivable.

A release sequence that treats review time and update lag as facts to plan around rather than surprises:

  1. Confirm the server side is already compatible

    Deploy any API change before the client that needs it, and keep it working for the versions already in the field. A client that requires a simultaneous server change strands every user who has not updated, and you cannot make them update.

  2. Write the metadata as part of the work, not after it

    Description, release notes, screenshots at the sizes each platform requires, and the privacy disclosure updated for anything the release adds — a new permission, a new analytics library, a new field you collect. Screenshots in particular take longer than teams expect, because they need to be produced at several sizes and to show the current design.

  3. Build a release configuration and check what is in it

    Verbose logging off, no development server addresses, no test credentials, no relaxed transport settings, and the signing identity your store account expects. This is the moment the security lesson's list is worth re-reading, because a debug setting that ships is live until the next release.

  4. Distribute to internal testers first

    Both platforms provide a pre-release distribution channel. Put the exact build you intend to ship through it on real devices, including the oldest one you support, and install it over the previous released version rather than fresh so the upgrade path is exercised.

  5. Submit, and plan in days

    Assume hours to days, and that a rejection is a normal outcome rather than a failure. Submit well before any date you have committed to, keep the reviewer notes accurate, and give test credentials if the app cannot be used without signing in. Never plan a release for the afternoon of a deadline.

  6. Release to a fraction of users

    Start a phased rollout at a small percentage and watch crash rates, error rates and your API logs against the previous version before increasing it. The point is to be looking at real data from real devices while most of your users are still on a build you trust.

  7. Watch the things a user would not report

    Crash-free rate by app version and operating system version, error rates per endpoint per client version, sign-in failures, and queue drain failures. Support calls tell you about visible problems; a broken background path shows up in your own numbers first or not at all.

  8. Keep the previous version working

    After release, your API is serving at least two client versions, and realistically more for months. Removing a field or tightening a rule the day after a release breaks the users who have not updated, who are the ones least likely to update in response.

A remote configuration document, fetched from your own API at launch
JSON
{
  "configVersion": 47,
  "minimumSupportedVersion": "4.1.0",
  "features": {
    "photoAttachments": "on",
    "routeOptimiser": "off",
    "offlineQueueRewrite": "internal"
  },
  "notice": {
    "level": "warning",
    "text": "Update the app to keep syncing visit notes"
  },
  "maxAgeSeconds": 900
}
  • The field names are invented for this lesson and the document is served by your own API rather than by a platform service. The shape is the transferable part: a few switches, a version floor, and something you can say to the user.
  • minimumSupportedVersion is the forced-update lever. It has to be decided on the server, because the one thing you cannot do is change the behaviour of an app already installed on a device. A build that reads this can be told to stop; a build that does not read it cannot.
  • The feature switches have three states rather than being booleans. On and off give you the ability to disable one broken feature without a release, which is the difference between a bad afternoon and a bad week. The internal state lets a feature reach your own testers through the real store build, so it is exercised on real devices before anybody else sees it.
  • The notice is there because silence is the worst option during an incident. If photo attachments are switched off, an engineer standing in front of a piece of equipment needs to know that, and telling them is cheaper than the support calls.
  • maxAgeSeconds means the app caches this document. That matters more than it looks: an app that refuses to start until a configuration fetch succeeds has made your configuration endpoint a single point of failure for every device, including the ones in a basement. Cache the last good copy, and start with it when the fetch fails.
  • configVersion is for you, not the app. When somebody reports odd behaviour, knowing which configuration their device had at the time is frequently what closes the investigation.
  • Keep this document small and cheap. It is fetched on every cold launch, so it is the wrong place for anything large, and nothing in it should be confidential — it travels to every installation, exactly like the app itself.
Applying the configuration at launch, and failing open when it is missing
TypeScript
type StartupGate =
  | { kind: "run" }
  | { kind: "forceUpdate"; text: string }
  | { kind: "notice"; text: string };

interface RemoteConfig {
  minimumSupportedVersion: string;
  features: Record<string, "on" | "off" | "internal">;
  notice?: { level: "info" | "warning"; text: string };
}

/** Numeric comparison, because "4.10.0" sorts before "4.9.0" as a string. */
function isOlderThan(installed: string, required: string): boolean {
  const left = installed.split(".").map(Number);
  const right = required.split(".").map(Number);

  for (let i = 0; i < 3; i += 1) {
    const a = left[i] ?? 0;
    const b = right[i] ?? 0;
    if (a !== b) return a < b;
  }
  return false;
}

export function startupGate(
  installedVersion: string,
  config: RemoteConfig | null,
): StartupGate {
  if (config === null) return { kind: "run" };

  if (isOlderThan(installedVersion, config.minimumSupportedVersion)) {
    return { kind: "forceUpdate", text: "This version can no longer sync. Update to continue." };
  }

  if (config.notice) return { kind: "notice", text: config.notice.text };
  return { kind: "run" };
}

export function featureEnabled(
  config: RemoteConfig | null,
  name: string,
  buildDefault: boolean,
  isInternalUser: boolean,
): boolean {
  const state = config?.features[name];
  if (state === undefined) return buildDefault;
  if (state === "internal") return isInternalUser;
  return state === "on";
}
  • As with every sample in this course, the shape is what transfers. Reading the installed version and opening a store page are platform calls that look nothing like this.
  • A null configuration returns run, which is the most important line here. Failing open means a device with no connection still launches into the offline experience you built. Failing closed would mean your configuration endpoint having a bad five minutes takes every device offline, which is a larger outage than most of the problems this mechanism exists to contain.
  • That is a genuine trade-off, stated honestly: a kill switch cannot reach a device that does not connect, and a forced update cannot stop a build that never asks. This mechanism limits the blast radius of a bad release; it does not eliminate it.
  • The version comparison is written out because comparing version strings directly gets 4.10.0 and 4.9.0 the wrong way round. It is a small bug with a large effect, since it decides whether you block the wrong users.
  • A forced update is a blocking screen that stops somebody working, so treat it as a serious step reserved for data loss, a security problem or a client that can no longer talk to your API safely. The text has to say why and lead straight to the store page, and a user with no connection needs to be told that too rather than being left on a dead screen.
  • featureEnabled takes the build's own default so an unknown switch behaves the way that build was tested to behave. Defaulting everything to off would silently disable features on an older client the moment you rename a switch.
  • Both functions are pure, so the rules are testable without a device or a server: assert that an old build is gated, that a current one runs, and that a missing configuration never blocks the app.

Summary

  • Store metadata, screenshots and an accurate privacy disclosure are part of the release, and review takes days and can reject, so plan releases with slack rather than to a fixed hour
  • Deploy compatible server changes before the client that needs them, and expect your API to serve several client versions for months
  • Use a phased rollout and watch crash and error rates by client version while most users are still on a build you trust
  • You cannot recall a release: halting a rollout does nothing for devices that already updated, and a hotfix arrives after review and then trickles out
  • A server-side feature switch and a minimum supported version check are architecture decisions to make before launch, because they cannot be added to a build already in the field

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Challenge

Write the incident plan before you need it

Version 4.2.0 of the field app reaches 20% of users at 09:00. By 09:40 your logs show that visit notes written offline are lost when the app is terminated before the queue drains. Around four hundred engineers have the build.

Write the sequence of actions for the next hour, in order, and say what each one achieves and what it does not. Then list the levers that had to exist before this morning for your plan to work.

Show solution

First, halt the rollout. It takes a minute and it stops the build reaching anyone else. It does nothing for the four hundred who already have it, which is the fact that shapes the rest of the response.

Second, use the server-side switch to disable offline note capture for affected clients, or to route submissions through the previous code path if you have one. This is the only action that changes behaviour on devices already carrying the defect, and it reaches each one the next time it fetches configuration.

Third, tell people. A notice in the app and a message through whatever channel reaches field staff, saying plainly that notes should not be written offline for now and what to do instead. Engineers making decisions about how they record a site visit need this within the hour, not in a release note next week.

Fourth, work out what has already been lost and whether any of it is recoverable from the device. Some queued notes may still be on handsets and drainable once a fixed build arrives, which is a reason not to have the fix clear local storage on upgrade.

Fifth, submit the fix, expecting hours to days. Fifth in the order, not first, because everything above it helps sooner.

The levers that had to exist beforehand: a feature switch fetched at launch with a cached fallback, a way to show a notice in the app, telemetry good enough to spot the failure in forty minutes rather than by support call, a phased rollout so the exposure was four hundred people rather than all of them, and an upgrade path that does not discard local data. Every one of those is cheap before launch. None can be added to the build that is already in the field, which is the entire argument for treating them as architecture rather than operational polish.

Think about it

One API, three client versions

Six months after launch, your logs show three client versions in active use: 4.2.0 on most devices, 4.0.1 on a few hundred, and 3.6.0 on about thirty that have not updated in a year.

The team wants to rename a field in the visit note response, make a previously optional field required on submission, and remove an endpoint that only 3.6.0 uses.

Decide what you can do now, what needs a different approach, and how you would know when the last one is safe.

Show solution

Renaming a response field breaks every client that reads the old name, so it is not a rename. Add the new field alongside the old one, have new clients read the new name, and keep sending both until the versions reading the old one are gone. The cost is a slightly untidy response for a while, which is a small price for not breaking working devices.

Making an optional field required on submission is the more dangerous change, because it fails at the moment an engineer tries to save work. An older client does not send the field, the request is rejected, and a visit note written in a plant room has nowhere to go. Enforce it for clients that can satisfy it and accept a documented default for those that cannot, or ship the requirement in the client first and enforce it on the server only once the older versions have drained.

Removing the endpoint is genuinely possible, and it is a judgement about thirty devices rather than a technical question. Check who they belong to, because thirty stale installations are often a handful of devices in a store cupboard and one person who turned off automatic updates. Reaching out is usually quicker than a compatibility layer maintained for years.

How you know it is safe: log the client version with every request and keep a view of requests per endpoint per version. Without that measurement you are guessing, and the usual outcome of guessing is either an avoidable breakage or an old code path kept alive indefinitely because nobody can prove it is unused.

The general principle worth carrying: on mobile, removal is the expensive operation and addition is the cheap one. Design responses and payloads so that adding is always available to you, and plan removals on the timescale of your slowest users rather than your fastest.

Knowledge check

Nothing is recorded and there is no score. The explanation appears either way.

Forty minutes after a release begins, you find a bug that loses user data. What is the most effective first response?
Why does a phased rollout matter more for a mobile app than for a web service?

Saved in this browser only.

End of the published lessons

That is everything written so far in Mobile

More lessons in this course are on the way. In the meantime, the course page shows the full roadmap, and the projects are the best way to consolidate what you have covered.