Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Michael Kassing and Simon Free and Steve Johnson
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Michael Kassing , Simon Free@simonfree ) , and Steve Johnson@zombiefusion )

Syncing Local PouchDB Data With Remote IBM Cloudant Database In Angular 2.4.1

By Ben Nadel on

Last week, I was super excited to figure out how to provision IBM Cloudant / CouchDB databases during an Auth0 login transaction. This allows me to drive a database-per-user architecture on the remote side; and, thanks to PouchDB, I can do the same thing on the local side. The last piece of the puzzle was being able to keep the remote Cloudant database and the local PouchDB database in sync. Thankfully, the CouchDB replication API, leveraged by the PouchDB client library, makes this super easy!


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

Replication between two CouchDB-compatible databases happens in a single direction - a source database replicates to a target database. But, thanks to the magical master-master replication capabilities of CouchDB, bi-directional replication can be achieved by essentially performing uni-directional replication in both directions. In the PouchDB client library, they even provide a convenience method for this bi-directional replication setup, called .sync().


 
 
 

 
 Syncing data between a local PouchDB database and remote IBM cloudant database using CouchDB replication API. 
 
 
 

The .sync() method can perform replication in a discrete, one-off manor; or, it can perform "live" syncing in which the target database is "long polled" for updates that need to be replicated back down to the source database. In my exploration, I am performing discrete sync operations for a few reasons:

  • First, I don't believe that IBM Cloudant allows for "live sync" by default (though I also believe that I may be confusing the terminology, assuming that "continuous replication" and "live sync" are the same thing). Based on the documentation, it appears to be disabled for both performance and cost reasons - both of which relate to reducing network activity.
  • Second, I think it might make more sense from an "offline first" web application perspective to perform synchronization at specific points in the application life-cycle. Focus on the local experience first; then, figure out when it makes sense to sync to the remote database (balancing user experience with an actual dollars-and-cents cost).

To explore data synchronization across CouchDB instances, I took my previous Angular 2 demo in which I deferred PouchDB configuration until after login; then, I added some remote database credentials (hard-coded) and an explicit syncing function. In this case, since the demo has two "users", I went into Cloudant, provisioned two new databases and, for each database, assigned a new API Key that had read, write, and replicate permissions.

