Skip to main content
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Brad Wood
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Brad Wood ( @bdw429s )

Provisioning Cloudant / CouchDB Databases From Auth0 For A Database-Per-User Architecture In Angular 2.4.1

By on

In the CouchDB world, there is an emergent architectural pattern known as the "database per user" pattern. In this pattern, every user of your application has their own single-tenant database. In CouchDB, databases are just flat files (more or less); so, provisioning a new database for a new user isn't as crazy as it sounds. But, only server administrators can provision new databases. And, without a backend server to act as the administrator, I'm experimenting with the use of Auth0's authorization pipeline as a means to provision new Cloudant (CouchDB as a Service) databases when a new user authenticates with Auth0.

Run this demo in my JavaScript Demos project on GitHub.

CouchDB security is interesting. To be honest, I'm still trying to wrap my head around it. And, to make matters a bit murkier, I'm using IBM Cloudant, which I believe adds its own layer of security logic on top of the underlying CouchDB logic. With Cloudant, for example, there is no "admin party", as far as I can tell. And, it seems that databases are locked down even before you associate any users with the database. So, in that regard, using Cloudant means I'm probably less likely to shoot myself in the foot - but, again, I'm still learning here.

Since I'm building an offline-first app that has no backend-server, other than Auth0 (probably) for user management, I need to rely on Auth0 to provision new Cloudant / CouchDB databases when new users sign-up. Luckily, the Auth0 Rules engine executes in Node.js (v4.5.5 at the time of this writing), which means that we can make external HTTP requests to IBM Cloudant whenever the user signs-up or logs-in:

Auth0 Cloudant database provisioning workflow with Angular 2.

The Auth0 Rules engine is implemented in WebTask.io, which means that there are hundreds of Node modules ready to be require()'d. And, in fact, one of them happens to be Cloudant v1.4.1. The Cloudant node library is a wrapper around the Nano library, which is a minimal CouchDB driver for Node.js. The Cloudant library augments the Nano API with Cloudant-specific features like API Key management and Search functions. This means that we don't have to deal directly with HTTP requests; instead, we can simply instantiate a Cloudant Client from within our Rules engine.

Now, in order for Auth0 to act as my Cloudant server administrator, it needs to have access to Cloudant credentials. Unfortunately, at this time, there is no way to create a secondary server administrator. As such, I have to tell Auth0 about the root login for my IBM Cloudant account (yikes!). But, I definitely don't want to store those credentials in my code. Luckily, Auth0 allows you to store encrypted configuration data along with your Rule definitions:

Auth0 configuration values for the authorization Rules engine.

These encrypted values will then be exposed in the sandboxed Rules engine - in Node.js - as part of the global "configuration" object. This allows me to keep my Rules in version control without hard-coding my Cloudant credentials.

Once we have our Cloudant configuration data available, our "database provisioning" Rule will execute the following plan:

  1. Check for cached credentials - if they exist, return the credentials.
  2. Otherwise, create a new Cloudant database for the current user.
  3. Create a new API Key for the current user.
  4. Grant read, write, replication permissions for the new API Key on the new Database.
  5. Cache the credentials in the user's app_metadata (for subsequent logins).
  6. Return the credentials.

Since the following experiment is part of a public demo, I'm not actually going to create a new database for every user. Instead, I'm going to MD5-hash the user's email and then bucket the users based on the first character of their email hash. This way, I don't create a ton of noise in my Cloudant test account.

The following Auth0 Rule attempts to build up a "couchDB" object that it caches in the user's Auth0 "app_metadata". This is the read-only data that is persisted across login transactions for the given user. This app_metadata is then [naturally] available in the User Profile that we can access after the user authenticates.

CAUTION: This code is a proof-of-concept and can probably be improved. For example, this code makes no differentiation between failed API requests and failed HTTP requests. Perhaps I could add things like retry-logic to help with failed HTTP requests.

