Merge changes from topic 'baku'

* changes:
  Baku - incrementally adding docs
  Baku - Usability improvements
  Baku - Derived SG suffixes
  Baku - Fixing cloud sync permissions.
  Baku Toolkit - Fixing blessings stuff
  Baku Toolkit - upgrading to BlessingsManager
diff --git a/baku-toolkit/bootstrap-cloudsync b/baku-toolkit/bootstrap-cloudsync
new file mode 100755
index 0000000..cf944e9
--- /dev/null
+++ b/baku-toolkit/bootstrap-cloudsync
@@ -0,0 +1,22 @@
+#!/bin/bash
+# Copyright 2016 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.
+
+if [ ${host-} ]; then
+  client=${client-608941808256-43vtfndets79kf5hac8ieujto8837660.apps.googleusercontent.com}
+  
+  blessings="dev.v.io:o:$client"
+  
+  name="tmp/clients/$client/cloudsync"
+  echo "Running syncbased on $host as $name"
+  
+  $JIRI_ROOT/release/go/bin/dmrun --debug --ssh $host \
+    --sshoptions "-i $HOME/.ssh/google_compute_engine" \
+    $JIRI_ROOT/release/go/bin/syncbased --root-dir="/tmp/syncbase" \
+    --name=$name --v23.tcp.address=:8199 \
+    --v23.namespace.root=/ns.dev.v.io:8101 \
+    --v23.permissions.literal="{\"Admin\":{\"In\":[\"${blessings}\"]},\"Write\":{\"In\":[\"${blessings}\"]},\"Read\":{\"In\":[\"${blessings}\"]},\"Resolve\":{\"In\":[\"${blessings}\"]},\"Debug\":{\"In\":[\"${blessings}\"]}}"
+else
+  echo "Missing required environment variable host."
+fi
diff --git a/baku-toolkit/build.gradle b/baku-toolkit/build.gradle
index be586c1..62608d7 100644
--- a/baku-toolkit/build.gradle
+++ b/baku-toolkit/build.gradle
@@ -6,11 +6,13 @@
         mavenCentral()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:1.3.1'
-        classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3'
-        classpath 'com.jakewharton.sdkmanager:gradle-plugin:0.12.0'
-        classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4'
-        classpath 'me.tatarka:gradle-retrolambda:3.2.4'
+        classpath (
+                'com.android.tools.build:gradle:1.5.0',
+                'com.github.dcendents:android-maven-gradle-plugin:1.3',
+                'com.jakewharton.sdkmanager:gradle-plugin:0.12.0',
+                'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.6',
+                'me.tatarka:gradle-retrolambda:3.2.4'
+        )
 
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
diff --git a/baku-toolkit/clean-cloudsync b/baku-toolkit/clean-cloudsync
new file mode 100755
index 0000000..deadc90
--- /dev/null
+++ b/baku-toolkit/clean-cloudsync
@@ -0,0 +1,21 @@
+#!/bin/bash
+# Copyright 2016 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.
+
+
+if [ ${host-} ]; then
+    ssh="/usr/bin/ssh -i $HOME/.ssh/google_compute_engine $host"
+
+    fullinstallation=`$ssh ls /tmp/dmrun/dm/dmroot/app'*'`
+    installation=${fullinstallation:13}
+
+    fullinstance=`$ssh ls /tmp/dmrun/dm/dmroot/app*/*/instances`
+    instance=${fullinstance:9}
+    
+     ${JIRI_ROOT}/release/go/bin/device kill /$host:8150/apps/app/$installation/$instance
+     eval "$ssh sudo killall -9 agentd deviced"
+     eval "$ssh rm -rf /tmp/'*'"
+else
+    echo "Missing required environment variable host."
+fi
diff --git a/baku-toolkit/gradle/wrapper/gradle-wrapper.properties b/baku-toolkit/gradle/wrapper/gradle-wrapper.properties
index 89af0bd..b39d8fb 100644
--- a/baku-toolkit/gradle/wrapper/gradle-wrapper.properties
+++ b/baku-toolkit/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.9-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-all.zip
diff --git a/baku-toolkit/lib/build.gradle b/baku-toolkit/lib/build.gradle
index e6563bd..1a6d960 100644
--- a/baku-toolkit/lib/build.gradle
+++ b/baku-toolkit/lib/build.gradle
@@ -1,6 +1,11 @@
+import org.gradle.api.tasks.javadoc.internal.JavadocSpec
+import org.gradle.jvm.internal.toolchain.JavaToolChainInternal
+import org.gradle.jvm.platform.JavaPlatform
+import org.gradle.jvm.platform.internal.DefaultJavaPlatform
+import org.gradle.language.base.internal.compile.Compiler
 // You should change this after releasing a new version of the Baku Toolkit. See the
 // list of published versions at https://repo1.maven.org/maven2/io/v/baku-toolkit.
-version = '0.6.0'
+version = '0.7.0'
 group = 'io.v'
 
 def siteUrl = 'https://github.com/vanadium/java'
