TBR: todosapp: sync works!!!!1

except for one bug - see demo.md

Change-Id: I9de074adeb755fef64f33107053358d6660ab36e
diff --git a/Makefile b/Makefile
index 5cf4f9f..810b922 100644
--- a/Makefile
+++ b/Makefile
@@ -40,9 +40,12 @@
 
 .DELETE_ON_ERROR:
 
+# Builds mounttabled, principal, and syncbased.
 bin: $(shell $(FIND) $(V23_ROOT) -name "*.go")
+	v23 go build -a -o $@/mounttabled v.io/x/ref/services/mounttable/mounttabled
 	v23 go build -a -o $@/principal v.io/x/ref/cmd/principal
 	v23 go build -a -o $@/syncbased v.io/syncbase/x/ref/services/syncbase/syncbased
+	touch $@
 
 node_modules: package.json $(shell $(FIND) $(V23_ROOT)/roadmap/javascript/syncbase/{package.json,src} $(V23_ROOT)/release/javascript/core/{package.json,src})
 	npm prune
diff --git a/README.md b/README.md
index dfe3066..e6fb7dd 100644
--- a/README.md
+++ b/README.md
@@ -1,37 +1,37 @@
 # Todos app
 
-Todos is an example app that demonstrates use of [Syncbase][syncbase].
-
-Originally based on the [Meteor Todos demo app][meteor-todos].
+Todos is an example app that demonstrates use of [Syncbase][syncbase], and is
+originally based on the [Meteor Todos demo app][meteor-todos]. The high-level
+requirements for this app are [described here][requirements].
 
 ## Running the web application
 
 The commands below assume that the current working directory is
-`$V23_ROOT/experimental/projects/todosapp`.
+`$V23_ROOT/experimental/projects/todosapp` and that you've installed the
+[Vanadium Chrome extension][crx].
 
 First, build all necessary binaries.
 
     DEBUG=1 make build
 
 Next, if you haven't already, generate credentials to use for running the local
-Syncbase daemon. When prompted, specify blessing extension "syncbase". Note, the
-value of `--v23.credentials` should correspond to the `$TMPDIR` value specified
-when running `start_syncbased.sh`.
+daemons (mounttabled and syncbased). Leave the blessing extension field empty.
 
     ./bin/principal seekblessings --v23.credentials tmp/creds
 
-Next, start a local Syncbase daemon (in another terminal). Note, this script
-expects credentials in `$TMPDIR/creds`, and configures Syncbase to persist data
-under root directory `$TMPDIR/syncbase`.
+Next, start local daemons (in another terminal). This script runs mounttabled
+and syncbased at ports `$PORT+1` and `$PORT+2` respectively, and configures
+Syncbase to persist its data under `tmp/syncbase_$PORT`. It expects to find
+credentials in `tmp/creds`.
 
-    TMPDIR=tmp ./tools/start_syncbased.sh
+    PORT=4000 ./tools/start_services.sh
 
     # Or, start from a clean slate.
-    rm -rf tmp/syncbase* && TMPDIR=tmp ./tools/start_syncbased.sh
+    rm -rf tmp/syncbase* && PORT=4000 ./tools/start_services.sh
 
 Finally, start the web app.
 
-    DEBUG=1 make serve
+    DEBUG=1 PORT=4000 make serve
 
 Visit `http://localhost:4000` in your browser to access the app.
 
@@ -42,30 +42,38 @@
 Syncbase, add `d=syncbase` to the url query params, or simply click the storage
 engine indicator in the top right corner to toggle it.
 
-When using Syncbase, by default the app attempts to contact the Syncbase service
-using the Vanadium object name `/localhost:8200`. To specify a different name,
-add `n=<name>` to the url query params.
+When using Syncbase, by default the app attempts to contact the Vanadium object
+name `/localhost:($PORT+1)/syncbase`, where `/localhost:($PORT+1)` is the local
+mount table name and `syncbase` is the relative name of the Syncbase service. To
+specify a different mount table name, add `mt=<name>` to the url query params,
+e.g. `mt=/localhost:5001`. To specify a different Syncbase service name, add
+`sb=<name>`, e.g. `sb=/localhost:4002`.
 
