Blog Post

100x Less Code!

J. Chris Anderson of Couchbase Published

Couchbase Mobile can make you 100 times more productive. Similarly to the way Ruby on Rails revolutionized the way we think about web services by offering a concise way to model REST-based web applications and APIs servers, the sync function at the heart of Couchbase Sync Gateway can revolutionize the way you manage mobile application data.

Gone are the days of handwriting API servers and mobile REST client code. You'll never get those hours devoted to handling network operation errors back, but at least it doesn't have to happen again.

With a local database on the phone (something JSON-based might be preferable to Core Data or SQLite since it's 2014) it can be as simple as asking a model object to save itself. Whether the phone is in airplane mode, or on an oversubscribed 3G connection, or connected to high performance WiFi, the user will never see a difference in responsiveness. 

Now if only there was a way to get the performance and reliability of local first data, without giving up the good parts of the cloud. With a sync capable local database, changes can be automatically synced to the cloud, without you writing a ton of network code. This can radically simplify mobile client applications, but the edge is only half the story. When the data arrives at the cloud something needs to decide if the changes are valid or if they should be rejected. The cloud is also responsible for deciding who can see what.

In a traditional remote data architecture (the kind that riddles your mobile app with network calls) update validation and read access control are applied by the web application server, which is typically custom code with an ad-hoc relationship to various backend services and databases. We've grown accustomed to writing API servers this way, but that doesn't mean we have to like it.

Instead of an expensive approach where each REST endpoint requires custom code, the Sync Gateway speaks the Couchbase Mobile sync protocol and provides it's own concise abstraction for validation, access control, and data routing. This means intead of writing a server that spends most of its logic translating between user input and database operations and queries, you can just specify the behavior you want and let Sync Gateway handle the details.

In Sync Gateway, custom application behavior is expressed in a JavaScript function which offers simple but fine-grained control over your data. For the details see the sync function documentation, for now I'll just show you one and then describe how it works.

Here is the sync function from our example app, ToDo Lite. You can learn about the mobile code for ToDo Lite in our developer portal. The same backend supports the iOS, Android, and PhoneGap versions.

function(doc, oldDoc) {
  if (doc.type == "task") {
    if (!doc.list_id) {
      throw({forbidden : "Items must have a list_id"})
    }
    requireAccess("list-"+doc.list_id);
    channel("list-"+doc.list_id);
  } else if (doc.type == "list") {
    channel("list-"+doc._id);
    if (!doc.owner) {
      throw({forbidden : "List must have an owner"})
    }
    if (oldDoc) {
      requireUser(oldDoc.owner)
    }
    access(doc.owner, "list-"+doc._id);
    if (Array.isArray(doc.members)) {
      access(doc.members, "list-"+doc._id);
    }
  } else if (doc.type == "profile") {
    channel("profiles");
    var user = doc._id.substring(doc._id.indexOf(":")+1);
    if (user !== doc.user_id) {
      throw({forbidden : "Profile user_id must match docid : " + user + " : " + doc.user_id})
    }
    requireUser(user);
    access(user, "profiles");
  }
}

Let's go through it one bit at a time and see how we can fit most of what your REST web application server does, into a page of code.

function(doc, oldDoc) {

The sync function is run on each mutation independently, and takes two arguments: the proposed version of the document, and the previous version of the document, if one exists. The old version is useful for validating things like that the person making the change is the person who owns this document, or perhaps your application enforces that certain fields on the document are immutable.

  if (doc.type == "task") {
    if (!doc.list_id) {
      throw({forbidden : "Items must have a list_id"})
    }
    requireAccess("list-"+doc.list_id);
    channel("list-"+doc.list_id);

This block runs if the document represents an individual task in ToDo Lite. It requires that the document have a list_id field, and that the user who is updating or creating the task has access to that list. If the validation passes, the document is routed to a channel for that list. The next time anyone who has access to the list connects, they'll sync the updated task.

  } else if (doc.type == "list") {
    channel("list-"+doc._id);

This section is concerned with the metadata associated with a list. The call to "channel" will route the list metadata to the channel associated with the list (assuming the validations for this document pass).

    if (!doc.owner) {
      throw({forbidden : "List must have an owner"})
    }
    if (oldDoc) {
      requireUser(oldDoc.owner)
    }

The validations for list documents are simple: all lists must have an owner, and lists may only be updated by their owner. (Note that we can express the validations and routing in any order -- if the validation fails then any other calls from this invocation of the sync function will have no effect.)

    access(doc.owner, "list-"+doc._id);
    if (Array.isArray(doc.members)) {
      access(doc.members, "list-"+doc._id);
    }

The last thing we want to do for the list metadata document, is use it to grant access so the right people can read from the list's channel. The first call to "access" makes sure that the list owner can sync items from this todo list. The second call is passed an array of members, granting each of them access to the list. These access grants will also impact who can write tasks to the list, because of the call to "requireAccess" we made in the first block of this function.

  } else if (doc.type == "profile") {
    channel("profiles");

The final block of the ToDo Lite sync function is responsible for making sure that the list of application users is available to each user, so that users may select each other as members of lists. The first line of code in this block routes user profile documents to the "profiles" channel.

    var user = doc._id.substring(doc._id.indexOf(":")+1);
    if (user !== doc.user_id) {
      throw({forbidden : "Profile user_id must match docid : " + user + " : " + doc.user_id})
    }
    requireUser(user);

The validation for profiles ensures that the document meets a schema requirement (the document ID must match the username of it's creator). It also prevents anyone from editing anyone else's profile.

The final line of code might be deceptively simple. It grants the user access to the "profiles" channel. What this means is that when a new user creates their own profile, the system reacts by giving them read access to everyone else's profile via the "profiles" channel.

    access(user, "profiles");
  }
}

This tour of an example sync function shows most (but not all) of the tools it offers. You can imagine how much code it would take to write a REST API server that enforces similar rules. Lots more. And solving the offine sync problem would still be on your todo list.

So, will you be 100 times more productive with Couchbase Mobile?