function addCloudantInfo( user, context, callback ) {

	// CAUTION: Since all of my JavaScript Demos are going through the same Auth0
	// account, I need to make the demos "forward compatible". As such, each Rule will
	// be tied to a specific client ID that I'll be creating anew for each demo. This
	// way, only the rules associated with a particular demo will be applied to the
	// authenticating user.
	if ( context.clientID !== "BxjlFCNofp00qollw06emBULwBCxkELn" ) {

		return( callback( null, user, context ) );

	}

	// Ensure application meta-data exists.
	user.app_metadata = ( user.app_metadata || {} );

	// If the user already has a CouchDB database provisioned, move on to the next Rule -
	// no need to do any work!
	if ( user.app_metadata.couchDB ) {

		return( callback( null, user, context ) );

	}


	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //


	// If we've made it this far, the user does NOT yet have a CouchDB database cached
	// in their app_metadata. As such, we'll need to provision one if it doesn't exist
	// and create login credentials for it (ie, an API key).

	// Require the core node modules.
	var Cloudant = require( "cloudant@1.4.1" );
	var crypto = require( "crypto" );
	var Q = require( "q@1.4.1" );

	// Create our Cloudant API client - this is a thin wrapper around Nano - a minimal
	// CouchDB driver for Node.js. Cloudand adds additional Cloudant-specific
	// functionality like API key, CORS, and Search methods.
	// --
	// Cloudant: https://github.com/cloudant/nodejs-cloudant/tree/627095c3c71b5af503e295f3be7db30809335676#authorization-and-api-keys
	// Nano: https://github.com/apache/couchdb-nano
	// --
	// NOTE: I'm using a specific commit for Cloudant since only v1.4.1 is available
	// inside WebTask.io, which is where this Rule will execute.
	var cloudant = new Cloudant({
		account: configuration.CloudantApiUsername,
		password: configuration.CloudantApiPassword
	});

	// Prepare the CouchDB configuration that we're going to build and cache.
	var couchDB = {
		host: configuration.CloudantHost,
		name: getDatabaseName( user ),
		key: "",
		password: ""
	};

	Q.when()
		// First, we're going to create the CouchDB database on Cloudant, if it doesn't
		// exist; or, return the existing one for this user.
		.then(
			function handleResolve() {

				return( provisionDatabase( couchDB.name ) );

			}
		)
		// Once we have the database, we're going to provision a new API key for this
		// user and grant Read, Write, and Replicator access to the above database.
		.then(
			function handleResolve( result ) {

				return( provisionApiKey( couchDB.name ) );

			}
		)
		// Once we have the API key provisioned, we'll want to cache it with the
		// app_metadata so that it will be available across logins (for this user).
		.then(
			function handleResolve( result ) {

				couchDB.key = result.key;
				couchDB.password = result.password;

				// Inject the CouchDB into the app_metadata for this login transaction -
				// this will make it available on the next get-profile request.
				// --
				// CAUTION: The content of the app_metadata is APPENDED to the user
				// Profile in addition to be being made available as the app_metadata key.
				// As such, be careful not to overwrite any core Profile properties.
				user.app_metadata.couchDB = couchDB;

				// Persist the augmented app_metadata across logins.
				return( setAppMetadata( user.user_id, user.app_metadata ) );

			}
		)
		// Once the CouchDB credentials have been persisted to the app_metadata, we can
		// move onto the next authorization rule.
		.then(
			function handleResolve( result ) {

				callback( null, user, context );

			}
		)
		.catch(
			function handleError( error ) {

				console.log( "Something went wrong with authorization:" );
				console.log( error );
				console.log( ( error && error.stack ) || "No stacktrace available." );

				// If something went wrong during the database provisioning, then we
				// can't let the user into the application, even if they authenticated
				// properly - they will need to try and login again so that the database
				// can try to be provisioned again.
				callback( new UnauthorizedError( "Database could not be provisioned at this time, please try to login again." ) );

			}
		)
	;


	// ------------------------------------------------------------------------------- //
	// Workflow Methods.
	// ------------------------------------------------------------------------------- //


	// I create a new API key and assign it permissions to the given database. Returns
	// a promise that resolves to the new API key credentials.
	function provisionApiKey( name ) {

		var apiKeyResult = null;

		var promise = Q
			.all([
				createApiKey(), // Create a new API key.
				getSecurity( name ) // Get the current permissions for the given database.
			])
			.spread(
				function handleResolve( apiKey, security ) {

					// Hold on to the key so that we can return it in the final resolve.
					apiKeyResult = apiKey;

					// Grant the new API key permissions on the given database.
					security[ apiKey.key ] = [ "_reader", "_writer", "_replicator" ];

					// Persist the permissions back to Cloudant.
					return( setSecurity( name, security ) );

				}
			)
			// Once the security has been persisted, resolve with the API key so that we
			// can expose the new credentials.
			.then(
				function handleResolve( result ) {

					return( apiKeyResult );

				}
			)
		;

		return( promise );

	}


	// I create the given database if doesn't exist. Resolves with "get" promise.
	function provisionDatabase( name ) {

		// Rather than trying to GET the database first to see if it exists, let's just
		// try to create it and let the resultant error indicate that it already exists.
		// This is less efficient, but more straightforward.
		var promise = createDatabase( name ).then(
			function handleResult( result ) {

				// If it was just created, return the NEW database.
				return( getDatabase( name ) );

			},
			function handleError( error ) {

				// If it already existed, return the EXISTING database.
				return( getDatabase( name ) );

			}
		);

		return( promise );

	}


	// ------------------------------------------------------------------------------- //
	// Utility Methods.
	// ------------------------------------------------------------------------------- //


	// General note about Q (Promise) and Cloudant -- the Callback for API calls through
	// the Cloudant lib are invoked with two parameters:
	// --
	// - Body: The HTTP response body from CouchDB, if no error. JSON or binary.
	// - Header: The HTTP response header from CouchDB, if no error.
	// --
	// The problem with this is that if the Q (our Promise library) proxy methods are
	// invoked with more than one resolution parameter, Q will resolve the promise with
	// an Array of the given values. Since I only want the first parameter - the body -
	// I will pluck the first result with .get(0). For example:
	// --
	// Q.ninvoke( cloudant, "some_method" ).get( 0 )
	// --


	// I generate a new API key for Cloudant security. Returns a promise.
	function createApiKey() {

		// Resolves to type:
		// {
		// key: string;
		// password: string;
		// ok: boolean;
		// }
		var promise = Q
			.ninvoke( cloudant, "generate_api_key" )
			.get( 0 )
		;

		return( promise );

	}


	// I create a new database with the given name.
	function createDatabase( name ) {

		// Resolves to type:
		// {
		// ok: boolean;
		// }
		var promise = Q
			.ninvoke( cloudant.db, "create", name )
			.get( 0 )
		;

		return( promise );

	}


	// I get the database with the given name.
	function getDatabase( name ) {

		// Resolves to type:
		// {
		// db_name: string;
		// doc_count: number;
		// ... several others ...
		// }
		var promise = Q
			.ninvoke( cloudant.db, "get", name )
			.get( 0 )
		;

		return( promise );

	}


	// I calculate the normalized database name that should be used for the user with
	// the given email address. Since databases are based on physical file storage,
	// database names are constrained to the lowest common denominator for all operating
	// systems:
	// --
	// - All lower case.
	// - Must start with a-z.
	// - Can only have a-z, 0-9, and any of following ==> _$()+-/ <==.
	// --
	function getDatabaseName( user ) {

		// Since this is a PUBLIC DEMO, I don't want to end up creating thousands of
		// databases as people try this out. As such, I'm going to bucket the databases
		// by the first letter of the hash of the email.
		var hash = crypto
			.createHash( "md5" )
			.update( user.email )
			.digest( "hex" )
			.toLowerCase()
		;

		var matches = hash.match( /[a-z]/ );
		var letter = ( ( matches && matches[ 0 ] ) || "a" );

		return( "email-hash--" + letter );

	}


	// I get the security settings for the given database.
	function getSecurity( name ) {

		// The Security API has a weird asymmetry. When you SET the security, it expects
		// an object containing the users / keys being assigned to the database. But,
		// when you GET the security, it returns the settings name-spaced to "cloudant".
		// I believe this is because Cloudant applies its own layer of security on top
		// of the CouchDB security. In order to normalize the API, we're going to pluck
		// the "cloudant" key out of the result so that the GET and SET operations both
		// use an object containing security assignments.
		var promise = Q
			.ninvoke( cloudant.use( name ), "get_security" )
			.spread(
				function( result, headers ) {

					// CAUTION: If the database was just provisioned, it won't have the
					// "cloudant" name-space on it yet. On subsequent requests, however,
					// it may be defined if the security has been updated.
					return( result.cloudant || {} );

				}
			)
		;

		return( promise );

	}


	// I set the app_metadata for the given Auth0 user_id.
	function setAppMetadata( userID, appMetadata ) {

		var promise = Q.ninvoke( auth0.users, "updateAppMetadata", userID, appMetadata );

		return( promise );

	}


	// I set the security configuration for the given database.
	function setSecurity( name, security ) {

		// Resolves to type:
		// {
		// ok: boolean;
		// }
		var promise = Q
			.ninvoke( cloudant.use( name ), "set_security", security )
			.get( 0 )
		;

		return( promise );

	}

}

There's a lot of code there! The concept is simple; but, with asynchronous code, nothing is ever that easy. I'm using the Q Promise library to try and keep things under control. And, I tried to write the code in a top-down readable manner - algorithm, followed by workflow branches, followed by utility methods. That said, I left a lot of comments in the JavaScript, so I'll leave it up to you to noodle on it further.

Once we have this Auth0 Rule in place, we can then try to access the provisioned CouchDB / Cloudant database from within our Angular 2 application. To keep things simple, I'm not actually going to consume the CouchDB credentials; instead, in my Angular 2 root component, I'm just going to dump them out to the screen, after authentication, so you can see what would have been available.

// Import the core angular services.
import { Component } from "@angular/core";

// Import the application components and services.
import { AuthenticationService } from "./authentication.service";
import { IAuthorization } from "./authentication.service";
import { IProfile } from "./authentication.service";

@Component({
	moduleId: module.id,
	selector: "my-app",
	styleUrls: [ "./app.component.css" ],
	template:
	`
		<strong>Email:</strong>
		<input type="text" [value]="email" (input)="email = $event.target.value;" />
		<input type="button" value="Send Email" (click)="sendEmail()" />

		<br /><br />

		<strong>Code:</strong>
		<input type="text" [value]="code" (input)="code = $event.target.value;" />
		<input type="button" value="Verify Code" (click)="verifyCode()" />

		<div *ngIf="couchDB">

			<h3>
				Welcome {{ name }}
			</h3>

			<img [src]="avatarUrl" />

			<h2>
				Database Credentials <em>(for future PouchDB replication)</em>
			</h2>

			<ul>
				<li>
					<strong>Database:</strong> {{ couchDB.host }}/{{ couchDB.name }}
				</li>
				<li>
					<strong>Key:</strong> {{ couchDB.key }}
				</li>
				<li>
					<strong>Password:</strong> {{ couchDB.password }}
				</li>
			</ul>

		</div>
	`
})
export class AppComponent {

	public avatarUrl: string;
	public code: string;
	public couchDB: {
		host: string;
		name: string;
		key: string;
		password: string;
	};
	public email: string;
	public name: string;

	private authenticationService: AuthenticationService;


	// I initialize the component.
	constructor( authenticationService: AuthenticationService ) {

		this.authenticationService = authenticationService;

		this.avatarUrl = "";
		this.code = "";
		this.couchDB = null;
		this.email = "";
		this.name = "";

	}


	// ---
	// PUBLIC METHODS.
	// ---


	// I send the one-time use password to the currently-entered email address. The
	// one-time password is valid for about 5-minutes.
	public sendEmail() : void {

		this.code = "";
		this.couchDB = null;

		this.authenticationService
			.requestEmailCode( this.email )
			.then(
				() => {

					console.log( "Email sent (with one-time use code)." );

				}
			)
			.catch(
				( error: any ) : void => {

					console.error( error );

				}
			)
		;

	}


	// I log the current user into the application using the currently-entered email
	// address and the one-time use token.
	public verifyCode() : void {

		// In the following workflow, first, we're going to log the user into the app;
		// then, once the user is authenticated, we'll go back to the Auth0 API to get
		// the user's full profile (included persisted app_metadata with contains our
		// CouchDB / Cloudant / PouchDB login credentials).
		this.authenticationService
			.verifyEmailCode( this.email, this.code )
			.then(
				( authorization: IAuthorization ) : Promise<IProfile> => {

					console.group( "Verify Email Code / Authorization Result" );
					console.log( authorization );
					console.groupEnd();

					// Now that the user is logged-in, let's go back to the API to get
					// the full user profile.
					// --
					// NOTE: There is an earlier API method call .getProfile() which
					// takes the idToken. That workflow, however, is being deprecated
					// in favor of a new workflow that emphasizes accessToken.
					return( this.authenticationService.getUserInfo( authorization.accessToken ) );

				}
			)
			.then(
				( profile: IProfile ) : void => {

					console.group( "Profile Result" );
					console.log( profile );
					console.groupEnd();

					this.name = profile.nickname;
					this.avatarUrl = profile.picture;

					// NOTE: The CouchDB property is technically available on both the
					// app_metadata and the profile itself (the app_metadata appears to
					// get automatically merged into the Profile). However, I don't like
					// that it is dumped into the profile; as such, I choose to access
					// it on the app_metadata explicitly.
					this.couchDB = profile.app_metadata.couchDB;

				}
			)
			.catch(
				( error: any ) : void => {

					console.warn( "Something went wrong!" );
					console.error( error );

				}
			)
		;

	}

}

As you can see, once the user authenticates with Auth0 using their email address and one-time-use token for passwordless authentication, we can make a subsequent request to get the user's Profile from the previous login transaction. This Profile contains the app_metadata, which in turn, contains our remote CouchDB credentials.

NOTE: The content of the app_metadata appears to get merged into the Profile itself, in addition to being available as the app_metadata property. I am not sure why this happens; but, I believe Auth0 is doing this automatically.

If I run this app and authenticate with my passwordless login, we get the following output:

Using auth0 to provision Cloudant databases in an Angular 2 application.

As you can see, the Auth0 authorization workflow provisioned a new database for me over in IBM Cloudant. It then created a new API Key that grants me access privileges on that database. Now, in this demo, I'm not really doing anything with these credentials; but, my future plan is to use them as the remote replication database for my PouchDB-based offline-application.

Though not really important for the goal of this demo, here's my Angular 2 authentication service that proxies the client-side Auth0 library:

// Import the core angular services.
import { Injectable } from "@angular/core";
import * as Auth0 from "auth0";

// CAUTION: I cobbled together the following interfaces in an attempt to self-document
// what the API calls were doing. These are NOT OFFICIAL interfaces provided by Auth0.
// I tried to find a "Definitely Typed" set of interfaces; but, they didn't appear to
// be up-to-date.
// --
// Definitely Types for JS - https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/auth0-js/index.d.ts

export interface IAppMetadata {
	couchDB: {
		host: string;
		name: string;
		key: string;
		password: string;
	};
}

export interface IAuthorization {
	accessToken: string;
	idToken: string; // JWT token.
	idTokenPayload: { // Parsed JWT content.
		aud: string; // The audience. Either a single case-sensitive string or URI or an array of such values that uniquely identify the intended recipients of this JWT. For an Auth0 issued id_token, this will be the Client ID of your Auth0 Client.
		exp: number; // Expires at (UTC seconds).
		iat: number; // Issued at (UTC seconds).
		iss: string; // The issuer. A case-sensitive string or URI that uniquely identifies the party that issued the JWT. For an Auth0 issued id_token, this will be the URL of your Auth0 tenant.
		sub: string; // The unique identifier of the user. This is guaranteed to be unique per user and will be in the format (identity provider)|(unique id in the provider), e.g. github|1234567890.
	};
	refreshToken?: string; // Optional, if the offline_access scope has been requested.
	state?: any; // Echoed value for cross-site request forgery protection.
}

export interface IIdentity {
	connection: string;
	isSocial: boolean;
	provider: string;
	user_id: string;
}

export interface IProfile {
	// Fields that are always generated - https://auth0.com/docs/user-profile/normalized
	identities: IIdentity[];
	name: string;
	nickname: string;
	picture: string; // The profile picture of the user which is returned from the Identity Provider.
	user_id: string; // The unique identifier of the user. This is guaranteed to be unique per user and will be in the format (identity provider)|(unique id in the provider), e.g. github|1234567890.

	// Optional fields, but still "core" ?? !! The documentation is confusing !!
	app_metadata?: IAppMetadata;
	clientID: string; // The unique ID of the Auth0 client.
	created_at: string; // TZ formatted date string.
	sub: string; // The unique identifier of the user. This is guaranteed to be unique per user and will be in the format (identity provider)|(unique id in the provider), e.g. github|1234567890.
	updated_at: string; // TZ formatted date string.
	user_metadata?: IUserMetadata;

	// Fields that are generated when the details are available:
	email: string; // The email address of the user which is returned from the Identity Provider.
	email_verified: boolean;
}

export interface IUserMetadata {
	[ key: string ]: any;
}

