This week, PouchDB v3.4.0 was released with Couchbase Sync Gateway compatibility.

In this post, we’ll take the existing ￼TodoMVC example￼ and add filtered sync using Facebook authentication. In addition to syncing between Web clients, we’ll change the data model slightly to sync with the existing ToDoLite apps running on iOS￼ and ￼Android:

To follow along, you can open and run the TodoMVC example at commit ￼d9bb961. For convenience, I’ve added the Facebook login code in app.js. When the user logs in, the function startSessionAndSync(accessToken, userId) is called. Now let’s add the code to make it happen!

QuickStart

Make sure to serve the app on port 9000 as we will configure CORS for localhost:9000 later. The simple python command should do:

$ python -m SimpleHTTPServer 9000 1 $ python - m SimpleHTTPServer 9000

Data modelling

TodoMVC and ToDoLite have a slightly different data model. In ToDoLite apps, a user can create multiple lists and share them with multiple users.

First let’s look at a Task document with the expected title property and a list_id referencing the List it belongs to (in this case 123):

{ "_id": "E9W3-9I2Y-O8W2-6Y4D", "type": "task", "list_id": "123", "title": "A task title" } 1 2 3 4 5 6 { "_id" : "E9W3-9I2Y-O8W2-6Y4D" , "type" : "task" , "list_id" : "123" , "title" : "A task title" }

Likewise, a List document also has a title and an owner referencing the user it belongs to:

{ "_id": "123", "type": "list", "title": "TodoMVC list", "owner": "p:1234567890" } 1 2 3 4 5 6 { "_id" : "123" , "type" : "list" , "title" : "TodoMVC list" , "owner" : "p:1234567890" }

TodoMVC however is a single list app. To keep things simple we can insert the List document in code as soon as the user logs in.

Let’s create a new function called migrateGuestToUser to create the List document with the user id and save it to PouchDB:

function migrateGuestToUser(userId) { var list = { _id: '123', type: 'list', title: 'TodoMVC list', owner: 'p:' + userId }; db.put(list, function(err, result) { if (!err) { console.log('Successfully saved user list'); } }) } 1 2 3 4 5 6 7 8 9 10 11 12 13 function migrateGuestToUser ( userId ) { var list = { _id : '123' , type : 'list' , title : 'TodoMVC list' , owner : 'p:' + userId } ; db . put ( list , function ( err , result ) { if ( ! err ) { console . log ( 'Successfully saved user list' ) ; } } ) }

Note: It’s very important to set the owner field, the sync function will reject the document otherwise.

And we can call it in startSessionAndSync:

function startSessionAndSync(accessToken, userId) { migrateGuestToUser(userId); } 1 2 3 function startSessionAndSync ( accessToken , userId ) { migrateGuestToUser ( userId ) ; }

Task documents belong to a list and so they have a list_id property we need to set. Change the addTodo function in app.js to look like below. Notice we set the list_id field to the hardcoded _id of the list we inserted above:

function addTodo(text) { var todo = { _id: new Date().toISOString(), title: text, checked: false, type: 'task', list_id: '123', created_at: new Date() }; db.put(todo, function callback(err, result) { if (!err) { console.log('Successfully posted a todo!'); } }); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function addTodo ( text ) { var todo = { _id : new Date ( ) . toISOString ( ) , title : text , checked : false , type : 'task' , list_id : '123' , created_at : new Date ( ) } ; db . put ( todo , function callback ( err , result ) { if ( ! err ) { console . log ( 'Successfully posted a todo!' ) ; } } ) ; }

Now we have the appropriate documents conforming to ToDoLite’s data model we can take a look at authentication and replication.

Enabling CORS on Sync Gateway

In this tutorial, we’ll run a local instance of Sync Gateway on localhost:4984 but we’re serving our web app on localhost:9000. At this point we’d get a same origin policy error. But Chris recently added CORS support to Sync Gateway for this purpose. So we don’t have to write one line of server side code 🙂

CORS allows web apps to access resources on other domains than the origin domain. By enabling CORS on Sync Gateway we’re telling the browser “Yes, the Sync Gateway domain name is allowed to communicate with this web app”. Open sync-gateway-config.json with the extra CORS configuration to enable it on localhost:9000:

{ ... "CORS": { "Origin": ["http://localhost:9000"], "LoginOrigin": ["http://localhost:9000"], "Headers": ["Content-Type"], "MaxAge": 17280000 }, ... } 1 2 3 4 5 6 7 8 9 10 { . . . "CORS" : { "Origin" : [ "http://localhost:9000" ] , "LoginOrigin" : [ "http://localhost:9000" ] , "Headers" : [ "Content-Type" ] , "MaxAge" : 17280000 } , . . . }

Download this version (build of commit e8cf146) to run it with the CORS and PouchDB compatibility features that will ship in the next release of Sync Gateway (1.1). Start it with the config file:

$ ~/Downloads/sync_gateway sync-gateway-config.json 1 $ ~ / Downloads / sync_gateway sync - gateway - config . json

Back in app.js, update the remoteCouch url accordingly:

var SYNC_GATEWAY_URL = 'http://127.0.0.1:4984/todos/'; var remoteCouch = SYNC_GATEWAY_URL; 1 2 var SYNC_GATEWAY_URL = 'http://127.0.0.1:4984/todos/' ; var remoteCouch = SYNC_GATEWAY_URL ;

Now if we tried to sync the list to Sync Gateway we’d get a 401 Unauthorized error. Let’s fix that by creating a user session with Facebook login.

Authenticating with Sync Gateway

To authenticate with Sync Gateway we can send a POST request to /todos/_facebook with the access token, if we get back a 200 OK, the browser will set the session cookie returned from Sync Gateway for future push/pull replications.

function startSyncGatewaySession(accessToken) { var request = new XMLHttpRequest(); request.open('POST', SYNC_GATEWAY_URL + '/_facebook', true); request.setRequestHeader('Content-Type', 'application/json'); request.onreadystatechange = function() { if (request.readyState == 4 && request.status == 200) { console.log('New SG session, starting sync!'); sync(); }; request.withCredentials = true; request.send(JSON.stringify({"access_token": accessToken})); } 1 2 3 4 5 6 7 8 9 10 11 12 function startSyncGatewaySession ( accessToken ) { var request = new XMLHttpRequest ( ) ; request . open ( 'POST' , SYNC_GATEWAY_URL + '/_facebook' , true ) ; request . setRequestHeader ( 'Content-Type' , 'application/json' ) ; request . onreadystatechange = function ( ) { if ( request . readyState == 4 & & request . status == 200 ) { console . log ( 'New SG session, starting sync!' ) ; sync ( ) ; } ; request . withCredentials = true ; request . send ( JSON . stringify ( { "access_token" : accessToken } ) ) ; }

Note: It’s important to set request.withCredentials = true in a CORS request to save the cookie returned from Sync Gateway for future authenticated requests (push/pull replications).

Call it in startSessionAndSync passing the accessToken we got back from Facebook:

function startSessionAndSync(accessToken, userId) { migrateGuestToUser(userId); startSyncGatewaySession(accessToken); } 1 2 3 4 function startSessionAndSync ( accessToken , userId ) { migrateGuestToUser ( userId ) ; startSyncGatewaySession ( accessToken ) ; }

Wrap up

Now open up your browser, you can test todo items are syncing with another browser window opened or the native apps running TodoLite.

Users can still create tasks without being logged in. But the minute a user logs in, the tasks belong to the user’s list. Providing a guest account feature is one of the many benefits of building for offline-first capabilities.

Notice the TodoMVC list document is displayed as a task in Chrome. That’s because this example was using the allDocs query to display tasks.

In the next post, we’ll use Map/Reduce queries to add the multi-list capability and a profile document as well to share them with other users.

Read more: