java: Switch the high-level Syncbase API to the new JNI

The DatabaseOptions.rootDir is not yet used and that is causing one
test to fail. I'm going to fix that in a subsequent change because it
will touch also the Go code.

Also note that I temporary disable the test for POJO because it was
trying to call a native function from the v23.so.

Change-Id: If28925bebbafafcf22df407162e8f2626f8a5f64
diff --git a/syncbase/build.gradle b/syncbase/build.gradle
index 035fec1..38dc2bc 100644
--- a/syncbase/build.gradle
+++ b/syncbase/build.gradle
@@ -131,8 +131,11 @@
     compile fileTree(dir: 'libs', include: ['*.jar'])
     compile 'com.android.support:appcompat-v7:23.4.0'
     compile 'io.v:vanadium-android:2.1.7'
+    testCompile 'com.google.truth:truth:0.28'
     testCompile 'junit:junit:4.12'
-    testCompile group: 'com.google.truth', name: 'truth', version: '0.28'
+    // We need to overwrite org.json because it's mocked by default. For some reason setting
+    // the unitTests.returnDefaultValues doesn't help in this case.
+    testCompile 'org.json:org.json:2.0'
     androidTestCompile 'com.android.support.test:runner:0.4.1'
     androidTestCompile 'com.android.support.test:rules:0.4.1'
 }
diff --git a/syncbase/src/main/java/io/v/syncbase/AccessList.java b/syncbase/src/main/java/io/v/syncbase/AccessList.java
index d791586..26a6d47 100644
--- a/syncbase/src/main/java/io/v/syncbase/AccessList.java
+++ b/syncbase/src/main/java/io/v/syncbase/AccessList.java
@@ -9,9 +9,7 @@
 import java.util.Map;
 import java.util.Set;
 
-import io.v.v23.security.BlessingPattern;
-import io.v.v23.security.access.Constants;
-import io.v.v23.security.access.Permissions;
+import io.v.syncbase.core.Permissions;
 
 /**
  * Specifies access levels for a set of users. Each user has an associated access level: read-only,
@@ -26,14 +24,15 @@
 
     public Map<String, AccessLevel> users;
 
-    private static Set<String> vAccessListToUserIds(io.v.v23.security.access.AccessList accessList) {
-        if (!accessList.getNotIn().isEmpty()) {
+    private static Set<String> parsedAccessListToUserIds(Map<String, Set<String>> accessList) {
+        Set<String> res = new HashSet<>();
+        if (accessList.containsKey(Permissions.NOT_IN) &&
+                !accessList.get(Permissions.NOT_IN).isEmpty()) {
             throw new RuntimeException("Non-empty not-in section: " + accessList);
         }
-        Set<String> res = new HashSet<>();
-        for (BlessingPattern bp : accessList.getIn()) {
+        for (String blessingPattern : accessList.get(Permissions.IN)) {
             // TODO(sadovsky): Ignore cloud peer's blessing pattern?
-            res.add(Syncbase.getEmailFromBlessingPattern(bp));
+            res.add(Syncbase.getEmailFromBlessingPattern(blessingPattern));
         }
         return res;
     }
@@ -45,11 +44,13 @@
         this.users = new HashMap<>();
     }
 
-    protected AccessList(Permissions perms) {
-        Set<String> resolvers = vAccessListToUserIds(perms.get(Constants.RESOLVE.getValue()));
-        Set<String> readers = vAccessListToUserIds(perms.get(Constants.READ.getValue()));
-        Set<String> writers = vAccessListToUserIds(perms.get(Constants.WRITE.getValue()));
-        Set<String> admins = vAccessListToUserIds(perms.get(Constants.ADMIN.getValue()));
+    protected AccessList(Permissions corePermissions) {
+        Map<String, Map<String, Set<String>>> parsedPermissions = corePermissions.parse();
+        Set<String> resolvers = parsedAccessListToUserIds(parsedPermissions.get(Permissions.Tags.RESOLVE));
+        Set<String> readers = parsedAccessListToUserIds(parsedPermissions.get(Permissions.Tags.READ));
+        Set<String> writers = parsedAccessListToUserIds(parsedPermissions.get(Permissions.Tags.WRITE));
+        Set<String> admins = parsedAccessListToUserIds(parsedPermissions.get(Permissions.Tags.ADMIN));
+
         if (!readers.containsAll(writers)) {
             throw new RuntimeException("Some readers are not resolvers: " + readers + ", " + resolvers);
         }
@@ -70,50 +71,52 @@
         }
     }
 
-    private static void addToVAccessList(io.v.v23.security.access.AccessList accessList, BlessingPattern bp) {
-        if (!accessList.getIn().contains(bp)) {
-            accessList.getIn().add(bp);
+    private static void addToVAccessList(Map<String, Set<String>> accessList, String blessing) {
+        if (!accessList.get(Permissions.IN).contains(blessing)) {
+            accessList.get(Permissions.IN).add(blessing);
         }
     }
 
-    private static void removeFromVAccessList(io.v.v23.security.access.AccessList accessList, BlessingPattern bp) {
-        accessList.getIn().remove(bp);
+    private static void removeFromVAccessList(Map<String, Set<String>> accessList, String blessing) {
+        accessList.get(Permissions.IN).remove(blessing);
     }
 
     /**
-     * Applies delta to perms, modifying perms in place.
+     * Computes a new Permissions object based on delta.
      */
-    protected static void applyDelta(Permissions perms, AccessList delta) {
+    protected static Permissions applyDelta(Permissions corePermissions, AccessList delta) {
+        Map<String, Map<String, Set<String>>> parsedPermissions = corePermissions.parse();
         for (String userId : delta.users.keySet()) {
             AccessLevel level = delta.users.get(userId);
-            BlessingPattern bp = Syncbase.getBlessingPatternFromEmail(userId);
+            String blessing = Syncbase.getBlessingStringFromEmail(userId);
             if (level == null) {
-                removeFromVAccessList(perms.get(Constants.RESOLVE.getValue()), bp);
-                removeFromVAccessList(perms.get(Constants.READ.getValue()), bp);
-                removeFromVAccessList(perms.get(Constants.WRITE.getValue()), bp);
-                removeFromVAccessList(perms.get(Constants.ADMIN.getValue()), bp);
+                removeFromVAccessList(parsedPermissions.get(Permissions.Tags.RESOLVE), blessing);
+                removeFromVAccessList(parsedPermissions.get(Permissions.Tags.READ), blessing);
+                removeFromVAccessList(parsedPermissions.get(Permissions.Tags.WRITE), blessing);
+                removeFromVAccessList(parsedPermissions.get(Permissions.Tags.ADMIN), blessing);
                 continue;
             }
             switch (level) {
                 case READ:
-                    addToVAccessList(perms.get(Constants.RESOLVE.getValue()), bp);
-                    addToVAccessList(perms.get(Constants.READ.getValue()), bp);
-                    removeFromVAccessList(perms.get(Constants.WRITE.getValue()), bp);
-                    removeFromVAccessList(perms.get(Constants.ADMIN.getValue()), bp);
+                    addToVAccessList(parsedPermissions.get(Permissions.Tags.RESOLVE), blessing);
+                    addToVAccessList(parsedPermissions.get(Permissions.Tags.READ), blessing);
+                    removeFromVAccessList(parsedPermissions.get(Permissions.Tags.WRITE), blessing);
+                    removeFromVAccessList(parsedPermissions.get(Permissions.Tags.ADMIN), blessing);
                     break;
                 case READ_WRITE:
-                    addToVAccessList(perms.get(Constants.RESOLVE.getValue()), bp);
-                    addToVAccessList(perms.get(Constants.READ.getValue()), bp);
-                    addToVAccessList(perms.get(Constants.WRITE.getValue()), bp);
-                    removeFromVAccessList(perms.get(Constants.ADMIN.getValue()), bp);
+                    addToVAccessList(parsedPermissions.get(Permissions.Tags.RESOLVE), blessing);
+                    addToVAccessList(parsedPermissions.get(Permissions.Tags.READ), blessing);
+                    addToVAccessList(parsedPermissions.get(Permissions.Tags.WRITE), blessing);
+                    removeFromVAccessList(parsedPermissions.get(Permissions.Tags.ADMIN), blessing);
                     break;
                 case READ_WRITE_ADMIN:
-                    addToVAccessList(perms.get(Constants.RESOLVE.getValue()), bp);
-                    addToVAccessList(perms.get(Constants.READ.getValue()), bp);
-                    addToVAccessList(perms.get(Constants.WRITE.getValue()), bp);
-                    addToVAccessList(perms.get(Constants.ADMIN.getValue()), bp);
+                    addToVAccessList(parsedPermissions.get(Permissions.Tags.RESOLVE), blessing);
+                    addToVAccessList(parsedPermissions.get(Permissions.Tags.READ), blessing);
+                    addToVAccessList(parsedPermissions.get(Permissions.Tags.WRITE), blessing);
+                    addToVAccessList(parsedPermissions.get(Permissions.Tags.ADMIN), blessing);
                     break;
             }
         }
+        return new Permissions(parsedPermissions);
     }
 }