First, let's look at the Angular 2 root component. In this component, the user can log into the application using one of two hard-coded identities. Once "authenticated", both a local and remote PouchDB database are provisioned and the list of friends is loaded. The user can then interact with the list of friends, adding and deleting entires. These mutations are performed against the local PouchDB database only. When the user selects to "sync" the two databases, however, changes are pushed and pulled between the local and remote databases and the list of friends is re-rendered:

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  •  
  • // Import the application components and services.
  • import { FriendService } from "./friend.service";
  • import { IFriend } from "./friend.service";
  • import { ISyncResult } from "./pouchdb.service";
  • import { PouchDBService } from "./pouchdb.service";
  •  
  • interface IAddForm {
  • name: string;
  • }
  •  
  • @Component({
  • moduleId: module.id,
  • selector: "my-app",
  • styleUrls: [ "./app.component.css" ],
  • template:
  • `
  • <!-- BEIGN: Logged-out view. -->
  • <template [ngIf]="( user === null )">
  •  
  • <ul>
  • <li>
  • <a (click)="login( 'ben' )">Login as Ben</a>
  • </li>
  • <li>
  • <a (click)="login( 'kim' )">Login as Kim</a>
  • </li>
  • </ul>
  •  
  • </template>
  • <!-- END: Logged-out view. -->
  •  
  •  
  • <!-- BEIGN: Logged-in view. -->
  • <template [ngIf]="( user !== null )">
  •  
  • <p>
  • <strong>Logged-in as {{ user }}</strong>.
  • <a (click)="logout()">Logout</a>
  • &nbsp;|&nbsp;
  • <a (click)="syncData()">Sync remote database</a>.
  • </p>
  •  
  • <ul>
  • <li *ngFor="let friend of friends">
  • {{ friend.name }}
  • &mdash;
  • <a (click)="deleteFriend( friend )">Delete</a>
  • </li>
  • </ul>
  •  
  • <div class="form">
  •  
  • <input
  • type="text"
  • [value]="addForm.name"
  • (input)="addForm.name = $event.target.value"
  • (keydown.Enter)="processAddForm()"
  • />
  • <button type="button" (click)="processAddForm()">Add Friend</button>
  •  
  • </div>
  •  
  • </template>
  • <!-- END: Logged-in view. -->
  • `
  • })
  • export class AppComponent {
  •  
  • public addForm: IAddForm;
  • public friends: IFriend[];
  • public user: string;
  •  
  • private friendService: FriendService;
  • private pouchdbService: PouchDBService;
  •  
  •  
  • // I initialize the component.
  • constructor(
  • friendService: FriendService,
  • pouchdbService: PouchDBService
  • ) {
  •  
  • this.friendService = friendService;
  • this.pouchdbService = pouchdbService;
  •  
  • this.addForm = {
  • name: ""
  • };
  •  
  • // To start out, the Friends collection will be empty; and, it must remain
  • // empty until the user logs-in because, until then, the PouchDB database has
  • // not been configured and we won't know where to read data from.
  • this.friends = [];
  • this.user = null;
  •  
  • }
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I delete the given friend from the list.
  • public deleteFriend( friend: IFriend ) : void {
  •  
  • this.friendService
  • .deleteFriend( friend.id )
  • .then(
  • () : void => {
  •  
  • this.loadFriends();
  •  
  • },
  • ( error: Error ) : void => {
  •  
  • console.log( "Error:", error );
  •  
  • }
  • )
  • ;
  •  
  • }
  •  
  •  
  • // I login the user with the given identifier.
  • public login( userIdentifier: string ) : void {
  •  
  • // In order to keep this demo as simple as possible, I've already created the two
  • // remote databases in Cloudant (CouchDB as a Service) and have provisioned API
  • // keys for them (one for each remote database), which I am hard-coding here.
  • // When the user logs-in, they will be able to sync with the associated remote
  • // database in our database-per-user model.
  • // --
  • // NOTE: I am not doing any automatic sync because sync => HTTP requests to
  • // Cloudant, which has a dollars-and-cents cost to it. In each application, you
  • // have to figure out where the right balance of real-time syncing, usability,
  • // and cost can be found.
  • var demoCredentials = {
  • ben: {
  • local: {
  • identifier: "ben"
  • },
  • remote: {
  • url: "https://bennadel.cloudant.com/js-demo-pouchdb-cloudant-sync-ben",
  • key: "sedenawaysizediesettedur",
  • password: "3c9b6ca8303e9b34e42296c87a22aa1223ad7770"
  • }
  • },
  • kim: {
  • local: {
  • identifier: "kim"
  • },
  • remote: {
  • url: "https://bennadel.cloudant.com/js-demo-pouchdb-cloudant-sync-kim",
  • key: "tintralowallsedidiatedis",
  • password: "209e6040a87352e428fcb3c8f6b922924c300ddc"
  • }
  • }
  • };
  •  
  • // Now that a new user is logging in, we want to teardown any existing PouchDB
  • // database and reconfigure a new PouchDB setup for the the current user. This
  • // includes both the local database as well as the remote CouchDB (Cloudant)
  • // database acting as our remote replica. This way, each user gets their own
  • // database in our database-per-user model.
  • // --
  • // CAUTION: For simplicity, this is in the app-component; but, it should probably
  • // be encapsulated in some sort of "session" service.
  • this.pouchdbService.configureForUser( demoCredentials[ userIdentifier ] );
  • this.user = userIdentifier;
  •  
  • // Once the new database is configured (synchronously), load the user's friends.
  • this.loadFriends();
  •  
  • }
  •  
  •  
  • // I log the current user out.
  • public logout() : void {
  •  
  • // When logging the user out, we want to teardown the currently configured
  • // PouchDB database. This way, we can ensure that rogue asynchronous actions
  • // aren't going to accidentally try to interact with the database.
  • this.pouchdbService.teardown();
  • this.user = null;
  •  
  • this.friends = [];
  •  
  • }
  •  
  •  
  • // I process the "add" form, creating a new friend with the given name.
  • public processAddForm() : void {
  •  
  • if ( ! this.addForm.name ) {
  •  
  • return;
  •  
  • }
  •  
  • this.friendService
  • .addFriend( this.addForm.name )
  • .then(
  • ( id: string ) : void => {
  •  
  • console.log( "New friend added:", id );
  •  
  • this.loadFriends();
  • this.addForm.name = "";
  •  
  • },
  • ( error: Error ) : void => {
  •  
  • console.log( "Error:", error );
  •  
  • }
  • )
  • ;
  •  
  • }
  •  
  •  
  • // I sync the local PouchDB database with the remote CouchDB / Cloudant database. If
  • // new documents are pulled down, I reload the list of friends to help keep the user
  • // interface up-to-date.
  • public syncData() : void {
  •  
  • console.info( "Synchronizing remote database." );
  •  
  • this.pouchdbService
  • .sync()
  • .then(
  • ( results: ISyncResult ) : void => {
  •  
  • // When we "sync" the two databases, documents may move in either
  • // direction - Push or Pull. And, since this is performed using
  • // "bulk" operations, it's possible that some of the documents will
  • // create errors (version conflicts) while each overall request still
  • // completes successfully.
  • console.group( "Remote sync completed." );
  • console.log( "Docs pulled:", results.pull.docs.length );
  • console.log( "Docs pushed:", results.push.docs.length );
  • console.log( "Errors:", ( results.pull.errors.length + results.push.errors.length ) );
  • console.groupEnd();
  •  
  • // We don't really care if we PUSHED docs to the remote server; but,
  • // if we PULLED new docs down, we'll want to re-render the list of
  • // friends to display the newly acquired documents.
  • if ( results.pull.docs.length ) {
  •  
  • console.log( `Since we pulled ${ results.pull.docs.length } docs, re-render friends.` );
  • this.loadFriends();
  •  
  • }
  •  
  • // Since replication / syncing is performed using bulk operations,
  • // it's possible that some of the documents failed to replicate due
  • // to version conflicts - warn for errors.
  • if ( results.pull.errors.length || results.push.errors.length ) {
  •  
  • console.warn( "Some of the documents resulted in error:" );
  • console.log( results.pull.errors );
  • console.log( results.push.errors );
  •  
  • }
  •  
  • },
  • ( error: any ) : void => {
  •  
  • console.warn( "Remote sync failed, critically." );
  • console.error( error );
  •  
  • }
  • )
  • ;
  •  
  • }
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I load the persisted friends collection into the list.
  • private loadFriends() : void {
  •  
  • this.friendService
  • .getFriends()
  • .then(
  • ( friends: IFriend[] ) : void => {
  •  
  • // NOTE: Since the persistence layer is not returning the data
  • // in any particular order, we're going to explicitly sort the
  • // collection by name.
  • this.friends = this.friendService.sortFriendsCollection( friends );
  •  
  • },
  • ( error: Error ) : void => {
  •  
  • console.log( "Error", error );
  •  
  • }
  • )
  • ;
  •  
  • }
  •  
  • }