@@ -39,10 +44,6 @@
 
 dependencies {
     provided(
-            /* If the application wishes to use support libraries, it should include them as compile
-            dependencies in its own build.gradle. */
-            'com.android.support:appcompat-v7:23.0.1',
-            'com.android.support:recyclerview-v7:23.0.1',
             /*
             https://projectlombok.org/setup/android.html
             Follow Android Studio instructions at the bottom of the page to install the Lombok
@@ -52,14 +53,13 @@
             'org.slf4j:slf4j-api:1.7.12'
     )
 
-    androidTestCompile('org.slf4j:slf4j-android:1.7.12')
+    androidTestCompile 'org.slf4j:slf4j-android:1.7.12'
 
     testCompile(
             'org.mockito:mockito-core:1.10.19',
             'org.powermock:powermock-classloading-xstream:1.6.3',
             'org.powermock:powermock-module-junit4:1.6.3',
             'org.powermock:powermock-module-junit4-rule:1.6.3',
-            'org.robolectric:robolectric:3.0',
             'org.slf4j:slf4j-simple:1.7.12'
     )
 
@@ -68,12 +68,18 @@
     }
 
     compile(
+            /*
+            Ideally this would be optional, but this unexpectedly becomes required if you touch
+            the Baku CollectionBinding Builder, even if you don't actually try to bind to a
+            RecyclerView.
+             */
+            'com.android.support:recyclerview-v7:23.0.1',
             'com.jakewharton.rxbinding:rxbinding:0.3.0',
             'commons-io:commons-io:2.4',
-            'io.reactivex:rxandroid:1.0.1',
-            'io.reactivex:rxjava:1.0.16',
+            'io.reactivex:rxandroid:1.1.0',
+            'io.reactivex:rxjava:1.0.17',
             'io.reactivex:rxjava-async-util:0.21.0',
-            'io.v:vanadium-android:1.0',
+            'io.v:vanadium-android:1.7',
             'net.javacrumbs.future-converter:future-converter-guava-rxjava:0.3.0',
             'net.sourceforge.streamsupport:streamsupport:1.3.2',
             'org.robotninjas:fluent-futures:1.0'
@@ -128,16 +134,98 @@
     classifier = 'sources'
 }
 
-task javadoc (type: Javadoc) {
+task javadoc (type: FullProcessingJavadoc) {
     source = android.sourceSets.main.java.srcDirs
     classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
 
+    options.overview = 'src/main/java/overview.html'
+
     if (JavaVersion.current().isJava8Compatible()) {
         // TODO(rosswang): Can we get rid of this?
         options.addStringOption('Xdoclint:none', '-quiet')
     }
 }
 
+/**
+ * https://groups.google.com/d/topic/gradle-dev/R83dy_6PHMc/discussion
+ */
+@ParallelizableTask
+class FullProcessingJavadoc extends Javadoc {
+    private Set<File> sourceDirs
+
+    @Override
+    public void setSource(Object sourceDirs) {
+        this.sourceDirs = sourceDirs
+        super.setSource(sourceDirs)
+    }
+
+    @Override
+    protected void generate() {
+        final File destinationDir = getDestinationDir();
+
+        if (options.getDestinationDirectory() == null) {
+            options.destinationDirectory(destinationDir);
+        }
+
+        options.classpath(new ArrayList<File>(getClasspath().getFiles()));
+
+        if (!GUtil.isTrue(options.getWindowTitle()) && GUtil.isTrue(getTitle())) {
+            options.windowTitle(getTitle());
+        }
+        if (options instanceof StandardJavadocDocletOptions) {
+            StandardJavadocDocletOptions docletOptions = (StandardJavadocDocletOptions) options;
+            if (!GUtil.isTrue(docletOptions.getDocTitle()) && GUtil.isTrue(getTitle())) {
+                docletOptions.setDocTitle(getTitle());
+            }
+        }
+
+        if (maxMemory != null) {
+            final List<String> jFlags = options.getJFlags();
+            final Iterator<String> jFlagsIt = jFlags.iterator();
+            boolean containsXmx = false;
+            while (!containsXmx && jFlagsIt.hasNext()) {
+                final String jFlag = jFlagsIt.next();
+                if (jFlag.startsWith("-Xmx")) {
+                    containsXmx = true;
+                }
+            }
+            if (!containsXmx) {
+                options.jFlags("-Xmx" + maxMemory);
+            }
+        }
+
+        Set<String> roots = new HashSet<>();
+        for (File sourceDir : sourceDirs) {
+            roots += sourceDir.absolutePath
+        }
+        /*Set<String> packages = new HashSet<>();
+        for (File sourceFile : getSource()) {
+            println sourceFile.getPath()
+        }*/
+        options.setSourceNames(Arrays.asList('io.v'));
+        options.addStringOption('subpackages', 'io.v')
+        options.addStringsOption('sourcepath').value = new ArrayList<>(roots)
+
+        executeExternalJavadoc();
+    }
+
+    private void executeExternalJavadoc() {
+        JavadocSpec spec = new JavadocSpec();
+        spec.setExecutable(executable);
+        spec.setOptions(options);
+        spec.setIgnoreFailures(!failOnError);
+        spec.setWorkingDir(getProject().getProjectDir());
+        spec.setOptionsFile(getOptionsFile());
+
+        Compiler<JavadocSpec> generator = ((JavaToolChainInternal) getToolChain()).select(getPlatform()).newCompiler(JavadocSpec.class);
+        generator.execute(spec);
+    }
+
+    private static JavaPlatform getPlatform() {
+        return DefaultJavaPlatform.current();
+    }
+}
+
 task javadocJar(type: Jar, dependsOn: javadoc) {
     from tasks.javadoc
     classifier = 'javadoc'
@@ -156,15 +244,7 @@
     user = project.properties.bintrayUsername
     key = project.properties.bintrayApiKey
 
-    // TODO(rosswang): use - https://github.com/bintray/gradle-bintray-plugin/issues/81
-    //configurations = ['archives']
-    // TODO(rosswang): remove - https://github.com/bintray/gradle-bintray-plugin/issues/81
-    filesSpec {
-        from 'build/libs', 'build/outputs/aar', 'build/poms'
-        rename '.+\\.aar', artifactPrefix + '.aar'
-        rename 'pom-default\\.xml', artifactPrefix + '.pom'
-        into 'io/v/baku-toolkit/' + version
-    }
+    configurations = ['archives']
 
     pkg {
         desc = pkgDesc
diff --git a/baku-toolkit/lib/src/androidTest/java/io/v/baku/toolkit/VAndroidTestCase.java b/baku-toolkit/lib/src/androidTest/java/io/v/baku/toolkit/VAndroidTestCase.java
index 5974054..824af5a 100644
--- a/baku-toolkit/lib/src/androidTest/java/io/v/baku/toolkit/VAndroidTestCase.java
+++ b/baku-toolkit/lib/src/androidTest/java/io/v/baku/toolkit/VAndroidTestCase.java
@@ -11,7 +11,7 @@
 import java.util.concurrent.TimeUnit;
 
 import io.v.android.v23.V;
-import io.v.debug.SyncbaseClient;
+import io.v.debug.SyncbaseAndroidClient;
 import io.v.v23.context.VContext;
 import lombok.Getter;
 import lombok.experimental.Accessors;
@@ -72,15 +72,12 @@
 
     @Override
     protected void tearDown() throws Exception {
-        // TODO(rosswang): https://github.com/vanadium/issues/issues/809
-        // We can't shut down Vanadium because we can't shut down Syncbase. Nothing will fail
-        // outright, but operations on subsequent uses of Syncbase will hang indefinitely.
-        //V.shutdown();
+        mVContext.cancel();
         super.tearDown();
     }
 
-    public SyncbaseClient createSyncbaseClient() {
+    public SyncbaseAndroidClient createSyncbaseClient() {
         // TODO(rosswang): zero duration after https://github.com/vanadium/issues/issues/809
-        return new SyncbaseClient(getContext(), null, true, Duration.standardMinutes(2));
+        return new SyncbaseAndroidClient(getContext(), null, true, Duration.standardMinutes(2));
     }
 }
\ No newline at end of file
diff --git a/baku-toolkit/lib/src/androidTest/java/io/v/baku/toolkit/bind/CollectionBindingTest.java b/baku-toolkit/lib/src/androidTest/java/io/v/baku/toolkit/bind/CollectionBindingTest.java
index 60ababf..2aca0b0 100644
--- a/baku-toolkit/lib/src/androidTest/java/io/v/baku/toolkit/bind/CollectionBindingTest.java
+++ b/baku-toolkit/lib/src/androidTest/java/io/v/baku/toolkit/bind/CollectionBindingTest.java
@@ -9,17 +9,17 @@
 import com.google.common.base.Throwables;
 
 import io.v.baku.toolkit.VAndroidTestCase;
-import io.v.rx.syncbase.RxSyncbase;
+import io.v.rx.syncbase.RxAndroidSyncbase;
 import io.v.rx.syncbase.RxTable;
 
 public class CollectionBindingTest extends VAndroidTestCase {
-    private RxSyncbase mRxSyncbase;
+    private RxAndroidSyncbase mRxSyncbase;
     private RxTable mTable;
 
     @Override
     protected void setUp() throws Exception {
         super.setUp();
-        mRxSyncbase = new RxSyncbase(getVContext(), createSyncbaseClient());
+        mRxSyncbase = new RxAndroidSyncbase(getVContext(), createSyncbaseClient());
         mTable = mRxSyncbase.rxApp(getClass().getName()).rxDb("db").rxTable("t");
     }
 
diff --git a/baku-toolkit/lib/src/androidTest/java/io/v/debug/SyncbaseClientTest.java b/baku-toolkit/lib/src/androidTest/java/io/v/debug/SyncbaseClientTest.java
index bb319b5..c1f9d47 100644
--- a/baku-toolkit/lib/src/androidTest/java/io/v/debug/SyncbaseClientTest.java
+++ b/baku-toolkit/lib/src/androidTest/java/io/v/debug/SyncbaseClientTest.java
@@ -17,7 +17,7 @@
     public void testAppPersistence() throws Exception {
         final VContext ctx = getVContext();
 
-        try (final SyncbaseClient sb = createSyncbaseClient()) {
+        try (final SyncbaseAndroidClient sb = createSyncbaseClient()) {
             final SyncbaseApp app = first(sb.getRxClient()).getApp(APP);
             try {
                 sync(app.create(ctx, null));
@@ -26,7 +26,7 @@
             assertEquals(true, (boolean) sync(app.exists(ctx)));
         }
 
-        try (final SyncbaseClient sb = createSyncbaseClient()) {
+        try (final SyncbaseAndroidClient sb = createSyncbaseClient()) {
             final SyncbaseApp app = first(sb.getRxClient()).getApp(APP);
             assertEquals(true, (boolean) sync(app.exists(ctx)));
             sync(app.destroy(ctx));
diff --git a/baku-toolkit/lib/src/androidTest/java/io/v/rx/syncbase/RxSyncbaseTest.java b/baku-toolkit/lib/src/androidTest/java/io/v/rx/syncbase/RxSyncbaseTest.java
index 1e9ba56..62b6a97 100644
--- a/baku-toolkit/lib/src/androidTest/java/io/v/rx/syncbase/RxSyncbaseTest.java
+++ b/baku-toolkit/lib/src/androidTest/java/io/v/rx/syncbase/RxSyncbaseTest.java
@@ -21,13 +21,13 @@
 @Accessors(prefix = "m")
 @Getter
 public class RxSyncbaseTest extends VAndroidTestCase {
-    private RxSyncbase mRxSyncbase;
+    private RxAndroidSyncbase mRxSyncbase;
     private RxTable mTable;
 
     @Override
     protected void setUp() throws Exception {
         super.setUp();
-        mRxSyncbase = new RxSyncbase(getVContext(), createSyncbaseClient());
+        mRxSyncbase = new RxAndroidSyncbase(getVContext(), createSyncbaseClient());
         mTable = mRxSyncbase.rxApp(getClass().getName()).rxDb("db").rxTable("t");
     }
 
diff --git a/baku-toolkit/lib/src/androidTest/java/io/v/rx/syncbase/SgHostUtilLocalTest.java b/baku-toolkit/lib/src/androidTest/java/io/v/rx/syncbase/SgHostUtilLocalTest.java
index c078b27..c2ea275 100644
--- a/baku-toolkit/lib/src/androidTest/java/io/v/rx/syncbase/SgHostUtilLocalTest.java
+++ b/baku-toolkit/lib/src/androidTest/java/io/v/rx/syncbase/SgHostUtilLocalTest.java
@@ -51,7 +51,6 @@
 
     @Override
     protected void tearDown() throws Exception {
-        mMountTable.stop();
         FileUtils.deleteDirectory(mStorageRoot);
         super.tearDown();
     }
diff --git a/baku-toolkit/lib/src/androidTest/java/io/v/rx/syncbase/SgHostUtilTestCases.java b/baku-toolkit/lib/src/androidTest/java/io/v/rx/syncbase/SgHostUtilTestCases.java
index 5467439..c969d05 100644
--- a/baku-toolkit/lib/src/androidTest/java/io/v/rx/syncbase/SgHostUtilTestCases.java
+++ b/baku-toolkit/lib/src/androidTest/java/io/v/rx/syncbase/SgHostUtilTestCases.java
@@ -10,7 +10,7 @@
 
 import java.util.concurrent.TimeUnit;
 
-import io.v.debug.SyncbaseClient;
+import io.v.debug.SyncbaseAndroidClient;
 import io.v.impl.google.naming.NamingUtil;
 import io.v.v23.context.VContext;
 import lombok.RequiredArgsConstructor;
@@ -48,7 +48,7 @@
 
     public void testEnsureSgHost() {
         final String name = name("users/jenkins.veyron@gmail.com/integ/ensuredsghost");
-        try (final SyncbaseClient sb = new SyncbaseClient(mContext, null)) {
+        try (final SyncbaseAndroidClient sb = new SyncbaseAndroidClient(mContext, null)) {
             block(SgHostUtil.ensureSyncgroupHost(mVContext, sb.getRxServer(), name)).first();
             assertTrue(block(SgHostUtil.isSyncbaseOnline(mVContext, name)).first());
         }
@@ -58,13 +58,13 @@
     /*public void testGlobalUserSyncgroup() {
         final Observable<Blessings> blessings =
                 Observable.just(V.getPrincipal(mVContext).blessingStore().forPeer("..."));
-        try (final SyncbaseClient sb = new SyncbaseClient(mContext, blessings)) {
+        try (final SyncbaseAndroidClient sb = new SyncbaseAndroidClient(mContext, blessings)) {
             final RxSyncbase rsb = new RxSyncbase(mVContext, sb);
-            block(GlobalUserSyncgroup.builder()
+            block(UserPeerSyncgroup.builder()
                     .syncbase(rsb)
                     .db(rsb.rxApp("app").rxDb("db"))
                     .sgSuffix("test")
-                    .syncHostLevel(new UserAppSyncHost("integ"))
+                    .syncHostLevel(new ClientLevelCloudSync("integ"))
                     .rxBlessings(blessings)
                     .build()
                     .rxJoin()).first();
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuActivity.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuActivity.java
index 3e424ea..0277502 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuActivity.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuActivity.java
@@ -14,6 +14,8 @@
 /**
  * A default integration with {@link BakuActivityTrait} extending {@link android.app.Activity}. Most
  * activities with distributed state should inherit from this.
+ *
+ * @see io.v.baku.toolkit
  */
 @Slf4j
 public abstract class BakuActivity extends VActivity implements BakuActivityTrait<Activity> {
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuActivityMixin.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuActivityMixin.java
index 79975d6..f222070 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuActivityMixin.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuActivityMixin.java
@@ -7,19 +7,19 @@
 import android.app.Activity;
 import android.os.Bundle;
 
-import io.v.baku.toolkit.bind.SyncbaseBinding;
 import io.v.baku.toolkit.bind.CollectionBinding;
+import io.v.baku.toolkit.bind.SyncbaseBinding;
 import io.v.baku.toolkit.syncbase.BakuDb;
 import io.v.baku.toolkit.syncbase.BakuSyncbase;
 import io.v.baku.toolkit.syncbase.BakuTable;
-import io.v.rx.syncbase.GlobalUserSyncgroup;
+import io.v.rx.syncbase.UserCloudSyncgroup;
 import lombok.Getter;
 import lombok.experimental.Accessors;
 import lombok.extern.slf4j.Slf4j;
 import rx.subscriptions.CompositeSubscription;
 
 /**
- * Activity trait for activities with distributed UI state. By default, shared state is stored
+ * Activity mix-in for activities with distributed UI state. By default, shared state is stored
  * in Syncbase under <i>app.package.name</i>/db/ui.
  * <p>
  * Default activity extensions incorporating this mix-in are available:
@@ -29,7 +29,11 @@
  * </ul>
  * <p>
  * Since Java doesn't actually support multiple inheritance, clients requiring custom inheritance
- * hierarchies will need to wire in manually, like any of the examples above.
+ * hierarchies will need to wire in manually, like any of the examples above. Alternatively, this
+ * class may be used via pure composition, as detailed at
+ * {@link BakuActivityMixin#BakuActivityMixin(Activity, Bundle)}.
+ *
+ * @see io.v.baku.toolkit
  */
 @Accessors(prefix = "m")
 @Slf4j
@@ -70,7 +74,7 @@
      *     private BakuActivityTrait<SampleCompositionActivity> mBaku;
      *
      *     &#64;Override
-     *     protected void onCreate(Bundle savedInstanceState) {
+     *     protected void onCreate(final Bundle savedInstanceState) {
      *         super.onCreate(savedInstanceState);
      *         setContentView(R.layout.activity_hello);
      *
@@ -108,7 +112,7 @@
     }
 
     protected void joinInitialSyncGroup() {
-        GlobalUserSyncgroup.forActivity(this).join();
+        UserCloudSyncgroup.forActivity(this).join();
     }
 
     public void onSyncError(final Throwable t) {
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuActivityTrait.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuActivityTrait.java
index 9fbd4ec..9967814 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuActivityTrait.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuActivityTrait.java
@@ -13,6 +13,9 @@
 import io.v.baku.toolkit.syncbase.BakuTable;
 import rx.subscriptions.CompositeSubscription;
 
+/**
+ * @see BakuActivityMixin
+ */
 public interface BakuActivityTrait<T extends Activity> extends AutoCloseable {
     VAndroidContextTrait<T> getVAndroidContextTrait();
     BakuSyncbase getSyncbase();
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuAppCompatActivity.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuAppCompatActivity.java
index 14220dd..85c0da8 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuAppCompatActivity.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuAppCompatActivity.java
@@ -14,6 +14,8 @@
 /**
  * A default integration with {@link BakuActivityTrait} extending
  * {@link android.support.v7.app.AppCompatActivity}.
+ *
+ * @see io.v.baku.toolkit
  */
 @Slf4j
 public abstract class BakuAppCompatActivity
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/VActivity.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/VActivity.java
index 8ebd442..1164a0f 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/VActivity.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/VActivity.java
@@ -32,4 +32,10 @@
         super.onCreate(savedInstanceState, persistentState);
         mVAndroidContextTrait = createVActivityTrait(savedInstanceState);
     }
+
+    @Override
+    protected void onDestroy() {
+        mVAndroidContextTrait.close();
+        super.onDestroy();
+    }
 }
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/VAndroidContextMixin.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/VAndroidContextMixin.java
index 467cc7e..03a3ad4 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/VAndroidContextMixin.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/VAndroidContextMixin.java
@@ -12,7 +12,7 @@
 import android.os.Bundle;
 
 import io.v.android.v23.V;
-import io.v.baku.toolkit.blessings.AccountManagerBlessingsFragment;
+import io.v.baku.toolkit.blessings.BlessingsManagerBlessingsProvider;
 import io.v.baku.toolkit.blessings.BlessingsProvider;
 import io.v.baku.toolkit.blessings.BlessingsUtils;
 import io.v.baku.toolkit.debug.DebugFragment;
@@ -21,20 +21,21 @@
 import io.v.v23.context.VContext;
 import io.v.v23.security.Blessings;
 import io.v.v23.verror.VException;
+import java8.util.function.BiFunction;
 import lombok.Getter;
 import lombok.experimental.Accessors;
 import lombok.extern.slf4j.Slf4j;
 
 /**
- * Android context mix-in incorporating common Vanadium utilities:
+ * Android context mix-in incorporating common Vanadium utilities. These include:
  * <ul>
  * <li>Vanadium initialization during {@code onCreate}; context available via
  * {@code getVContext}</li>
- * <li>Blessings management, available via {@code getBlessingsProvider().getRxBlessings}.
- * Upon {@code subscribe}, blessings are refreshed from the {@code BlessingsManager} or sought from
- * the {@code BlessingsProvider} (by default, the Account Manager).</li>
+ * <li>Blessings management, available via {@link BlessingsProvider#getRxBlessings()
+ * getBlessingsProvider().getRxBlessings()}. Upon {@code subscribe}, blessings are refreshed from
+ * the {@code BlessingsManager} or sought from the {@code BlessingsProvider} (by default, the
+ * Vanadium {@link io.v.android.libs.security.BlessingsManager}).</li>
  * </ul>
- * <p>
  * Default activity extensions incorporating this mix-in are available:
  * <ul>
  * <li>{@link VActivity} (extends {@link Activity})</li>
@@ -70,17 +71,6 @@
         try {
             return V.init(mAndroidContext, getSavedOptions());
         } catch (final RuntimeException e) {
-            try {
-                /* V.shutdown so we might try V.init again if warranted. If we don't V.shutdown
-                first, the process can abruptly die. It seems that if this happens, Android might
-                just restart the app immediately, i.e. before we've been able to display an
-                intelligible error message. */
-                V.shutdown();
-            } catch (final RuntimeException e2) {
-                log.error("Unable to clean up failed Vanadium initialization", e2);
-                e.addSuppressed(e2);
-            }
-
             if (mVanadiumPreferences.getAll().isEmpty()) {
                 throw e;
             } else {
@@ -93,18 +83,30 @@
 
     public VAndroidContextMixin(final T androidContext, final BlessingsProvider blessingsProvider,
                                 final ErrorReporter errorReporter) {
+        this(androidContext, (x, y) -> blessingsProvider, errorReporter);
+    }
+
+    public VAndroidContextMixin(
+            final T androidContext, final BiFunction<? super VContext, ? super T, BlessingsProvider>
+            blessingsProviderFactory, final ErrorReporter errorReporter) {
         mAndroidContext = androidContext;
-        mBlessingsProvider = blessingsProvider;
         mErrorReporter = errorReporter;
 
         mVanadiumPreferences = getVanadiumPreferences(mAndroidContext);
         mVContext = vinit();
 
+        mBlessingsProvider = blessingsProviderFactory.apply(mVContext, mAndroidContext);
+
         //Any time our blessings change, we need to attach them to our principal.
         mBlessingsProvider.getPassiveRxBlessings().subscribe(this::processBlessings,
                 t -> errorReporter.onError(R.string.err_blessings_misc, t));
     }
 
+    @Override
+    public void close() {
+        mVContext.cancel();
+    }
+
     protected void processBlessings(final Blessings blessings) {
         try {
             BlessingsUtils.assumeBlessings(mVContext, blessings);
@@ -116,15 +118,12 @@
     public static <T extends Activity> VAndroidContextMixin<T> withDefaults(
             final T activity, final Bundle savedInstanceState) {
         final FragmentManager mgr = activity.getFragmentManager();
-        final AccountManagerBlessingsFragment blessingsProvider;
         final ErrorReporterFragment errorReporter;
 
         if (savedInstanceState == null) {
-            blessingsProvider = new AccountManagerBlessingsFragment();
             errorReporter = new ErrorReporterFragment();
 
             final FragmentTransaction t = mgr.beginTransaction()
-                    .add(blessingsProvider, AccountManagerBlessingsFragment.TAG)
                     .add(errorReporter, ErrorReporterFragment.TAG);
 
             if (DebugUtils.isApkDebug(activity)) {
@@ -133,9 +132,9 @@
             }
             t.commit();
         } else {
-            blessingsProvider = AccountManagerBlessingsFragment.find(mgr);
             errorReporter = ErrorReporterFragment.find(mgr);
         }
-        return new VAndroidContextMixin<>(activity, blessingsProvider, errorReporter);
+        return new VAndroidContextMixin<>(activity, BlessingsManagerBlessingsProvider::new,
+                errorReporter);
     }
 }
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/VAndroidContextTrait.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/VAndroidContextTrait.java
index 4744a8b..63eaaf4 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/VAndroidContextTrait.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/VAndroidContextTrait.java
@@ -9,11 +9,12 @@
 import io.v.baku.toolkit.blessings.BlessingsProvider;
 import io.v.v23.context.VContext;
 
-public interface VAndroidContextTrait<T extends Context> {
+public interface VAndroidContextTrait<T extends Context> extends AutoCloseable {
     String VANADIUM_OPTIONS_SHARED_PREFS = "VanadiumOptions";
 
     T getAndroidContext();
     BlessingsProvider getBlessingsProvider();
     ErrorReporter getErrorReporter();
     VContext getVContext();
+    void close();
 }
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/VAppCompatActivity.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/VAppCompatActivity.java
index c3ca36c..f5b453e 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/VAppCompatActivity.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/VAppCompatActivity.java
@@ -37,4 +37,10 @@
         super.onCreate(savedInstanceState, persistentState);
         mVAndroidContextTrait = createVActivityTrait(savedInstanceState);
     }
+
+    @Override
+    protected void onDestroy() {
+        mVAndroidContextTrait.close();
+        super.onDestroy();
+    }
 }
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/CollectionAdapterBuilder.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/CollectionAdapterBuilder.java
index d11f93a..0c4d7b2 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/CollectionAdapterBuilder.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/CollectionAdapterBuilder.java
@@ -4,6 +4,7 @@
 
 package io.v.baku.toolkit.bind;
 
+import android.support.annotation.IdRes;
 import android.support.v7.widget.RecyclerView;
 import android.view.View;
 import android.widget.ListView;
@@ -88,4 +89,12 @@
             throw new IllegalArgumentException("No default binding for view " + view);
         }
     }
+
+    /**
+     * Binds to the view identified by {@code viewId}.
+     * @see #bindTo(View)
+     */
+    public RangeAdapter bindTo(final @IdRes int viewId) {
+        return bindTo(mBase.mActivity.findViewById(viewId));
+    }
 }
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixBindingBuilder.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixBindingBuilder.java
index b1085f3..600c3f5 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixBindingBuilder.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixBindingBuilder.java
@@ -6,6 +6,8 @@
 
 import com.google.common.collect.Ordering;
 
+import java.util.Comparator;
+
 import io.v.rx.syncbase.RangeWatchBatch;
 import io.v.rx.syncbase.RxTable;
 import io.v.v23.syncbase.nosql.PrefixRange;
@@ -21,7 +23,7 @@
         extends CollectionAdapterBuilder<PrefixBindingBuilder<T, A>, RxTable.Row<T>, A> {
     private Class<T> mType;
     private PrefixRange mPrefix;
-    private Ordering<? super RxTable.Row<T>> mOrdering;
+    private Comparator<? super RxTable.Row<T>> mOrdering;
     private Func1<String, Boolean> mKeyFilter;
 
     public PrefixBindingBuilder(final CollectionBinding.Builder base) {
@@ -38,9 +40,11 @@
     }
 
     /**
+     * The element type for the collection, i.e. the value type for rows matching the prefix and key
+     * filter.
+     * <p>
      * This setter is minimally typesafe; after setting the {@code type}, clients should
-     * probably also update {@code ordering} and {@code viewAdapter}. If intending to use a
-     * collection binding that requires a
+     * probably also update {@code ordering} and {@code viewAdapter}.
      */
     public <U> PrefixBindingBuilder<U, A> type(final Class<U> type) {
         @SuppressWarnings("unchecked")
@@ -68,12 +72,19 @@
         return new TextViewAdapter(mBase.getDefaultViewAdapterContext()).map(RxTable.Row::getValue);
     }
 
+    private Class<T> getType() {
+        if (mType == null) {
+            throw new IllegalStateException("Missing required type property");
+        }
+        return mType;
+    }
+
     /**
      * For comparable {@code T}, default to natural ordering on values. Otherwise, default to
      * natural ordering on row names.
      */
     private Ordering<? super RxTable.Row<? extends T>> getDefaultOrdering() {
-        if (mOrdering == null && Comparable.class.isAssignableFrom(mType)) {
+        if (mOrdering == null && Comparable.class.isAssignableFrom(getType())) {
             return Ordering.natural().onResultOf(r -> (Comparable) r.getValue());
         } else {
             return Ordering.natural().onResultOf(RxTable.Row::getRowName);
@@ -86,14 +97,11 @@
     }
 
     public Observable<RangeWatchBatch<T>> buildPrefixWatch() {
-        if (mType == null) {
-            throw new IllegalStateException("Missing required type property");
-        }
         return mBase.mRxTable.watch(mPrefix == null? RowRange.prefix("") : mPrefix,
-                mKeyFilter, mType);
+                mKeyFilter, getType());
     }
 
-    private Ordering<? super RxTable.Row<T>> getOrdering() {
+    private Comparator<? super RxTable.Row<T>> getOrdering() {
         return mOrdering == null ? getDefaultOrdering() : mOrdering;
     }
 
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixListAccumulator.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixListAccumulator.java
index 9bd3c57..117fc87 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixListAccumulator.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixListAccumulator.java
@@ -10,6 +10,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.ConcurrentModificationException;
 import java.util.HashMap;
 import java.util.List;
@@ -35,13 +36,17 @@
 
     private final Map<String, T> mRows = new HashMap<>();
     private final List<RxTable.Row<T>> mSorted = new ArrayList<>();
-    private final Ordering<? super RxTable.Row<T>> mOrdering;
+    private final Comparator<? super RxTable.Row<T>> mOrdering;
 
-    public PrefixListAccumulator(final Ordering<? super RxTable.Row<T>> ordering) {
+    public PrefixListAccumulator(final Comparator<? super RxTable.Row<T>> ordering) {
         // ensure deterministic ordering by always applying secondary order on row name
-        mOrdering = ordering.compound(Ordering.natural().onResultOf(RxTable.Row::getRowName));
+        mOrdering = Ordering.from(ordering).compound(
+                Ordering.natural().onResultOf(RxTable.Row::getRowName));
     }
 
+    /**
+     * The generic wildcard is for the benefit of subclass overrides.
+     */
     public Observable<? extends PrefixListAccumulator<T>> scanFrom(
             final Observable<RangeWatchBatch<T>> watch) {
         return watch
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixListDeltaAccumulator.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixListDeltaAccumulator.java
index d804799..7d5a232 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixListDeltaAccumulator.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixListDeltaAccumulator.java
@@ -6,7 +6,7 @@
 
 import android.support.v7.widget.RecyclerView;
 
-import com.google.common.collect.Ordering;
+import java.util.Comparator;
 
 import io.v.rx.syncbase.RangeWatchBatch;
 import io.v.rx.syncbase.RxTable;
@@ -24,7 +24,7 @@
     private Consumer<RecyclerView.Adapter> mDeltas;
     private final NumericIdMapper mIds = new NumericIdMapper();
 
-    public PrefixListDeltaAccumulator(final Ordering<? super RxTable.Row<T>> ordering) {
+    public PrefixListDeltaAccumulator(final Comparator<? super RxTable.Row<T>> ordering) {
         super(ordering);
     }
 
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/doc-files/bindings.png b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/doc-files/bindings.png
new file mode 100644
index 0000000..bbfad53
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/doc-files/bindings.png
Binary files differ
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/doc-files/mvvm.png b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/doc-files/mvvm.png
new file mode 100644
index 0000000..25b2909
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/doc-files/mvvm.png
Binary files differ
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/package-info.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/package-info.java
index 4bdca83..10ff1a4 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/package-info.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/package-info.java
@@ -3,10 +3,44 @@
 // license that can be found in the LICENSE file.
 
 /**
- * Constructs:
- * <ul>
- * <li>Binding - maps one or more controls/properties to a Syncbase row.</li>
- * <li>Coordination policy - integrates uplinks and downlinks.</li>
- * </ul>
+ * These classes provide bindings between Android widgets and Syncbase data. For the reasons
+ * outlined in {@link io.v.rx.syncbase}, Vanadium state distribution with Syncbase would ideally be
+ * done with pure FRP <a href="https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel"
+ * target="_blank">MVVM</a>, with distributed state elements modeled in Syncbase.
+ * <p>
+ * <img src="doc-files/mvvm.png">
+ * <p>
+ * <b>Fig: Ideal MVVM for distributed apps</b>
+ * <p>
+ * However, while this is easily accomplished with
+ * <a href="https://flutter.io/" target="_blank">Flutter</a>, the Baku Toolkit would need to
+ * implement an Android viewmodel layer to achieve the same for Android Java, which starts to amount
+ * to reimplementing Flutter for Java. Instead, the Baku Android Toolkit offers data bindings that
+ * allow Syncbase to drive more conventional Android Java UI widgets, and to allow those widgets to
+ * update distributed state in Syncbase.
+ * <p>
+ * <img src="doc-files/bindings.png">
+ * <p>
+ * <b>Fig: Baku data bindings without viewmodel</b>
+ * <p>
+ * Even though these data bindings are not true MVVM, app developers are encouraged to treat them
+ * declaratively, and to make use of pure functional transformations wherever possible to simplify
+ * their code. Imperative code is however still useful for Android initialization, implementing
+ * reactive widget update logic, and writing to Syncbase.
+ * <p>
+ * Data bindings are offered to client applications via builders. Many facets of bindings are
+ * derived from their usage context. For example, {@code bindTo(...)} methods perform type
+ * introspection to construct appropriate binding types for the widget being bound (and possibly the
+ * row type being bound to). In the future, we may add a plug-in to preprocess Android layout markup
+ * similar to the <a href="http://developer.android.com/tools/data-binding/guide.html"
+ * target="_blank">Android Data Binding Library</a>.
+ * <p>
+ * At present, for simplicity, each data binding that reads from Syncbase has its own Syncbase watch
+ * stream. If this ends up wasting resources and degrading performance, we can optimize to minimize
+ * the number of watch streams and broadcast filtered streams to each data binding.
+ * <p>
+ * Offering data bindings rather than pure functional MVVM transforms does introduce some
+ * coordination concerns between read and write bindings. Strategies for dealing with coordination
+ * are included in the toolkit.
  */
 package io.v.baku.toolkit.bind;
\ No newline at end of file
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/blessings/AbstractRefreshableBlessingsProvider.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/blessings/AbstractRefreshableBlessingsProvider.java
new file mode 100644
index 0000000..e6ef88c
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/blessings/AbstractRefreshableBlessingsProvider.java
@@ -0,0 +1,95 @@
+// 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.
+
+package io.v.baku.toolkit.blessings;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import net.javacrumbs.futureconverter.guavarx.FutureConverter;
+
+import io.v.v23.security.Blessings;
+import lombok.Getter;
+import lombok.Synchronized;
+import lombok.experimental.Accessors;
+import rx.Observable;
+import rx.subjects.PublishSubject;
+
+@Accessors(prefix = "m")
+public abstract class AbstractRefreshableBlessingsProvider implements RefreshableBlessingsProvider {
+    /**
+     * An observable that, when subscribed to, refreshes the blessing. If the
+     * {@link io.v.android.libs.security.BlessingsManager} needs to be invoked for the refresh, the
+     * subscription will not produce results until the invocation completes. Subsequently, it will
+     * receive all blessings refreshed via {@link #refreshBlessings()} and other subscriptions to
+     * {@link #getRxBlessings()}.
+     */
+    @Getter
+    private final Observable<Blessings> mRxBlessings;
+    /**
+     * An observable for the blessings that does not refresh when subscribed to. Upon subscription,
+     * this will produce the last known blessing. It will subsequently receive all blessings
+     * refreshed via {@link #refreshBlessings()} and subscriptions to {@link #getRxBlessings()}.
+     */
+    @Getter
+    private final Observable<Blessings> mPassiveRxBlessings;
+
+    private final PublishSubject<ListenableFuture<Blessings>> mPub;
+    private Blessings mLastBlessings;
+    private ListenableFuture<Blessings> mCurrentSeek;
+    private final Object mSeekLock = new Object();
+
+    public AbstractRefreshableBlessingsProvider() {
+        this(null);
+    }
+
+    public AbstractRefreshableBlessingsProvider(final ListenableFuture<Blessings> seekInProgress) {
+        mCurrentSeek = seekInProgress;
+
+        mPub = PublishSubject.create();
+        mPassiveRxBlessings = mPub
+                .flatMap(FutureConverter::toObservable)
+                .distinctUntilChanged()
+                .replay(1).autoConnect();
+        /* It might make more sense for b -> mLastBlessings = b to be an onNext before the above
+        replay rather than a subscription (especially if we start getting
+        OnErrorNotImplementedException or have to include a possibly redundant error reporter).
+        However, replay, even with autoConnect(0), does not offer backpressure support unless it has
+        subscribers. We can get around this by adding .onBackpressureBuffer(1), but if this turns
+        out to be a better way of doing this, we should submit an issue requesting that
+        OperatorReplay use its buffer size for backpressure. */
+        mPassiveRxBlessings.subscribe(b -> mLastBlessings = b);
+        mRxBlessings = Observable.defer(() -> FutureConverter.toObservable(refreshBlessings()))
+                .ignoreElements()
+                .concatWith(mPassiveRxBlessings);
+    }
+
+    @Synchronized("mSeekLock")
+    public boolean isAwaitingBlessings() {
+        return mCurrentSeek != null;
+    }
+
+    protected abstract ListenableFuture<Blessings> handleBlessingsRefresh();
+
+    @Synchronized("mSeekLock")
+    private void onBlessingsHandled() {
+        mCurrentSeek = null;
+    }
+
+    @Override
+    @Synchronized("mSeekLock")
+    public ListenableFuture<Blessings> refreshBlessings() {
+        if (isAwaitingBlessings()) {
+            return mCurrentSeek;
+        }
+
+        // Store in a local variable as well in case onBlessingsHandled immediately clears the
+        // Future
+        final ListenableFuture<Blessings> seek = mCurrentSeek = handleBlessingsRefresh();
+        mPub.onNext(seek);
+        seek.addListener(this::onBlessingsHandled, MoreExecutors.directExecutor());
+
+        return seek;
+    }
+}
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/blessings/AccountManagerBlessingsFragment.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/blessings/AccountManagerBlessingsFragment.java
deleted file mode 100644
index 8e95fa5..0000000
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/blessings/AccountManagerBlessingsFragment.java
+++ /dev/null
@@ -1,87 +0,0 @@
-// 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.
-
-package io.v.baku.toolkit.blessings;
-
-import android.app.Fragment;
-import android.app.FragmentManager;
-import android.content.Intent;
-import android.os.Bundle;
-
-import io.v.android.v23.services.blessing.BlessingService;
-import io.v.baku.toolkit.ErrorReporters;
-import io.v.v23.security.Blessings;
-import lombok.experimental.Accessors;
-import lombok.experimental.Delegate;
-import lombok.extern.slf4j.Slf4j;
-import rx.Observable;
-import rx.subjects.ReplaySubject;
-
-@Accessors(prefix = "m")
-@Slf4j
-public class AccountManagerBlessingsFragment extends Fragment
-        implements RefreshableBlessingsProvider {
-    public static final String TAG = AccountManagerBlessingsFragment.class.getName();
-
-    private static final int BLESSING_REQUEST = 0;
-    private static final String SEEKING =
-            AccountManagerBlessingsFragment.class.getName() + ".seeking";
-
-    public static AccountManagerBlessingsFragment find(final FragmentManager mgr) {
-        return (AccountManagerBlessingsFragment)mgr.findFragmentByTag(TAG);
-    }
-
-    @Delegate(types = RefreshableBlessingsProvider.class, excludes = BlessingsProvider.class)
-    private ActivityBlessingsSeeker mSeeker;
-    private ReplaySubject<ActivityBlessingsSeeker> mSeekers = ReplaySubject.createWithSize(1);
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        final boolean resumingSeek = savedInstanceState != null &&
-                savedInstanceState.getBoolean(SEEKING);
-        mSeeker = new ActivityBlessingsSeeker(
-                getActivity(), ErrorReporters.forFragment(this), resumingSeek) {
-            @Override
-            protected void seekBlessings() {
-                final Intent intent = BlessingService.newBlessingIntent(getActivity());
-                startActivityForResult(intent, BLESSING_REQUEST);
-            }
-        };
-        mSeekers.onNext(mSeeker);
-    }
-
-    @Override
-    public void onSaveInstanceState(Bundle outState) {
-        super.onSaveInstanceState(outState);
-        outState.putBoolean(SEEKING, mSeeker.isAwaitingBlessings());
-    }
-
-    @Override
-    public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
-        super.onActivityResult(requestCode, resultCode, data);
-
-        switch (requestCode) {
-            case BLESSING_REQUEST:
-                try {
-                    mSeeker.setBlessings(BlessingsUtils.fromActivityResult(resultCode, data));
-                } catch (final Exception e) {
-                    mSeeker.handleBlessingsError(e);
-                }
-                break;
-            default:
-        }
-    }
-
-    @Override
-    public Observable<Blessings> getRxBlessings() {
-        return mSeekers.switchMap(ActivityBlessingsSeeker::getRxBlessings);
-    }
-
-    @Override
-    public Observable<Blessings> getPassiveRxBlessings() {
-        return mSeekers.switchMap(ActivityBlessingsSeeker::getPassiveRxBlessings);
-    }
-}
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/blessings/ActivityBlessingsSeeker.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/blessings/ActivityBlessingsSeeker.java
deleted file mode 100644
index 302ef3c..0000000
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/blessings/ActivityBlessingsSeeker.java
+++ /dev/null
@@ -1,161 +0,0 @@
-// 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.
-
-package io.v.baku.toolkit.blessings;
-
-import android.app.Activity;
-
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.SettableFuture;
-
-import net.javacrumbs.futureconverter.guavarx.FutureConverter;
-
-import io.v.android.v23.services.blessing.BlessingCreationException;
-import io.v.baku.toolkit.ErrorReporter;
-import io.v.baku.toolkit.R;
-import io.v.v23.security.Blessings;
-import io.v.v23.verror.VException;
-import lombok.Getter;
-import lombok.Synchronized;
-import lombok.experimental.Accessors;
-import lombok.extern.slf4j.Slf4j;
-import rx.Observable;
-import rx.subjects.PublishSubject;
-
-@Accessors(prefix = "m")
-@Slf4j
-public abstract class ActivityBlessingsSeeker implements RefreshableBlessingsProvider {
-    /**
-     * An observable that, when subscribed to, refreshes the blessing. If the account manager needs
-     * to be invoked for the refresh, the subscription will not produce results until the invocation
-     * completes. Subsequently, it will receive all blessings refreshed via
-     * {@link #refreshBlessings()} and other subscriptions to {@link #getRxBlessings()}.
-     */
-    @Getter
-    private final Observable<Blessings> mRxBlessings;
-    /**
-     * An observable for the blessings that does not refresh when subscribed to. Upon subscription,
-     * this will produce the last known blessing. It will subsequently receive all blessings
-     * refreshed via {@link #refreshBlessings()} and subscriptions to {@link #getRxBlessings()}.
-     */
-    @Getter
-    private final Observable<Blessings> mPassiveRxBlessings;
-
-    private final Activity mActivity;
-    private final ErrorReporter mErrorReporter;
-    private final PublishSubject<ListenableFuture<Blessings>> mPub;
-    private Blessings mLastBlessings;
-    private SettableFuture<Blessings> mCurrentSeek;
-    private final Object mSeekLock = new Object();
-
-    public ActivityBlessingsSeeker(final Activity activity, final ErrorReporter errorReporter,
-                                   final boolean seekInProgress) {
-        mActivity = activity;
-        mErrorReporter = errorReporter;
-
-        if (seekInProgress) {
-            mCurrentSeek = SettableFuture.create();
-        }
-
-        mPub = PublishSubject.create();
-        mPassiveRxBlessings = mPub
-                .flatMap(FutureConverter::toObservable)
-                .distinctUntilChanged()
-                .replay(1).autoConnect();
-        /* It might make more sense for b -> mLastBlessings = b to be an onNext before the above
-        replay rather than a subscription (especially if we start getting
-        OnErrorNotImplementedException or have to include a possibly redundant error reporter).
-        However, replay, even with autoConnect(0), does not offer backpressure support unless it has
-        subscribers. We can get around this by adding .onBackpressureBuffer(1), but if this turns
-        out to be a better way of doing this, we should submit an issue requesting that
-        OperatorReplay use its buffer size for backpressure. */
-        mPassiveRxBlessings.subscribe(b -> mLastBlessings = b);
-        mRxBlessings = Observable.defer(() -> FutureConverter.toObservable(refreshBlessings()))
-                .ignoreElements()
-                .concatWith(mPassiveRxBlessings);
-    }
-
-    @Synchronized("mSeekLock")
-    public boolean isAwaitingBlessings() {
-        return mCurrentSeek != null;
-    }
-
-    @Override
-    @Synchronized("mSeekLock")
-    public ListenableFuture<Blessings> refreshBlessings() {
-        if (isAwaitingBlessings()) {
-            return mCurrentSeek;
-        }
-
-        Blessings mgrBlessings;
-        try {
-            mgrBlessings = BlessingsUtils.readSharedPrefs(mActivity.getApplicationContext());
-        } catch (final VException e) {
-            log.warn("Could not get blessings from shared preferences", e);
-            mgrBlessings = null;
-        }
-
-        final ListenableFuture<Blessings> nextBlessings;
-
-        if (mgrBlessings == null) {
-            nextBlessings = mCurrentSeek = SettableFuture.create();
-            seekBlessings();
-        } else {
-            nextBlessings = Futures.immediateFuture(mgrBlessings);
-        }
-        mPub.onNext(nextBlessings);
-
-        return nextBlessings;
-    }
-
-    protected abstract void seekBlessings();
-
-    /**
-     * It is an error to call this method when this instance is not awaiting blessings.
-     */
-    public void handleBlessingsError(final Throwable t) {
-        if (t instanceof BlessingCreationException) {
-            /* This exception can occur if a user hits "Deny" in Blessings Manager, so don't treat
-            it as an error if we have a fallback. */
-            if (mLastBlessings == null) {
-                mErrorReporter.onError(R.string.err_blessings_required, t);
-            } else {
-                log.warn("Could not create blessings", t);
-            }
-        } else if (t instanceof VException) {
-            mErrorReporter.onError(R.string.err_blessings_decode, t);
-        } else {
-            mCurrentSeek.setException(t);
-            synchronized (mSeekLock) {
-                mCurrentSeek = null;
-            }
-            return;
-        }
-
-        if (mLastBlessings == null) {
-            mActivity.finish();
-            /* Block while the app exits, as opposed to returning an error that would be reported
-            (redundantly) elsewhere. */
-        } else {
-            setBlessings(mLastBlessings);
-        }
-    }
-
-    /**
-     * It is an error to call this method when this instance is not awaiting blessings.
-     */
-    public void setBlessings(final Blessings b) {
-        try {
-            BlessingsUtils.writeSharedPrefs(mActivity.getApplicationContext(), b);
-        } catch (final VException e) {
-            mErrorReporter.onError(R.string.err_blessings_store, e);
-        } finally {
-            mCurrentSeek.set(b);
-            synchronized (mSeekLock) {
-                mCurrentSeek = null;
-            }
-        }
-    }
-}
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/blessings/BlessingsManagerBlessingsProvider.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/blessings/BlessingsManagerBlessingsProvider.java
new file mode 100644
index 0000000..9806443
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/blessings/BlessingsManagerBlessingsProvider.java
@@ -0,0 +1,37 @@
+// 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.
+
+package io.v.baku.toolkit.blessings;
+
+import android.app.Activity;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import io.v.android.libs.security.BlessingsManager;
+import io.v.baku.toolkit.VAndroidContextTrait;
+import io.v.v23.context.VContext;
+import io.v.v23.security.Blessings;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+public class BlessingsManagerBlessingsProvider extends AbstractRefreshableBlessingsProvider {
+    private final VContext mVContext;
+    private final Activity mActivity;
+    private final String mKey;
+    private final boolean mSetAsDefault;
+
+    public BlessingsManagerBlessingsProvider(
+            final VAndroidContextTrait<? extends Activity> activity) {
+        this(activity.getVContext(), activity.getAndroidContext());
+    }
+
+    public BlessingsManagerBlessingsProvider(final VContext vContext, final Activity activity) {
+        this(vContext, activity, BlessingsUtils.PREF_BLESSINGS, false);
+    }
+
+    @Override
+    protected ListenableFuture<Blessings> handleBlessingsRefresh() {
+        return BlessingsManager.getBlessings(mVContext, mActivity, mKey, mSetAsDefault);
+    }
+}
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/blessings/BlessingsUtils.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/blessings/BlessingsUtils.java
index 7381b16..3ea41e4 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/blessings/BlessingsUtils.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/blessings/BlessingsUtils.java
@@ -5,7 +5,6 @@
 package io.v.baku.toolkit.blessings;
 
 import android.content.Context;
-import android.content.Intent;
 import android.content.SharedPreferences;
 import android.preference.PreferenceManager;
 import android.util.Base64;
@@ -14,6 +13,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Maps;
 
@@ -30,8 +30,6 @@
 import java.util.regex.Pattern;
 
 import io.v.android.v23.V;
-import io.v.android.v23.services.blessing.BlessingCreationException;
-import io.v.android.v23.services.blessing.BlessingService;
 import io.v.impl.google.naming.NamingUtil;
 import io.v.v23.context.VContext;
 import io.v.v23.security.BlessingPattern;
@@ -62,7 +60,8 @@
     public static final String
             PREF_BLESSINGS = "VanadiumBlessings",
             GLOBAL_BLESSING_ROOT_URL = "https://dev.v.io/auth/blessing-root";
-    public static final Pattern DEV_V_IO_USER = Pattern.compile("dev\\.v\\.io:u:([^:]+).*");
+    public static final Pattern DEV_V_IO_CLIENT_USER =
+            Pattern.compile("dev\\.v\\.io:o:([^:]+):([^:]+).*");
 
     public static final AccessList OPEN_ACL = new AccessList(
             ImmutableList.of(new BlessingPattern("...")), ImmutableList.of());
@@ -77,12 +76,6 @@
             OPEN_DATA_PERMS = dataPermissions(OPEN_ACL),
             OPEN_MOUNT_PERMS = mountPermissions(OPEN_ACL);
 
-    public static Blessings fromActivityResult(final int resultCode, final Intent data)
-            throws BlessingCreationException, VException {
-        // The Account Manager will pass us the blessings to use as an array of bytes.
-        return decodeBlessings(BlessingService.extractBlessingReply(resultCode, data));
-    }
-
     public static void writeSharedPrefs(final Context context, final Blessings blessings)
             throws VException {
         writeSharedPrefs(PreferenceManager.getDefaultSharedPreferences(context), PREF_BLESSINGS,
@@ -124,34 +117,51 @@
                 ImmutableList.of());
     }
 
-    public static Stream<String> blessingsToUsernameStream(final VContext ctx,
-                                                           final Blessings blessings) {
+    /**
+     * This method adds the given {@link BlessingPattern} to the given {@link AccessList} but does
+     * not perform deduping or checking to make sure the new pattern is not in
+     * {@link AccessList#getNotIn()}.
+     */
+    public static AccessList augmentAcl(final AccessList acl, final BlessingPattern newBlessing) {
+        return new AccessList(ImmutableList.<BlessingPattern>builder()
+                .addAll(acl.getIn())
+                .add(newBlessing).build(),
+                ImmutableList.of());
+    }
+
+    public static Stream<ClientUser> blessingsToClientUserStream(final VContext ctx,
+                                                                 final Blessings blessings) {
         return StreamSupport.stream(getBlessingNames(ctx, blessings))
-                .map(DEV_V_IO_USER::matcher)
+                .map(DEV_V_IO_CLIENT_USER::matcher)
                 .filter(Matcher::matches)
-                .map(m -> m.group(1));
+                .map(m -> new ClientUser(m.group(1), m.group(2)));
     }
 
     /**
-     * This method finds and parses all blessings of the form dev.v.io/u/.... This is different from
-     * the method at https://v.io/tutorials/java/android.html, which can return additional
-     * extensions ("/android").
+     * This method finds and parses all blessings of the form dev.v.io/o/....
      */
-    public static Set<String> blessingsToUsernames(final VContext ctx, final Blessings blessings) {
-        return blessingsToUsernameStream(ctx, blessings).collect(Collectors.toSet());
+    public static Set<ClientUser> blessingsToClientUsers(
+            final VContext ctx, final Blessings blessings) {
+        return blessingsToClientUserStream(ctx, blessings).collect(Collectors.toSet());
     }
 
     public static String userMount(final String username) {
         return NamingUtil.join("users", username);
     }
 
-    public static Stream<String> blessingsToUserMountStream(final VContext ctx, final Blessings blessings) {
-        return blessingsToUsernameStream(ctx, blessings)
-                .map(BlessingsUtils::userMount);
+    public static String clientMount(final String clientId) {
+        return NamingUtil.join("tmp", "clients", clientId);
     }
 
-    public static Set<String> blessingsToUserMounts(final VContext ctx, final Blessings blessings) {
-        return blessingsToUserMountStream(ctx, blessings).collect(Collectors.toSet());
+    public static Stream<String> blessingsToClientMountStream(final VContext ctx,
+                                                              final Blessings blessings) {
+        return blessingsToClientUserStream(ctx, blessings)
+                .map(cu -> BlessingsUtils.clientMount(cu.getClientId()));
+    }
+
+    public static Set<String> blessingsToClientMounts(final VContext ctx,
+                                                      final Blessings blessings) {
+        return blessingsToClientMountStream(ctx, blessings).collect(Collectors.toSet());
     }
 
     public static Permissions homogeneousPermissions(final Set<Tag> tags, final AccessList acl) {
@@ -171,6 +181,22 @@
     }
 
     /**
+     * TODO(rosswang): This probably won't be best practice in the long run, but we'll need it until
+     * we can bless the cloud Syncbase instance remotely.
+     */
+    public static Permissions cloudSyngroupPermissions(final AccessList userAcl,
+                                                       final BlessingPattern sgHostBlessing) {
+        final AccessList cloudAcl = augmentAcl(userAcl, sgHostBlessing);
+        return new Permissions(ImmutableMap.of(
+                Constants.ADMIN.getValue(), cloudAcl,
+                Constants.READ.getValue(), cloudAcl,
+                Constants.WRITE.getValue(), userAcl,
+                Constants.RESOLVE.getValue(), userAcl,
+                Constants.DEBUG.getValue(), userAcl
+        ));
+    }
+
+    /**
      * Standard blessing handling for Vanadium applications:
      * <ul>
      * <li>Provide the given blessings when anybody connects to us.</li>
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/blessings/ClientUser.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/blessings/ClientUser.java
new file mode 100644
index 0000000..af0f44b
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/blessings/ClientUser.java
@@ -0,0 +1,14 @@
+// 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.
+
+package io.v.baku.toolkit.blessings;
+
+import lombok.Value;
+import lombok.experimental.Accessors;
+
+@Value
+@Accessors(prefix = "m")
+public class ClientUser {
+    String mClientId, mUsername;
+}
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/debug/DebugUtils.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/debug/DebugUtils.java
index 1d4ce86..56bf749 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/debug/DebugUtils.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/debug/DebugUtils.java
@@ -13,7 +13,6 @@
 
 import java.io.IOException;
 
-import io.v.android.v23.V;
 import io.v.baku.toolkit.R;
 import io.v.baku.toolkit.VAndroidContextTrait;
 import lombok.experimental.UtilityClass;
@@ -79,7 +78,6 @@
      */
     public static void killProcess(final Context context) {
         stopPackageServices(context);
-        V.shutdown();
         System.exit(0);
     }
 
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/package-info.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/package-info.java
new file mode 100644
index 0000000..83e196e
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/package-info.java
@@ -0,0 +1,44 @@
+// Copyright 2016 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.
+
+/**
+ * This package is the entry point into the Baku Toolkit. The easiest way for an application to take
+ * advantage of Baku is for its activities with distributed state to inherit from
+ * {@link io.v.baku.toolkit.BakuActivity} (or {@link io.v.baku.toolkit.BakuAppCompatActivity}; see
+ * {@link io.v.baku.toolkit.BakuActivityMixin} for custom inheritance trees). Then, for any UI
+ * widget that should have distributed state, the client application should build data bindings by
+ * chaining methods from a {@link io.v.baku.toolkit.BakuActivityMixin#binder() binder()} call,
+ * binding shared data fields to UI widget properties. For <a href="https://goo.gl/P0Ag9a"
+ * target="_blank">example</a>, the following binds a data key named {@code "text"} to the text of a
+ * {@link android.widget.TextView} with ID {@code textView}:
+ * <pre><code>
+ * &#64;Override
+ * protected void onCreate(final Bundle savedInstanceState) {
+ *     super.onCreate(savedInstanceState);
+ *     setContentView(R.layout.activity_layout);
+ *
+ *     binder().key("text")
+ *             .bindTo(R.id.textView);
+ *     }
+ * }
+ * </code></pre>
+ * Collection bindings (from vector data to list/recycler views) are similarly exposed through a
+ * {@link io.v.baku.toolkit.BakuActivityMixin#collectionBinder() collectionBinder()} builder. Writes
+ * can be performed directly via {@link io.v.baku.toolkit.syncbase.BakuTable#put(java.lang.String,
+ * java.lang.Object) getSyncbaseTable().put(key, value)}. More information about data bindings is
+ * available in the {@link io.v.baku.toolkit.bind} package documentation.
+ * <p>
+ * The Baku Toolkit creates a Syncbase table to use by default for data binding, and creates and
+ * manages a default {@linkplain io.v.rx.syncbase.UserCloudSyncgroup global user-level cloud
+ * syncgroup} to sync distributed data across all instances of the application belonging to a user.
+ * <p>
+ * Baku components are built in layers bundling common sets of functionality. This allows
+ * application developers the flexibility to selectively interact with APIs when they need to work
+ * around our high-level abstractions which potentially don't meet their use cases.
+ * <p>
+ * Sample code is available in the
+ * <a href="https://vanadium.googlesource.com/release.projects.baku/" target="_blank">baku projects
+ * repo</a>.
+ */
+package io.v.baku.toolkit;
\ No newline at end of file
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/syncbase/BakuSyncbase.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/syncbase/BakuSyncbase.java
index 4ceec0f..b38ad0f 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/syncbase/BakuSyncbase.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/syncbase/BakuSyncbase.java
@@ -5,13 +5,13 @@
 package io.v.baku.toolkit.syncbase;
 
 import io.v.baku.toolkit.BakuActivityTrait;
-import io.v.rx.syncbase.RxSyncbase;
+import io.v.rx.syncbase.RxAndroidSyncbase;
 import lombok.Getter;
 import lombok.experimental.Accessors;
 
 @Accessors(prefix = "m")
 @Getter
-public class BakuSyncbase extends RxSyncbase {
+public class BakuSyncbase extends RxAndroidSyncbase {
     private final BakuActivityTrait<?> mBakuContext;
 
     public BakuSyncbase(final BakuActivityTrait bakuContext) {
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/syncbase/package-info.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/syncbase/package-info.java
index 26d08b8..f9f2953 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/syncbase/package-info.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/syncbase/package-info.java
@@ -3,8 +3,8 @@
 // license that can be found in the LICENSE file.
 
 /**
- * For each {@link io.v.rx.syncbase.SyncbaseEntity RxSyncbase entity} class, this package adds a
- * {@link io.v.baku.toolkit.BakuActivityTrait} as context information from which to derive other
+ * For each {@linkplain io.v.rx.syncbase.SyncbaseEntity RxSyncbase entity} class, this package adds
+ * a {@link io.v.baku.toolkit.BakuActivityTrait} as context information from which to derive other
  * dependencies and to use for error handling. This allows Baku client code to use Syncbase more
  * easily, without needing to explicitly subscribe error handlers for every operation. For more
  * information, see {@link io.v.baku.toolkit.syncbase.BakuTable#exec(rx.functions.Func1)}.
diff --git a/baku-toolkit/lib/src/main/java/io/v/debug/SyncbaseClient.java b/baku-toolkit/lib/src/main/java/io/v/debug/SyncbaseAndroidClient.java
similarity index 92%
rename from baku-toolkit/lib/src/main/java/io/v/debug/SyncbaseClient.java
rename to baku-toolkit/lib/src/main/java/io/v/debug/SyncbaseAndroidClient.java
index 492b5cd..653aa64 100644
--- a/baku-toolkit/lib/src/main/java/io/v/debug/SyncbaseClient.java
+++ b/baku-toolkit/lib/src/main/java/io/v/debug/SyncbaseAndroidClient.java
@@ -30,7 +30,7 @@
  * {@link Observable}.
  */
 @Accessors(prefix = "m")
-public class SyncbaseClient implements AutoCloseable {
+public class SyncbaseAndroidClient implements AutoCloseable {
     public static class BindException extends Exception {
         public BindException(final String message) {
             super(message);
@@ -100,8 +100,8 @@
      *                    will not be started until blessings are available.
      *                    TODO(rosswang): this should either handle blessings changes or not care.
      */
-    public SyncbaseClient(final Context androidContext, final Observable<Blessings> rxBlessings,
-                          final boolean cleanStart, final Duration keepAlive) {
+    public SyncbaseAndroidClient(final Context androidContext, final Observable<Blessings> rxBlessings,
+                                 final boolean cleanStart, final Duration keepAlive) {
         mAndroidContext = androidContext;
 
         /*
@@ -120,7 +120,7 @@
         if (rxBlessings == null) {
             startService(androidContext, intent);
         } else {
-            rxBlessings.first()
+            rxBlessings.take(1)
                     .subscribe(s -> {
                         startService(androidContext, intent);
                     }, rpl::onError);
@@ -129,7 +129,7 @@
         mObservable = rpl.filter(s -> s != null);
     }
 
-    public SyncbaseClient(final Context androidContext, final Observable<Blessings> rxBlessings) {
+    public SyncbaseAndroidClient(final Context androidContext, final Observable<Blessings> rxBlessings) {
         this(androidContext, rxBlessings, false, null);
     }
 
diff --git a/baku-toolkit/lib/src/main/java/io/v/debug/SyncbaseAndroidService.java b/baku-toolkit/lib/src/main/java/io/v/debug/SyncbaseAndroidService.java
index 4edcc83..142db3f 100644
--- a/baku-toolkit/lib/src/main/java/io/v/debug/SyncbaseAndroidService.java
+++ b/baku-toolkit/lib/src/main/java/io/v/debug/SyncbaseAndroidService.java
@@ -81,7 +81,7 @@
         try {
             mObservable.doOnNext(VFn.unchecked(b -> {
                 log.info("Stopping Syncbase");
-                b.mServer.stop();
+                mVContext.cancel();
             }))
                     .timeout(STOP_TIMEOUT.getMillis(), TimeUnit.MILLISECONDS)
                     .toBlocking()
diff --git a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/ClientLevelCloudSync.java b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/ClientLevelCloudSync.java
new file mode 100644
index 0000000..d8e3e92
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/ClientLevelCloudSync.java
@@ -0,0 +1,32 @@
+// 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.
+
+package io.v.rx.syncbase;
+
+import java.util.Arrays;
+import java.util.List;
+
+import io.v.baku.toolkit.blessings.BlessingsUtils;
+import io.v.baku.toolkit.blessings.ClientUser;
+import io.v.impl.google.naming.NamingUtil;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+public class ClientLevelCloudSync implements SyncHostLevel {
+    public static final ClientLevelCloudSync DEFAULT =
+            new ClientLevelCloudSync(DEFAULT_CLOUD_SYNC_SUFFIX, DEFAULT_RENDEZVOUS_SUFFIX);
+
+    private final String mSgHostSuffix, mRendezvousSuffix;
+
+    @Override
+    public String getSyncgroupHostName(final ClientUser clientUser) {
+        return NamingUtil.join(BlessingsUtils.clientMount(clientUser.getClientId()), mSgHostSuffix);
+    }
+
+    @Override
+    public List<String> getRendezvousTableNames(final ClientUser clientUser) {
+        return Arrays.asList(NamingUtil.join(
+                BlessingsUtils.clientMount(clientUser.getClientId()), mRendezvousSuffix));
+    }
+}
diff --git a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/GlobalUserSyncgroup.java b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/GlobalUserSyncgroup.java
deleted file mode 100644
index 7bafee7..0000000
--- a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/GlobalUserSyncgroup.java
+++ /dev/null
@@ -1,191 +0,0 @@
-// 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.
-
-package io.v.rx.syncbase;
-
-import android.content.Context;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.NoSuchElementException;
-
-import io.v.baku.toolkit.BakuActivityTrait;
-import io.v.baku.toolkit.R;
-import io.v.baku.toolkit.VAndroidContextTrait;
-import io.v.baku.toolkit.blessings.BlessingsUtils;
-import io.v.v23.context.VContext;
-import io.v.v23.security.Blessings;
-import io.v.v23.security.access.AccessList;
-import io.v.v23.security.access.Permissions;
-import io.v.v23.services.syncbase.nosql.SyncgroupJoinFailedException;
-import io.v.v23.services.syncbase.nosql.SyncgroupMemberInfo;
-import io.v.v23.services.syncbase.nosql.SyncgroupSpec;
-import io.v.v23.services.syncbase.nosql.TableRow;
-import io.v.v23.syncbase.nosql.Database;
-import io.v.v23.syncbase.nosql.Syncgroup;
-import java8.util.function.Function;
-import java8.util.stream.Collectors;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.experimental.Accessors;
-import lombok.extern.slf4j.Slf4j;
-import rx.Observable;
-import rx.Subscription;
-import rx.functions.Action2;
-
-import static net.javacrumbs.futureconverter.guavarx.FutureConverter.toObservable;
-
-@Accessors(prefix = "m")
-@AllArgsConstructor
-@Builder(builderClassName = "Builder")
-@Slf4j
-public class GlobalUserSyncgroup {
-    public static final String
-            DEFAULT_SYNCGROUP_HOST_NAME = "usersync",
-            DEFAULT_SYNCGROUP_SUFFIX = "user",
-            DEFAULT_RENDEZVOUS_MOUNT = "sgmt";
-    public static final SyncgroupMemberInfo
-            DEFAULT_SYNCGROUP_MEMBER_INFO = new SyncgroupMemberInfo();
-
-    public static GlobalUserSyncgroup forActivity(final BakuActivityTrait t) {
-        return builder().activity(t).build();
-    }
-
-    public static class Builder {
-        private String sgSuffix = DEFAULT_SYNCGROUP_SUFFIX;
-        private Function<String, String> descriptionForUsername = u -> "User syncgroup for " + u;
-        private Function<AccessList, Permissions> permissionsForUserAcl =
-                BlessingsUtils::syncgroupPermissions;
-        private List<TableRow> prefixes = new ArrayList<>();
-        private SyncgroupMemberInfo memberInfo = DEFAULT_SYNCGROUP_MEMBER_INFO;
-
-        /**
-         * This is an additive setter to {@link #prefixes(List)}.
-         */
-        public Builder prefix(final TableRow prefix) {
-            prefixes.add(prefix);
-            return this;
-        }
-
-        /**
-         * This is an additive setter to {@link #prefixes(List)}.
-         */
-        public Builder prefix(final String tableName, final String rowPrefix) {
-            return prefix(new TableRow(tableName, rowPrefix));
-        }
-
-        /**
-         * This is an additive setter to {@link #prefixes(List)}.
-         */
-        public Builder prefix(final String tableName) {
-            return prefix(tableName, "");
-        }
-
-        /**
-         * This is a composite setter for:
-         * <ul>
-         * <li>{@code vContext}</li>
-         * <li>{@code rxBlessings}</li>
-         * <li>{@code syncHostLevel}</li> (as a new
-         * {@link UserAppSyncHost#UserAppSyncHost(Context)})
-         * <li>{@code onError}</li>
-         * </ul>
-         * and should be called prior to any overrides for those fields.
-         */
-        public Builder activity(final VAndroidContextTrait<?> t) {
-            return vContext(t.getVContext())
-                    .rxBlessings(t.getBlessingsProvider().getRxBlessings())
-                    .syncHostLevel(new UserAppSyncHost(t.getAndroidContext()))
-                    .onError(t.getErrorReporter()::onError);
-        }
-
-        /**
-         * In addition to those fields in {@link #activity(VAndroidContextTrait)}, this
-         * additionally sets:
-         * <ul>
-         * <li>{@code syncbase}</li>
-         * <li>{@code db}</li>
-         * <li>and adds to {@code prefixes}</li>
-         * </ul>
-         */
-        public Builder activity(final BakuActivityTrait<?> t) {
-            return activity(t.getVAndroidContextTrait())
-                    .syncbase(t.getSyncbase())
-                    .db(t.getSyncbaseDb())
-                    .prefix(t.getSyncbaseTableName());
-        }
-    }
-
-    private final VContext mVContext;
-    private final Observable<Blessings> mRxBlessings;
-    private final SyncHostLevel mSyncHostLevel;
-    private final String mSgSuffix;
-    private final RxSyncbase mSyncbase;
-    private final RxDb mDb;
-    private final Function<String, String> mDescriptionForUsername;
-    private final Function<AccessList, Permissions> mPermissionsForUserAcl;
-    private final List<TableRow> mPrefixes;
-    private final SyncgroupMemberInfo mMemberInfo;
-    /**
-     * @see io.v.baku.toolkit.ErrorReporter#onError(int, Throwable)
-     */
-    private final Action2<Integer, Throwable> mOnError;
-
-    private SyncgroupSpec createSpec(final String username, final AccessList userAcl) {
-        return new SyncgroupSpec(mDescriptionForUsername.apply(username),
-                mPermissionsForUserAcl.apply(userAcl), mPrefixes,
-                mSyncHostLevel.getRendezvousTableNames(username), false);
-    }
-
-    private Observable<SyncgroupSpec> createOrJoinSyncgroup(final Database db, final String sgName,
-                                                            final SyncgroupSpec spec) {
-        final Syncgroup sg = db.getSyncgroup(sgName);
-        return Observable.defer(() -> toObservable(sg.join(mVContext, mMemberInfo)))
-                .doOnCompleted(() -> log.info("Joined syncgroup " + sgName))
-                .onErrorResumeNext(t -> t instanceof SyncgroupJoinFailedException ?
-                        toObservable(sg.create(mVContext, spec, mMemberInfo))
-                                .doOnCompleted(() -> log.info("Created syncgroup " + sgName))
-                                .map(x -> spec) :
-                        Observable.error(t));
-    }
-
-    private Observable<Object> createOrJoinSyncgroup(final String username, final AccessList acl) {
-        final String sgHost = mSyncHostLevel.getSyncgroupHostName(username);
-        final String sgName = RxSyncbase.syncgroupName(sgHost, mSgSuffix);
-        final SyncgroupSpec spec = createSpec(username, acl);
-
-        final Observable<Object> mount = SgHostUtil.ensureSyncgroupHost(
-                mVContext, mSyncbase.getRxServer(), sgHost).share();
-
-        return mDb.getObservable()
-                .switchMap(db -> Observable.merge(mount.first().ignoreElements().concatWith(
-                        createOrJoinSyncgroup(db, sgName, spec)), mount));
-    }
-
-    public Observable<?> rxJoin() {
-        return Observable.switchOnNext(mRxBlessings
-                .map(b -> {
-                    final AccessList acl = BlessingsUtils.blessingsToAcl(mVContext, b);
-                    final List<Observable<?>> createOrJoins =
-                            BlessingsUtils.blessingsToUsernameStream(mVContext, b)
-                                    .distinct()
-                                    .map(u -> createOrJoinSyncgroup(u, acl))
-                                    .collect(Collectors.toList());
-                    if (createOrJoins.isEmpty()) {
-                        throw new NoSuchElementException("GlobalUserSyncgroup requires a " +
-                                "username; no username blessings found. Blessings: " + b);
-                    }
-                    return Observable.merge(createOrJoins);
-                }));
-    }
-
-    /**
-     * It is not generally necessary to unsubscribe explicitly from this subscription since the
-     * lifecycle of the Syncbase client is generally tied to a Baku Activity.
-     */
-    public Subscription join() {
-        return rxJoin().subscribe(x -> {
-        }, t -> mOnError.call(R.string.err_syncgroup_join, t));
-    }
-}
diff --git a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxAndroidSyncbase.java b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxAndroidSyncbase.java
new file mode 100644
index 0000000..120f97b
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxAndroidSyncbase.java
@@ -0,0 +1,55 @@
+// 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.
+
+package io.v.rx.syncbase;
+
+import android.content.Context;
+
+import io.v.baku.toolkit.VAndroidContextTrait;
+import io.v.debug.SyncbaseAndroidClient;
+import io.v.v23.context.VContext;
+import io.v.v23.rpc.Server;
+import io.v.v23.security.Blessings;
+import io.v.v23.syncbase.SyncbaseService;
+import lombok.experimental.Accessors;
+import rx.Observable;
+
+/**
+ * Models a binding to a Syncbase Android service as an {@code Observable} of
+ * {@link SyncbaseService}s. The binding will be asynchronously made and then potentially
+ * periodically lost and regained, so modeling further operations as subscriptions works well.
+ */
+@Accessors(prefix = "m")
+public class RxAndroidSyncbase extends RxSyncbase implements AutoCloseable {
+    private final SyncbaseAndroidClient mClient;
+
+    public Observable<Server> getRxServer() {
+        return mClient.getRxServer();
+    }
+
+    @Override
+    public Observable<SyncbaseService> getRxClient() {
+        return mClient.getRxClient();
+    }
+
+    public RxAndroidSyncbase(final VContext vContext, final SyncbaseAndroidClient client) {
+        super(vContext);
+        mClient = client;
+    }
+
+    public RxAndroidSyncbase(final Context androidContext, final VContext ctx,
+                             final Observable<Blessings> rxBlessings) {
+        this(ctx, new SyncbaseAndroidClient(androidContext, rxBlessings));
+    }
+
+    public RxAndroidSyncbase(final VAndroidContextTrait trait) {
+        this(trait.getAndroidContext(), trait.getVContext(),
+                trait.getBlessingsProvider().getRxBlessings());
+    }
+
+    @Override
+    public void close() {
+        mClient.close();
+    }
+}
diff --git a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxApp.java b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxApp.java
index 9291593..9676387 100644
--- a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxApp.java
+++ b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxApp.java
@@ -40,9 +40,7 @@
     @Override
     public Observable<SyncbaseApp> mapFrom(final SyncbaseService sb) {
         final SyncbaseApp app = sb.getApp(mName);
-        return toObservable(SyncbaseEntity.compose(app::exists, app::create)
-                .ensureExists(mVContext, null))
-                .map(x -> app);
+        return toObservable(SyncbaseEntity.forApp(app).ensureExists(mVContext)).map(x -> app);
     }
 
     public RxDb rxDb(final String name) {
diff --git a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxDb.java b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxDb.java
index 7d89b27..2fcf8a0 100644
--- a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxDb.java
+++ b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxDb.java
@@ -40,9 +40,7 @@
     @Override
     public Observable<Database> mapFrom(final SyncbaseApp app) {
         final Database db = app.getNoSqlDatabase(mName, null);
-        return toObservable(SyncbaseEntity.compose(db::exists, db::create)
-                .ensureExists(mVContext, null))
-                .map(x -> db);
+        return toObservable(SyncbaseEntity.forDb(db).ensureExists(mVContext)).map(x -> db);
     }
 
     public RxTable rxTable(final String name) {
diff --git a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxSyncbase.java b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxSyncbase.java
index e441be4..00ff353 100644
--- a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxSyncbase.java
+++ b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxSyncbase.java
@@ -4,57 +4,47 @@
 
 package io.v.rx.syncbase;
 
-import android.content.Context;
-
-import io.v.baku.toolkit.VAndroidContextTrait;
-import io.v.debug.SyncbaseClient;
 import io.v.impl.google.naming.NamingUtil;
 import io.v.v23.context.VContext;
-import io.v.v23.rpc.Server;
-import io.v.v23.security.Blessings;
+import io.v.v23.syncbase.Syncbase;
 import io.v.v23.syncbase.SyncbaseService;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 import lombok.experimental.Accessors;
 import rx.Observable;
 
-/**
- * Models a binding to a Syncbase Android service as an {@code Observable} of
- * {@link SyncbaseService}s. The binding will be asynchronously made and then potentially
- * periodically lost and regained, so modeling further operations as subscriptions works well.
- */
 @Accessors(prefix = "m")
 @AllArgsConstructor
-public class RxSyncbase implements AutoCloseable {
+public abstract class RxSyncbase {
     public static String syncgroupName(final String sgHost, final String sgSuffix) {
         return NamingUtil.join(sgHost, "%%sync", sgSuffix);
     }
 
-    @Getter private final VContext mVContext;
-    private final SyncbaseClient mClient;
-
-    public Observable<Server> getRxServer() {
-        return mClient.getRxServer();
+    /**
+     * The {@link RxSyncbase#getRxClient()} produced by this factory method will produce exactly
+     * one {@link SyncbaseService}.
+     */
+    public static RxSyncbase fromSyncbaseService(final VContext vContext,
+                                                 final SyncbaseService sb) {
+        return new RxSyncbase(vContext) {
+            @Override
+            public Observable<SyncbaseService> getRxClient() {
+                return Observable.just(sb);
+            }
+        };
     }
 
-    public Observable<SyncbaseService> getRxClient() {
-        return mClient.getRxClient();
+    /**
+     * @see #fromSyncbaseService(VContext, SyncbaseService)
+     */
+    public static RxSyncbase fromSyncbaseAt(final VContext vContext, final String name) {
+        return fromSyncbaseService(vContext, Syncbase.newService(name));
     }
 
-    public RxSyncbase(final Context androidContext, final VContext ctx,
-                      final Observable<Blessings> rxBlessings) {
-        mVContext = ctx;
-        mClient = new SyncbaseClient(androidContext, rxBlessings);
-    }
+    @Getter
+    private final VContext mVContext;
 
-    public RxSyncbase(final VAndroidContextTrait trait) {
-        this(trait.getAndroidContext(), trait.getVContext(),
-                trait.getBlessingsProvider().getRxBlessings());
-    }
-
-    public void close() {
-        mClient.close();
-    }
+    public abstract Observable<SyncbaseService> getRxClient();
 
     public RxApp rxApp(final String name) {
         return new RxApp(name, this);
diff --git a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxSyncgroup.java b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxSyncgroup.java
new file mode 100644
index 0000000..c645c79
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxSyncgroup.java
@@ -0,0 +1,36 @@
+// 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.
+
+package io.v.rx.syncbase;
+
+import io.v.baku.toolkit.R;
+import io.v.v23.services.syncbase.nosql.SyncgroupMemberInfo;
+import lombok.AllArgsConstructor;
+import lombok.experimental.Accessors;
+import rx.Observable;
+import rx.Subscription;
+import rx.functions.Action2;
+
+@Accessors(prefix = "m")
+@AllArgsConstructor
+public abstract class RxSyncgroup {
+    public static final SyncgroupMemberInfo
+            DEFAULT_SYNCGROUP_MEMBER_INFO = new SyncgroupMemberInfo();
+
+    /**
+     * @see io.v.baku.toolkit.ErrorReporter#onError(int, Throwable)
+     */
+    protected final Action2<Integer, Throwable> mOnError;
+
+    public abstract Observable<?> rxJoin();
+
+    /**
+     * It is not generally necessary to unsubscribe explicitly from this subscription since the
+     * lifecycle of the Syncbase client is generally tied to a Baku Activity.
+     */
+    public Subscription join() {
+        return rxJoin().subscribe(x -> {
+        }, t -> mOnError.call(R.string.err_syncgroup_join, t));
+    }
+}
diff --git a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxTable.java b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxTable.java
index ba7e941..2aa9e0a 100644
--- a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxTable.java
+++ b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxTable.java
@@ -16,7 +16,6 @@
 import io.v.rx.RxInputChannel;
 import io.v.rx.VFn;
 import io.v.v23.InputChannel;
-import io.v.v23.context.CancelableVContext;
 import io.v.v23.context.VContext;
 import io.v.v23.services.syncbase.nosql.BatchOptions;
 import io.v.v23.services.syncbase.nosql.KeyValue;
@@ -87,9 +86,7 @@
     @Override
     public Observable<Table> mapFrom(final DatabaseCore db) {
         final Table t = db.getTable(mName);
-        return toObservable(SyncbaseEntity.compose(t::exists, t::create)
-                .ensureExists(mVContext, null))
-                .map(x -> t);
+        return toObservable(SyncbaseEntity.forTable(t).ensureExists(mVContext)).map(x -> t);
     }
 
     private <T> Observable<T> getInitial(
@@ -212,7 +209,7 @@
     }
 
     private void cancelContextOnDisconnect(final Subscriber<?> subscriber,
-                                           final CancelableVContext cancelable,
+                                           final VContext cancelable,
                                            final String prefix) {
         subscriber.add(Subscriptions.create(() -> {
             log.debug("Cancelling watch on {}: {}", mName, prefix);
@@ -239,7 +236,7 @@
                                     "Unable to abort watch initial read query", t))), r));
                 })
                 .switchMap(i -> {
-                    final CancelableVContext cancelable = mVContext.withCancel();
+                    final VContext cancelable = mVContext.withCancel();
                     cancelContextOnDisconnect(subscriber, cancelable, prefix);
                     log.debug("Watching {}: {}", mName, prefix);
                     return mergeInitial.call(i, observeWatchStream.call(
diff --git a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/SgHostUtil.java b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/SgHostUtil.java
index ef952b4..b895f9f 100644
--- a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/SgHostUtil.java
+++ b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/SgHostUtil.java
@@ -4,6 +4,8 @@
 
 package io.v.rx.syncbase;
 
+import com.google.common.collect.Iterables;
+
 import org.joda.time.Duration;
 
 import io.v.rx.MountEvent;
@@ -11,6 +13,7 @@
 import io.v.v23.context.VContext;
 import io.v.v23.rpc.Server;
 import io.v.v23.syncbase.Syncbase;
+import io.v.v23.syncbase.nosql.Database;
 import io.v.v23.verror.TimeoutException;
 import lombok.experimental.UtilityClass;
 import lombok.extern.slf4j.Slf4j;
@@ -87,4 +90,19 @@
         return rxServer.switchMap(s -> isSyncbaseOnline(vContext, name)
                 .flatMap(online -> online ? Observable.just(0) : mountSgHost(rxServer, name)));
     }
+
+    /**
+     * @return an observable that echos the db after the each db emitted by
+     * {@link RxDb#getObservable()} has been ensured to possess the given table names. Upon
+     * subscription, for each db emitted, the observable will create these app/db/table hierarchies
+     * if not already present.
+     */
+    public static Observable<Database> ensureSyncgroupHierarchies(
+            final RxDb rxDb, final Iterable<String> tableNames) {
+        return rxDb.getObservable().switchMap(db -> Observable.merge(Iterables.transform(tableNames,
+                t -> rxDb.rxTable(t)
+                        .mapFrom(db)
+                        .map(rxt -> db)))
+                .lastOrDefault(db));
+    }
 }
diff --git a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/SgSuffixFormat.java b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/SgSuffixFormat.java
new file mode 100644
index 0000000..970d315
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/SgSuffixFormat.java
@@ -0,0 +1,12 @@
+// 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.
+
+package io.v.rx.syncbase;
+
+import java8.lang.FunctionalInterface;
+
+@FunctionalInterface
+public interface SgSuffixFormat<T> {
+    String get(final T parameters);
+}
diff --git a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/SgSuffixFormats.java b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/SgSuffixFormats.java
new file mode 100644
index 0000000..38eac86
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/SgSuffixFormats.java
@@ -0,0 +1,32 @@
+// 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.
+
+package io.v.rx.syncbase;
+
+import com.google.common.escape.CharEscaperBuilder;
+import com.google.common.escape.Escaper;
+
+import lombok.experimental.UtilityClass;
+
+@UtilityClass
+public class SgSuffixFormats {
+    public static final String SG_SUFFIX_DELIMITER = "/";
+    public static final Escaper SG_SUFFIX_COMPONENT_ESCAPER = new CharEscaperBuilder()
+            .addEscape('/', "\\/")
+            .addEscape('\\', "\\\\")
+            .toEscaper();
+
+    public static SgSuffixFormat<Object> simple(final String sgSuffix) {
+        return x -> sgSuffix;
+    }
+
+    public static SgSuffixFormat<UserSyncgroup.Parameters> discriminated(
+            final String customSuffix) {
+        return p -> SG_SUFFIX_COMPONENT_ESCAPER.escape(p.getDb().getRxApp().getName()) +
+                SG_SUFFIX_DELIMITER +
+                SG_SUFFIX_COMPONENT_ESCAPER.escape(p.getDb().getName()) +
+                SG_SUFFIX_DELIMITER +
+                customSuffix;
+    }
+}
diff --git a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/SyncHostLevel.java b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/SyncHostLevel.java
index 260609e..69eff0d 100644
--- a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/SyncHostLevel.java
+++ b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/SyncHostLevel.java
@@ -6,9 +6,13 @@
 
 import java.util.List;
 
-public interface SyncHostLevel {
-    String DEFAULT_SG_HOST_SUFFIX = "sghost", DEFAULT_RENDEZVOUS_SUFFIX = "sgmt";
+import io.v.baku.toolkit.blessings.ClientUser;
 
-    String getSyncgroupHostName(String username);
-    List<String> getRendezvousTableNames(String username);
+public interface SyncHostLevel {
+    String DEFAULT_CLOUD_SYNC_SUFFIX = "cloudsync",
+            DEFAULT_SG_HOST_SUFFIX = "sghost",
+            DEFAULT_RENDEZVOUS_SUFFIX = "sgmt";
+
+    String getSyncgroupHostName(ClientUser clientUser);
+    List<String> getRendezvousTableNames(ClientUser clientUser);
 }
diff --git a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/SyncbaseEntity.java b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/SyncbaseEntity.java
index baa13d7..5d672c4 100644
--- a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/SyncbaseEntity.java
+++ b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/SyncbaseEntity.java
@@ -4,15 +4,22 @@
 
 package io.v.rx.syncbase;
 
+import com.google.common.base.Function;
 import com.google.common.util.concurrent.AsyncFunction;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 
+import org.robotninjas.concurrent.FluentFuture;
+import org.robotninjas.concurrent.FluentFutures;
+
 import io.v.v23.context.VContext;
 import io.v.v23.security.access.Permissions;
+import io.v.v23.syncbase.SyncbaseApp;
+import io.v.v23.syncbase.nosql.Database;
+import io.v.v23.syncbase.nosql.Table;
 import io.v.v23.verror.ExistException;
 
-abstract class SyncbaseEntity implements ExistenceAware, Creatable {
+public abstract class SyncbaseEntity implements ExistenceAware, Creatable {
     public static SyncbaseEntity compose(final ExistenceAware fnExists, final Creatable fnCreate) {
         return new SyncbaseEntity() {
             @Override
@@ -27,17 +34,45 @@
         };
     }
 
+    public static SyncbaseEntity forApp(final SyncbaseApp app) {
+        return compose(app::exists, app::create);
+    }
+
+    public static SyncbaseEntity forDb(final Database db) {
+        return compose(db::exists, db::create);
+    }
+
+    public static SyncbaseEntity forTable(final Table table) {
+        return compose(table::exists, table::create);
+    }
+
+    private SyncbaseEntity(){}
+
     /**
      * Utility for Syncbase entities with lazy creation semantics. It would be great if this were
      * instead factored into a V23 interface and utility.
+     *
+     * @return a future that completes with {@code true} if this call created the entity,
+     * {@code false} if the entity already existed, or fails if an unexpected error occurred.
      */
-    public ListenableFuture<Void> ensureExists(final VContext vContext,
-                                               final Permissions permissions) {
-        return Futures.transform(exists(vContext), (AsyncFunction<Boolean, Void>) (e -> e ?
-                Futures.immediateFuture(null) :
-                Futures.withFallback(create(vContext, permissions), t ->
-                        t instanceof ExistException ?
-                                Futures.immediateFuture(null) : Futures.immediateFailedFuture(t))
-        ));
+    public FluentFuture<Boolean> ensureExists(final VContext vContext,
+                                              final Permissions permissions) {
+        return FluentFutures.from(exists(vContext))
+                .transform((AsyncFunction<Boolean, Boolean>) (e -> e ?
+                        Futures.immediateFuture(false) :
+                        FluentFutures.from(create(vContext, permissions))
+                                .transform((Function<Void, Boolean>) (x -> true))
+                                .withFallback(t -> t instanceof ExistException ?
+                                        Futures.<Boolean>immediateFuture(false) :
+                                        Futures.<Boolean>immediateFailedFuture(t))
+                ));
+    }
+
+    /**
+     * Equivalent to calling {@link #ensureExists(VContext, Permissions)} with null permissions,
+     * inheriting permissions from the hierarchy.
+     */
+    public FluentFuture<Boolean> ensureExists(final VContext vContext) {
+        return ensureExists(vContext, null);
     }
 }
diff --git a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/UserAppSyncHost.java b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/UserAppSyncHost.java
index 1fa2fce..921ae72 100644
--- a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/UserAppSyncHost.java
+++ b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/UserAppSyncHost.java
@@ -10,6 +10,7 @@
 import java.util.List;
 
 import io.v.baku.toolkit.blessings.BlessingsUtils;
+import io.v.baku.toolkit.blessings.ClientUser;
 import io.v.impl.google.naming.NamingUtil;
 import lombok.RequiredArgsConstructor;
 
@@ -26,13 +27,14 @@
     }
 
     @Override
-    public String getSyncgroupHostName(final String username) {
-        return NamingUtil.join(BlessingsUtils.userMount(username), mAppName, mSgHostSuffix);
+    public String getSyncgroupHostName(final ClientUser clientUser) {
+        return NamingUtil.join(BlessingsUtils.userMount(clientUser.getUsername()),
+                mAppName, mSgHostSuffix);
     }
 
     @Override
-    public List<String> getRendezvousTableNames(String username) {
+    public List<String> getRendezvousTableNames(final ClientUser clientUser) {
         return Arrays.asList(NamingUtil.join(
-                BlessingsUtils.userMount(username), mAppName, mRendezvousSuffix));
+                BlessingsUtils.userMount(clientUser.getUsername()), mAppName, mRendezvousSuffix));
     }
 }
diff --git a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/UserCloudSyncgroup.java b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/UserCloudSyncgroup.java
new file mode 100644
index 0000000..b29ba4c
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/UserCloudSyncgroup.java
@@ -0,0 +1,85 @@
+// 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.
+
+package io.v.rx.syncbase;
+
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+import io.v.baku.toolkit.BakuActivityTrait;
+import io.v.v23.security.BlessingPattern;
+import io.v.v23.services.syncbase.nosql.SyncgroupSpec;
+import io.v.v23.services.syncbase.nosql.TableRow;
+import io.v.v23.syncbase.nosql.Syncgroup;
+import io.v.v23.verror.ExistException;
+import lombok.extern.slf4j.Slf4j;
+import rx.Observable;
+
+import static net.javacrumbs.futureconverter.guavarx.FutureConverter.toObservable;
+
+@Slf4j
+public class UserCloudSyncgroup extends UserSyncgroup {
+    /**
+     * TODO(rosswang): seriously very temporary!
+     */
+    public static BlessingPattern DEBUG_SG_HOST_BLESSING =
+            new BlessingPattern("dev.v.io:u:rosswang@google.com:app");
+
+    public static UserCloudSyncgroup forActivity(final BakuActivityTrait t) {
+        return builder().activity(t).buildCloud();
+    }
+
+    public UserCloudSyncgroup(final Parameters params) {
+        super(params);
+    }
+
+    private Observable<Void> ensureSyncgroup(final String sgHost, final String sgName,
+                                             final SyncgroupSpec spec) {
+        // We need app/db/table to sync even on the cloud.
+        // https://github.com/vanadium/issues/issues/857
+        // Use idempotent APIs to allow failure recovery and avoid race conditions. Most of the
+        // time, we'll just short-circuit and join the syncgroup from the get-go.
+        final RxDb remoteDb = RxSyncbase.fromSyncbaseAt(mParams.getVContext(), sgHost)
+                .rxApp(mParams.getDb().getRxApp().getName())
+                .rxDb(mParams.getDb().getName());
+        final List<String> tableNames =
+                Lists.transform(mParams.getPrefixes(), TableRow::getTableName);
+
+        return SgHostUtil.ensureSyncgroupHierarchies(remoteDb, tableNames)
+                // Syncgroup create is implicitly deferred via flatMap from a real observable.
+                // Create this syncgroup on the remote Syncbase to auto-join that remote and sync
+                // data to it. Otherwise, we won't actually write anything to the cloud syncbase.
+                .switchMap(db -> toObservable(db.getSyncgroup(sgName)
+                        .create(mParams.getVContext(), spec, mParams.getMemberInfo()))
+                        .doOnCompleted(() ->
+                                log.info("Created syncgroup " + sgName + " remotely"))
+                        .onErrorResumeNext(t -> t instanceof ExistException ?
+                                Observable.just(null) : Observable.error(t)));
+    }
+
+    private Observable<?> joinExistingSyncgroup(final String sgName,
+                                                final SyncgroupSpec expectedSpec) {
+        return mParams.getDb().getObservable().switchMap(db -> {
+            final Syncgroup sg = db.getSyncgroup(sgName);
+
+            // These toObservables are implicitly deferred via switchMap from a real observable
+            return toObservable(sg.join(mParams.getVContext(), mParams.getMemberInfo()))
+                    .doOnCompleted(() -> log.info("Joined syncgroup " + sgName))
+                    .flatMap(spec -> spec.equals(expectedSpec) ? Observable.just(null) :
+                            toObservable(sg.setSpec(mParams.getVContext(), expectedSpec, ""))
+                                    .doOnCompleted(() ->
+                                            log.info("Updated spec for syncgroup " + sgName)));
+        });
+    }
+
+    @Override
+    protected Observable<?> rxJoin(final String sgHost, final String sgName,
+                                   final SyncgroupSpec spec) {
+        // TODO(rosswang) try to join first
+        return Observable.concat(
+                ensureSyncgroup(sgHost, sgName, spec).ignoreElements(),
+                joinExistingSyncgroup(sgName, spec));
+    }
+}
diff --git a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/UserPeerSyncgroup.java b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/UserPeerSyncgroup.java
new file mode 100644
index 0000000..555ad51
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/UserPeerSyncgroup.java
@@ -0,0 +1,61 @@
+// 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.
+
+package io.v.rx.syncbase;
+
+import io.v.baku.toolkit.BakuActivityTrait;
+import io.v.v23.services.syncbase.nosql.SyncgroupJoinFailedException;
+import io.v.v23.services.syncbase.nosql.SyncgroupSpec;
+import io.v.v23.syncbase.nosql.Database;
+import io.v.v23.syncbase.nosql.Syncgroup;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
+import rx.Observable;
+
+import static net.javacrumbs.futureconverter.guavarx.FutureConverter.toObservable;
+
+/**
+ * This syncgroup strategy is a bit of a hack and its future is uncertain.
+ */
+@Accessors(prefix = "m")
+@Slf4j
+public class UserPeerSyncgroup extends UserSyncgroup {
+    public static UserPeerSyncgroup forActivity(final BakuActivityTrait t) {
+        return builder().activity(t).buildPeer();
+    }
+
+    public UserPeerSyncgroup(final Parameters params) {
+        super(params);
+        if (!(params.getDb().getRxApp().getRxSyncbase() instanceof RxAndroidSyncbase)) {
+            throw new IllegalArgumentException("UserPeerSyncgroup must be constructed with a " +
+                    "local Syncbase server (RxAndroidSyncbase).");
+        }
+    }
+
+    private Observable<SyncgroupSpec> createOrJoinSyncgroup(final Database db, final String sgName,
+                                                            final SyncgroupSpec spec) {
+        final Syncgroup sg = db.getSyncgroup(sgName);
+        return Observable.defer(() ->
+                toObservable(sg.join(mParams.getVContext(), mParams.getMemberInfo())))
+                .doOnCompleted(() -> log.info("Joined syncgroup " + sgName))
+                .onErrorResumeNext(t -> t instanceof SyncgroupJoinFailedException ?
+                        toObservable(
+                                sg.create(mParams.getVContext(), spec, mParams.getMemberInfo()))
+                                .doOnCompleted(() -> log.info("Created syncgroup " + sgName))
+                                .map(x -> spec) :
+                        Observable.error(t));
+    }
+
+    @Override
+    protected Observable<?> rxJoin(final String sgHost, final String sgName,
+                                   final SyncgroupSpec spec) {
+        final RxAndroidSyncbase sb = (RxAndroidSyncbase) mParams.getDb().getRxApp().getRxSyncbase();
+        final Observable<Object> mount = SgHostUtil.ensureSyncgroupHost(
+                mParams.getVContext(), sb.getRxServer(), sgHost).share();
+
+        return mParams.getDb().getObservable()
+                .switchMap(db -> Observable.merge(mount.first().ignoreElements()
+                        .concatWith(createOrJoinSyncgroup(db, sgName, spec)), mount));
+    }
+}
diff --git a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/UserSyncgroup.java b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/UserSyncgroup.java
new file mode 100644
index 0000000..f942a40
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/UserSyncgroup.java
@@ -0,0 +1,268 @@
+// 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.
+
+package io.v.rx.syncbase;
+
+import android.content.Context;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import io.v.baku.toolkit.BakuActivityTrait;
+import io.v.baku.toolkit.VAndroidContextTrait;
+import io.v.baku.toolkit.blessings.BlessingsUtils;
+import io.v.baku.toolkit.blessings.ClientUser;
+import io.v.v23.context.VContext;
+import io.v.v23.security.Blessings;
+import io.v.v23.security.access.AccessList;
+import io.v.v23.security.access.Permissions;
+import io.v.v23.services.syncbase.nosql.SyncgroupMemberInfo;
+import io.v.v23.services.syncbase.nosql.SyncgroupSpec;
+import io.v.v23.services.syncbase.nosql.TableRow;
+import java8.util.function.Function;
+import java8.util.function.Supplier;
+import java8.util.stream.Collectors;
+import lombok.Value;
+import lombok.experimental.Accessors;
+import rx.Observable;
+import rx.functions.Action2;
+
+// TODO(rosswang): Generalize this to other possible syncgroup strategies.
+@Accessors(prefix = "m")
+public abstract class UserSyncgroup extends RxSyncgroup {
+    public static final SgSuffixFormat<Parameters> DEFAULT_SYNCGROUP_SUFFIX =
+            SgSuffixFormats.discriminated("user");
+
+    public static class Builder {
+        protected VContext mVContext;
+        protected Observable<Blessings> mRxBlessings;
+        protected SyncHostLevel mSyncHostLevel;
+        protected SgSuffixFormat<? super Parameters> mSgSuffixFormat =
+                DEFAULT_SYNCGROUP_SUFFIX;
+        protected RxDb mDb;
+        protected Function<String, String> mDescriptionForUsername = u -> "User syncgroup for " + u;
+        protected Function<AccessList, Permissions> mPermissionsForAcl;
+        protected List<TableRow> mPrefixes = new ArrayList<>();
+        protected SyncgroupMemberInfo mMemberInfo = DEFAULT_SYNCGROUP_MEMBER_INFO;
+        protected Action2<Integer, Throwable> mOnError;
+
+        // helper for constructing a default UserAppSyncHost if needed
+        protected Context mAndroidContext;
+
+        public Builder vContext(final VContext vContext) {
+            mVContext = vContext;
+            return this;
+        }
+
+        public Builder rxBlessings(final Observable<Blessings> rxBlessings) {
+            mRxBlessings = rxBlessings;
+            return this;
+        }
+
+        public Builder syncHostLevel(final SyncHostLevel syncHostLevel) {
+            mSyncHostLevel = syncHostLevel;
+            mAndroidContext = null;
+            return this;
+        }
+
+        public Builder sgSuffixFormat(final SgSuffixFormat sgSuffixFormat) {
+            mSgSuffixFormat = sgSuffixFormat;
+            return this;
+        }
+
+        public Builder sgSuffix(final String sgSuffix) {
+            return sgSuffixFormat(SgSuffixFormats.simple(sgSuffix));
+        }
+
+        public Builder discriminatedSgSuffix(final String customSuffix) {
+            return sgSuffixFormat(SgSuffixFormats.discriminated(customSuffix));
+        }
+
+        public Builder db(final RxDb db) {
+            mDb = db;
+            return this;
+        }
+
+        public Builder descriptionForUsername(
+                final Function<String, String> descriptionForUsername) {
+            mDescriptionForUsername = descriptionForUsername;
+            return this;
+        }
+
+        public Builder permissionsForAcl(
+                final Function<AccessList, Permissions> permissionsForAcl) {
+            mPermissionsForAcl = permissionsForAcl;
+            return this;
+        }
+
+        /**
+         * This setter is not additive.
+         */
+        public Builder prefixes(final List<TableRow> prefixes) {
+            mPrefixes.clear();
+            mPrefixes.addAll(prefixes);
+            return this;
+        }
+
+        /**
+         * This setter is not additive.
+         */
+        public Builder prefixes(final TableRow... prefixes) {
+            mPrefixes.clear();
+            mPrefixes.addAll(Arrays.asList(prefixes));
+            return this;
+        }
+
+        /**
+         * This is an additive setter.
+         */
+        public Builder prefix(final TableRow prefix) {
+            mPrefixes.add(prefix);
+            return this;
+        }
+
+        /**
+         * This is an additive setter.
+         */
+        public Builder prefix(final String tableName, final String rowPrefix) {
+            return prefix(new TableRow(tableName, rowPrefix));
+        }
+
+        /**
+         * This is an additive setter.
+         */
+        public Builder prefix(final String tableName) {
+            return prefix(tableName, "");
+        }
+
+        public Builder memberInfo(final SyncgroupMemberInfo memberInfo) {
+            mMemberInfo = memberInfo;
+            return this;
+        }
+
+        public Builder onError(final Action2<Integer, Throwable> onError) {
+            mOnError = onError;
+            return this;
+        }
+
+        /**
+         * This is a composite setter for:
+         * <ul>
+         * <li>{@code vContext}</li>Context
+         * <li>{@code rxBlessings}</li>
+         * <li>{@code onError}</li>
+         * </ul>
+         * and should be called prior to any overrides for those fields.
+         */
+        public Builder activity(final VAndroidContextTrait<?> t) {
+            mAndroidContext = t.getAndroidContext();
+            return vContext(t.getVContext())
+                    .rxBlessings(t.getBlessingsProvider().getRxBlessings())
+                    .onError(t.getErrorReporter()::onError);
+        }
+
+        /**
+         * In addition to those fields in {@link #activity(VAndroidContextTrait)}, this
+         * additionally sets:
+         * <ul>
+         * <li>{@code db}</li>
+         * <li>and adds to {@code prefixes}</li>
+         * </ul>
+         */
+        public Builder activity(final BakuActivityTrait<?> t) {
+            return activity(t.getVAndroidContextTrait())
+                    .db(t.getSyncbaseDb())
+                    .prefix(t.getSyncbaseTableName());
+        }
+
+        protected Parameters buildParameters(
+                final Supplier<SyncHostLevel> defaultSyncHost,
+                final Function<AccessList, Permissions> defaultPermissionsForAcl) {
+            return new Parameters(mVContext, mRxBlessings,
+                    mSyncHostLevel == null ? defaultSyncHost.get() : mSyncHostLevel,
+                    mSgSuffixFormat, mDb, mDescriptionForUsername,
+                    mPermissionsForAcl == null? defaultPermissionsForAcl : mPermissionsForAcl,
+                    ImmutableList.copyOf(mPrefixes), mMemberInfo, mOnError);
+        }
+
+        public UserCloudSyncgroup buildCloud() {
+            return new UserCloudSyncgroup(buildParameters(
+                    () -> ClientLevelCloudSync.DEFAULT,
+                    acl -> BlessingsUtils.cloudSyngroupPermissions(acl,
+                            UserCloudSyncgroup.DEBUG_SG_HOST_BLESSING)));
+        }
+
+        public UserPeerSyncgroup buildPeer() {
+            return new UserPeerSyncgroup(buildParameters(
+                    () -> new UserAppSyncHost(mAndroidContext),
+                    BlessingsUtils::syncgroupPermissions));
+        }
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    @Value
+    public static class Parameters {
+        VContext mVContext;
+        Observable<Blessings> mRxBlessings;
+        SyncHostLevel mSyncHostLevel;
+        SgSuffixFormat<? super Parameters> mSgSuffixFormat;
+        RxDb mDb;
+        Function<String, String> mDescriptionForUsername;
+        Function<AccessList, Permissions> mPermissionsForAcl;
+        ImmutableList<TableRow> mPrefixes;
+        SyncgroupMemberInfo mMemberInfo;
+        Action2<Integer, Throwable> mOnError;
+    }
+
+    protected final Parameters mParams;
+
+    public UserSyncgroup(final Parameters params) {
+        super(params.getOnError());
+        mParams = params;
+    }
+
+    private SyncgroupSpec createSpec(final ClientUser clientUser, final AccessList acl) {
+        return new SyncgroupSpec(
+                mParams.getDescriptionForUsername().apply(clientUser.getUsername()),
+                mParams.getPermissionsForAcl().apply(acl), mParams.getPrefixes(),
+                mParams.getSyncHostLevel().getRendezvousTableNames(clientUser), false);
+    }
+
+    protected abstract Observable<?> rxJoin(final String sgHost, final String sgName,
+                                            final SyncgroupSpec spec);
+
+    private Observable<?> rxJoin(final ClientUser clientUser, final AccessList acl) {
+        final String sgHost = mParams.getSyncHostLevel().getSyncgroupHostName(clientUser);
+        final String sgName = RxSyncbase.syncgroupName(sgHost,
+                mParams.getSgSuffixFormat().get(mParams));
+        final SyncgroupSpec spec = createSpec(clientUser, acl);
+
+        return rxJoin(sgHost, sgName, spec);
+    }
+
+    @Override
+    public Observable<?> rxJoin() {
+        return Observable.switchOnNext(mParams.getRxBlessings()
+                .map(b -> {
+                    final AccessList acl = BlessingsUtils.blessingsToAcl(mParams.getVContext(), b);
+                    final List<Observable<?>> joins =
+                            BlessingsUtils.blessingsToClientUserStream(mParams.getVContext(), b)
+                                    .distinct()
+                                    .map(cu -> rxJoin(cu, acl))
+                                    .collect(Collectors.toList());
+                    if (joins.isEmpty()) {
+                        throw new NoSuchElementException("UserSyncgroup requires a username; no " +
+                                "username blessings found. Blessings: " + b);
+                    }
+                    return Observable.merge(joins);
+                }));
+    }
+}
diff --git a/baku-toolkit/lib/src/main/java/overview.html b/baku-toolkit/lib/src/main/java/overview.html
new file mode 100644
index 0000000..0548c5f
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/overview.html
@@ -0,0 +1,14 @@
+<body>
+The Baku Android Toolkit includes software components facilitating the development of
+applications with distributed user interfaces. It is available from JCenter and Maven Central. The
+available versions are listed
+<a href="https://bintray.com/vanadium/io.v/baku-toolkit" target="_blank">here</a>. To use the Baku
+Toolkit from an Android Java project, ensure that the {@code build.gradle} has either
+{@code jcenter()} or {@code mavenCentral()} in its repositories, add
+{@code 'io.v:baku-toolkit:version'} as a {@code compile} dependency, and bind an
+<a href="http://www.slf4j.org/" target="_blank">SLF4J</a> logger as an APK dependency, like
+{@code apk ('org.slf4j:slf4j-android:1.7.12')}.
+
+<p>
+    For common usage, see the {@link io.v.baku.toolkit} package docs.
+</body>
\ No newline at end of file
diff --git a/baku-toolkit/lib/src/test/java/io/v/baku/toolkit/RobolectricTestCase.java b/baku-toolkit/lib/src/test/java/io/v/baku/toolkit/RobolectricTestCase.java
deleted file mode 100644
index c3c3a72..0000000
--- a/baku-toolkit/lib/src/test/java/io/v/baku/toolkit/RobolectricTestCase.java
+++ /dev/null
@@ -1,28 +0,0 @@
-// 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.
-
-package io.v.baku.toolkit;
-
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricGradleTestRunner;
-import org.robolectric.annotation.Config;
-
-import java.util.concurrent.TimeUnit;
-
-import rx.Observable;
-import rx.observables.BlockingObservable;
-
-@RunWith(RobolectricGradleTestRunner.class)
-@Config(constants = BuildConfig.class)
-public abstract class RobolectricTestCase {
-    private static final long TIMEOUT_SECONDS = 5;
-
-    public static <T> BlockingObservable<T> block(final Observable<T> source) {
-        return source.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS).toBlocking();
-    }
-
-    public static <T> T first(final Observable<T> source) {
-        return block(source).first();
-    }
-}
\ No newline at end of file
diff --git a/baku-toolkit/lib/src/test/java/io/v/baku/toolkit/bind/PrefixBindingBuilderTest.java b/baku-toolkit/lib/src/test/java/io/v/baku/toolkit/bind/PrefixBindingBuilderTest.java
new file mode 100644
index 0000000..15757d2
--- /dev/null
+++ b/baku-toolkit/lib/src/test/java/io/v/baku/toolkit/bind/PrefixBindingBuilderTest.java
@@ -0,0 +1,18 @@
+// 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.
+
+package io.v.baku.toolkit.bind;
+
+import android.widget.ListView;
+
+import org.junit.Test;
+
+public class PrefixBindingBuilderTest {
+    @Test(expected = IllegalStateException.class)
+    public void testMissingType() {
+        CollectionBinding.builder()
+                .onPrefix("foo")
+                .bindTo((ListView)null);
+    }
+}
diff --git a/baku-toolkit/lib/src/test/java/io/v/baku/toolkit/blessings/AccountManagerBlessingsFragmentTest.java b/baku-toolkit/lib/src/test/java/io/v/baku/toolkit/blessings/AccountManagerBlessingsFragmentTest.java
deleted file mode 100644
index 7991ff7..0000000
--- a/baku-toolkit/lib/src/test/java/io/v/baku/toolkit/blessings/AccountManagerBlessingsFragmentTest.java
+++ /dev/null
@@ -1,81 +0,0 @@
-// 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.
-
-package io.v.baku.toolkit.blessings;
-
-import android.content.Intent;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.powermock.api.mockito.PowerMockito;
-import org.powermock.core.classloader.annotations.PowerMockIgnore;
-import org.powermock.core.classloader.annotations.PrepareForTest;
-import org.powermock.core.classloader.annotations.SuppressStaticInitializationFor;
-import org.powermock.modules.junit4.rule.PowerMockRule;
-import org.robolectric.util.FragmentTestUtil;
-
-import java.util.concurrent.TimeUnit;
-
-import io.v.android.v23.services.blessing.BlessingService;
-import io.v.baku.toolkit.RobolectricTestCase;
-import io.v.rx.RxTestCase;
-import io.v.v23.security.Blessings;
-import rx.android.schedulers.AndroidSchedulers;
-import rx.util.async.Async;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.powermock.api.mockito.PowerMockito.mockStatic;
-
-@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"})
-@SuppressStaticInitializationFor("io.v.baku.toolkit.blessings.BlessingsUtils")
-@PrepareForTest({Blessings.class, BlessingService.class, BlessingsUtils.class})
-public class AccountManagerBlessingsFragmentTest extends RobolectricTestCase {
-    private static final int MOCK_RESULT_CODE = 42;
-    private static final Intent
-            BLESSING_REQUEST = mock(Intent.class),
-            BLESSING_RESULT = mock(Intent.class);
-
-    /**
-     * We have to mock this fragment by extension rather than a Mockito spy because PowerMock won't
-     * let Mockito properly mock methods inherited from android classes pulled in by Robolectric.
-     */
-    public static class MockFragment extends AccountManagerBlessingsFragment {
-        @Override
-        public void startActivityForResult(final Intent intent, final int requestCode) {
-            assertEquals(BLESSING_REQUEST, intent);
-            Async.start(() -> {
-                onActivityResult(requestCode, MOCK_RESULT_CODE, BLESSING_RESULT);
-                return null;
-            }, AndroidSchedulers.mainThread());
-        }
-    }
-
-    @Rule
-    public final PowerMockRule rule = new PowerMockRule();
-
-    @Test
-    public void test() throws Exception {
-        mockStatic(BlessingService.class);
-        when(BlessingService.newBlessingIntent(any())).thenReturn(BLESSING_REQUEST);
-
-        // Would ideally be private static final but we need PowerMockito to mock the final
-        // Blessings class and since we're running with Robolectric, PowerMock is bound on the
-        // instance rather than the class.
-        final Blessings MOCK_BLESSINGS = PowerMockito.mock(Blessings.class);
-
-        mockStatic(BlessingsUtils.class);
-        when(BlessingsUtils.fromActivityResult(MOCK_RESULT_CODE, BLESSING_RESULT))
-                .thenReturn(MOCK_BLESSINGS);
-
-        final MockFragment fragment = new MockFragment();
-
-        FragmentTestUtil.startVisibleFragment(fragment);
-
-        assertEquals(MOCK_BLESSINGS, first(fragment.getRxBlessings()
-                .timeout(RxTestCase.BLOCKING_DELAY_MS, TimeUnit.MILLISECONDS)));
-    }
-}
diff --git a/baku-toolkit/lib/src/test/java/io/v/baku/toolkit/blessings/ActivityBlessingsSeekerTest.java b/baku-toolkit/lib/src/test/java/io/v/baku/toolkit/blessings/ActivityBlessingsSeekerTest.java
deleted file mode 100644
index 462afee..0000000
--- a/baku-toolkit/lib/src/test/java/io/v/baku/toolkit/blessings/ActivityBlessingsSeekerTest.java
+++ /dev/null
@@ -1,129 +0,0 @@
-// 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.
-
-package io.v.baku.toolkit.blessings;
-
-import android.app.Activity;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.powermock.api.mockito.PowerMockito;
-import org.powermock.core.classloader.annotations.PrepareForTest;
-import org.powermock.core.classloader.annotations.SuppressStaticInitializationFor;
-import org.powermock.modules.junit4.PowerMockRunner;
-
-import io.v.v23.security.Blessings;
-import rx.functions.Action1;
-
-import static org.junit.Assert.fail;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.powermock.api.mockito.PowerMockito.mockStatic;
-
-@RunWith(PowerMockRunner.class)
-@SuppressStaticInitializationFor("io.v.baku.toolkit.blessings.BlessingsUtils")
-@PrepareForTest({Blessings.class, BlessingsUtils.class})
-public class ActivityBlessingsSeekerTest {
-    private static class MockActivityBlessingsSeeker extends ActivityBlessingsSeeker {
-        public MockActivityBlessingsSeeker(final Activity activity) {
-            super(activity, null, false);
-        }
-
-        @Override
-        protected void seekBlessings() {
-        }
-    }
-
-    @Before
-    public void setUp() {
-        mockStatic(BlessingsUtils.class);
-    }
-
-    @Test
-    public void testColdPassive() {
-        // would NPE if it tries to do anytyhing
-        new MockActivityBlessingsSeeker(null)
-                .getPassiveRxBlessings()
-                .subscribe(b -> fail("Unexpected blessings " + b));
-    }
-
-    @Test
-    public void testBlessingsFromManager() throws Exception {
-        final Activity activity = mock(Activity.class);
-        @SuppressWarnings("unchecked")
-        final Action1<Blessings>
-                cold = mock(Action1.class),
-                hot = mock(Action1.class);
-        final Blessings
-                b1 = PowerMockito.mock(Blessings.class),
-                b2 = PowerMockito.mock(Blessings.class);
-
-        PowerMockito.when(BlessingsUtils.readSharedPrefs(any())).thenReturn(b1, b2);
-
-        final MockActivityBlessingsSeeker t = new MockActivityBlessingsSeeker(activity);
-        t.getPassiveRxBlessings().subscribe(cold);
-
-        t.getRxBlessings().subscribe(hot);
-        verify(hot).call(b1);
-        verify(cold).call(b1);
-        verify(hot, never()).call(b2);
-
-        t.refreshBlessings();
-        verify(hot).call(b2);
-        verify(cold).call(b2);
-    }
-
-    @Test
-    public void testBlessingsFromProvider() throws Exception {
-        final Activity activity = mock(Activity.class);
-        @SuppressWarnings("unchecked")
-        final Action1<Blessings> s = mock(Action1.class);
-        final Blessings
-                b1 = PowerMockito.mock(Blessings.class),
-                b2 = PowerMockito.mock(Blessings.class);
-
-        final MockActivityBlessingsSeeker t = new MockActivityBlessingsSeeker(activity);
-        t.getRxBlessings().subscribe(s);
-        verify(s, never()).call(any());
-
-        t.setBlessings(b1);
-        verify(s).call(b1);
-
-        t.refreshBlessings();
-        // The mock BlessingsManager will default to null, so it will seek blessings again.
-        t.setBlessings(b2);
-        verify(s).call(b2);
-    }
-
-    /**
-     * Verifies that if a new subscriber needs to seek blessings, the new subscriber does not
-     * receive blessings until the seek completes (does not receive old blessings), and that the old
-     * subscriber is refreshed as well.
-     */
-    @Test
-    public void testDeferOnNewSubscriber() throws Exception {
-        final Activity activity = mock(Activity.class);
-        @SuppressWarnings("unchecked")
-        final Action1<Blessings>
-                s1 = mock(Action1.class),
-                s2 = mock(Action1.class);
-        final Blessings
-                b1 = PowerMockito.mock(Blessings.class),
-                b2 = PowerMockito.mock(Blessings.class);
-
-        final MockActivityBlessingsSeeker t = new MockActivityBlessingsSeeker(activity);
-        t.getRxBlessings().subscribe(s1);
-        t.setBlessings(b1);
-
-        t.getRxBlessings().subscribe(s2);
-        verify(s2, never()).call(any());
-
-        t.setBlessings(b2);
-        verify(s1).call(b2);
-        verify(s2).call(b2);
-    }
-}
diff --git a/baku-toolkit/lib/src/test/java/io/v/baku/toolkit/blessings/BlessingsManagerBlessingsProviderTest.java b/baku-toolkit/lib/src/test/java/io/v/baku/toolkit/blessings/BlessingsManagerBlessingsProviderTest.java
new file mode 100644
index 0000000..c720a2a
--- /dev/null
+++ b/baku-toolkit/lib/src/test/java/io/v/baku/toolkit/blessings/BlessingsManagerBlessingsProviderTest.java
@@ -0,0 +1,133 @@
+// 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.
+
+package io.v.baku.toolkit.blessings;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.SettableFuture;
+
+import junit.framework.AssertionFailedError;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.core.classloader.annotations.SuppressStaticInitializationFor;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import io.v.android.libs.security.BlessingsManager;
+import io.v.v23.security.Blessings;
+import rx.functions.Action1;
+
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.powermock.api.mockito.PowerMockito.mockStatic;
+
+@RunWith(PowerMockRunner.class)
+@SuppressStaticInitializationFor({"io.v.android.libs.security.BlessingsManager", "android.app.Fragment"})
+@PrepareForTest({Blessings.class, BlessingsManager.class})
+public class BlessingsManagerBlessingsProviderTest {
+    @Before
+    public void setUp() {
+        mockStatic(BlessingsManager.class);
+    }
+
+    @Test
+    public void testColdPassive() {
+        new BlessingsManagerBlessingsProvider(null, null)
+                .getPassiveRxBlessings()
+                .subscribe(b -> fail("Unexpected blessings " + b));
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testBlessingsFromManager() throws Exception {
+        final Action1<Blessings>
+                cold = mock(Action1.class),
+                hot = mock(Action1.class);
+        final Blessings
+                b1 = PowerMockito.mock(Blessings.class),
+                b2 = PowerMockito.mock(Blessings.class);
+
+        when(BlessingsManager.getBlessings(any(), any(), any(), anyBoolean()))
+                .thenReturn(Futures.immediateFuture(b1), Futures.immediateFuture(b2));
+
+        final RefreshableBlessingsProvider t = new BlessingsManagerBlessingsProvider(null, null);
+        t.getPassiveRxBlessings().subscribe(cold);
+
+        t.getRxBlessings().subscribe(hot);
+        verify(hot).call(b1);
+        verify(cold).call(b1);
+        verify(hot, never()).call(b2);
+
+        t.refreshBlessings();
+        verify(hot).call(b2);
+        verify(cold).call(b2);
+    }
+
+    /**
+     * Verifies that if a new subscriber needs to seek blessings, the new subscriber does not
+     * receive blessings until the seek completes (does not receive old blessings), and that the old
+     * subscriber is refreshed as well.
+     */
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testDeferOnNewSubscriber() throws Exception {
+        final Action1<Blessings>
+                s1 = mock(Action1.class),
+                s2 = mock(Action1.class);
+        final Blessings
+                b1 = PowerMockito.mock(Blessings.class),
+                b2 = PowerMockito.mock(Blessings.class);
+
+        final SettableFuture<Blessings>
+                bf1 = SettableFuture.create(),
+                bf2 = SettableFuture.create();
+
+        when(BlessingsManager.getBlessings(any(), any(), any(), anyBoolean()))
+                .thenReturn(bf1, bf2);
+
+        final RefreshableBlessingsProvider t = new BlessingsManagerBlessingsProvider(null, null);
+        t.getRxBlessings().subscribe(s1);
+        bf1.set(b1);
+
+        t.getRxBlessings().subscribe(s2);
+        verify(s2, never()).call(any());
+
+        bf2.set(b2);
+        verify(s1).call(b2);
+        verify(s2).call(b2);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testConcurrentSeeks() {
+        final Action1<Blessings>
+                s1 = mock(Action1.class),
+                s2 = mock(Action1.class);
+        final Blessings b = PowerMockito.mock(Blessings.class);
+
+        final SettableFuture<Blessings>
+                bf = SettableFuture.create();
+
+        when(BlessingsManager.getBlessings(any(), any(), any(), anyBoolean()))
+                .thenReturn(bf, Futures.immediateFailedFuture(
+                        new AssertionFailedError("Expected at most one getBlessings call.")));
+
+        final RefreshableBlessingsProvider t = new BlessingsManagerBlessingsProvider(null, null);
+        t.getRxBlessings().subscribe(s1);
+        t.getRxBlessings().subscribe(s2);
+
+        bf.set(b);
+
+        verify(s1).call(b);
+        verify(s2).call(b);
+    }
+}
diff --git a/baku-toolkit/lib/src/test/java/io/v/rx/RxInputChannelTest.java b/baku-toolkit/lib/src/test/java/io/v/rx/RxInputChannelTest.java
index ef016a2..dd7e6d6 100644
--- a/baku-toolkit/lib/src/test/java/io/v/rx/RxInputChannelTest.java
+++ b/baku-toolkit/lib/src/test/java/io/v/rx/RxInputChannelTest.java
@@ -13,7 +13,6 @@
 import org.powermock.core.classloader.annotations.PrepareForTest;
 import org.powermock.modules.junit4.PowerMockRunner;
 
-import io.v.baku.toolkit.RobolectricTestCase;
 import io.v.v23.InputChannel;
 import io.v.v23.verror.EndOfFileException;
 
@@ -32,7 +31,7 @@
                 Futures.immediateFuture(1),
                 Futures.immediateFuture(2),
                 Futures.immediateFailedFuture(PowerMockito.mock(EndOfFileException.class)));
-        assertEquals(ImmutableList.of(1, 2), RobolectricTestCase.first(
+        assertEquals(ImmutableList.of(1, 2), RxTestCase.first(
                 RxInputChannel.wrap(mockInputChannel).autoConnect().toList()));
     }
 }
diff --git a/baku-toolkit/lib/src/test/java/io/v/rx/RxTestCase.java b/baku-toolkit/lib/src/test/java/io/v/rx/RxTestCase.java
index d5b33e2..0562a60 100644
--- a/baku-toolkit/lib/src/test/java/io/v/rx/RxTestCase.java
+++ b/baku-toolkit/lib/src/test/java/io/v/rx/RxTestCase.java
@@ -13,9 +13,12 @@
 import org.junit.After;
 
 import java.util.Iterator;
+import java.util.concurrent.TimeUnit;
 
 import java8.util.stream.Collectors;
 import java8.util.stream.StreamSupport;
+import rx.Observable;
+import rx.observables.BlockingObservable;
 
 import static org.junit.Assert.fail;
 
@@ -28,6 +31,14 @@
         return 2 * nominal.getMillis();
     }
 
+    public static <T> BlockingObservable<T> block(final Observable<T> source) {
+        return source.timeout(BLOCKING_DELAY_MS, TimeUnit.SECONDS).toBlocking();
+    }
+
+    public static <T> T first(final Observable<T> source) {
+        return block(source).first();
+    }
+
     private final Multimap<Class<? extends Throwable>, Throwable> mErrors =
             Multimaps.synchronizedListMultimap(ArrayListMultimap.create());
 
diff --git a/baku-toolkit/lib/src/test/java/io/v/rx/syncbase/RxTableTest.java b/baku-toolkit/lib/src/test/java/io/v/rx/syncbase/RxTableTest.java
index 8d70acb..fc6f3da 100644
--- a/baku-toolkit/lib/src/test/java/io/v/rx/syncbase/RxTableTest.java
+++ b/baku-toolkit/lib/src/test/java/io/v/rx/syncbase/RxTableTest.java
@@ -21,7 +21,6 @@
 
 import io.v.rx.RxTestCase;
 import io.v.rx.SubscriberInputChannel;
-import io.v.v23.context.CancelableVContext;
 import io.v.v23.context.VContext;
 import io.v.v23.services.syncbase.nosql.KeyValue;
 import io.v.v23.services.watch.ResumeMarker;
@@ -77,7 +76,6 @@
         mChanges.subscribe(watchChan);
 
         final VContext ctx = mock(VContext.class);
-        final CancelableVContext cctx = mock(CancelableVContext.class);
         final RxDb rxdb = mock(RxDb.class);
         final Database db = mock(Database.class);
         final BatchDatabase bdb = mock(BatchDatabase.class);
@@ -85,7 +83,7 @@
 
         mockStatic(VomUtil.class);
 
-        when(ctx.withCancel()).thenReturn(cctx);
+        when(ctx.withCancel()).thenReturn(ctx);
         when(rxdb.getVContext()).thenReturn(ctx);
         when(rxdb.getObservable()).thenReturn(Observable.just(db));
         when(db.getTable("t")).thenReturn(t);
diff --git a/baku-toolkit/lib/src/test/java/io/v/rx/syncbase/GlobalUserSyncgroupTest.java b/baku-toolkit/lib/src/test/java/io/v/rx/syncbase/UserPeerSyncgroupTest.java
similarity index 91%
rename from baku-toolkit/lib/src/test/java/io/v/rx/syncbase/GlobalUserSyncgroupTest.java
rename to baku-toolkit/lib/src/test/java/io/v/rx/syncbase/UserPeerSyncgroupTest.java
index 1102bb0..6e8da27 100644
--- a/baku-toolkit/lib/src/test/java/io/v/rx/syncbase/GlobalUserSyncgroupTest.java
+++ b/baku-toolkit/lib/src/test/java/io/v/rx/syncbase/UserPeerSyncgroupTest.java
@@ -21,7 +21,8 @@
 import java.util.concurrent.atomic.AtomicInteger;
 
 import io.v.baku.toolkit.blessings.BlessingsUtils;
-import io.v.debug.SyncbaseClient;
+import io.v.baku.toolkit.blessings.ClientUser;
+import io.v.debug.SyncbaseAndroidClient;
 import io.v.rx.RxMountState;
 import io.v.rx.RxTestCase;
 import io.v.v23.context.VContext;
@@ -50,7 +51,7 @@
 @RunWith(PowerMockRunner.class)
 @SuppressStaticInitializationFor("io.v.baku.toolkit.blessings.BlessingsUtils")
 @PrepareForTest({BlessingsUtils.class, SgHostUtil.class})
-public class GlobalUserSyncgroupTest extends RxTestCase {
+public class UserPeerSyncgroupTest extends RxTestCase {
     private static final long STATUS_POLLING_DELAY_MS = verificationDelay(
             RxMountState.DEFAULT_POLLING_INTERVAL);
 
@@ -65,11 +66,11 @@
     private final Database mDb = mock(Database.class);
     private final Syncgroup mSg = mock(Syncgroup.class);
 
-    private RxSyncbase mSb;
+    private RxAndroidSyncbase mSb;
 
     @Before
     public void setUp() throws Exception {
-        final SyncbaseClient sbClient = mock(SyncbaseClient.class);
+        final SyncbaseAndroidClient sbClient = mock(SyncbaseAndroidClient.class);
         final SyncbaseApp app = mock(SyncbaseApp.class);
 
         when(sbClient.getRxServer()).thenReturn(mRxServer);
@@ -86,7 +87,7 @@
 
         when(mSg.join(any(), any())).thenReturn(Futures.immediateFuture(null));
 
-        mSb = new RxSyncbase(null, sbClient);
+        mSb = new RxAndroidSyncbase(null, sbClient);
 
         PowerMockito.spy(SgHostUtil.class);
     }
@@ -96,20 +97,19 @@
                 .thenReturn(mSg);
         final Stopwatch t = Stopwatch.createStarted();
 
-        final Subscription subscription = GlobalUserSyncgroup.builder()
+        final Subscription subscription = UserPeerSyncgroup.builder()
                 .vContext(mVContext)
                 .rxBlessings(mRxBlessings)
                 .syncHostLevel(new UserAppSyncHost("app", "sghost", "sgmt"))
                 .sgSuffix("sg")
-                .syncbase(mSb)
                 .db(mSb.rxApp("app").rxDb("db"))
                 .onError((m, e) -> catchAsync(e))
-                .build()
+                .buildPeer()
                 .join();
 
         PowerMockito.spy(BlessingsUtils.class);
-        PowerMockito.doReturn(RefStreams.of("foo@bar.com"))
-                .when(BlessingsUtils.class, "blessingsToUsernameStream", any(), any());
+        PowerMockito.doReturn(RefStreams.of(new ClientUser("fooclient", "foo@bar.com")))
+                .when(BlessingsUtils.class, "blessingsToClientUserStream", any(), any());
         PowerMockito.doReturn(null)
                 .when(BlessingsUtils.class, "blessingsToAcl", any(), any());
         PowerMockito.doReturn(null)
@@ -117,7 +117,7 @@
 
         long elapsed = t.elapsed(TimeUnit.MILLISECONDS);
         if (elapsed > BLOCKING_DELAY_MS) {
-            fail("GlobalUserSyncgroup.join should not block; took " + elapsed + " ms (threshold " +
+            fail("UserPeerSyncgroup.join should not block; took " + elapsed + " ms (threshold " +
                     BLOCKING_DELAY_MS + " ms)");
         }
         return subscription;