diff --git a/syncbase/src/main/java/io/v/syncbase/BatchDatabase.java b/syncbase/src/main/java/io/v/syncbase/BatchDatabase.java
index 48338b9..1f9ca83 100644
--- a/syncbase/src/main/java/io/v/syncbase/BatchDatabase.java
+++ b/syncbase/src/main/java/io/v/syncbase/BatchDatabase.java
@@ -4,19 +4,18 @@
 
 package io.v.syncbase;
 
-import io.v.v23.VFutures;
-import io.v.v23.verror.VException;
+import io.v.syncbase.core.VError;
 
 /**
  * Provides a way to perform a set of operations atomically on a database. See
  * {@code Database.beginBatch} for concurrency semantics.
  */
 public class BatchDatabase extends DatabaseHandle {
-    private final io.v.v23.syncbase.BatchDatabase mVBatchDatabase;
+    protected io.v.syncbase.core.BatchDatabase mCoreBatchDatabase;
 
-    protected BatchDatabase(io.v.v23.syncbase.BatchDatabase vBatchDatabase) {
-        super(vBatchDatabase);
-        mVBatchDatabase = vBatchDatabase;
+    protected BatchDatabase(io.v.syncbase.core.BatchDatabase coreBatchDatabase) {
+        super(coreBatchDatabase);
+        mCoreBatchDatabase = coreBatchDatabase;
     }
 
     @Override
@@ -33,13 +32,9 @@
      * Persists the pending changes to Syncbase. If the batch is read-only, {@code commit} will
      * throw {@code ConcurrentBatchException}; abort should be used instead.
      */
-    public void commit() {
+    public void commit() throws VError {
         // TODO(sadovsky): Throw ConcurrentBatchException where appropriate.
-        try {
-            VFutures.sync(mVBatchDatabase.commit(Syncbase.getVContext()));
-        } catch (VException e) {
-            throw new RuntimeException("commit failed", e);
-        }
+        mCoreBatchDatabase.commit();
     }
 
     /**
@@ -47,11 +42,7 @@
      * strictly required, but may allow Syncbase to release locks or other resources sooner than if
      * {@code abort} was not called.
      */
-    public void abort() {
-        try {
-            VFutures.sync(mVBatchDatabase.abort(Syncbase.getVContext()));
-        } catch (VException e) {
-            throw new RuntimeException("abort failed", e);
-        }
+    public void abort() throws VError {
+        mCoreBatchDatabase.abort();
     }
 }
diff --git a/syncbase/src/main/java/io/v/syncbase/Collection.java b/syncbase/src/main/java/io/v/syncbase/Collection.java
index 29efd82..3757e95 100644
--- a/syncbase/src/main/java/io/v/syncbase/Collection.java
+++ b/syncbase/src/main/java/io/v/syncbase/Collection.java
@@ -4,40 +4,42 @@
 
 package io.v.syncbase;
 
-import io.v.v23.VFutures;
-import io.v.v23.security.access.Permissions;
-import io.v.v23.verror.ExistException;
-import io.v.v23.verror.NoExistException;
+import io.v.syncbase.core.Permissions;
+import io.v.syncbase.core.VError;
 import io.v.v23.verror.VException;
+import io.v.v23.vom.VomUtil;
 
 /**
  * Represents an ordered set of key-value pairs.
  * To get a Collection handle, call {@code Database.collection}.
  */
 public class Collection {
-    private final io.v.v23.syncbase.Collection mVCollection;
+    private final io.v.syncbase.core.Collection mCoreCollection;
     private final DatabaseHandle mDatabaseHandle;
+    private final Id mId;
+
+    protected Collection(io.v.syncbase.core.Collection coreCollection, DatabaseHandle databaseHandle) {
+        mCoreCollection = coreCollection;
+        mDatabaseHandle = databaseHandle;
+        mId = new Id(coreCollection.id());
+    }
 
     protected void createIfMissing() {
         try {
-            VFutures.sync(mVCollection.create(Syncbase.getVContext(), Syncbase.defaultPerms()));
-        } catch (ExistException e) {
-            // Collection already exists.
-        } catch (VException e) {
-            throw new RuntimeException("Failed to create collection", e);
+            mCoreCollection.create(Syncbase.defaultCollectionPerms());
+        } catch (VError vError) {
+            if (vError.id.equals(VError.EXIST)) {
+                return;
+            }
+            throw new RuntimeException("Failed to create collection", vError);
         }
     }
 
-    protected Collection(io.v.v23.syncbase.Collection vCollection, DatabaseHandle databaseHandle) {
-        mVCollection = vCollection;
-        mDatabaseHandle = databaseHandle;
-    }
-
     /**
      * Returns the id of this collection.
      */
     public Id getId() {
-        return new Id(mVCollection.id());
+        return mId;
     }
 
     /**
@@ -62,82 +64,68 @@
     /**
      * Returns the value associated with {@code key}.
      */
-    public <T> T get(String key, Class<T> cls) {
+    public <T> T get(String key, Class<T> cls) throws VError {
         try {
-            return VFutures.sync(mVCollection.getRow(key).get(Syncbase.getVContext(), cls));
-        } catch (NoExistException e) {
-            return null;
+            return (T) VomUtil.decode(mCoreCollection.get(key), cls);
+        } catch (VError vError) {
+            if (vError.id.equals(VError.NO_EXIST)) {
+                return null;
+            }
+            throw vError;
         } catch (VException e) {
-            throw new RuntimeException("get failed: " + key, e);
+            throw new VError(e);
         }
     }
 
     /**
      * Returns true if there is a value associated with {@code key}.
      */
-    public boolean exists(String key) {
-        try {
-            return VFutures.sync(mVCollection.getRow(key).exists(Syncbase.getVContext()));
-        } catch (VException e) {
-            throw new RuntimeException("exists failed: " + key, e);
-        }
+    public boolean exists(String key) throws VError {
+        return mCoreCollection.row(key).exists();
     }
 
     /**
      * Puts {@code value} for {@code key}, overwriting any existing value. Idempotent.
      */
-    public <T> void put(String key, T value) {
+    public <T> void put(String key, T value) throws VError {
         try {
-            VFutures.sync(mVCollection.put(Syncbase.getVContext(), key, value));
+            mCoreCollection.put(key, VomUtil.encode(value, value.getClass()));
         } catch (VException e) {
-            throw new RuntimeException("put failed: " + key, e);
+            throw new VError(e);
         }
     }
 
     /**
      * Deletes the value associated with {@code key}. Idempotent.
      */
-    public void delete(String key) {
-        try {
-            VFutures.sync(mVCollection.getRow(key).delete(Syncbase.getVContext()));
-        } catch (VException e) {
-            throw new RuntimeException("delete failed: " + key, e);
-        }
+    public void delete(String key) throws VError {
+        mCoreCollection.delete(key);
     }
 
     /**
      * FOR ADVANCED USERS. Returns the {@code AccessList} for this collection. Users should
      * typically manipulate access lists via {@code collection.getSyncgroup()}.
      */
-    public AccessList getAccessList() {
-        try {
-            return new AccessList(VFutures.sync(mVCollection.getPermissions(Syncbase.getVContext())));
-        } catch (VException e) {
-            throw new RuntimeException("getPermissions failed", e);
-        }
+    public AccessList getAccessList() throws VError {
+        return new AccessList(mCoreCollection.getPermissions());
     }
 
     /**
      * FOR ADVANCED USERS. Updates the {@code AccessList} for this collection. Users should
      * typically manipulate access lists via {@code collection.getSyncgroup()}.
      */
-    public void updateAccessList(final AccessList delta) {
+    public void updateAccessList(final AccessList delta) throws VError {
         final Id id = this.getId();
         Database.BatchOperation op = new Database.BatchOperation() {
             @Override
             public void run(BatchDatabase db) {
-                io.v.v23.syncbase.Collection vCx = db.getCollection(id).mVCollection;
-                Permissions perms;
+                io.v.syncbase.core.Collection coreCollection = db.getCollection(id).mCoreCollection;
                 try {
-                    perms = VFutures.sync(vCx.getPermissions(Syncbase.getVContext()));
-                } catch (VException e) {
-                    throw new RuntimeException("getPermissions failed", e);
-                }
-                AccessList.applyDelta(perms, delta);
-                try {
-                    VFutures.sync(vCx.setPermissions(Syncbase.getVContext(), perms));
-                } catch (VException e) {
-                    throw new RuntimeException("setPermissions failed", e);
+                    Permissions newPermissions = AccessList.applyDelta(
+                            coreCollection.getPermissions(), delta);
+                    coreCollection.setPermissions(newPermissions);
+                } catch (VError vError) {
+                    throw new RuntimeException("updateAccessList failed", vError);
                 }
             }
         };
diff --git a/syncbase/src/main/java/io/v/syncbase/Database.java b/syncbase/src/main/java/io/v/syncbase/Database.java
index a669fd7..46422d4 100644
--- a/syncbase/src/main/java/io/v/syncbase/Database.java
+++ b/syncbase/src/main/java/io/v/syncbase/Database.java
@@ -5,10 +5,6 @@
 package io.v.syncbase;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.util.concurrent.FutureCallback;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.SettableFuture;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -16,47 +12,40 @@
 import java.util.List;
 import java.util.Map;
 
-import javax.annotation.Nullable;
-
-import io.v.v23.InputChannel;
-import io.v.v23.InputChannelCallback;
-import io.v.v23.InputChannels;
-import io.v.v23.VFutures;
-import io.v.v23.services.syncbase.CollectionRowPattern;
-import io.v.v23.services.syncbase.SyncgroupSpec;
-import io.v.v23.syncbase.Batch;
-import io.v.v23.verror.ExistException;
-import io.v.v23.verror.VException;
+import io.v.syncbase.core.CollectionRowPattern;
+import io.v.syncbase.core.SyncgroupMemberInfo;
+import io.v.syncbase.core.VError;
 
 /**
  * A set of collections and syncgroups.
  * To get a Database handle, call {@code Syncbase.database}.
  */
 public class Database extends DatabaseHandle {
-    private final io.v.v23.syncbase.Database mVDatabase;
+    private final io.v.syncbase.core.Database mCoreDatabase;
 
     private final Object mSyncgroupInviteHandlersMu = new Object();
     private final Object mWatchChangeHandlersMu = new Object();
     private Map<SyncgroupInviteHandler, Runnable> mSyncgroupInviteHandlers = new HashMap<>();
     private Map<WatchChangeHandler, Runnable> mWatchChangeHandlers = new HashMap<>();
 
-    protected void createIfMissing() {
+    protected Database(io.v.syncbase.core.Database coreDatabase) {
+        super(coreDatabase);
+        mCoreDatabase = coreDatabase;
+    }
+
+    protected void createIfMissing() throws VError {
         try {
-            VFutures.sync(mVDatabase.create(Syncbase.getVContext(), Syncbase.defaultPerms()));
-        } catch (ExistException e) {
-            // Database already exists, presumably from a previous run of the app.
-        } catch (VException e) {
-            throw new RuntimeException("Failed to create database", e);
+            mCoreDatabase.create(Syncbase.defaultDatabasePerms());
+        } catch (VError vError) {
+            if (vError.id.equals(VError.EXIST)) {
+                return;
+            }
+            throw vError;
         }
     }
 
-    protected Database(io.v.v23.syncbase.Database vDatabase) {
-        super(vDatabase);
-        mVDatabase = vDatabase;
-    }
-
     @Override
-    public Collection collection(String name, CollectionOptions opts) {
+    public Collection collection(String name, CollectionOptions opts) throws VError {
         Collection res = getCollection(new Id(Syncbase.getPersonalBlessingString(), name));
         res.createIfMissing();
         // TODO(sadovsky): Unwind collection creation on syncgroup creation failure? It would be
@@ -84,25 +73,26 @@
      * @param opts        options for syncgroup creation
      * @return the syncgroup
      */
-    public Syncgroup syncgroup(String name, List<Collection> collections, SyncgroupOptions opts) {
+    public Syncgroup syncgroup(String name, List<Collection> collections, SyncgroupOptions opts)
+            throws VError {
         if (collections.isEmpty()) {
             throw new RuntimeException("No collections specified");
         }
         Id id = new Id(collections.get(0).getId().getBlessing(), name);
-        for (Collection cx : collections) {
-            if (!cx.getId().getBlessing().equals(id.getBlessing())) {
+        for (Collection collection : collections) {
+            if (!collection.getId().getBlessing().equals(id.getBlessing())) {
                 throw new RuntimeException("Collections must all have the same creator");
             }
         }
-        Syncgroup res = new Syncgroup(mVDatabase.getSyncgroup(id.toVId()), this, id);
-        res.createIfMissing(collections);
-        return res;
+        Syncgroup syncgroup = new Syncgroup(mCoreDatabase.syncgroup(id.toCoreId()), this);
+        syncgroup.createIfMissing(collections);
+        return syncgroup;
     }
 
     /**
      * Calls {@code syncgroup(name, collections, opts)} with default {@code SyncgroupOptions}.
      */
-    public Syncgroup syncgroup(String name, List<Collection> collections) {
+    public Syncgroup syncgroup(String name, List<Collection> collections) throws VError {
         return syncgroup(name, collections, new SyncgroupOptions());
     }
 
@@ -113,24 +103,18 @@
         // TODO(sadovsky): Consider throwing an exception or returning null if the syncgroup does
         // not exist. But note, a syncgroup can get destroyed via sync after a client obtains a
         // handle for it, so perhaps we should instead add an 'exists' method.
-        return new Syncgroup(mVDatabase.getSyncgroup(id.toVId()), this, id);
+        return new Syncgroup(mCoreDatabase.syncgroup(id.toCoreId()), this);
     }
 
     /**
      * Returns an iterator over all syncgroups in the database.
      */
-    public Iterator<Syncgroup> getSyncgroups() {
-        List<io.v.v23.services.syncbase.Id> vIds;
-        try {
-            vIds = VFutures.sync(mVDatabase.listSyncgroups(Syncbase.getVContext()));
-        } catch (VException e) {
-            throw new RuntimeException("listSyncgroups failed", e);
+    public Iterator<Syncgroup> getSyncgroups() throws VError {
+        ArrayList<Syncgroup> syncgroups = new ArrayList<>();
+        for (io.v.syncbase.core.Id id : mCoreDatabase.listSyncgroups()) {
+            syncgroups.add(getSyncgroup(new Id(id)));
         }
-        ArrayList<Syncgroup> sgs = new ArrayList<>(vIds.size());
-        for (io.v.v23.services.syncbase.Id vId : vIds) {
-            sgs.add(new Syncgroup(mVDatabase.getSyncgroup(vId), this, new Id(vId)));
-        }
-        return sgs.iterator();
+        return syncgroups.iterator();
     }
 
     /**
@@ -219,23 +203,27 @@
      * @param invite the syncgroup invite
      * @param cb     the callback to call with the syncgroup handle
      */
-    public void acceptSyncgroupInvite(SyncgroupInvite invite, final AcceptSyncgroupInviteCallback cb) {
+    public void acceptSyncgroupInvite(final SyncgroupInvite invite,
+                                      final AcceptSyncgroupInviteCallback cb) {
         // TODO(sadovsky): Should we add "accept" and "ignore" methods to the SyncgroupInvite class,
         // or should we treat it as a POJO (with no reference to Database)?
-        io.v.v23.syncbase.Syncgroup vSyncgroup = mVDatabase.getSyncgroup(invite.getId().toVId());
-        final Syncgroup syncgroup = new Syncgroup(vSyncgroup, this, invite.getId());
-        ListenableFuture<SyncgroupSpec> future = vSyncgroup.join(Syncbase.getVContext(), invite.getRemoteSyncbaseName(), invite.getExpectedSyncbaseBlessings(), Syncgroup.newSyncgroupMemberInfo());
-        Futures.addCallback(future, new FutureCallback<SyncgroupSpec>() {
+        final io.v.syncbase.core.Syncgroup coreSyncgroup =
+                mCoreDatabase.syncgroup(invite.getId().toCoreId());
+        final Database database = this;
+        // TODO(razvanm): Figure out if we should use an AsyncTask or something else.
+        new Thread(new Runnable() {
             @Override
-            public void onSuccess(@Nullable SyncgroupSpec result) {
-                cb.onSuccess(syncgroup);
+            public void run() {
+                try {
+                    coreSyncgroup.join(invite.getRemoteSyncbaseName(),
+                            invite.getExpectedSyncbaseBlessings(), new SyncgroupMemberInfo());
+                } catch (VError vError) {
+                    cb.onFailure(vError);
+                    return;
+                }
+                cb.onSuccess(new Syncgroup(coreSyncgroup, database));
             }
-
-            @Override
-            public void onFailure(Throwable e) {
-                cb.onFailure(e);
-            }
-        });
+        }).start();
     }
 
     /**
@@ -256,10 +244,11 @@
     public static class BatchOptions {
         public boolean readOnly;
 
-        protected io.v.v23.services.syncbase.BatchOptions toVBatchOptions() {
-            io.v.v23.services.syncbase.BatchOptions res = new io.v.v23.services.syncbase.BatchOptions();
-            res.setReadOnly(true);
-            return res;
+        public io.v.syncbase.core.BatchOptions toCore() {
+            io.v.syncbase.core.BatchOptions coreBatchOptions =
+                    new io.v.syncbase.core.BatchOptions();
+            coreBatchOptions.readOnly = readOnly;
+            return coreBatchOptions;
         }
     }
 
@@ -277,32 +266,20 @@
      * @param op   the operation to run
      * @param opts options for this batch
      */
-    public void runInBatch(final BatchOperation op, BatchOptions opts) {
-        ListenableFuture<Void> future = Batch.runInBatch(Syncbase.getVContext(), mVDatabase, opts.toVBatchOptions(), new Batch.BatchOperation() {
+    public void runInBatch(final BatchOperation op, BatchOptions opts) throws VError {
+        mCoreDatabase.runInBatch(new io.v.syncbase.core.Database.BatchOperation() {
             @Override
-            public ListenableFuture<Void> run(io.v.v23.syncbase.BatchDatabase vBatchDatabase) {
-                final SettableFuture<Void> res = SettableFuture.create();
-                try {
-                    op.run(new BatchDatabase(vBatchDatabase));
-                    res.set(null);
-                } catch (Exception e) {
-                    res.setException(e);
-                }
-                return res;
+            public void run(io.v.syncbase.core.BatchDatabase batchDatabase) {
+                op.run(new BatchDatabase(batchDatabase));
             }
-        });
-        try {
-            VFutures.sync(future);
-        } catch (VException e) {
-            throw new RuntimeException("runInBatch failed", e);
-        }
+        }, opts.toCore());
     }
 
     /**
      * Creates a new batch. Instead of calling this function directly, clients are encouraged to use
      * the {@code runInBatch} helper function, which detects "concurrent batch" errors and handles
      * retries internally.
-     * <p>
+     * <p/>
      * Default concurrency semantics:
      * <ul>
      * <li>Reads (e.g. gets, scans) inside a batch operate over a consistent snapshot taken during
@@ -313,23 +290,17 @@
      * <li>Other methods will never fail with error {@code ConcurrentBatchException}, even if it is
      * known that {@code commit} will fail with this error.</li>
      * </ul>
-     * <p>
+     * <p/>
      * Once a batch has been committed or aborted, subsequent method calls will fail with no
      * effect.
-     * <p>
+     * <p/>
      * Concurrency semantics can be configured using BatchOptions.
      *
      * @param opts options for this batch
      * @return the batch handle
      */
-    public BatchDatabase beginBatch(BatchOptions opts) {
-        io.v.v23.syncbase.BatchDatabase vBatchDatabase;
-        try {
-            vBatchDatabase = VFutures.sync(mVDatabase.beginBatch(Syncbase.getVContext(), opts.toVBatchOptions()));
-        } catch (VException e) {
-            throw new RuntimeException("beginBatch failed", e);
-        }
-        return new BatchDatabase(vBatchDatabase);
+    public BatchDatabase beginBatch(BatchOptions opts) throws VError {
+        return new BatchDatabase(mCoreDatabase.beginBatch(opts.toCore()));
     }
 
     /**
@@ -384,42 +355,37 @@
         if (opts.resumeMarker != null && opts.resumeMarker.length != 0) {
             throw new RuntimeException("Specifying resumeMarker is not yet supported");
         }
-        InputChannel<io.v.v23.syncbase.WatchChange> ic = mVDatabase.watch(Syncbase.getVContext(), ImmutableList.of(new CollectionRowPattern("%", "%", "%")));
-        ListenableFuture<Void> future = InputChannels.withCallback(ic, new InputChannelCallback<io.v.v23.syncbase.WatchChange>() {
-            private boolean mGotFirstBatch = false;
-            private List<WatchChange> mBatch = new ArrayList<>();
 
-            @Override
-            public ListenableFuture<Void> onNext(io.v.v23.syncbase.WatchChange vChange) {
-                WatchChange change = new WatchChange(vChange);
-                // Ignore changes to userdata collection.
-                if (change.getCollectionId().getName().equals(Syncbase.USERDATA_SYNCGROUP_NAME)) {
-                    return null;
-                }
-                mBatch.add(change);
-                if (!change.isContinued()) {
-                    if (!mGotFirstBatch) {
-                        mGotFirstBatch = true;
-                        h.onInitialState(mBatch.iterator());
-                    } else {
-                        h.onChangeBatch(mBatch.iterator());
+        mCoreDatabase.watch(null, ImmutableList.of(new CollectionRowPattern("%", "%", "%")),
+                new io.v.syncbase.core.Database.WatchPatternsCallbacks() {
+                    private boolean mGotFirstBatch = false;
+                    private List<WatchChange> mBatch = new ArrayList<>();
+
+                    @Override
+                    public void onChange(io.v.syncbase.core.WatchChange coreWatchChange) {
+                        // Ignore changes to userdata collection.
+                        if (coreWatchChange.collection.name.equals(Syncbase.USERDATA_SYNCGROUP_NAME)) {
+                            return;
+                        }
+                        mBatch.add(new WatchChange(coreWatchChange));
+                        if (!coreWatchChange.continued) {
+                            if (!mGotFirstBatch) {
+                                mGotFirstBatch = true;
+                                h.onInitialState(mBatch.iterator());
+                            } else {
+                                h.onChangeBatch(mBatch.iterator());
+                            }
+                            mBatch.clear();
+                        }
                     }
-                    mBatch.clear();
-                }
-                return null;
-            }
-        });
-        Futures.addCallback(future, new FutureCallback<Void>() {
-            @Override
-            public void onSuccess(@Nullable Void result) {
-            }
 
-            @Override
-            public void onFailure(Throwable e) {
-                // TODO(sadovsky): Make sure cancellations are surfaced as such (or ignored).
-                h.onError(e);
-            }
-        });
+                    @Override
+                    public void onError(VError vError) {
+                        // TODO(sadovsky): Make sure cancellations are surfaced as such (or ignored).
+                        h.onError(vError);
+                    }
+                });
+
         synchronized (mWatchChangeHandlersMu) {
             mWatchChangeHandlers.put(h, new Runnable() {
                 @Override
diff --git a/syncbase/src/main/java/io/v/syncbase/DatabaseHandle.java b/syncbase/src/main/java/io/v/syncbase/DatabaseHandle.java
index 18cdc00..8c0d138 100644
--- a/syncbase/src/main/java/io/v/syncbase/DatabaseHandle.java
+++ b/syncbase/src/main/java/io/v/syncbase/DatabaseHandle.java
@@ -6,27 +6,25 @@
 
 import java.util.ArrayList;
 import java.util.Iterator;
-import java.util.List;
 
-import io.v.v23.VFutures;
-import io.v.v23.syncbase.DatabaseCore;
-import io.v.v23.verror.VException;
+import io.v.syncbase.core.VError;
+
 
 /**
  * Represents a handle to a database, possibly in a batch.
  */
 public abstract class DatabaseHandle {
-    protected DatabaseCore mVDatabaseCore;
+    protected io.v.syncbase.core.DatabaseHandle mCoreDatabaseHandle;
 
-    protected DatabaseHandle(DatabaseCore vDatabaseCore) {
-        mVDatabaseCore = vDatabaseCore;
+    protected DatabaseHandle(io.v.syncbase.core.DatabaseHandle coreDatabaseHandle) {
+        mCoreDatabaseHandle = coreDatabaseHandle;
     }
 
     /**
      * Returns the id of this database.
      */
     public Id getId() {
-        return new Id(mVDatabaseCore.id());
+        return new Id(mCoreDatabaseHandle.id());
     }
 
     /**
@@ -47,12 +45,12 @@
      * @param opts options for collection creation
      * @return the collection handle
      */
-    public abstract Collection collection(String name, CollectionOptions opts);
+    public abstract Collection collection(String name, CollectionOptions opts) throws VError;
 
     /**
      * Calls {@code collection(name, opts)} with default {@code CollectionOptions}.
      */
-    public Collection collection(String name) {
+    public Collection collection(String name) throws VError {
         return collection(name, new CollectionOptions());
     }
 
@@ -63,23 +61,17 @@
         // TODO(sadovsky): Consider throwing an exception or returning null if the collection does
         // not exist. But note, a collection can get destroyed via sync after a client obtains a
         // handle for it, so perhaps we should instead add an 'exists' method.
-        return new Collection(mVDatabaseCore.getCollection(id.toVId()), this);
+        return new Collection(mCoreDatabaseHandle.collection(id.toCoreId()), this);
     }
 
     /**
      * Returns an iterator over all collections in the database.
      */
-    public Iterator<Collection> getCollections() {
-        List<io.v.v23.services.syncbase.Id> vIds;
-        try {
-            vIds = VFutures.sync(mVDatabaseCore.listCollections(Syncbase.getVContext()));
-        } catch (VException e) {
-            throw new RuntimeException("listCollections failed", e);
+    public Iterator<Collection> getCollections() throws VError {
+        ArrayList<Collection> collections = new ArrayList<>();
+        for (io.v.syncbase.core.Id id : mCoreDatabaseHandle.listCollections()) {
+            collections.add(getCollection(new Id(id)));
         }
-        ArrayList<Collection> cxs = new ArrayList<>(vIds.size());
-        for (io.v.v23.services.syncbase.Id vId : vIds) {
-            cxs.add(new Collection(mVDatabaseCore.getCollection(vId), this));
-        }
-        return cxs.iterator();
+        return collections.iterator();
     }
 }
diff --git a/syncbase/src/main/java/io/v/syncbase/Id.java b/syncbase/src/main/java/io/v/syncbase/Id.java
index f60a6bb..0e8cb2f 100644
--- a/syncbase/src/main/java/io/v/syncbase/Id.java
+++ b/syncbase/src/main/java/io/v/syncbase/Id.java
@@ -8,12 +8,14 @@
  * Uniquely identifies a database, collection, or syncgroup.
  */
 public class Id {
-    private final String mBlessing;
-    private final String mName;
+    private io.v.syncbase.core.Id mId;
+
+    protected Id(io.v.syncbase.core.Id id) {
+        mId = id;
+    }
 
     protected Id(String blessing, String name) {
-        mBlessing = blessing;
-        mName = name;
+        mId = new io.v.syncbase.core.Id(blessing, name);
     }
 
     // TODO(sadovsky): Replace encode and decode method implementations with calls to Cgo.
@@ -28,22 +30,26 @@
     }
 
     public String encode() {
-        return mBlessing + SEPARATOR + mName;
+        return mId.encode();
+    }
+
+    protected io.v.syncbase.core.Id toCoreId() {
+        return mId;
     }
 
     protected String getBlessing() {
-        return mBlessing;
+        return mId.blessing;
     }
 
     public String getName() {
-        return mName;
+        return mId.name;
     }
 
     @Override
     public boolean equals(Object other) {
         if (other instanceof Id) {
             Id otherId = (Id) other;
-            return mBlessing.equals(otherId.getBlessing()) && mName.equals(otherId.getName());
+            return mId.blessing.equals(otherId.getBlessing()) && mId.name.equals(otherId.getName());
         }
         return false;
     }
@@ -54,9 +60,9 @@
         int result = 1;
         int prime = 31;
 
-        result = prime * result + (mBlessing == null ? 0 : mBlessing.hashCode());
+        result = prime * result + (mId.blessing == null ? 0 : mId.blessing.hashCode());
 
-        result = prime * result + (mName == null ? 0 : mName.hashCode());
+        result = prime * result + (mId.name == null ? 0 : mId.name.hashCode());
 
         return result;
     }
@@ -65,15 +71,4 @@
     public String toString() {
         return "Id(" + encode() + ")";
     }
-
-    // TODO(sadovsky): Eliminate the code below once we've switched to io.v.syncbase.core.
-
-    protected Id(io.v.v23.services.syncbase.Id id) {
-        mBlessing = id.getBlessing();
-        mName = id.getName();
-    }
-
-    protected io.v.v23.services.syncbase.Id toVId() {
-        return new io.v.v23.services.syncbase.Id(mBlessing, mName);
-    }
 }
diff --git a/syncbase/src/main/java/io/v/syncbase/Syncbase.java b/syncbase/src/main/java/io/v/syncbase/Syncbase.java
index d0bb453..e6f7d47 100644
--- a/syncbase/src/main/java/io/v/syncbase/Syncbase.java
+++ b/syncbase/src/main/java/io/v/syncbase/Syncbase.java
@@ -4,26 +4,17 @@
 
 package io.v.syncbase;
 
-import android.util.Log;
-
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 
-import java.io.File;
 import java.util.List;
+import java.util.Map;
 import java.util.Timer;
 import java.util.TimerTask;
 
-import io.v.impl.google.services.syncbase.SyncbaseServer;
-import io.v.v23.V;
-import io.v.v23.context.VContext;
-import io.v.v23.rpc.Server;
-import io.v.v23.security.BlessingPattern;
-import io.v.v23.security.Blessings;
-import io.v.v23.security.access.Constants;
-import io.v.v23.security.access.Permissions;
-import io.v.v23.syncbase.SyncbaseService;
-import io.v.v23.verror.VException;
+import io.v.syncbase.core.Permissions;
+import io.v.syncbase.core.Service;
+import io.v.syncbase.core.VError;
 
 // FIXME(sadovsky): Currently, various methods throw RuntimeException on any error. We need to
 // decide which error types to surface to clients, and define specific Exception subclasses for
@@ -57,8 +48,6 @@
         public boolean disableSyncgroupPublishing;
         // FOR ADVANCED USERS. If true, the user's data will not be synced across their devices.
         public boolean disableUserdataSyncgroup;
-        // TODO(sadovsky): Drop this once we switch from io.v.v23.syncbase to io.v.syncbase.core.
-        public VContext vContext;
 
         protected String getPublishSyncbaseName() {
             if (disableSyncgroupPublishing) {
@@ -74,6 +63,7 @@
 
     protected static DatabaseOptions sOpts;
     private static Database sDatabase;
+    private static Map sSelfAndCloud;
 
     // TODO(sadovsky): Maybe set DB_NAME to "db__" so that it is less likely to collide with
     // developer-specified names.
@@ -127,24 +117,34 @@
             return;
         }
         sOpts = opts;
-        // TODO(sadovsky): Call ctx.cancel in sDatabase destructor?
-        VContext ctx = getVContext().withCancel();
+        sSelfAndCloud = ImmutableMap.of(
+                Permissions.IN, ImmutableList.of(getPersonalBlessingString(),
+                        sOpts.getCloudBlessingString()));
+        // TODO(razvanm): Surface Cgo function to shut down syncbase.
         try {
-            sDatabase = startSyncbaseAndInitDatabase(ctx);
-        } catch (final Exception e) {
-            ctx.cancel();
-            Syncbase.enqueue(new Runnable() {
+            // TODO(razvanm): Use just the name after Blessings.AppBlessingFromContext starts
+            // working.
+            sDatabase = new Database(Service.database(new io.v.syncbase.core.Id("...", DB_NAME)));
+            sDatabase.createIfMissing();
+        } catch (final VError vError) {
+            enqueue(new Runnable() {
                 @Override
                 public void run() {
-                    cb.onError(e);
+                    cb.onError(vError);
                 }
             });
             return;
         }
+
         if (sOpts.disableUserdataSyncgroup) {
             Database.CollectionOptions cxOpts = new DatabaseHandle.CollectionOptions();
             cxOpts.withoutSyncgroup = true;
-            sDatabase.collection(USERDATA_SYNCGROUP_NAME, cxOpts);
+            try {
+                sDatabase.collection(USERDATA_SYNCGROUP_NAME, cxOpts);
+            } catch (VError vError) {
+                cb.onError(vError);
+                return;
+            }
             Syncbase.enqueue(new Runnable() {
                 @Override
                 public void run() {
@@ -174,10 +174,10 @@
 
     /**
      * Logs in the user associated with the given OAuth token and provider.
-     *
+     * <p/>
      * A mapping of providers and OAuth token scopes are listed below:
      * google: https://www.googleapis.com/auth/userinfo.email
-     *
+     * <p/>
      * Note: Unlisted providers are unsupported.
      *
      * @param authToken The OAuth token for the user to be logged in.
@@ -201,7 +201,9 @@
 
     public static abstract class ScanNeighborhoodForUsersCallback {
         public abstract void onFound(User user);
+
         public abstract void onLost(User user);
+
         public void onError(Throwable e) {
             throw new RuntimeException(e);
         }
@@ -237,106 +239,43 @@
         throw new RuntimeException("Not implemented");
     }
 
-    ////////////////////////////////////////////////////////////////////////////////////////////////
-    // TODO(sadovsky): Remove much of the code below once we switch from io.v.v23.syncbase to
-    // io.v.syncbase.core. Note, much of this code was copied from the Todos app.
-
-    protected static VContext getVContext() {
-        return sOpts.vContext;
+    protected static String getBlessingStringFromEmail(String email) {
+        return sOpts.defaultBlessingStringPrefix + email;
     }
 
-    private static Database startSyncbaseAndInitDatabase(VContext ctx) {
-        SyncbaseService s;
-        try {
-            s = io.v.v23.syncbase.Syncbase.newService(startSyncbase(ctx, sOpts.rootDir));
-        } catch (SyncbaseServer.StartException e) {
-            throw new RuntimeException("Failed to start Syncbase", e);
-        }
-        // Create database, if needed.
-        Database res = new Database(s.getDatabase(getVContext(), DB_NAME, null));
-        res.createIfMissing();
-        return res;
-    }
-
-    private static String startSyncbase(VContext vContext, String rootDir)
-            throws SyncbaseServer.StartException {
-        try {
-            // TODO(sadovsky): Make proxy configurable?
-            vContext = V.withListenSpec(vContext, V.getListenSpec(vContext).withProxy("proxy"));
-        } catch (VException e) {
-            Log.w(TAG, "Failed to set up Vanadium proxy", e);
-        }
-        File dir = new File(rootDir, DIR_NAME);
-        dir.mkdirs();
-        SyncbaseServer.Params params = new SyncbaseServer.Params()
-                .withStorageRootDir(dir.getAbsolutePath());
-        VContext serverContext = SyncbaseServer.withNewServer(vContext, params);
-        Server server = V.getServer(serverContext);
-        String name = server.getStatus().getEndpoints()[0].name();
-        Log.i(TAG, "Started Syncbase: " + name);
-        return name;
-    }
-
-    private static void checkHasOneBlessing(Blessings blessings) {
-        int n = blessings.getCertificateChains().size();
-        if (n != 1) {
-            throw new RuntimeException("Expected one blessing, got " + n);
-        }
-    }
-
-    private static String getEmailFromBlessings(Blessings blessings) {
-        checkHasOneBlessing(blessings);
-        return getEmailFromBlessingString(blessings.toString());
-    }
-
-    protected static String getEmailFromBlessingPattern(BlessingPattern pattern) {
-        return getEmailFromBlessingString(pattern.toString());
-    }
-
-    private static String getEmailFromBlessingString(String blessingStr) {
+    protected static String getEmailFromBlessingPattern(String blessingStr) {
         String[] parts = blessingStr.split(":");
         return parts[parts.length - 1];
     }
 
-    private static String getBlessingStringFromEmail(String email) {
-        return sOpts.defaultBlessingStringPrefix + email;
-    }
-
-    protected static BlessingPattern getBlessingPatternFromEmail(String email) {
-        return new BlessingPattern(getBlessingStringFromEmail(email));
-    }
-
-    private static Blessings getPersonalBlessings() {
-        return V.getPrincipal(getVContext()).blessingStore().defaultBlessings();
-    }
-
     protected static String getPersonalBlessingString() {
-        Blessings blessings = getPersonalBlessings();
-        checkHasOneBlessing(blessings);
-        return blessings.toString();
+        // TODO(razvanm): Switch to Blessings.UserBlessingFromContext() after the lower level
+        // starts working.
+        return "...";
     }
 
-    private static String getPersonalEmail() {
-        return getEmailFromBlessings(getPersonalBlessings());
-    }
-
-    protected static Permissions defaultPerms() {
+    protected static Permissions defaultDatabasePerms() throws VError {
         // TODO(sadovsky): Revisit these default perms, which were copied from the Todos app.
-        io.v.v23.security.access.AccessList anyone =
-                new io.v.v23.security.access.AccessList(
-                        ImmutableList.of(
-                                new BlessingPattern("...")),
-                        ImmutableList.<String>of());
-        io.v.v23.security.access.AccessList selfAndCloud =
-                new io.v.v23.security.access.AccessList(
-                        ImmutableList.of(
-                                new BlessingPattern(getPersonalBlessingString()),
-                                new BlessingPattern(sOpts.getCloudBlessingString())),
-                        ImmutableList.<String>of());
+        Map anyone = ImmutableMap.of(Permissions.IN, ImmutableList.of("..."));
         return new Permissions(ImmutableMap.of(
-                Constants.RESOLVE.getValue(), anyone,
-                Constants.READ.getValue(), selfAndCloud,
-                Constants.WRITE.getValue(), selfAndCloud,
-                Constants.ADMIN.getValue(), selfAndCloud));
+                Permissions.Tags.RESOLVE, anyone,
+                Permissions.Tags.READ, sSelfAndCloud,
+                Permissions.Tags.WRITE, sSelfAndCloud,
+                Permissions.Tags.ADMIN, sSelfAndCloud));
+    }
+
+    protected static Permissions defaultCollectionPerms() throws VError {
+        // TODO(sadovsky): Revisit these default perms, which were copied from the Todos app.
+        return new Permissions(ImmutableMap.of(
+                Permissions.Tags.READ, sSelfAndCloud,
+                Permissions.Tags.WRITE, sSelfAndCloud,
+                Permissions.Tags.ADMIN, sSelfAndCloud));
+    }
+
+    protected static Permissions defaultSyncgroupPerms() throws VError {
+        // TODO(sadovsky): Revisit these default perms, which were copied from the Todos app.
+        return new Permissions(ImmutableMap.of(
+                Permissions.Tags.READ, sSelfAndCloud,
+                Permissions.Tags.ADMIN, sSelfAndCloud));
     }
 }
\ No newline at end of file
diff --git a/syncbase/src/main/java/io/v/syncbase/Syncgroup.java b/syncbase/src/main/java/io/v/syncbase/Syncgroup.java
index 07aa9f0..ea2a048 100644
--- a/syncbase/src/main/java/io/v/syncbase/Syncgroup.java
+++ b/syncbase/src/main/java/io/v/syncbase/Syncgroup.java
@@ -7,13 +7,12 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 
-import io.v.v23.VFutures;
-import io.v.v23.services.syncbase.SyncgroupMemberInfo;
-import io.v.v23.services.syncbase.SyncgroupSpec;
-import io.v.v23.verror.ExistException;
-import io.v.v23.verror.VException;
+import io.v.syncbase.core.Permissions;
+import io.v.syncbase.core.SyncgroupMemberInfo;
+import io.v.syncbase.core.SyncgroupSpec;
+import io.v.syncbase.core.VError;
+import io.v.syncbase.core.VersionedSyncgroupSpec;
 
 /**
  * Represents a set of collections, synced amongst a set of users.
@@ -21,61 +20,52 @@
  */
 public class Syncgroup {
     private final Database mDatabase;
-    private final Id mId;
-    private final io.v.v23.syncbase.Syncgroup mVSyncgroup;
+    private final io.v.syncbase.core.Syncgroup mCoreSyncgroup;
 
-    protected static SyncgroupMemberInfo newSyncgroupMemberInfo() {
-        SyncgroupMemberInfo info = new SyncgroupMemberInfo();
-        // TODO(sadovsky): Still have no idea how to set sync priority.
-        info.setSyncPriority((byte) 3);
-        return info;
-    }
-
-    protected void createIfMissing(List<Collection> collections) {
-        ArrayList<io.v.v23.services.syncbase.Id> cxVIds = new ArrayList<>(collections.size());
-        for (Collection cx : collections) {
-            cxVIds.add(cx.getId().toVId());
-        }
-        SyncgroupSpec spec = new SyncgroupSpec(
-                "", Syncbase.sOpts.getPublishSyncbaseName(), Syncbase.defaultPerms(), cxVIds,
-                Syncbase.sOpts.mountPoints, false);
-        try {
-            VFutures.sync(mVSyncgroup.create(Syncbase.getVContext(), spec, newSyncgroupMemberInfo()));
-        } catch (ExistException e) {
-            // Syncgroup already exists.
-            // TODO(sadovsky): Verify that the existing syncgroup has the specified configuration,
-            // e.g. the specified collections?
-        } catch (VException e) {
-            throw new RuntimeException("Failed to create collection", e);
-        }
-    }
-
-    // TODO(sadovsky): We take 'id' because io.v.v23.syncbase.Syncgroup is missing the 'getId'
-    // method. Drop the 'id' argument once we switch to io.v.syncbase.core.
-    protected Syncgroup(io.v.v23.syncbase.Syncgroup vSyncgroup, Database database, Id id) {
-        mVSyncgroup = vSyncgroup;
+    protected Syncgroup(io.v.syncbase.core.Syncgroup coreSyncgroup, Database database) {
+        mCoreSyncgroup = coreSyncgroup;
         mDatabase = database;
-        mId = id;
+    }
+
+    protected void createIfMissing(List<Collection> collections) throws VError {
+        ArrayList<io.v.syncbase.core.Id> ids = new ArrayList<>();
+        for (Collection cx : collections) {
+            ids.add(cx.getId().toCoreId());
+        }
+
+        SyncgroupSpec spec = new SyncgroupSpec();
+        spec.publishSyncbaseName = Syncbase.sOpts.getPublishSyncbaseName();
+        spec.permissions = Syncbase.defaultSyncgroupPerms();
+        spec.collections = ids;
+        spec.mountTables = Syncbase.sOpts.mountPoints;
+        spec.isPrivate = false;
+
+        try {
+            // TODO(razvanm): Figure out to what value we should set the sync priority in the
+            // SyncgroupMemberInfo.
+            mCoreSyncgroup.create(spec, new SyncgroupMemberInfo());
+        } catch (VError vError) {
+            if (vError.id.equals(VError.NO_EXIST)) {
+                // Syncgroup already exists.
+                // TODO(sadovsky): Verify that the existing syncgroup has the specified
+                // configuration, e.g., the specified collections?
+            }
+            throw vError;
+        }
     }
 
     /**
      * Returns the id of this syncgroup.
      */
     public Id getId() {
-        return mId;
+        return new Id(mCoreSyncgroup.getId());
     }
 
     /**
      * Returns the {@code AccessList} for this syncgroup.
      */
-    public AccessList getAccessList() {
-        Map<String, SyncgroupSpec> versionedSpec;
-        try {
-            versionedSpec = VFutures.sync(mVSyncgroup.getSpec(Syncbase.getVContext()));
-        } catch (VException e) {
-            throw new RuntimeException("getSpec failed", e);
-        }
-        return new AccessList(versionedSpec.values().iterator().next().getPerms());
+    public AccessList getAccessList() throws VError {
+        return new AccessList(mCoreSyncgroup.getSpec().syncgroupSpec.permissions);
     }
 
     /**
@@ -94,10 +84,11 @@
     /**
      * FOR ADVANCED USERS. Adds the given users to the syncgroup, with the specified access level.
      */
-    public void inviteUsers(List<User> users, AccessList.AccessLevel level, UpdateAccessListOptions opts) {
+    public void inviteUsers(List<User> users, AccessList.AccessLevel level,
+                            UpdateAccessListOptions opts) throws VError {
         AccessList delta = new AccessList();
         for (User u : users) {
-            delta.users.put(u.getId(), level);
+            delta.users.put(u.getAlias(), level);
         }
         updateAccessList(delta, opts);
     }
@@ -105,24 +96,24 @@
     /**
      * Adds the given users to the syncgroup, with the specified access level.
      */
-    public void inviteUsers(List<User> users, AccessList.AccessLevel level) {
+    public void inviteUsers(List<User> users, AccessList.AccessLevel level) throws VError {
         inviteUsers(users, level, new UpdateAccessListOptions());
     }
 
     /**
      * Adds the given user to the syncgroup, with the specified access level.
      */
-    public void inviteUser(User user, AccessList.AccessLevel level) {
+    public void inviteUser(User user, AccessList.AccessLevel level) throws VError {
         inviteUsers(Collections.singletonList(user), level);
     }
 
     /**
      * FOR ADVANCED USERS. Removes the given users from the syncgroup.
      */
-    public void ejectUsers(List<User> users, UpdateAccessListOptions opts) {
+    public void ejectUsers(List<User> users, UpdateAccessListOptions opts) throws VError {
         AccessList delta = new AccessList();
         for (User u : users) {
-            delta.users.put(u.getId(), null);
+            delta.users.put(u.getAlias(), null);
         }
         updateAccessList(delta, opts);
     }
@@ -130,44 +121,46 @@
     /**
      * Removes the given users from the syncgroup.
      */
-    public void ejectUsers(List<User> users) {
+    public void ejectUsers(List<User> users) throws VError {
         ejectUsers(users, new UpdateAccessListOptions());
     }
 
     /**
      * Removes the given user from the syncgroup.
      */
-    public void ejectUser(User user) {
+    public void ejectUser(User user) throws VError {
         ejectUsers(Collections.singletonList(user));
     }
 
     /**
      * FOR ADVANCED USERS. Applies {@code delta} to the {@code AccessList}.
      */
-    public void updateAccessList(final AccessList delta, UpdateAccessListOptions opts) {
+    public void updateAccessList(final AccessList delta, UpdateAccessListOptions opts)
+            throws VError {
         // TODO(sadovsky): Make it so SyncgroupSpec can be updated as part of a batch?
-        Map<String, SyncgroupSpec> versionedSpec;
+        VersionedSyncgroupSpec versionedSyncgroupSpec;
         try {
-            versionedSpec = VFutures.sync(mVSyncgroup.getSpec(Syncbase.getVContext()));
-        } catch (VException e) {
-            throw new RuntimeException("getSpec failed", e);
+            versionedSyncgroupSpec = mCoreSyncgroup.getSpec();
+        } catch (VError vError) {
+            throw new RuntimeException("getSpec failed", vError);
         }
-        String version = versionedSpec.keySet().iterator().next();
-        SyncgroupSpec spec = versionedSpec.values().iterator().next();
-        AccessList.applyDelta(spec.getPerms(), delta);
-        try {
-            VFutures.sync(mVSyncgroup.setSpec(Syncbase.getVContext(), spec, version));
-        } catch (VException e) {
-            throw new RuntimeException("setSpec failed", e);
-        }
+        Permissions newPermissions = AccessList.applyDelta(
+                versionedSyncgroupSpec.syncgroupSpec.permissions, delta);
+        versionedSyncgroupSpec.syncgroupSpec.permissions = newPermissions;
+        mCoreSyncgroup.setSpec(versionedSyncgroupSpec);
         // TODO(sadovsky): There's a race here - it's possible for a collection to get destroyed
-        // after spec.getCollections() but before db.getCollection().
-        final List<io.v.v23.services.syncbase.Id> cxVIds = spec.getCollections();
+        // after getSpec() but before db.getCollection().
+        final List<io.v.syncbase.core.Id> collectionsIds =
+                versionedSyncgroupSpec.syncgroupSpec.collections;
         mDatabase.runInBatch(new Database.BatchOperation() {
             @Override
             public void run(BatchDatabase db) {
-                for (io.v.v23.services.syncbase.Id vId : cxVIds) {
-                    db.getCollection(new Id(vId)).updateAccessList(delta);
+                for (io.v.syncbase.core.Id id : collectionsIds) {
+                    try {
+                        db.getCollection(new Id(id)).updateAccessList(delta);
+                    } catch (VError vError) {
+                        throw new RuntimeException("getCollection failed", vError);
+                    }
                 }
             }
         }, new Database.BatchOptions());
diff --git a/syncbase/src/main/java/io/v/syncbase/WatchChange.java b/syncbase/src/main/java/io/v/syncbase/WatchChange.java
index 6878dad..1c77b0a 100644
--- a/syncbase/src/main/java/io/v/syncbase/WatchChange.java
+++ b/syncbase/src/main/java/io/v/syncbase/WatchChange.java
@@ -4,17 +4,13 @@
 
 package io.v.syncbase;
 
-import com.google.common.collect.Lists;
-
-import java.util.List;
-
 /**
  * Describes a change to a database.
  */
 public class WatchChange {
     public enum ChangeType {
         PUT,
-        DELETE;
+        DELETE
     }
 
     private final ChangeType mChangeType;
@@ -25,6 +21,17 @@
     private final boolean mFromSync;
     private final boolean mContinued;
 
+    protected WatchChange(io.v.syncbase.core.WatchChange change) {
+        mChangeType = change.changeType == io.v.syncbase.core.WatchChange.ChangeType.PUT ?
+                ChangeType.PUT : ChangeType.DELETE;
+        mCollectionId = new Id(change.collection);
+        mRowKey = change.row;
+        mValue = change.value;
+        mResumeMarker = change.resumeMarker.getBytes();
+        mFromSync = change.fromSync;
+        mContinued = change.continued;
+    }
+
     public ChangeType getChangeType() {
         return mChangeType;
     }
@@ -52,18 +59,4 @@
     public boolean isContinued() {
         return mContinued;
     }
-
-    // TODO(sadovsky): Eliminate the code below once we've switched to io.v.syncbase.core.
-
-    protected WatchChange(io.v.v23.syncbase.WatchChange c) {
-        mChangeType = c.getChangeType() == io.v.v23.syncbase.ChangeType.PUT_CHANGE ? ChangeType.PUT : ChangeType.DELETE;
-        mCollectionId = new Id(c.getCollectionId());
-        mRowKey = c.getRowName();
-        mValue = c.getValue();
-        List<Byte> bytes = Lists.newArrayList(c.getResumeMarker().iterator());
-        mResumeMarker = new byte[bytes.size()];
-        for (int i = 0; i < mResumeMarker.length; i++) mResumeMarker[i] = (byte) bytes.get(i);
-        mFromSync = c.isFromSync();
-        mContinued = c.isContinued();
-    }
 }
\ No newline at end of file
diff --git a/syncbase/src/main/java/io/v/syncbase/core/CollectionRowPattern.java b/syncbase/src/main/java/io/v/syncbase/core/CollectionRowPattern.java
index 1306b2b..b78b1f5 100644
--- a/syncbase/src/main/java/io/v/syncbase/core/CollectionRowPattern.java
+++ b/syncbase/src/main/java/io/v/syncbase/core/CollectionRowPattern.java
@@ -8,4 +8,13 @@
     public String collectionBlessing;
     public String collectionName;
     public String rowKey;
+
+    public CollectionRowPattern() {
+    }
+
+    public CollectionRowPattern(String collectionBlessing, String collectionName, String rowKey) {
+        this.collectionBlessing = collectionBlessing;
+        this.collectionName = collectionName;
+        this.rowKey = rowKey;
+    }
 }
\ No newline at end of file
diff --git a/syncbase/src/main/java/io/v/syncbase/core/Database.java b/syncbase/src/main/java/io/v/syncbase/core/Database.java
index 21a7600..de536de 100644
--- a/syncbase/src/main/java/io/v/syncbase/core/Database.java
+++ b/syncbase/src/main/java/io/v/syncbase/core/Database.java
@@ -51,9 +51,55 @@
 
     public interface WatchPatternsCallbacks {
         void onChange(WatchChange watchChange);
+
         void onError(VError vError);
     }
 
     public void watch(byte[] resumeMarker, List<CollectionRowPattern> patterns,
-                      WatchPatternsCallbacks callbacks) throws VError {}
+                      final WatchPatternsCallbacks callbacks) {
+        try {
+            io.v.syncbase.internal.Database.WatchPatterns(fullName, resumeMarker, patterns,
+                    new io.v.syncbase.internal.Database.WatchPatternsCallbacks() {
+                        @Override
+                        public void onChange(WatchChange watchChange) {
+                            callbacks.onChange(watchChange);
+                        }
+
+                        @Override
+                        public void onError(VError vError) {
+                            callbacks.onError(vError);
+                        }
+                    });
+        } catch (VError vError) {
+            callbacks.onError(vError);
+        }
+    }
+
+    public interface BatchOperation {
+        void run(BatchDatabase batchDatabase);
+    }
+
+    public void runInBatch(final BatchOperation op, BatchOptions options) throws VError {
+        // TODO(sadovsky): Make the number of attempts configurable.
+        for (int i = 0; i < 3; i++) {
+            BatchDatabase batchDatabase = beginBatch(options);
+            op.run(batchDatabase);
+            // A readonly batch should be Aborted; Commit would fail.
+            if (options.readOnly) {
+                batchDatabase.abort();
+                return;
+            }
+            try {
+                batchDatabase.commit();
+                return;
+            } catch (VError vError) {
+                // TODO(sadovsky): Commit() can fail for a number of reasons, e.g. RPC
+                // failure or ErrConcurrentTransaction. Depending on the cause of failure,
+                // it may be desirable to retry the Commit() and/or to call Abort().
+                if (!vError.id.equals(VError.SYNCBASE_CONCURRENT_BATCH)) {
+                    throw vError;
+                }
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/syncbase/src/main/java/io/v/syncbase/core/Permissions.java b/syncbase/src/main/java/io/v/syncbase/core/Permissions.java
index 690d834..f043075 100644
--- a/syncbase/src/main/java/io/v/syncbase/core/Permissions.java
+++ b/syncbase/src/main/java/io/v/syncbase/core/Permissions.java
@@ -4,6 +4,89 @@
 
 package io.v.syncbase.core;
 
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
 public class Permissions {
+    public static class Tags {
+        public static final String ADMIN = "Admin";
+        public static final String READ = "Read";
+        public static final String RESOLVE = "Resolve";
+        public static final String WRITE = "Write";
+    }
+
+    public static final String IN = "In";
+    public static final String NOT_IN = "NotIn";
+
     public byte[] json;
+
+    public Permissions() {
+    }
+
+    public Permissions(byte[] json) {
+        this.json = json;
+    }
+
+    public Permissions(Map copyFrom) {
+        this.json = new JSONObject(copyFrom).toString().getBytes();
+    }
+
+    /**
+     * Parses the JSON string and returns a map describing the permissions.
+     * <p/>
+     * Example:
+     * <p/>
+     * <pre>
+     * {
+     *      "Admin":{"In":["..."]},
+     *      "Write":{"In":["..."]},
+     *      "Read":{"In":["..."]},
+     *      "Resolve":{"In":["..."]},
+     *      "Debug":{"In":["..."]}
+     * }
+     * </pre>
+     *
+     * @return
+     */
+    public Map<String, Map<String, Set<String>>> parse() {
+        Map<String, Map<String, Set<String>>> permissions = new HashMap<>();
+
+        try {
+            JSONObject jsonObject = new JSONObject(new String(this.json));
+            for (Iterator<String> iter = jsonObject.keys(); iter.hasNext(); ) {
+                String tag = iter.next();
+                permissions.put(tag, parseAccessList(jsonObject.getJSONObject(tag)));
+            }
+        } catch (JSONException e) {
+            // TODO(razvanm): Should we do something else? Logging?
+            throw new RuntimeException("Permissions parsing failure", e);
+        }
+
+        return permissions;
+    }
+
+    private static Map<String, Set<String>> parseAccessList(JSONObject jsonObject)
+            throws JSONException {
+        Map<String, Set<String>> accessList = new HashMap<>();
+        for (Iterator<String> iter = jsonObject.keys(); iter.hasNext(); ) {
+            String type = iter.next();
+            accessList.put(type, parseBlessingPatternList(jsonObject.getJSONArray(type)));
+        }
+        return accessList;
+    }
+
+    private static Set<String> parseBlessingPatternList(JSONArray jsonArray) throws JSONException {
+        Set<String> blessings = new HashSet<>();
+        for (int i = 0; i < jsonArray.length(); i++) {
+            blessings.add(jsonArray.getString(i));
+        }
+        return blessings;
+    }
 }
\ No newline at end of file
diff --git a/syncbase/src/main/java/io/v/syncbase/core/Syncgroup.java b/syncbase/src/main/java/io/v/syncbase/core/Syncgroup.java
index 995efaf..eb63a87 100644
--- a/syncbase/src/main/java/io/v/syncbase/core/Syncgroup.java
+++ b/syncbase/src/main/java/io/v/syncbase/core/Syncgroup.java
@@ -16,6 +16,10 @@
         this.id = id;
     }
 
+    public Id getId() {
+        return id;
+    }
+
     public void create(SyncgroupSpec spec, SyncgroupMemberInfo info) throws VError {
         io.v.syncbase.internal.Database.CreateSyncgroup(dbFullName, id, spec, info);
     }
@@ -47,7 +51,7 @@
         io.v.syncbase.internal.Database.SetSyncgroupSpec(dbFullName, id, spec);
     }
 
-    public Map<String,SyncgroupMemberInfo> getMembers() throws VError {
+    public Map<String, SyncgroupMemberInfo> getMembers() throws VError {
         return io.v.syncbase.internal.Database.GetSyncgroupMembers(dbFullName, id);
     }
 }
diff --git a/syncbase/src/main/java/io/v/syncbase/core/VError.java b/syncbase/src/main/java/io/v/syncbase/core/VError.java
index 6fb54c5..2e80bf7 100644
--- a/syncbase/src/main/java/io/v/syncbase/core/VError.java
+++ b/syncbase/src/main/java/io/v/syncbase/core/VError.java
@@ -4,12 +4,28 @@
 
 package io.v.syncbase.core;
 
+import io.v.v23.verror.VException;
+
 public class VError extends Exception {
+    public static final String EXIST = "v.io/v23/verror.Exist";
+    public static final String NO_EXIST = "v.io/v23/verror.NoExist";
+    public static final String SYNCBASE_CONCURRENT_BATCH = "v.io/v23/services/syncbase.ConcurrentBatch";
+
     public String id;
     public long actionCode;
     public String message;
     public String stack;
 
+    public VError() {
+    }
+
+    public VError(VException e) {
+        this.id = e.getID();
+        this.actionCode = e.getAction().getValue();
+        this.message = e.getMessage();
+        this.stack = e.getStackTrace().toString();
+    }
+
     public String toString() {
         return String.format("{\n  id: \"%s\"\n  actionCode: %d\n  message: \"%s\"\n  stack: \"%s\"}",
                 id, actionCode, message, stack);
diff --git a/syncbase/src/main/java/io/v/syncbase/core/WatchChange.java b/syncbase/src/main/java/io/v/syncbase/core/WatchChange.java
index 4ef08e1..1722c4a 100644
--- a/syncbase/src/main/java/io/v/syncbase/core/WatchChange.java
+++ b/syncbase/src/main/java/io/v/syncbase/core/WatchChange.java
@@ -11,6 +11,7 @@
     public String row;
     public ChangeType changeType;
     public byte[] value;
+    // TODO(razvanm): Switch to byte[].
     public String resumeMarker;
     public boolean fromSync;
     public boolean continued;
diff --git a/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java b/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java
index 8e24228..922eb29 100644
--- a/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java
+++ b/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java
@@ -18,18 +18,15 @@
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 
-import io.v.v23.V;
-import io.v.v23.context.VContext;
-import io.v.v23.rpc.ListenSpec;
+import io.v.syncbase.core.VError;
 
 import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 public class SyncbaseTest {
-    private VContext ctx;
-
     @Rule
     public TemporaryFolder folder = new TemporaryFolder();
 
@@ -38,9 +35,7 @@
     // -Djava.library.path=/Users/sadovsky/vanadium/release/java/syncbase/build/libs
     @Before
     public void setUp() throws Exception {
-        ctx = V.init();
-        ctx = V.withListenSpec(ctx, V.getListenSpec(ctx).withAddress(
-                new ListenSpec.Address("tcp", "localhost:0")));
+        System.loadLibrary("syncbase");
     }
 
     private Syncbase.DatabaseOptions newDatabaseOptions() {
@@ -53,7 +48,6 @@
         }
         opts.disableUserdataSyncgroup = true;
         opts.disableSyncgroupPublishing = true;
-        opts.vContext = ctx;
         return opts;
     }
 
@@ -75,7 +69,7 @@
         return future.get(5, TimeUnit.SECONDS);
     }
 
-    private static Iterable<Id> getCollectionIds(Database db) {
+    private static Iterable<Id> getCollectionIds(Database db) throws VError {
         List<Id> res = new ArrayList<>();
         for (Iterator<Collection> it = db.getCollections(); it.hasNext(); ) {
             res.add(it.next().getId());
@@ -83,7 +77,7 @@
         return res;
     }
 
-    private static Iterable<Id> getSyncgroupIds(Database db) {
+    private static Iterable<Id> getSyncgroupIds(Database db) throws VError {
         List<Id> res = new ArrayList<>();
         for (Iterator<Syncgroup> it = db.getSyncgroups(); it.hasNext(); ) {
             res.add(it.next().getId());
@@ -102,6 +96,7 @@
         DatabaseHandle.CollectionOptions opts = new DatabaseHandle.CollectionOptions();
         opts.withoutSyncgroup = true;
         Collection cxA = db.collection("a", opts);
+        assertNotNull(cxA);
         // TODO(sadovsky): Should we omit the userdata collection?
         assertThat(getCollectionIds(db)).containsExactly(
                 new Id(Syncbase.getPersonalBlessingString(), "a"),
@@ -127,6 +122,7 @@
     public void testRowCrudMethods() throws Exception {
         Database db = createDatabase();
         Collection cx = db.collection("cx");
+        assertNotNull(cx);
         assertFalse(cx.exists("foo"));
         assertEquals(cx.get("foo", String.class), null);
         cx.put("foo", "bar");
@@ -141,18 +137,19 @@
         cx.put("foo", 5);
         assertEquals(cx.get("foo", Integer.class), Integer.valueOf(5));
 
-        // This time, with a POJO.
-        class MyObject {
-            String str;
-            int num;
-        }
-        MyObject putObj = new MyObject();
-        putObj.str = "hello";
-        putObj.num = 7;
-        cx.put("foo", putObj);
-        MyObject getObj = cx.get("foo", MyObject.class);
-        assertEquals(putObj.str, getObj.str);
-        assertEquals(putObj.num, getObj.num);
+        // TODO(razvanm): Figure out a way to get the POJOs to work.
+//        // This time, with a POJO.
+//        class MyObject {
+//            String str;
+//            int num;
+//        }
+//        MyObject putObj = new MyObject();
+//        putObj.str = "hello";
+//        putObj.num = 7;
+//        cx.put("foo", putObj);
+//        MyObject getObj = cx.get("foo", MyObject.class);
+//        assertEquals(putObj.str, getObj.str);
+//        assertEquals(putObj.num, getObj.num);
     }
 
     @Test
@@ -163,6 +160,7 @@
         Collection cxA = db.collection("a", opts);
         Collection cxB = db.collection("b", opts);
         Collection cxC = db.collection("c");
+        assertNotNull(cxA);
         // Note, there's no userdata syncgroup since we set disableUserdataSyncgroup to true.
         assertThat(getSyncgroupIds(db)).containsExactly(
                 new Id(Syncbase.getPersonalBlessingString(), "c"));