If you look at the syncData() method, you can see that the results are divided into "Push" and "Pull". This is because document updates are moving in both directions. The syncing operation is performed, in part, using the "bulk" API. And, as we saw in a previous post, some documents in a bulk operation may result in version conflict errors (a byproduct of master-master replication). As such, the syncing result provides both the successful documents and the subset of errors.

The Sync results, consumed in the root component, are not the raw results returned from the sync method of the PouchDB database. Internally, the PouchDB sync() operation results in an event stream that works for both "live" replication (aka, continuous replication) and discrete replication. To simplify the interaction, however, I'm aggregating the "change" events (one for each replication direction) and the "complete" event in a Promise that resolves in the composite result you see above.

In the PouchDBService, you can see the local and remote PouchDB databases being configured and the logic for the .sync() operation:

  • // Import the core angular services.
  • // --
  • // The PouchDB library is delivered as a CommonJS module and I am not yet sure how to
  • // configure my System.js setup to allow for a more simple import statement. This is the
  • // only thing that I can get to work at this time.
  • // --
  • // CAUTION: TypeScript still complains, "Cannot find module 'pouchdb'."
  • import * as PouchDB from "pouchdb";
  •  
  • // Import the application components and services.
  • import { IPouchDBSyncChangeResult } from "./pouchdb.interfaces";
  • import { IPouchDBSyncCompleteResult } from "./pouchdb.interfaces";
  •  
  • interface IUserCredentials {
  • local: {
  • identifier: string;
  • };
  • remote: {
  • url: string;
  • key: string;
  • password: string;
  • };
  • }
  •  
  • export interface ISyncResult {
  • pull: {
  • docs: any[],
  • errors: any[]
  • };
  • push: {
  • docs: any[],
  • errors: any[]
  • };
  • };
  •  
  • export class PouchDBService {
  •  
  • private localDatabase: any;
  • private pendingSync: Promise<ISyncResult>;
  • private remoteDatabase: any;
  •  
  •  
  • // I initialize the service.
  • constructor() {
  •  
  • this.localDatabase = null;
  • this.pendingSync = null;
  • this.remoteDatabase = null;
  •  
  • }
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I teardown any existing PouchDB instance and configure a new one for the given
  • // user identifier. All subsequent calls to getDB() will return the newly configured
  • // PouchDB instance.
  • public configureForUser( userCredentials: IUserCredentials ) : void {
  •  
  • this.teardown();
  •  
  • this.localDatabase = new PouchDB( this.getDatabaseName( userCredentials.local.identifier ) );
  • this.remoteDatabase = new PouchDB(
  • userCredentials.remote.url,
  • {
  • auth: {
  • username: userCredentials.remote.key,
  • password: userCredentials.remote.password
  • },
  • // The database already exists - no need for PouchDB to check to see
  • // if it exists (and try to create it). This saves on some API requests.
  • skip_setup: true
  • }
  • );
  •  
  • // NOTE: We are not doing any active Sync / replication when the two databases
  • // are configured; for this demo, it is being performed explicitly in the root
  • // component by the user.
  •  
  • console.warn( "Configured new PouchDB database for,", this.localDatabase.name );
  •  
  • }
  •  
  •  
  • // I get the active PouchDB instance. Throws an error if no PouchDB instance is
  • // available (ie, user has not yet been configured with call to .configureForUser()).
  • public getDB() : any {
  •  
  • if ( ! this.localDatabase ) {
  •  
  • throw( new Error( "Database is not available - please configure an instance." ) );
  •  
  • }
  •  
  • return( this.localDatabase );
  •  
  • }
  •  
  •  
  • // I sync the remote and local CouchDB databases. Returns a promise that results to
  • // a custom sync result.
  • public sync() : Promise<ISyncResult> {
  •  
  • // If there's already a sync operation in progress, just return the pending
  • // Promise. This will provide light throttling of sync requests.
  • if ( this.pendingSync ) {
  •  
  • return( this.pendingSync );
  •  
  • }
  •  
  • var promise = this.pendingSync = new Promise(
  • ( resolve, reject ) : void => {
  •  
  • var result: ISyncResult = {
  • pull: {
  • docs: [],
  • errors: []
  • },
  • push: {
  • docs: [],
  • errors: []
  • }
  • };
  •  
  • this.localDatabase.sync( this.remoteDatabase )
  • // A change event is emitted for each direction - one for "push" and
  • // one for "pull"; but, only if there are changes for that direction.
  • // We want to aggregate the change events, so when each one happens,
  • // we'll just overwrite the "direction" results.
  • .on(
  • "change",
  • ( eventValue: IPouchDBSyncChangeResult ) : void => {
  •  
  • result[ eventValue.direction ].docs = eventValue.change.docs;
  • result[ eventValue.direction ].errors = eventValue.change.errors;
  •  
  • }
  • )
  • // The complete event just shows some overall stats about the sync
  • // operation that could have been deduced, in part, by the various
  • // "change" events that were fired.
  • .on(
  • "complete",
  • ( eventValue: IPouchDBSyncCompleteResult ) : void => {
  •  
  • // We don't actually need any of the data from the completed
  • // event - it just signified that the result has been fully
  • // populated.
  • resolve( result );
  •  
  • // Once the sync operation has completed, clear out the
  • // pending promise. This won't affect the out-of-scope
  • // references to it; but, it will allow new sync operations
  • // to be initiated.
  • this.pendingSync = null;
  •  
  • }
  • )
  • // An error event signifies a critical error - not a document-level
  • // problem in the bulk operations.
  • .on(
  • "error",
  • ( eventValue: any ) : void => {
  •  
  • reject( eventValue );
  •  
  • // Allow new sync operations to be initiated.
  • this.pendingSync = null;
  •  
  • }
  • )
  • ;
  •  
  • }
  • );
  •  
  • return( promise );
  •  
  • }
  •  
  •  
  • // I teardown / deconfigure the existing database instance (if there is one).
  • // --
  • // CAUTION: Subsequent calls to .getDB() will fail until a new instance is configured
  • // with a call to .configureForUser().
  • public teardown() : void {
  •  
  • if ( ! this.localDatabase ) {
  •  
  • return;
  •  
  • }
  •  
  • this.pendingSync = null;
  •  
  • this.localDatabase.close();
  • this.localDatabase = null;
  •  
  • this.remoteDatabase.close();
  • this.remoteDatabase = null;
  •  
  • }
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I return a normalized database name for the given user identifier.
  • private getDatabaseName( userIdentifier: string ) : string {
  •  
  • // Database naming restrictions from https://wiki.apache.org/couchdb/HTTP_database_API
  • // --
  • // A database must be named with all lowercase letters (a-z), digits (0-9), or
  • // any of the _$()+-/ characters and must end with a slash in the URL. The name
  • // has to start with a lowercase letter (a-z)... Uppercase characters are NOT
  • // ALLOWED in database names.
  • var dbName = userIdentifier
  • .toLowerCase()
  • .replace( /[^a-z0-9_$()+-]/g, "-" )
  • ;
  •  
  • return( "js-demo-pouchdb-cloudant-sync-angular2-" + dbName );
  •  
  • }
  •  
  • }

