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.

Kinvey client libraries allow you to specify a version for your app. The library sends this version to the Kinvey backend with each request using the X-Kinvey-Client-App-Version header. 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.

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.

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. Examples of version strings specified in this format include: "1.1.5", "2.6", "3".

Setting Version on the Client

Depending on the SDK type, you can set the client app version either on instance level or on request level.

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.appVersion option allows you to set the client app version globally.

// on initialization
Kinvey.init({
  appKey: '<appKey>',
  appSecret: '<appSecret>'
  appVersion: '1.1.5'
});

// on an existing instance
Kinvey.appVersion('1.1.5');

You can override the instance-level version in individual requests, as shown in the code snippet below.

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

REST

If you rely on the Kinvey REST API to connect to your app backend, you can choose to include the X-Kinvey-Client-App-Version header in any request.

POST /blob/:appKey HTTP/1.1
Host: baas.kinvey.com
Content-Type: application/json
X-Kinvey-Client-App-Version: 1.1.5
Authorization: [user credentials]

{
    "_filename": "myFilename.png",
    "_acl": { ... },
    "myProperty": "some metadata",
    "someOtherProperty": "some more metadata"
}

Business Logic

On the backend side, versioning is handled by your server-side logic. The requestContext module, available in both Flex and Business Logic, provides a clientAppVersion namespace, which includes APIs to read the version of the client app making the request.

Assume you've got 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 "fixes" requests from version 1.x clients. For rooms already in the collection, it copies the known capacity to the request. For new rooms, it sets a default capacity of 10.

Business Logic code:

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();
  }
}

Flex code:

function roomsPreSave(context, complete, modules) {
  var versionContext = modules.requestContext.clientAppVersion;
  var rooms = modules.dataStore().collection('rooms');

  // The client making this request is older than release 2.0
  if (versionContext.majorVersion() < 2) {

  // Find the room that matches the _id in the request
  rooms.findById(context.body._id, function(err, result) {
    if (err) {
      // Database query error
      // End the request chain with no further processing
      complete().setBody(err).runtimeError().done();
    } else {
      if (!result) {
      // No room was found, set a default capacity
      context.body.capacity = 10;
      // Continue up the request chain
      complete().next();
      } else {
        // Room was found, set the capacity in the request
        // to the capacity of the room we found
        context.body.capacity = result.capacity;
        // Continue up the request chain
        complete().next();
      }
    }
  });
  } else { //Client version is 2.0 or above
    // Continue up the request chain
    complete().next();
  }
}

With this hook, a request from a version 1.0 client that does not include the capacity column will be saved to the collection with it:

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

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

Let's now consider the bookings collection, which records room reservations. Until version 2.0 of our app, we represented the conference room in a booking by its name, as shown below:

{
  "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 an entire JSON object. As a result, we want 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 older clients, because they were coded to expect the room as a string, not as a JSON object. To solve this, we can write a hook that attaches to the bookings collection and sends data to clients in the format they expect.

Business Logic code:

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();
  }
}

Flex code:

function roomsPostFetch(context, complete, modules) {
  var versionContext = modules.requestContext.clientAppVersion;
  var majorVersion = versionContext.majorVersion();
  var minorVersion = versionContext.minorVersion();

  if((majorVersion >= 2) && (minorVersion >= 1)){
    // Version 2.1 and above
    complete().next();
  } else { // All versions prior to 2.1
    var bookings = context.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
      context.body[i].room = bookings[i].room.name;
    }
    complete().next();
  }
}

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

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 because it is automatically available in all business logic contexts.

Got a question?