-Beware that `start_syncbased.sh` starts Syncbase with completely open ACLs. This
-is safe if Syncbase is only accessible locally (the default), but more dangerous
-if this Syncbase instance is configured to be accessible via a global mount
-table.
+Beware that `start_services.sh` starts Syncbase with completely open ACLs. This
+is safe if Syncbase is only accessible on the local network (the default), but
+more dangerous if this Syncbase instance is configured to be accessible via a
+global mount table.
 
 ## Design and implementation
 
 Todos is implemented as a single-page JavaScript web application that
-communicates with a local Syncbase daemon through the
-[Vanadium Chrome extension][crx]. The app UI is built using HTML and CSS, using
-React as a model-view framework. The high-level requirements for this app are
-[described here][requirements].
+communicates with a local Syncbase daemon through the [Vanadium Chrome
+extension][crx]. The app UI is built with HTML and CSS, using React as a
+model-view framework.
 
-The Syncbase data layout and conflict resolution scheme for this app are
-[described here][design], and the v0 sync setup is
-[described here][demo-sync-setup]. For now, when an item is deleted, any
-sub-items that were added concurrently (on some other device) are orphaned.
-Eventually, we'll GC orphaned records; for now, we don't bother. This
-orphaning-based approach enables us to use simple last-one-wins conflict
-resolution for all records stored in Syncbase.
+The data layout and conflict resolution policies for this app are [detailed
+here][design], and the v0 sync setup is [described here][demo-sync-setup]. The
+basic data layout is as follows, where `todos`, `db`, and `tb` are the Syncbase
+app, database, and table names respectively.
+
+    todos/db/tb/<listId>                               --> List
+    todos/db/tb/<listId>/todos/<todoId>                --> Todo
+    todos/db/tb/<listId>/todos/<todoId>/tags/<tagName> --> nil
+
+For now, when an item is deleted, any sub-items that were added concurrently (on
+some other device) are orphaned. Eventually, we'll GC orphaned records; for now,
+we don't bother. This orphaning-based approach enables us to use simple
+last-one-wins conflict resolution for all records stored in Syncbase.
 
 At startup, the web app checks whether its backing store (e.g. Syncbase) is
 empty; if so, it writes some todo lists to the store (see
@@ -96,21 +104,21 @@
 
 Signature
 
-    $V23_ROOT/release/go/bin/vrpc -v23.credentials=tmp/creds signature /localhost:8200
+    $V23_ROOT/release/go/bin/vrpc -v23.credentials=tmp/creds signature /localhost:4002
 
 Method call
 
-    $V23_ROOT/release/go/bin/vrpc -v23.credentials=tmp/creds call /localhost:8200 GetPermissions
-    $V23_ROOT/release/go/bin/vrpc -v23.credentials=tmp/creds call /localhost:8200/todos/db/tb Scan '""' '""'
+    $V23_ROOT/release/go/bin/vrpc -v23.credentials=tmp/creds call /localhost:4002 GetPermissions
+    $V23_ROOT/release/go/bin/vrpc -v23.credentials=tmp/creds call /localhost:4002/todos/db/tb Scan '""' '""'
 
 Glob
 
-    $V23_ROOT/release/go/bin/namespace -v23.credentials=tmp/creds glob "/localhost:8200/..."
+    $V23_ROOT/release/go/bin/namespace -v23.credentials=tmp/creds glob "/localhost:4002/..."
 
 Debug
 
-    $V23_ROOT/release/go/bin/debug -v23.credentials=tmp/creds glob "/localhost:8200/__debug/stats/rpc/server/routing-id/..."
-    $V23_ROOT/release/go/bin/debug -v23.credentials=tmp/creds stats read "/localhost:8200/__debug/stats/rpc/server/routing-id/c61964ab4c72ee522067eb6d5ddd22fc/methods/BeginBatch/latency-ms"
+    $V23_ROOT/release/go/bin/debug -v23.credentials=tmp/creds glob "/localhost:4002/__debug/stats/rpc/server/routing-id/..."
+    $V23_ROOT/release/go/bin/debug -v23.credentials=tmp/creds stats read "/localhost:4002/__debug/stats/rpc/server/routing-id/c61964ab4c72ee522067eb6d5ddd22fc/methods/BeginBatch/latency-ms"
 
 ### Integration test setup
 
@@ -130,25 +138,18 @@
 
     $V23_ROOT/release/go/bin/namespace -v23.credentials=/usr/local/google/home/sadovsky/vanadium/roadmap/javascript/syncbase/tmp/test-credentials glob "/@5@ws@127.0.0.1:41249@7d24de5a57f6532b184562654ad2c554@m@test/child@@/test/syncbased/..."
 
-Visit `http://localhost:4000/?d=syncbase&n=test/syncbased` in the launched
+Visit `http://localhost:4000/?d=syncbase&sb=test/syncbased` in the launched
 Chrome instance to talk to your test syncbase.
 
-To run a simple benchmark (100 puts, followed by a scan of those rows), specify
-query param `bm=1`.
+### Performance benchmarking
 
-### Benchmark performance
+To run a simple benchmark (parallel 100 puts, followed by a scan of those rows),
+specify query param `bm=1`.
 
 All numbers assume dev console is closed, and assume non-test setup as described
 above.
 
-Currently, parallel 100 puts takes 4s, and scanning 100 rows takes 0.6s.
-
-For the puts, avoiding unnecessarily cautious Signature RPC and avoiding 2x
-blowup from unnecessary Resolve calls will help. Parallelism doesn't help as
-much as one would hope, need to understand why.
-
-For the scan, 100ms comes from JS encode/decode, and probably much of the rest
-from WSPR. Needs further digging.
+Currently, parallel 100 puts takes 700ms, and scanning 100 rows takes 300ms.
 
 [syncbase]: https://docs.google.com/document/d/12wS_IEPf8HTE7598fcmlN-Y692OWMSneoe2tvyBEpi0/edit#
 [crx]: https://v.io/tools/vanadium-chrome-extension.html
diff --git a/browser/index.js b/browser/index.js
index 5b9b82c..c91704f 100644
--- a/browser/index.js
+++ b/browser/index.js
@@ -21,7 +21,6 @@
 
 var DISP_TYPE_COLLECTION = 'mem-collection';
 var DISP_TYPE_SYNCBASE = 'syncbase';
-var SYNCBASE_NAME = '/localhost:8200';  // default value
 
 ////////////////////////////////////////
 // Global state
@@ -32,6 +31,16 @@
 // Used for query params.
 var u = url.parse(window.location.href, true);
 
+// Mount table name.
+var mt = u.query.mt || (function() {
+  var loc = window.location;
+  return '/' + loc.hostname + ':' + (Number(loc.port) + 1);
+}());
+
+// See TODO in SyncbaseDispatcher.listIdToSgName to understand why we use an
+// absolute name here.
+var syncbaseName = u.query.sb || (mt + '/syncbase');
+
 ////////////////////////////////////////
 // Helpers
 
@@ -41,7 +50,7 @@
   cb = util.logFn('initVanadium', cb);
   var vanadiumConfig = {
     logLevel: vanadium.vlog.levels.INFO,
-    namespaceRoots: u.query.mounttable ? [u.query.mounttable] : undefined,
+    namespaceRoots: [mt],
     proxy: u.query.proxy
   };
   vanadium.init(vanadiumConfig, cb);
@@ -381,8 +390,12 @@
   },
   render: function() {
     var that = this, list = this.props.list, shared = Boolean(list.sg);
-    var loc = window.location;
-    var shareUrl = loc.origin + '/share/' + list._id + loc.search;
+    var shareUrl;
+    if (shared) {
+      var encodedSgName = util.strToHex(list.sg.name);
+      var loc = window.location;
+      shareUrl = loc.origin + '/share/' + encodedSgName + loc.search;
+    }
     return h('div#status-pane', {
       onClick: function(e) {
         e.stopPropagation();
@@ -409,10 +422,10 @@
             alert('Invalid email address.');
             return;
           }
-          disp.createSyncGroup(list._id, [
+          disp.createSyncGroup(disp.listIdToSgName(list._id), [
             emailToBlessing(userEmail),
             emailToBlessing(value)
-          ]);
+          ], mt);
         },
         cancel: function(e) {
           e.target.value = '';
@@ -521,7 +534,10 @@
   },
   setListId_: function(listId) {
     if (listId === this.state.listId) return;
-    this.setState({listId: listId, tagFilter: null});
+    this.setState({
+      listId: listId,
+      tagFilter: null
+    });
   },
   updateURL: function() {
     var listId = this.state.listId;
@@ -529,6 +545,45 @@
     // Note, this doesn't trigger a re-render; it's purely visual.
     window.history.replaceState({}, '', pathname + window.location.search);
   },
+  // Updates state.lists. Calls cb once the setState call has completed.
+  // TODO(sadovsky): If possible, simplify how we deal with concurrent state
+  // updates, here and elsewhere. The current approach is fairly subtle and
+  // error-prone. Our goal is simple: never show stale data, even in the
+  // presence of sync.
+  updateLists_: function(cb) {
+    var that = this;
+    var listsSeq = this.state.lists.seq + 1;
+    this.getLists_(function(err, lists) {
+      if (err) return cb(err);
+      // Use setState(cb) form to ensure atomicity.
+      // References: https://goo.gl/CZ82Vp and https://goo.gl/vVCp8B
+      that.setState(function(state) {
+        if (listsSeq <= state.lists.seq) {
+          return {};
+        }
+        return {lists: {seq: listsSeq, items: lists}};
+      }, cb);
+    });
+  },
+  // Updates state.todos[listId]. Calls cb once the setState call has completed.
+  updateTodos_: function(listId, cb) {
+    var that = this;
+    var stateTodos = this.state.todos[listId];
+    var todosSeq = (stateTodos ? stateTodos.seq : 0) + 1;
+    this.getTodos_(listId, function(err, todos) {
+      if (err) return cb(err);
+      // Use setState(cb) form to ensure atomicity.
+      // References: https://goo.gl/CZ82Vp and https://goo.gl/vVCp8B
+      that.setState(function(state) {
+        var stateTodos = state.todos[listId];
+        if (stateTodos && todosSeq <= stateTodos.seq) {
+          return {};
+        }
+        state.todos[listId] = {seq: todosSeq, items: todos};
+        return {todos: state.todos};
+      }, cb);
+    });
+  },
   componentWillMount: function() {
     var that = this, props = this.props;
     if (props.benchmark) {
@@ -540,22 +595,26 @@
       });
       return;
     }
-    function done() {
-      that.setState({dispInitialized: true});
-    }
     initDispatcher(props.dispType, props.syncbaseName, function(err) {
       if (err) throw err;
-      if (props.joinListId) {
+      that.setState({dispInitialized: true}, function() {
+        if (!props.joinSgName) return;
+        // TODO(sadovsky): Show "please wait..." modal?
         console.assert(props.dispType === DISP_TYPE_SYNCBASE);
-        disp.joinSyncGroup(props.joinListId, function(err) {
+        disp.joinSyncGroup(props.joinSgName, function(err) {
           // Note, joinSyncGroup is a noop (no error) if the caller is already a
           // member, which is the desired behavior here.
           if (err) throw err;
-          done();
+          var listId = disp.sgNameToListId(props.joinSgName);
+          // TODO(sadovsky): Wait for all items to get synced before attempting
+          // to read them?
+          that.updateTodos_(listId, function(err) {
+            if (err) throw err;
+            // Note, componentDidUpdate() will update the url.
+            that.setState({listId: listId});
+          });
         });
-      } else {
-        done();
-      }
+      });
     });
   },
   componentDidMount: function() {
@@ -584,62 +643,22 @@
       return listId;
     }
 
-    // Updates lists. Calls cb once the setState call has completed.
-    // TODO(sadovsky): If possible, simplify how we deal with concurrent state
-    // updates, here and elsewhere. The current approach is fairly subtle and
-    // error-prone. Our goal is simple: never show stale data, even in the
-    // presence of sync.
-    function updateLists(cb) {
-      var listsSeq = that.state.lists.seq + 1;
-      that.getLists_(function(err, lists) {
-        if (err) return cb(err);
-        // Use setState(cb) form to ensure atomicity.
-        // References: https://goo.gl/CZ82Vp and https://goo.gl/vVCp8B
-        that.setState(function(state) {
-          if (listsSeq <= state.lists.seq) {
-            return {};
-          }
-          return {lists: {seq: listsSeq, items: lists}};
-        }, cb);
-      });
-    }
-
-    // Updates todos for the specified list. Calls cb once the setState call
-    // has completed.
-    function updateTodos(listId, cb) {
-      var stateTodos = that.state.todos[listId];
-      var todosSeq = (stateTodos ? stateTodos.seq : 0) + 1;
-      that.getTodos_(listId, function(err, todos) {
-        if (err) return cb(err);
-        // Use setState(cb) form to ensure atomicity.
-        // https://goo.gl/CZ82Vp
-        that.setState(function(state) {
-          var stateTodos = state.todos[listId];
-          if (stateTodos && todosSeq <= stateTodos.seq) {
-            return {};
-          }
-          state.todos[listId] = {seq: todosSeq, items: todos};
-          return {todos: state.todos};
-        }, cb);
-      });
-    }
-
     // TODO(sadovsky): Only read (and only redraw) what's needed based on what
     // changed.
     disp.on('change', function() {
       var doneCb = util.logFn('onChange', function(err) {
         if (err) throw err;
       });
-      updateLists(function(err) {
+      that.updateLists_(function(err) {
         if (err) throw err;
         var listId = getListId();
-        updateTodos(listId, doneCb);
+        that.updateTodos_(listId, doneCb);
       });
     });
 
     // Load initial lists and todos. Note that changes can come in concurrently
     // via sync.
-    updateLists(function(err) {
+    this.updateLists_(function(err) {
       if (err) throw err;
       // Set initial listId if needed.
       var listId = getListId();
@@ -648,7 +667,9 @@
       }
       // Get todos for all lists.
       var listIds = _.pluck(that.state.lists.items, '_id');
-      async.each(listIds, updateTodos, function(err) {
+      async.each(listIds, function(listId, cb) {
+        that.updateTodos_(listId, cb);
+      }, function(err) {
         if (err) throw err;
       });
     });
@@ -671,7 +692,7 @@
           if (that.props.dispType === DISP_TYPE_SYNCBASE) {
             newDispType = DISP_TYPE_COLLECTION;
           }
-          window.location.href = '/?d=' + newDispType + '&n=' + SYNCBASE_NAME;
+          window.location.replace('/?d=' + newDispType);
         }
       }),
       ListsPane({
@@ -688,9 +709,7 @@
           // Note, setState just schedules render, so setListId's state update
           // will be merged with ours.
           that.setListId_(listId);
-          that.setState({
-            showStatusDialog: true
-          });
+          that.setState({showStatusDialog: true});
         }
       }),
       h('div#tags-and-todos-pane', {key: 'tags-and-todos-pane'}, [
@@ -727,32 +746,25 @@
 // visibility of the log.
 domLog.init();
 
-function render(listId, doJoin) {
-  var dispType = u.query.d || DISP_TYPE_COLLECTION;
-  var syncbaseName = u.query.n || SYNCBASE_NAME;
-  var benchmark = Boolean(u.query.bm);
-  var props = {
-    initialListId: listId,
-    dispType: dispType,
+function render(props) {
+  props = _.assign({
+    dispType: u.query.d || DISP_TYPE_COLLECTION,
     syncbaseName: syncbaseName,
-    benchmark: benchmark
-  };
-  if (doJoin) {
-    props.joinListId = listId;
-  }
+    benchmark: Boolean(u.query.bm)
+  }, props);
   React.render(Page(props), document.querySelector('#page'));
 }
 
 // Configure Page.js routes. Note, ctx here is a Page.js context object, not a
 // Vanadium context object.
 page('/', function(ctx) {
-  render(null, false);
+  render();
 });
 page('/lists/:listId', function(ctx) {
-  render(ctx.params.listId, false);
+  render({initialListId: ctx.params.listId});
 });
-page('/share/:listId', function(ctx) {
-  render(ctx.params.listId, true);
+page('/share/:encodedSgName', function(ctx) {
+  render({joinSgName: util.hexToStr(ctx.params.encodedSgName)});
 });
 
 // Start Page.js router.
diff --git a/browser/syncbase_dispatcher.js b/browser/syncbase_dispatcher.js
index 8895f8e..2bea08c 100644
--- a/browser/syncbase_dispatcher.js
+++ b/browser/syncbase_dispatcher.js
@@ -146,7 +146,7 @@
   this.db_.getSyncGroupNames(ctx, function(err, sgNames) {
     if (err) return cb(err);
     async.map(sgNames, function(sgName, cb) {
-      that.getSyncGroup_(ctx, that.sgNameToListId_(sgName), cb);
+      that.getSyncGroup_(ctx, sgName, cb);
     }, cb);
   });
 });
@@ -282,20 +282,22 @@
 // Currently, SG names must be of the form <syncbaseName>/$sync/<suffix>.
 // We use <app>/<db>/<table>/<listId> for the suffix part.
 
-SyncbaseDispatcher.prototype.sgNameToListId_ = function(sgName) {
+SyncbaseDispatcher.prototype.sgNameToListId = function(sgName) {
   return sgName.replace(new RegExp('.*/\\$sync/todos/db/tb/'), '');
 };
 
-SyncbaseDispatcher.prototype.listIdToSgName_ = function(listId) {
-  var prefix = this.tb_.fullName.replace('/todos/db/tb',
-                                         '/$sync/todos/db/tb');
+SyncbaseDispatcher.prototype.listIdToSgName = function(listId) {
+  // TODO(sadovsky): fullName doesn't include the mount table name, i.e. the
+  // part corresponding to namespaceRoots. Our workaround is to specify a
+  // fully-qualified Syncbase name.
+  var prefix = this.tb_.fullName.replace('/todos/db/tb', '/$sync/todos/db/tb');
   return prefix + '/' + listId;
 };
 
-// Returns an object describing the SyncGroup for the given list id.
-// Currently, this object will have just one field, 'spec'.
-define('getSyncGroup_', function(ctx, listId, cb) {
-  var sgName = this.listIdToSgName_(listId), sg = this.db_.syncGroup(sgName);
+// Returns an object describing the SyncGroup with the given name.
+// Currently, this object will have two fields: 'name' and 'spec'.
+define('getSyncGroup_', function(ctx, sgName, cb) {
+  var sg = this.db_.syncGroup(sgName);
   async.parallel([
     function(cb) {
       sg.getSpec(ctx, function(err, spec, version) {
@@ -318,7 +320,8 @@
   ], function(err, results) {
     if (err) return cb(err);
     cb(null, {
-      spec: results[0],
+      name: sgName,
+      spec: results[0]
     });
   });
 });
@@ -329,8 +332,8 @@
   syncPriority: 8
 });
 
-define('createSyncGroup', function(ctx, listId, blessings, cb) {
-  var sgName = this.listIdToSgName_(listId), sg = this.db_.syncGroup(sgName);
+define('createSyncGroup', function(ctx, sgName, blessings, mt, cb) {
+  var sg = this.db_.syncGroup(sgName);
   var spec = new nosql.SyncGroupSpec({
     // TODO(sadovsky): Maybe make perms more restrictive.
     perms: new Map([
@@ -341,13 +344,14 @@
       ['Debug',   {'in': blessings}]
     ]),
     // TODO(sadovsky): Update this once we switch to {table, prefix} tuples.
-    prefixes: ['tb:' + listId]
+    prefixes: ['tb:' + this.sgNameToListId(sgName)],
+    mountTables: [vanadium.naming.join(mt, 'rendezvous')]
   });
   sg.create(ctx, spec, MEMBER_INFO, this.maybeEmit_(cb));
 });
 
-define('joinSyncGroup', function(ctx, listId, cb) {
-  var sgName = this.listIdToSgName_(listId), sg = this.db_.syncGroup(sgName);
+define('joinSyncGroup', function(ctx, sgName, cb) {
+  var sg = this.db_.syncGroup(sgName);
   sg.join(ctx, MEMBER_INFO, this.maybeEmit_(cb));
 });
 
diff --git a/browser/util.js b/browser/util.js
index 1234937..76f8346 100644
--- a/browser/util.js
+++ b/browser/util.js
@@ -36,6 +36,14 @@
   return randomBytes(Math.ceil(len / 2)).toString('hex').substr(0, len);
 };
 
+// Converts from binary to hex-encoded string and vice versa.
+exports.strToHex = function(s) {
+  return new Buffer(s, 'binary').toString('hex');
+}
+exports.hexToStr = function(s) {
+  return new Buffer(s, 'hex').toString('binary');
+}
+
 function logStart(name) {
   console.log(name + ' start');
   return Date.now();
diff --git a/demo.md b/demo.md
new file mode 100644
index 0000000..61ce4be
--- /dev/null
+++ b/demo.md
@@ -0,0 +1,68 @@
+# Demo setup
+
+This page describes how to set things up for a demo.
+For detailed explanations of the setup steps, see [README.md](README.md).
+
+FIXME: Currently, once anything is deleted, outgoing sync permanently stops
+working.
+
+## Single-machine setup
+
+Run these commands once:
+
+    DEBUG=1 make build
+    ./bin/principal seekblessings --v23.credentials tmp/creds
+
+Run these commands (each from its own terminal) on each reset:
+
+    rm -rf tmp/syncbase* && PORT=5000 ./tools/start_services.sh
+    PORT=5100 ./tools/start_services.sh
+
+    DEBUG=1 PORT=5000 make serve
+    DEBUG=1 PORT=5100 make serve
+
+Open these urls:
+
+    http://localhost:5000/?d=syncbase // Alice
+    http://localhost:5100/?d=syncbase // Bob
+
+### Syncing a list
+
+1. In Alice's window, create list "Groceries".
+2. Add todo items and tags (as desired).
+3. Click the status button, then type in Bob's email address.
+4. Copy the `/share/...` part of the url to the clipboard.
+5. Switch to Bob's window.
+6. Replace everything after `localhost:5100` with the copied path, hit enter.
+7. After a second, Bob should see the synced "Groceries" list.
+8. Add, edit, and remove todos and tags to your heart's content and watch sync
+   do its magic.
+
+## Two-machine setup
+
+Have Alice and Bob do the following on their respective machines.
+
+Run these commands once:
+
+    DEBUG=1 make build
+    ./bin/principal seekblessings --v23.credentials tmp/creds
+
+Run these commands (each from its own terminal) on each reset:
+
+    rm -rf tmp/syncbase* && PORT=5000 ./tools/start_services.sh
+
+    DEBUG=1 PORT=5000 make serve
+
+Open this url:
+
+    http://localhost:5000/?d=syncbase
+
+### Syncing a list
+
+1. In Alice's window, create list "Groceries".
+2. Add todo items and tags (as desired).
+3. Click the status button, then type in Bob's email address.
+4. Send Bob the entire url, and have him open that url.
+5. After a second, Bob should see the synced "Groceries" list.
+6. Add, edit, and remove todos and tags to your heart's content and watch sync
+   do its magic.
diff --git a/server.js b/server.js
index 1117234..237e3e5 100644
--- a/server.js
+++ b/server.js
@@ -16,6 +16,6 @@
   res.sendFile(pathTo('public/index.html'));
 });
 
-var server = app.listen(4000, function() {
+var server = app.listen(process.env.PORT || 4000, function() {
   console.log('Serving http://localhost:%d', server.address().port);
 });
diff --git a/stylesheets/index.less b/stylesheets/index.less
index 468e796..7bf81b5 100644
--- a/stylesheets/index.less
+++ b/stylesheets/index.less
@@ -205,6 +205,7 @@
 
   .shared-with, .url {
     margin-top: 24px;
+    overflow-wrap: break-word;
   }
 
   .subtitle {
diff --git a/tools/start_services.sh b/tools/start_services.sh
new file mode 100755
index 0000000..80c38fc
--- /dev/null
+++ b/tools/start_services.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+
+# Expects credentials in $TMPDIR/creds (where $TMPDIR defaults to /tmp),
+# generated as follows:
+#
+# make build
+# ./bin/principal seekblessings --v23.credentials tmp/creds
+
+set -euo pipefail
+
+trap kill_child_processes INT TERM EXIT
+
+silence() {
+  "$@" &> /dev/null || true
+}
+
+# Copied from chat example app.
+kill_child_processes() {
+  # Attempt to stop child processes using the TERM signal.
+  if [[ -n "$(jobs -p -r)" ]]; then
+    silence pkill -P $$
+    sleep 1
+    # Kill any remaining child processes using the KILL signal.
+    if [[ -n "$(jobs -p -r)" ]]; then
+      silence sudo -u "${SUDO_USER}" pkill -9 -P $$
+    fi
+  fi
+}
+
+main() {
+  local -r TMPDIR=tmp
+  local -r PORT=${PORT-4000}
+  local -r MOUNTTABLED_ADDR="localhost:$((PORT+1))"
+  local -r SYNCBASED_ADDR="localhost:$((PORT+2))"
+
+  mkdir -p $TMPDIR
+
+  # TODO(sadovsky): Run mounttabled and syncbased each with its own blessing
+  # extension.
+  ./bin/mounttabled \
+    --v23.tcp.address=${MOUNTTABLED_ADDR} \
+    --v23.credentials=${TMPDIR}/creds &
+
+  ./bin/syncbased \
+    --root-dir=${TMPDIR}/syncbase_${PORT} \
+    --name=syncbase \
+    --v23.namespace.root=/${MOUNTTABLED_ADDR} \
+    --v23.tcp.address=${SYNCBASED_ADDR} \
+    --v23.credentials=${TMPDIR}/creds \
+    --v23.permissions.literal='{"Admin":{"In":["..."]},"Write":{"In":["..."]},"Read":{"In":["..."]},"Resolve":{"In":["..."]},"Debug":{"In":["..."]}}'
+
+  tail -f /dev/null  # wait forever
+}
+
+main "$@"
diff --git a/tools/start_syncbased.sh b/tools/start_syncbased.sh
deleted file mode 100755
index 2e45010..0000000
--- a/tools/start_syncbased.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-# Copyright 2015 The Vanadium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file.
-
-# Expects credentials in $TMPDIR/creds (where $TMPDIR defaults to /tmp),
-# generated as follows:
-#
-# make build
-# ./bin/principal seekblessings --v23.credentials tmp/creds
-
-set -euo pipefail
-
-TMPDIR=${TMPDIR-/tmp}
-
-mkdir -p $TMPDIR
-
-set -x
-
-./bin/syncbased --root-dir=${TMPDIR}/syncbase --v23.tcp.address=localhost:8200 --v23.credentials=${TMPDIR}/creds --v23.permissions.literal='{"Admin":{"In":["..."]},"Write":{"In":["..."]},"Read":{"In":["..."]},"Resolve":{"In":["..."]},"Debug":{"In":["..."]}}'  # --enable-sync=0