Now, if I run the application in two different Chrome tabs (one using Incognito mode so that it uses a different IndexedDB storage), and I mutate both collections of friends, you can see documents being both pushed and pulled during synchronization:


 
 
 

 
 Syncing data between PouchDB and a remote CouchDB database requires a lot of network activity. 
 
 
 

As you can see, the sync operation pushed 7 documents from the local PouchDB database to the remote Cloudant database and pulled 2 documents down. And, even though the replication API uses "bulk" operations to perform the document transfer, you can see that there is quite a bit of network activity needed to figure out which documents have yet to be synchronized. And, this is for one operation - imagine that you were continuously syncing between two databases; it's easy to see how network activity volume needs to be taken into account, especially for an "offline first" application.

And, of course, let's not forget that since IBM Cloudant is a "CouchDB as a Service" (DaaS) provider, every API call that I make to the remote database has a real-world dollars-and-cents implication:


 
 
 

 
 IBM Cloudant costs per API call. 
 
 
 

Thankfully, Cloudant doesn't actually charge you unless you go over $50 / month in API costs. So, for demo purposes, I should be in the clear. But, this is certainly something you need to consider when you are devising a data synchronization strategy for your application.

Now, in this demo, the FriendService only interacts with the local PouchDB instance, which is configured after the user authentications. So, it's not really relevant to the point of this blog post - PouchDB / CouchDB synchronization. But, in the spirit of completeness, here is the FriendService implementation:

  • // Import the core angular services.
  • import { Injectable } from "@angular/core";
  •  
  • // Import the application components and services.
  • import { IPouchDBAllDocsResult } from "./pouchdb.interfaces";
  • import { IPouchDBGetResult } from "./pouchdb.interfaces";
  • import { IPouchDBPutResult } from "./pouchdb.interfaces";
  • import { IPouchDBRemoveResult } from "./pouchdb.interfaces";
  • import { PouchDBService } from "./pouchdb.service";
  •  
  •  
  • export interface IFriend {
  • id: string;
  • name: string;
  • }
  •  
  • interface IPouchDBGetFriendResult extends IPouchDBGetResult {
  • name: string;
  • }
  •  
  •  
  • @Injectable()
  • export class FriendService {
  •  
  • private pouchdbService: PouchDBService;
  •  
  •  
  • // I initialize the Friend service.
  • constructor( pouchdbService: PouchDBService ) {
  •  
  • // Rather than constructing a PouchDB instance directly, we're going to use the
  • // PouchDBService to provide a database instance on the fly. This way, the
  • // configuration for the PouchDB instance can be changed at any point during the
  • // application life-cycle. Each database interaction starts with a call to
  • // this.getDB() to access the "current" database rather than a cached one.
  • this.pouchdbService = pouchdbService;
  •  
  • }
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I add a new friend with the given name. Returns a promise of the generated id.
  • public addFriend( name: string ) : Promise<string> {
  •  
  • // NOTE: All friends are given the key-prefix of "friend:". This way, when we go
  • // to query for friends, we can limit the scope to keys with in this key-space.
  • var promise = this.getDB()
  • .put({
  • _id: ( "friend:" + Date.now() ),
  • name: name
  • })
  • .then(
  • ( result: IPouchDBPutResult ) : string => {
  •  
  • return( result.id );
  •  
  • }
  • )
  • ;
  •  
  • return( promise );
  •  
  • }
  •  
  •  
  • // I delete the friend with the given id. Returns a promise.
  • public deleteFriend( id: string ) : Promise<void> {
  •  
  • this.testId( id );
  •  
  • // NOTE: For the "delete" action, we need to perform a series of database calls.
  • // In reality, these will be "instantaneous". However, philosophically, these are
  • // asynchronous calls. As such, I am storing the DB reference in a function-local
  • // variable in order to ensure that both database calls - that compose the one
  • // workflow - are made on the same database. This eliminates the possibility that
  • // the "current database" may change in the middle of these chained actions.
  • var db = this.getDB();
  •  
  • // When we delete a document, we have to provide a document that contains, at
  • // least, the "_id" and the "_rev" property. Since the calling context doesn't
  • // have this, we'll use the .get() method to get the current doc, then use that
  • // result to delete the winning revision of the document.
  • var promise = db
  • .get( id )
  • .then(
  • ( doc: IPouchDBGetFriendResult ) : any => {
  •  
  • return( db.remove( doc ) );
  •  
  • }
  • )
  • .then(
  • ( result: IPouchDBRemoveResult ) : void => {
  •  
  • // Here, I'm just stripping out the result so that the PouchDB
  • // response isn't returned to the calling context.
  • return;
  •  
  • }
  • )
  • ;
  •  
  • return( promise );
  •  
  • }
  •  
  •  
  • // I get the collection of friends (in no particular sort order). Returns a promise.
  • public getFriends() : Promise<IFriend[]> {
  •  
  • var promise = this.getDB()
  • .allDocs({
  • include_docs: true,
  •  
  • // In PouchDB, all keys are stored in a single collection. So, in order
  • // to return just the subset of "Friends" keys, we're going to query for
  • // all documents that have a "friend:" key prefix. This is known as
  • // "creative keying" in the CouchDB world.
  • startkey: "friend:",
  • endKey: "friend:\uffff"
  • })
  • .then(
  • ( result: IPouchDBAllDocsResult ) : IFriend[] => {
  •  
  • // Convert the raw data storage into something more natural for the
  • // calling context to consume.
  • var friends = result.rows.map(
  • ( row: any ) : IFriend => {
  •  
  • return({
  • id: row.doc._id,
  • name: row.doc.name
  • });
  •  
  • }
  • );
  •  
  • return( friends );
  •  
  • }
  • )
  • ;
  •  
  • return( promise );
  •  
  • }
  •  
  •  
  • // I sort the given collection of friends (in place) based on the name property.
  • public sortFriendsCollection( friends: IFriend[] ) : IFriend[] {
  •  
  • friends.sort(
  • function( a: IFriend, b: IFriend ) : number {
  •  
  • if ( a.name.toLowerCase() < b.name.toLowerCase() ) {
  •  
  • return( -1 );
  •  
  • } else {
  •  
  • return( 1 );
  •  
  • }
  •  
  • }
  • );
  •  
  • return( friends );
  •  
  • }
  •  
  •  
  • // I test the given id to make sure it is valid for the Friends key-space. Since all
  • // PouchDB documents are stored in a single collection, we have to ensure that the
  • // given ID pertains to the subset of documents that represents Friends. If the id is
  • // valid, I return quietly; otherwise, I throw an error.
  • public testId( id: string ) : void {
  •  
  • if ( ! id.startsWith( "friend:" ) ) {
  •  
  • throw( new Error( "Invalid Id" ) );
  •  
  • }
  •  
  • }
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I return the currently-configured PouchDB instance.
  • private getDB() : any {
  •  
  • return( this.pouchdbService.getDB() );
  •  
  • }
  •  
  • }

