app-versioning

Versioning apps built with Kinvey

As a mobile developer, it is important to manage multiple active versions of an app and push periodic updates without breaking older clients. In addition to client changes, developers often need to account for differences in backend APIs between versions. These could be due to several factors, such as changes in the data model or business logic.

This tutorial illustrates how to use Kinvey's app versioning support, with sample solutions to the following common problems -

  1. A new column is added to a collection in version 2.0 of your application. Since the version 1.x clients do not know about this field, updates from version 1 clients erase the new field and break data integrity.

  2. The data type of a column changes between versions 2.0 and 2.1. Such a change may introduce unwanted behavior as well as potential app crashes.

Managing versions with Kinvey App Versioning

Kinvey client libraries allow you to specify a version for your app. The library sends this version to the Kinvey backend with each request. On the backend, we provide you with utilities to write business logic that is conditional based on the version of the client making the request.

The following sections describe this solution in detail.

Version Format

Versions are represented in Kinvey as strings. We strongly recommend (but do not require) using version strings that conform to the pattern major.minor.patch, where all values are integers and minor and patch are optional. Here are some examples for version strings specified in this format - “1.1.5”, “2.6”, “3”.

Capturing Version on the Client

Android

The setClientAppVersion() method on the Client class allows you to set the app version.

import com.kinvey.android.Client;
…
final Client mKinveyClient = new Client.Builder(your_app_key, your_app_secret
   , this.getApplicationContext()).build();

mKinveyClient.setClientAppVersion(“1.1.5”);

iOS

The clientAppVersion property of Options struct allows you to set the app version. An instance of Options is used as a global configuration attached to Client.

The following sample code shows how to set your version in iOS.

let options = Options(clientAppVersion: "1.0.0")
store.find(options: options) {
    switch $0 {
    case .success(let results):
        print(results)
    case .failure(let error):
        print(error)
    }
}

JavaScript

The Kinvey.ClientAppVersion module allows you to set the client app version globally. You can also override this version in individual requests, as shown in the code snippet below.

// Set the client app version globally
Kinvey.ClientAppVersion.setVersion('1.1.5');

// Set the client app version per request
Kinvey.DataStore.find('rooms', null, { clientAppVersion: '1.1.5' });

Business Logic

On the backend side, versioning is handled by your business logic. The requestContext module provides a clientAppVersion namespace, which includes APIs to read the version of the client app making the request.

Let's take the example of an app that lets users manage and book conference rooms. Conference rooms are stored with an id and a name in the "rooms" collection. Users can add new rooms from the app, or update (e.g. rename) existing ones.

In version 2.0 of this app, we added a new column to the collection to indicate the capacity of a room. Version 1.x clients do not know about this column, as a result any entities saved from 1.x will erase previously stored values for capacity, effectively setting them to 0.

The following code shows a onPreSave collection hook that "fixes" requests from version 1.x clients. For rooms already in our collection, we copy the known capacity to the request. For new rooms, we set a default capacity of 10.

function onPreSave(request, response, modules) {
  var context = modules.requestContext;
  var rooms = modules.collectionAccess.collection('rooms');

  //The client making this request is older than release 2.0
  if (context.clientAppVersion.majorVersion() < 2){
    //find the room that matches the _id in the request
    rooms.findOne({"_id":request.body._id},
      function(err, result){
        if(err){
          // database query error, return an error response
          // and terminate the request
          response.error(err);
        } else {
          if(!result){
            //No room was found, set a default capacity
            //before saving the request
            request.body.capacity = 10;
            response.continue();
          } else {
            //room was found, set the capacity in the request
            //to the capacity of the room we found
            request.body.capacity = result.capacity;
            response.continue();    //continue to save
          }
        }
      }
    );
  }
  else {    //Client version is 2.0 or above
      response.continue();
  }
}

With this hook, a request from a version 1.0 client that looks like this -

  {
    "id": "52f22d694609ba980401dd56",
    "name": "Conference Room A"
  }

will be saved to the collection as this -

  {
    "id": "52f22d694609ba980401dd56",
    "name": "Conference Room A",
    "capacity": 10
  }

Let's now consider the bookings collection, which records room reservations. Till version 2.0 of our app, we represented the conference room in a booking by its name. A booking looked like this -

{
  "user" : "johndoe",
  "bookingTime": "1396010640",
  "bookUntil": "1396011600",
  "room": "Conference Room A",
  "_acl": {
    "creator": "kid_TVcFFbFV1f"
  },
  "_kmd": {
    "lmt": "2014-03-28T12:46:44.322Z",
    "ect": "2014-03-28T12:46:44.322Z"
  },
  "_id": "53356f34403f26fb020fd2b7"
}

While developing version 2.1, we have realized that it is much better to represent a booked room by the entire JSON object. As a result, we'd like to change the booking to look like this -

{
  "user" : "johndoe",
  "bookingTime": "1396010640",
  "bookUntil": "1396011600",
  "room": {
    "id": "52f22d694609ba980401dd56",
    "name": "Conference Room A",
    "capacity": 10
  },
  "_acl": {
    "creator": "kid_TVcFFbFV1f"
  },
  "_kmd": {
    "lmt": "2014-03-28T12:46:44.322Z",
    "ect": "2014-03-28T12:46:44.322Z"
  },
  "_id": "53356f34403f26fb020fd2b7"
}

This change will pose a problem to our older clients, since they were coded to expect the room as a string, not as a JSON object. To solve this, we will write a onPostFetch hook on the bookings collection, that sends data to clients in the format they expect.

function onPostFetch(request, response, modules) {
    var context = modules.requestContext;
  var majorVersion = context.clientAppVersion.majorVersion();
  var minorVersion = context.clientAppVersion.minorVersion();

  if((majorVersion >= 2) && (minorVersion >= 1)){
    //version 2.1 and above
    response.continue();
  }
  else{   //all versions prior to 2.1
      var bookings = response.body;
      //loop over all the bookings in the response
      for (var i=0; i < bookings.length; i++){
        //return the value of "name" for the room
        response.body[i].room = bookings[i].room.name;
      }
      response.continue();
  }
}

With this hook, our older clients will continue working, while newer clients will receive data in the enhanced format.

Refer to our REST API documentation for details on using clientAppVersion.

Best Practices

We recommend following these best practices for your Kinvey apps.

  1. Set the client version to 1.0.0 from your first release, even if you don't anticipate breaking changes in the future. Continue to mark each subsequent release with an incremental version, so that you are able to identify the version of any client making a request.

  2. When adding conditional logic for versioning, remember to update all code paths that interact with a collection. You may have hooks or endpoints that interact with the data directly. Any such code will also have access to modules.requestContext.clientAppVersion, since is automatically available in all business logic contexts.

Got a question?