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:
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.
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:
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.
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.