All in all, this is super exciting! I feel like I actually have a solid roadmap for building an "offline first" application in Angular 2. Between Auth0's user management and authentication, IBM Cloudant's "CouchDB as a Service" offering, and now - painless master-master replication between the local and remote PouchDB databases, it's just a matter of putting in the effort and building out the application.



Looking For A New Job?

Ooops, there are no jobs. Post one now for only $29 and own this real estate!

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

Seriously, you are adding a ton of value to the ecosystem with these Auth0/pouchdb posts Ben. I am planning to implement a mobile app using Ionic 2, and the docs from Auth0 are maddening. Either things are out of date, or there's little typescript support, or things just don't work when trying to do the auth on an actual device.

Your post on passwordless auth opened my eyes to a method that is *so* much simpler than traditional "redirect" auth. On top of that these posts about PouchDB are helping me see a completely new way to build simple apps without creating a bunch of API infrastructure just to (basically) sync data between clients. For example, I have an older Angular 2 app that displays photos for family (it's angular 2 because it has some interactive functionality to it). To support that I have a full .NET API for simple CRUD operations, but I think it could be represented with a simple PouchDB where write-access is limited to me (using Auth0 of course!), and clients simply sync up when loading the page. There's the cost to watch of course, but that would just be so much less infra to maintain...

Anyway...keep up the great work and I'm really looking forward to your posts about this app you're working on.

Reply to this Comment

@Sam,

So awesome my man - I really appreciate the kind words. Glad this stuff is being helpful. And yeah, the documentation is frustrating. At first, it seems really robust; then you see it's kind of scattered here and there and things don't seem to be fully documented. For example, several of the demos refer to that "geoip" request object ... and yet, it isn't documented anywhere.

I'm also super interested in Ionic as well, but have never actually dug into it. I believe it's built on top of the Cordava / Phone Gap project, right? That's really appealing because as much as I want to built a progressive web app, it's [PWA] is still not a technology that is supported by Safari Mobile... and, I have an iPhone. And, I can't save Chrome links to the homepage. Which means that I don't get that smooth app-like experience. But, I think something like Ionic would give me the native App feel, but using the PWA technologies. But, like I said, I don't really know any of the details.

Right now, I'm actually digging into WebTask.io, which is where the Auth0 Rules engine is run. WebTask.io is basically a "Function as a Service" platform for being able to run server-side scripts, which it sounds like it might be right up your alley. But, I'm just tinkering.

That passwordless stuff has been great for me. I wanted to originally use something like a social login; but, then I was concerned that you couldn't use two different social logins to access the same data. But, people's phone numbers almost never change these days (your mobile number feels like your SSN these days :P). So, it seems like a natural way to allow auth, especially on something geared towards mobile devices.

Anyway, keep on rockin' it!

Reply to this Comment

I'm super stoked to try to consume the blog posts you've been writing. I'm working on a Ionic 2 app and its currently using SuperLogin, SuperLogin-Client, Cloudant and PouchDB. I wanted to swap out Superlogin for Auth0 but I couldn't find any help on how to do it.

Reply to this Comment

@Dan,

Awesome! Good luck, I'm sure we'll be learning a lot of this in parallel; so, if I run into any roadblocks, you can be sure I'll write up what I find :)

Reply to this Comment

Wow! Thank you for this extensive demonstration. It's great to see that it already helped a couple of people here. I wouldn't have thought how helpful this would be for actual devices. As I read in the posts, Sam and Dan can use the knowledge that you passed on with your demonstration for their apps. That's awesome!

Keep up the great work!

Reply to this Comment

Thanks for the great write up Ben. Just a quick note that Cloudant now offers capacity-based pricing (see https://www.ibm.com/blogs/bluemix/2016/09/new-cloudant-lite-standard-plans-are-live-in-bluemix-public/) which should be much friendlier to sync use cases than per-request pricing.

Also, I'm curious about your "live sync" comment - Cloudant should behave the same as CouchDB in relation to replication so it would be useful to know if you found a problem/restriction.

Reply to this Comment

@Will,

Awesome news! I was searching for a plan B. I really didn't want to leave Cloudant.

Reply to this Comment

@Will,

Thanks for the link - I'll take a look at the pricing. As far as live-sync, I think the issue is just that it makes more requests to the remote server. It's not that the behavior is any different from CouchDB -- it's just that with a DB-as-a-Service (well, this one specifically), you're paying for API requests, not just data storage. So, the point was that if I can reduce syncing to specific "batch" jobs, rather than trying to keep the local / remote in sync all the time, it will be less API calls which means less cost. So, it was purely a cost issue, not a behavioral one.

Reply to this Comment

@Lara,

My pleasure! It's a really interesting approach, and to be clear, I'm still in R&D mode, so I can't truly say that any of this is "battle tested". But, it's great to be able to have super fast DB access locally; then, when needed, still be able to sync to a remote server and subsequently back down to other devices.

Reply to this Comment

@Ben,

I got Auth0 working with a normal username/password sign up process with the help of your blog post series on the topic with Ionic!! Its pretty awesome. I used the Auth0 authentication API.

Would you mind if I wrote a blog post on my site about how I got that working and referenced your code/blog series? Most of the credit belongs to you for showing how this could work.

Thanks!

Reply to this Comment

@Ben,

I posted the blog series here:

http://www.dannguyen.io/2017/02/10/auth0-cloudant-pouchdb-ionic/

That took forever to write. Thanks for writing your series. Couldn't have done it without your help!

Reply to this Comment

I want to developp an angular app with CouchDB or Cloudant as a database .

Can I use directly the CouchDB or Cloudant Database and query directly from
there without using any serverside backed such as node js +(Nano or Cradle
or node-couchdb)

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.