export class AuthenticationService {

	private auth0: any;


	// I initialize the Authentication service.
	constructor() {

		this.auth0 = new Auth0({
			domain: "bennadel.auth0.com",
			clientID: "BxjlFCNofp00qollw06emBULwBCxkELn", // JavaScript Demos Client.
			responseType: "token"

			// Since I am using an email-based token workflow, I don't need to define
			// a callback URL - this would only be necessary if I was passing the user
			// control over to Auth0's website.
			// --
			// callbackURL: "{YOUR APP URL}"
		});

	}


	// ---
	// PUBLIC METHODS.
	// ---


	// I get the user info / profile for the given access token (which should have been
	// returned as part of the authorization workflow).
	// --
	// NOTE: Internally, I am using the .getUserInfo() method, which takes the
	// accessToken. In the Auth0 documentation, however, they discuss the .getProfile()
	// method that takes the idToken. But, if you try to use that method, you get the
	// following deprecation warning:
	// --
	// DEPRECATION NOTICE: This method will be soon deprecated, use `getUserInfo` instead.
	// --
	// Apparently Auth0 is trying to migrate to a slightly different workflow for
	// accessing the API based on accessTokens. But, it is not yet fully rolled-out.
	public getUserInfo( accessToken: string ) : Promise<IProfile> {

		var promise = new Promise<IProfile>(
			( resolve, reject ) : void => {

				this.auth0.getUserInfo(
					accessToken,
					( error: any, result: IProfile ) : void => {

						error
							? reject( error )
							: resolve( result )
						;

					}
				);

			}
		);

		return( promise );

	}


	// I send a one-time use password to the given email address.
	public requestEmailCode( email: string ) : Promise<void> {

		var promise = new Promise<void>(
			( resolve, reject ) : void => {

				this.auth0.requestEmailCode(
					{
						email: email
					},
					( error: any ) : void => {

						error
							? reject( error )
							: resolve()
						;

					}
				);

			}
		);

		return( promise );

	}


	// I log the user into the application by verifying that the given one-time use
	// password was provisioned for the given email address.
	public verifyEmailCode( email: string, code: string ) : Promise<IAuthorization> {

		var promise = new Promise<IAuthorization>(
			( resolve, reject ) : void => {

				this.auth0.verifyEmailCode(
					{
						email: email,
						code: code
					},
					( error: any, result: IAuthorization ) : void => {

						error
							? reject( error )
							: resolve( result )
						;

					}
				);

			}
		);

		return( promise );

	}

}

When I was looking at offline-first storage options, I narrowed in on PouchDB because of its master-master replication behavior provided by CouchDB. Of course, in order to replicate, we need a remote CouchDB instance to replace to. For my Anglar 2 application, I'm exploring the possibility of having Auth0 provision new CouchDB databases as part of the user authorization workflow. And, so far, things are looking pretty exciting!

Want to use code from this post? Check out the license.

Reader Comments

15,640 Comments

If anyone is interested, I couldn't find the documentation on what version of Node.js was actually running in WebTask.io. So, I logged-out the "process.versions" and got the following:

"ProcessV": {
. . "http_parser": "2.5.2",
. . "node": "4.4.5",
. . "v8": "4.5.103.35",
. . "uv": "1.8.0",
. . "zlib": "1.2.8",
. . "ares": "1.10.1-DEV",
. . "icu": "56.1",
. . "modules": "46",
. . "openssl": "1.0.2h"
}

It just helped me find the right Node.js docs to look at for functionality.

15,640 Comments

@Dan,

It could have been GitHub was down briefly - I know they've had a few issues lately. Or, it could just be a slower connection -- my demos load like 300 JavaScript files; so, sometimes it looks like it isn't do anything, but really its just making a ton of HTTP calls in the background.

1 Comments

Great post!
Cloudant is owned by IBM now. I tried their free Lite plan and constantly got timeout errors in my test app during live-replication. I've decided to use self-hosted CouchDB server and also wrote a blog post on how to provision databases and users with auth0 in CouchDB - https://ayastreb.me/provision-couchdb-with-auth0/

15,640 Comments

@Kevin,

Envoy sounds really cool; but, doing some Googling now and I don't really see anything on it in the 2-years since this post. That's unfortunate, it sounds like a super compelling idea that would greatly simplify CouchDB / PouchDB development.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel