swift: Create high-level Syncbase framework

Initial implementation of the Sycnbase high level API and framework.
Builds on top of the SyncbaseCore framework and its CGO library so
no changes yet needed for the jiri swift command. This CL achieves
parity with the current Java high-level API, although it has not
been tested much. Unit tests and bug fixes to come.

Note unit tests are currently not passing due to blessings issues
that are being worked on wrt to init and login.

This CL has following additional changes to the SyncbaseCore
framework to make this high-level work possible:

- Match the HLAPI in terms of configure/init/login, pass rootDir
  to v23_syncbase_Init()
- Remove Threads.swift as its largely unused.
- Implement Database.syncgroup, Database.listSyncgroups,
- Collection.exists(row)
- Make public many things that were incorrectly internal
- Create public initializers for structs
- Add runInBatchSync support function + unit test for Syncbase
  high-level
- Add Exist/NoExist errors to SyncbaseError
- Create a queue public variable to let the user control threads for
  callbacks
- Make ResumeMarker a typealias instead of a struct
- Refactors the OAuth credentials to not force the Google Credential
  in SyncbaseCore, uses the Syncbase.queue for the callback
- Consistently refers to UTF-8 instead of UTF8 in comments and
  error descriptions, matching the HLAPI.
- Make sure callbacks are always on Syncbase.queue and not main.

Change-Id: I82c765d269486a7e6c332f796a6896a0ef107677
diff --git a/Syncbase/Source/AccessList.swift b/Syncbase/Source/AccessList.swift
new file mode 100644
index 0000000..a9158ce
--- /dev/null
+++ b/Syncbase/Source/AccessList.swift
@@ -0,0 +1,140 @@
+// 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.
+
+import Foundation
+import SyncbaseCore
+
+/// Tag is used to associate methods with an AccessList in a Permissions.
+///
+/// While services can define their own tag type and values, many
+/// services should be able to use the type and values defined in
+/// the `Tags` enum.
+public typealias Tag = String
+
+public enum Tags: Tag {
+  /// Operations that require privileged access for object administration.
+  case Admin = "Admin"
+  /// Operations that return debugging information (e.g., logs, statistics etc.) about the object.
+  case Debug = "Debug"
+  /// Operations that do not mutate the state of the object.
+  case Read = "Read"
+  /// Operations that mutate the state of the object.
+  case Write = "Write"
+  /// Operations involving namespace navigation.
+  case Resolve = "Resolve"
+}
+
+/// Specifies access levels for a set of users. Each user has an associated access level: read-only,
+/// read-write, or read-write-admin.
+public struct AccessList {
+  public enum AccessLevel {
+    case READ
+    case READ_WRITE
+    case READ_WRITE_ADMIN
+    // INTERNAL_ONLY_REMOVE is used to remove users from permissions when applying deltas.
+    /// You should never use INTERNAL_ONLY_REMOVE; it is only used internally.
+    case INTERNAL_ONLY_REMOVE
+  }
+
+  public var users: [String: AccessLevel]
+
+  /// Creates an empty access list.
+  public init() {
+    users = [:]
+  }
+
+  static let emptyAccessList = SyncbaseCore.AccessList(allowed: [], notAllowed: [])
+  internal init(perms: Permissions) throws {
+    let resolvers = try (perms[Tags.Resolve.rawValue] ?? AccessList.emptyAccessList).toUserIds(),
+      readers = try (perms[Tags.Read.rawValue] ?? AccessList.emptyAccessList).toUserIds(),
+      writers = try (perms[Tags.Write.rawValue] ?? AccessList.emptyAccessList).toUserIds(),
+      admins = try (perms[Tags.Admin.rawValue] ?? AccessList.emptyAccessList).toUserIds()
+
+    if (readers.isSubsetOf(resolvers)) {
+      throw SyncbaseError.IllegalArgument(detail: "Some readers are not resolvers: \(readers), \(resolvers)")
+    }
+    if (readers.isSubsetOf(writers)) {
+      throw SyncbaseError.IllegalArgument(detail: "Some writers are not readers: \(writers), \(readers)")
+    }
+    if (writers.isSubsetOf(admins)) {
+      throw SyncbaseError.IllegalArgument(detail: "Some admins are not writers: \(admins), \(writers)")
+    }
+
+    users = [:]
+    for userId in readers {
+      users[userId] = AccessLevel.READ
+    }
+    for userId in writers {
+      users[userId] = AccessLevel.READ_WRITE
+    }
+    for userId in admins {
+      users[userId] = AccessLevel.READ_WRITE_ADMIN
+    }
+  }
+
+  /// Applies delta to perms, returning the updates permissions.
+  static func applyDelta(permissions: Permissions, delta: AccessList) -> Permissions {
+    var perms = permissions
+    for (userId, level) in delta.users {
+      let bp = blessingPatternFromEmail(userId)
+      switch level {
+      case .INTERNAL_ONLY_REMOVE:
+        perms[Tags.Resolve.rawValue] = perms[Tags.Resolve.rawValue]?.removeFromAllowed(bp)
+        perms[Tags.Read.rawValue] = perms[Tags.Read.rawValue]?.removeFromAllowed(bp)
+        perms[Tags.Write.rawValue] = perms[Tags.Write.rawValue]?.removeFromAllowed(bp)
+        perms[Tags.Admin.rawValue] = perms[Tags.Admin.rawValue]?.removeFromAllowed(bp)
+      case .READ:
+        perms[Tags.Resolve.rawValue] = perms[Tags.Resolve.rawValue]?.addToAllowed(bp)
+        perms[Tags.Read.rawValue] = perms[Tags.Read.rawValue]?.addToAllowed(bp)
+        perms[Tags.Write.rawValue] = perms[Tags.Write.rawValue]?.removeFromAllowed(bp)
+        perms[Tags.Admin.rawValue] = perms[Tags.Admin.rawValue]?.removeFromAllowed(bp)
+      case .READ_WRITE:
+        perms[Tags.Resolve.rawValue] = perms[Tags.Resolve.rawValue]?.addToAllowed(bp)
+        perms[Tags.Read.rawValue] = perms[Tags.Read.rawValue]?.addToAllowed(bp)
+        perms[Tags.Write.rawValue] = perms[Tags.Write.rawValue]?.addToAllowed(bp)
+        perms[Tags.Admin.rawValue] = perms[Tags.Admin.rawValue]?.removeFromAllowed(bp)
+      case .READ_WRITE_ADMIN:
+        perms[Tags.Resolve.rawValue] = perms[Tags.Resolve.rawValue]?.addToAllowed(bp)
+        perms[Tags.Read.rawValue] = perms[Tags.Read.rawValue]?.addToAllowed(bp)
+        perms[Tags.Write.rawValue] = perms[Tags.Write.rawValue]?.addToAllowed(bp)
+        perms[Tags.Admin.rawValue] = perms[Tags.Admin.rawValue]?.addToAllowed(bp)
+      }
+    }
+    return perms
+  }
+}
+
+extension SyncbaseCore.AccessList {
+  func toUserIds() throws -> Set<String> {
+    if (notAllowed.isEmpty) {
+      throw SyncbaseError.IllegalArgument(detail: "notAllow must be empty")
+    }
+    var res = Set<String>()
+    for bp in allowed {
+      // TODO(sadovsky): Ignore cloud peer's blessing pattern?
+      if let email = emailFromBlessingPattern(bp) {
+        res.insert(email)
+      }
+    }
+    return res
+  }
+
+  func addToAllowed(bp: BlessingPattern) -> SyncbaseCore.AccessList {
+    if (!allowed.contains(bp)) {
+      var a = allowed
+      a.append(bp)
+      return SyncbaseCore.AccessList(allowed: a, notAllowed: notAllowed)
+    }
+    return self
+  }
+
+  func removeFromAllowed(bp: BlessingPattern) -> SyncbaseCore.AccessList {
+    if let idx = allowed.indexOf(bp) {
+      var a = allowed
+      a.removeAtIndex(idx)
+      return SyncbaseCore.AccessList(allowed: a, notAllowed: notAllowed)
+    }
+    return self
+  }
+}
\ No newline at end of file
diff --git a/Syncbase/Source/BatchDatabase.swift b/Syncbase/Source/BatchDatabase.swift
new file mode 100644
index 0000000..43a652d
--- /dev/null
+++ b/Syncbase/Source/BatchDatabase.swift
@@ -0,0 +1,68 @@
+// 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.
+
+import Foundation
+import SyncbaseCore
+
+public typealias BatchOperation = BatchDatabase throws -> Void
+
+/// Provides a way to perform a set of operations atomically on a database. See
+/// `Database.beginBatch` for concurrency semantics.
+public class BatchDatabase: DatabaseHandle {
+  private let coreBatchDatabase: SyncbaseCore.BatchDatabase
+
+  init(coreBatchDatabase: SyncbaseCore.BatchDatabase) {
+    self.coreBatchDatabase = coreBatchDatabase
+  }
+
+  public var databaseId: Identifier {
+    return Identifier(coreId: coreBatchDatabase.databaseId)
+  }
+
+  public func collection(name: String, withoutSyncgroup: Bool = false) throws -> Collection {
+    if (!withoutSyncgroup) {
+      throw SyncbaseError.BatchError(detail: "Cannot create syncgroup in a batch")
+    }
+    let res = try collection(Identifier(name: name, blessing: personalBlessingString()))
+    try res.createIfMissing()
+    return res
+  }
+
+  public func collection(collectionId: Identifier) throws -> Collection {
+    // 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 try SyncbaseError.wrap {
+      return try Collection(
+        coreCollection: self.coreBatchDatabase.collection(collectionId.toCore()),
+        databaseHandle: self)
+    }
+  }
+
+  /// Returns all collections in the database.
+  public func collections() throws -> [Collection] {
+    return try SyncbaseError.wrap {
+      let coreIds = try self.coreBatchDatabase.listCollections()
+      return try coreIds.map { coreId in
+        return Collection(
+          coreCollection: try self.coreBatchDatabase.collection(coreId),
+          databaseHandle: self)
+      }
+    }
+  }
+
+  /// Persists the pending changes to Syncbase. If the batch is read-only, `commit` will
+  /// throw `ConcurrentBatchException`; abort should be used instead.
+  public func commit() throws {
+    // TODO(sadovsky): Throw ConcurrentBatchException where appropriate.
+    try SyncbaseError.wrap { try self.coreBatchDatabase.commit() }
+  }
+
+  /// Notifies Syncbase that any pending changes can be discarded. Calling `abort` is not
+  /// strictly required, but may allow Syncbase to release locks or other resources sooner than if
+  /// `abort` was not called.
+  public func abort() throws {
+    try SyncbaseError.wrap { try self.coreBatchDatabase.abort() }
+  }
+}
diff --git a/Syncbase/Source/Blessing.swift b/Syncbase/Source/Blessing.swift
new file mode 100644
index 0000000..024f6f9
--- /dev/null
+++ b/Syncbase/Source/Blessing.swift
@@ -0,0 +1,63 @@
+// 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.
+
+import Foundation
+import SyncbaseCore
+
+// TODO(zinman): This whole file needs to get updated with a consistent strategy for all cgo
+// bridges. In general this code should be moved to go.
+
+func emailFromBlessingPattern(pattern: BlessingPattern) -> String? {
+  return emailFromBlessingString(pattern)
+}
+
+private func emailFromBlessingString(blessingStr: String) -> String? {
+  guard blessingStr.containsString(":") else {
+    return nil
+  }
+  let parts = blessingStr.componentsSeparatedByString(":")
+  return parts[parts.count - 1]
+}
+
+private func blessingStringFromEmail(email: String) -> String {
+  return Syncbase.defaultBlessingStringPrefix + email
+}
+
+func blessingPatternFromEmail(email: String) -> BlessingPattern {
+  return BlessingPattern(blessingStringFromEmail(email))
+}
+
+func personalBlessingString() throws -> String {
+  return try SyncbaseCore.Principal.userBlessing()
+}
+
+func selfAndCloudAL() throws -> SyncbaseCore.AccessList {
+  return SyncbaseCore.AccessList(allowed:
+      [BlessingPattern(try personalBlessingString()), BlessingPattern(Syncbase.cloudBlessing)])
+}
+
+func defaultDatabasePerms() throws -> SyncbaseCore.Permissions {
+  let anyone = SyncbaseCore.AccessList(allowed: [BlessingPattern("...")])
+  let selfAndCloud = try selfAndCloudAL()
+  return [
+    Tags.Resolve.rawValue: anyone,
+    Tags.Read.rawValue: selfAndCloud,
+    Tags.Write.rawValue: selfAndCloud,
+    Tags.Admin.rawValue: selfAndCloud]
+}
+
+func defaultCollectionPerms() throws -> SyncbaseCore.Permissions {
+  let selfAndCloud = try selfAndCloudAL()
+  return [
+    Tags.Read.rawValue: selfAndCloud,
+    Tags.Write.rawValue: selfAndCloud,
+    Tags.Admin.rawValue: selfAndCloud]
+}
+
+func defaultSyncbasePerms() throws -> SyncbaseCore.Permissions {
+  let selfAndCloud = try selfAndCloudAL()
+  return [
+    Tags.Read.rawValue: selfAndCloud,
+    Tags.Admin.rawValue: selfAndCloud]
+}
diff --git a/Syncbase/Source/Collection.swift b/Syncbase/Source/Collection.swift
new file mode 100644
index 0000000..96f496f
--- /dev/null
+++ b/Syncbase/Source/Collection.swift
@@ -0,0 +1,101 @@
+// 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.
+
+import Foundation
+import SyncbaseCore
+
+/// Represents an ordered set of key-value pairs.
+/// To get a Collection handle, call `Database.collection`.
+public class Collection {
+  let coreCollection: SyncbaseCore.Collection
+  let databaseHandle: DatabaseHandle
+
+  func createIfMissing() throws {
+    do {
+      try SyncbaseError.wrap {
+        try self.coreCollection.create(defaultCollectionPerms())
+      }
+    } catch SyncbaseError.Exist {
+      // Collection already exists.
+    }
+  }
+
+  init(coreCollection: SyncbaseCore.Collection, databaseHandle: DatabaseHandle) {
+    self.coreCollection = coreCollection
+    self.databaseHandle = databaseHandle
+  }
+
+  /// Returns the id of this collection.
+  public var collectionId: Identifier {
+    return Identifier(coreId: coreCollection.collectionId)
+  }
+
+  /// Shortcut for `Database.getSyncgroup(collection.collectionId)`, helpful for the common case
+  /// of one syncgroup per collection.
+  public func syncgroup() throws -> Syncgroup {
+    switch databaseHandle {
+    case is BatchDatabase: throw SyncbaseError.BatchError(detail: "Must not call getSyncgroup within batch")
+    case let h as Database: return h.syncgroup(collectionId)
+    default: fatalError("Unexpected type")
+    }
+  }
+
+  /// Returns the value associated with `key`.
+  public func get<T: SyncbaseCore.SyncbaseConvertible>(key: String) throws -> T? {
+    return try SyncbaseError.wrap {
+      let value: T? = try self.coreCollection.get(key)
+      return value
+    }
+  }
+
+  /// Returns true if there is a value associated with `key`.
+  public func exists(key: String) throws -> Bool {
+    return try SyncbaseError.wrap {
+      return try self.coreCollection.exists(key)
+    }
+  }
+
+  /// Puts `value` for `key`, overwriting any existing value. This call is idempotent, meaning
+  /// you can safely call it multiple times in a row with the same values.
+  public func put<T: SyncbaseCore.SyncbaseConvertible>(key: String, value: T) throws {
+    try SyncbaseError.wrap {
+      try self.coreCollection.put(key, value: value)
+    }
+  }
+
+  /// Deletes the value associated with `key`. If the row does not exist for the associated key,
+  /// this call is a no-op. That is to say, `delete` is idempotent.
+  public func delete(key: String) throws {
+    try SyncbaseError.wrap {
+      try self.coreCollection.delete(key)
+    }
+  }
+
+  /// **FOR ADVANCED USERS**. Returns the `AccessList` for this collection. Users should
+  /// typically manipulate access lists via `collection.syncgroup()`.
+  public func accessList() throws -> AccessList {
+    return try SyncbaseError.wrap {
+      return try AccessList(perms: try self.coreCollection.getPermissions())
+    }
+  }
+
+  /// **FOR ADVANCED USERS**. Updates the `AccessList` for this collection. Users should
+  /// typically manipulate access lists via `collection.syncgroup()`}.
+  public func updateAccessList(delta: AccessList) throws {
+    let op = { (db: BatchDatabase) in
+      try SyncbaseError.wrap {
+        let vCx = try db.collection(self.collectionId).coreCollection
+        let perms = try vCx.getPermissions()
+        AccessList.applyDelta(perms, delta: delta)
+        try vCx.setPermissions(perms)
+      }
+    }
+    // Create a batch if we're not already in a batch.
+    if let batchDb = databaseHandle as? BatchDatabase {
+      try op(batchDb)
+    } else {
+      try (databaseHandle as! Database).runInBatch(op: op)
+    }
+  }
+}
diff --git a/Syncbase/Source/Database.swift b/Syncbase/Source/Database.swift
new file mode 100644
index 0000000..9f6436f
--- /dev/null
+++ b/Syncbase/Source/Database.swift
@@ -0,0 +1,407 @@
+// 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.
+
+import Foundation
+import SyncbaseCore
+
+// Counter to allow SyncgroupInviteHandler and WatchChangeHandler structs to be unique and hashable.
+var uniqueIdCounter: Int32 = 0
+
+/// Handles discovered syncgroup invites.
+public struct SyncgroupInviteHandler: Hashable, Equatable {
+  /// Called when a syncgroup invitation is discovered. Clients typically handle invites by
+  /// calling `acceptSyncgroupInvite` or `ignoreSyncgroupInvite`.
+  public let onInvite: SyncgroupInvite -> Void
+
+  /// Called when an error occurs while scanning for syncgroup invitations. Once
+  /// `onError` is called, no other methods will be called on this handler.
+  public let onError: ErrorType -> Void
+
+  // This internal-only variable allows us to test SyncgroupInviteHandler structs for equality.
+  // This cannot be done otherwise as function calls cannot be tested for equality.
+  // Equality is important to facilitate the add/remove APIs in Database where
+  // handlers are directly passed for removal instead of other indirect identifier.
+  let uniqueId = OSAtomicIncrement32(&uniqueIdCounter)
+
+  public var hashValue: Int {
+    return uniqueId.hashValue
+  }
+}
+
+public func == (lhs: SyncgroupInviteHandler, rhs: SyncgroupInviteHandler) -> Bool {
+  return lhs.uniqueId == rhs.uniqueId
+}
+
+/// Handles observed changes to the database.
+public struct WatchChangeHandler: Hashable, Equatable {
+  /// Called once, when a watch change handler is added, to provide the initial state of the
+  /// values being watched.
+  public let onInitialState: [WatchChange] -> Void
+
+  /// Called whenever a batch of changes is committed to the database. Individual puts/deletes
+  /// surface as a single-change batch.
+  public let onChangeBatch: [WatchChange] -> Void
+
+  /// Called when an error occurs while watching for changes. Once `onError` is called,
+  /// no other methods will be called on this handler.
+  public let onError: ErrorType -> Void
+
+  public init(
+    onInitialState: [WatchChange] -> Void,
+    onChangeBatch: [WatchChange] -> Void,
+    onError: ErrorType -> Void) {
+      self.onInitialState = onInitialState
+      self.onChangeBatch = onChangeBatch
+      self.onError = onError
+  }
+
+  // This internal-only variable allows us to test WatchChangeHandler structs for equality.
+  // This cannot be done otherwise as function calls cannot be tested for equality.
+  // Equality is important to facilitate the add/remove APIs in Database where
+  // handlers are directly passed for removal instead of other indirect identifier.
+  let uniqueId = OSAtomicIncrement32(&uniqueIdCounter)
+
+  public var hashValue: Int {
+    return uniqueId.hashValue
+  }
+}
+
+public func == (lhs: WatchChangeHandler, rhs: WatchChangeHandler) -> Bool {
+  return lhs.uniqueId == rhs.uniqueId
+}
+
+struct HandlerOperation {
+  let databaseId: SyncbaseCore.Identifier
+  let queue: dispatch_queue_t
+  let cancel: Void -> Void
+}
+
+/// A set of collections and syncgroups.
+/// To get a Database handle, call `Syncbase.database`.
+public class Database: DatabaseHandle {
+  let coreDatabase: SyncbaseCore.Database
+
+  // These are all static because we might have active handlers to a database while it goes
+  // out of scope (e.g. the user gets the database, then adds a watch handler, but doesn't retain
+  // the original reference to Database). Instead, we store the databaseId to make sure that any two
+  // Database instances generated from Syncbase.database behave the same with respect to
+  // adding/removing watch handlers.
+  private static let syncgroupInviteHandlersMu = NSLock()
+  private static let watchChangeHandlersMu = NSLock()
+  private static var syncgroupInviteHandlers: [SyncgroupInviteHandler: HandlerOperation] = [:]
+  private static var watchChangeHandlers: [WatchChangeHandler: HandlerOperation] = [:]
+
+  func createIfMissing() throws {
+    do {
+      try SyncbaseError.wrap {
+        try self.coreDatabase.create(try defaultDatabasePerms())
+      }
+    } catch SyncbaseError.Exist {
+      // Database already exists, presumably from a previous run of the app.
+    }
+  }
+
+  init(coreDatabase: SyncbaseCore.Database) {
+    self.coreDatabase = coreDatabase
+  }
+
+  public var databaseId: Identifier {
+    return Identifier(coreId: coreDatabase.databaseId)
+  }
+
+  public func collection(name: String, withoutSyncgroup: Bool = false) throws -> Collection {
+    let res = try collection(Identifier(name: name, blessing: personalBlessingString()))
+    try res.createIfMissing()
+    // TODO(sadovsky): Unwind collection creation on syncgroup creation failure? It would be
+    // nice if we could create the collection and syncgroup in a batch.
+    if (!withoutSyncgroup) {
+      try syncgroup(name, collections: [res])
+    }
+    return res
+  }
+
+  public func collection(collectionId: Identifier) throws -> Collection {
+    // 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 try SyncbaseError.wrap {
+      return try Collection(
+        coreCollection: self.coreDatabase.collection(collectionId.toCore()),
+        databaseHandle: self)
+    }
+  }
+
+  /// Returns all collections in the database.
+  public func collections() throws -> [Collection] {
+    return try SyncbaseError.wrap {
+      let coreIds = try self.coreDatabase.listCollections()
+      return try coreIds.map { coreId in
+        return Collection(coreCollection: try self.coreDatabase.collection(coreId), databaseHandle: self)
+      }
+    }
+  }
+
+  /// **FOR ADVANCED USERS**. Creates syncgroup and adds it to the user's "userdata" collection, as
+  /// needed. Idempotent. The id of the new syncgroup will include the creator's user id and the
+  /// given syncgroup name. Requires that all collections were created by the current user.
+  ///
+  /// - parameter name:        Name of the syncgroup.
+  /// - parameter collections: Collections in the syncgroup.
+  ///
+  /// - returns: The created syncgroup.
+  public func syncgroup(name: String, collections: [Collection]) throws -> Syncgroup {
+    if (collections.isEmpty) {
+      throw SyncbaseError.IllegalArgument(detail: "No collections specified")
+    }
+    let id = Identifier(name: name, blessing: collections[0].collectionId.blessing)
+    for cx in collections {
+      if (cx.collectionId.blessing != id.blessing) {
+        throw SyncbaseError.BlessingError(detail: "Collections must all have the same creator")
+      }
+    }
+    return try SyncbaseError.wrap {
+      let res = Syncgroup(
+        coreSyncgroup: self.coreDatabase.syncgroup(id.toCore()),
+        database: self)
+      try res.createIfMissing(collections)
+      return res
+    }
+  }
+
+  /// Returns the syncgroup with the given id.
+  public func syncgroup(syncgroupId: Identifier) -> Syncgroup {
+    // 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 Syncgroup(
+      coreSyncgroup: coreDatabase.syncgroup(syncgroupId.toCore()),
+      database: self)
+  }
+
+  /// Returns all syncgroups in the database.
+  public func syncgroups() throws -> [Syncgroup] {
+    return try SyncbaseError.wrap {
+      let coreIds = try self.coreDatabase.listSyncgroups()
+      return coreIds.map { coreId in
+        return Syncgroup(coreSyncgroup: self.coreDatabase.syncgroup(coreId), database: self)
+      }
+    }
+  }
+
+  /// Notifies `handler` of any existing syncgroup invites, and of all subsequent new invites.
+  public func addSyncgroupInviteHandler(handler: SyncgroupInviteHandler) {
+    Database.syncgroupInviteHandlersMu.lock()
+    defer { Database.syncgroupInviteHandlersMu.unlock() }
+    preconditionFailure("Not implemented")
+  }
+
+  /// Makes it so `handler` stops receiving notifications.
+  public func removeSyncgroupInviteHandler(handler: SyncgroupInviteHandler) {
+    Database.syncgroupInviteHandlersMu.lock()
+    let op = Database.syncgroupInviteHandlers[handler]
+    Database.syncgroupInviteHandlersMu.unlock()
+    if let op = op {
+      op.cancel()
+    }
+  }
+
+  /// Makes it so all syncgroup invite handlers stop receiving notifications.
+  public func removeAllSyncgroupInviteHandlers() {
+    // Grab a local copy by value so we don't need to worry about concurrency or deadlocking
+    // on the main mutex.
+    Database.syncgroupInviteHandlersMu.lock()
+    let handlers = Database.syncgroupInviteHandlers
+    Database.syncgroupInviteHandlersMu.unlock()
+    for op in handlers.values {
+      if op.databaseId == coreDatabase.databaseId {
+        op.cancel()
+      }
+    }
+  }
+
+  public typealias AcceptSyncgroupInviteCallback = (sg: Syncgroup?, err: ErrorType?) -> Void
+
+  /// Joins the syncgroup associated with the given invite and adds it to the user's "userdata"
+  /// collection, as needed.
+  ///
+  /// - parameter invite: The syncgroup invite.
+  /// - parameter callback: The callback run on Syncbase.`queue` with either the accepted Syncgroup
+  /// or an error.
+  public func acceptSyncgroupInvite(invite: SyncgroupInvite, callback: AcceptSyncgroupInviteCallback) {
+    dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
+      do {
+        try SyncbaseError.wrap {
+          let coreSyncgroup = self.coreDatabase.syncgroup(invite.syncgroupId.toCore())
+          let syncgroup = Syncgroup(coreSyncgroup: coreSyncgroup, database: self)
+          try coreSyncgroup.join(invite.remoteSyncbaseName,
+            expectedSyncbaseBlessings: invite.expectedSyncbaseBlessings,
+            myInfo: Syncgroup.syncgroupMemberInfo)
+          dispatch_async(Syncbase.queue) {
+            callback(sg: syncgroup, err: nil)
+          }
+        }
+      } catch let e {
+        dispatch_async(Syncbase.queue) {
+          callback(sg: nil, err: e)
+        }
+      }
+    }
+  }
+
+  /// Records that the user has ignored this invite, such that it's never surfaced again.
+  public func ignoreSyncgroupInvite(invite: SyncgroupInvite) {
+    preconditionFailure("Not implemented")
+  }
+
+  /// Runs the given operation in a batch, managing retries and commit/abort. Writable batches are
+  /// committed, retrying if commit fails due to a concurrent batch. Read-only batches are aborted.
+  ///
+  /// - parameter readOnly: Run batch in read-only mode.
+  /// - parameter op:       The operation to run.
+  public func runInBatch(readOnly: Bool = false, op: BatchOperation) throws {
+    // TODO(zinman): Is this suppose to be async?
+    try SyncbaseError.wrap {
+      try SyncbaseCore.Batch.runInBatchSync(
+        db: self.coreDatabase,
+        opts: SyncbaseCore.BatchOptions(readOnly: readOnly),
+        op: { coreDb in try op(BatchDatabase(coreBatchDatabase: coreDb)) })
+    }
+  }
+
+  /// Creates a new batch. Instead of calling this function directly, clients are encouraged to use
+  /// the `runInBatch` helper function, which detects "concurrent batch" errors and handles
+  /// retries internally.
+  ///
+  /// Default concurrency semantics:
+  /// - Reads (e.g. gets, scans) inside a batch operate over a consistent snapshot taken during
+  /// `beginBatch`, and will see the effects of prior writes performed inside the batch.
+  /// - `commit` may fail with `ConcurrentBatch` exception, indicating that after
+  /// `beginBatch` but before `commit`, some concurrent routine wrote to a key that
+  /// matches a key or row-range read inside this batch.
+  /// - Other methods will never fail with error `ConcurrentBatch`, even if it is
+  /// known that `commit` will fail with this error.
+  ///
+  /// Once a batch has been committed or aborted, subsequent method calls will fail with no
+  /// effect.
+  ///
+  /// Concurrency semantics can be configured using the optional parameters.
+  ///
+  /// - parameter readOnly: If true, set the transaction to read-only.
+  ///
+  /// - returns: The BatchDatabase object representing the transaction.
+  public func beginBatch(readOnly: Bool = false) throws -> BatchDatabase {
+    return try SyncbaseError.wrap {
+      return BatchDatabase(coreBatchDatabase:
+          try self.coreDatabase.beginBatch(BatchOptions(readOnly: readOnly)))
+    }
+  }
+
+  /// Notifies `handler` of initial state, and of all subsequent changes to this database. If this
+  /// handler is canceled by `removeWatchChangeHandler` then no subsequent calls will be made. Note
+  /// that there may be WatchChanges queued up for a `OnChangeBatch` that will be ignored.
+  /// Callbacks to `handler` occur on Syncbase.queue, which defaults to main.
+  public func addWatchChangeHandler(resumeMarker: ResumeMarker? = nil, handler: WatchChangeHandler) throws {
+    // Note: Eventually we'll add a watch variant that takes a query, where the query can be
+    // constructed using some sort of query builder API.
+    // TODO(sadovsky): Support specifying resumeMarker. Note, watch-from-resumeMarker may be
+    // problematic in that we don't track the governing ACL for changes in the watch log.
+    if let resumeMarker = resumeMarker where resumeMarker.length != 0 {
+      throw SyncbaseError.IllegalArgument(detail: "Specifying resumeMarker is not yet supported")
+    }
+    return try SyncbaseError.wrap {
+      let stream = try self.coreDatabase.watch(
+        [CollectionRowPattern(collectionName: "%", collectionBlessing: "%", rowKey: "%")],
+        resumeMarker: resumeMarker)
+      // Create a serial queue to immediately run this watch operation on via SyncbaseCore's
+      // blocking stream.
+      let watchQueue = dispatch_queue_create(
+        "Syncbase WatchChangeHandler \(handler.uniqueId)",
+        DISPATCH_QUEUE_SERIAL)
+      var isCanceled = false
+      Database.watchChangeHandlersMu.lock()
+      defer { Database.watchChangeHandlersMu.unlock() }
+      Database.watchChangeHandlers[handler] = HandlerOperation(
+        databaseId: self.coreDatabase.databaseId,
+        queue: watchQueue,
+        cancel: {
+          isCanceled = true
+          stream.cancel()
+      })
+      dispatch_async(watchQueue) {
+        var gotFirstBatch = false
+        var batch = [WatchChange]()
+        for coreChange in stream {
+          if isCanceled {
+            break
+          }
+          let change = WatchChange(coreChange: coreChange)
+          // Ignore changes to userdata collection.
+          if (change.collectionId.name == Syncbase.USERDATA_SYNCGROUP_NAME) {
+            continue
+          }
+          batch.append(change)
+          if (!change.isContinued) {
+            if (!gotFirstBatch) {
+              gotFirstBatch = true
+              let b = batch
+              // We synchronously run on Syncbase.queue to facilitate flow control. Go blocks
+              // until each callback is consumed before it calls with another WatchChange event.
+              // Backpressure in Swift is achieved by blocking until the WatchChange event is
+              // consumed by the app on Syncbase.queue using dispatch_sync. If we used
+              // dispatch_async, we could potentially queue up events faster than the
+              // ability for the app to consume them. By using dispatch_sync we also help the user
+              // mitigate against out-of-order events should Syncbase.queue be a concurrent queue
+              // rather than a serial queue.
+              dispatch_sync(Syncbase.queue, {
+                handler.onInitialState(b)
+              })
+            } else {
+              dispatch_sync(Syncbase.queue, {
+                handler.onChangeBatch(batch)
+              })
+            }
+            batch.removeAll()
+          }
+        }
+        // Notify of error if we're permitted (not canceled).
+        if let err = stream.err() where !isCanceled {
+          dispatch_sync(Syncbase.queue, {
+            handler.onError(err)
+          })
+        }
+        // Cleanup
+        Database.watchChangeHandlersMu.lock()
+        defer { Database.watchChangeHandlersMu.unlock() }
+        Database.watchChangeHandlers[handler] = nil
+      }
+    }
+  }
+
+  /// Makes it so `handler` stops receiving notifications. Note there may be queued WatchChanges
+  /// queued up for a `OnChangeBatch` that will be ignored.
+  public func removeWatchChangeHandler(handler: WatchChangeHandler) {
+    Database.watchChangeHandlersMu.lock()
+    let op = Database.watchChangeHandlers[handler]
+    Database.watchChangeHandlersMu.unlock()
+    if let op = op {
+      op.cancel()
+    }
+  }
+
+  /// Makes it so all watch change handlers stop receiving notifications attached to this database.
+  public func removeAllWatchChangeHandlers() {
+    // Grab a local copy by value so we don't need to worry about concurrency or deadlocking
+    // on the main mutex.
+    Database.watchChangeHandlersMu.lock()
+    let handlers = Database.watchChangeHandlers
+    Database.watchChangeHandlersMu.unlock()
+    for op in handlers.values {
+      // Because Database.watchChangeHandlers is static, we must make sure the database ids
+      // are the same before we cancel it.
+      if op.databaseId == self.coreDatabase.databaseId {
+        op.cancel()
+      }
+    }
+  }
+}
diff --git a/Syncbase/Source/DatabaseHandle.swift b/Syncbase/Source/DatabaseHandle.swift
new file mode 100644
index 0000000..d869735
--- /dev/null
+++ b/Syncbase/Source/DatabaseHandle.swift
@@ -0,0 +1,32 @@
+// 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.
+
+import Foundation
+import SyncbaseCore
+
+/// Represents a handle to a database, possibly in a batch.
+public protocol DatabaseHandle {
+  /// The id of this database.
+  var databaseId: Identifier { get }
+
+  /// Creates a collection and an associated syncgroup, as needed. Idempotent. The id of the new
+  /// collection will include the creator's user id and the given collection name. Upon creation,
+  /// both the collection and syncgroup are `READ_WRITE` for the creator. Setting
+  /// `opts.withoutSyncgroup` prevents syncgroup creation. May only be called within a batch
+  /// if `opts.withoutSyncgroup` is set.
+  ///
+  /// - parameter name: Name of the collection.
+  /// - parameter withoutSyncgroup: If true, don't create an associated syncgroup. Defaults to false.
+  ///
+  /// - throws: SyncbaseError on unicode errors, or if there was a problem creating the database.
+  ///
+  /// - returns: The collection handle.
+  func collection(name: String, withoutSyncgroup: Bool) throws -> Collection
+
+  /// Returns the collection with the given id.
+  func collection(collectionId: Identifier) throws -> Collection
+
+  /// Returns all collections in the database.
+  func collections() throws -> [Collection]
+}
diff --git a/Syncbase/Source/Error.swift b/Syncbase/Source/Error.swift
new file mode 100644
index 0000000..94216a9
--- /dev/null
+++ b/Syncbase/Source/Error.swift
@@ -0,0 +1,102 @@
+// 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.
+
+import Foundation
+import SyncbaseCore
+
+public enum SyncbaseError: ErrorType {
+  case AlreadyConfigured
+  case NotConfigured
+  case NotLoggedIn
+  case IllegalArgument(detail: String)
+  case BatchError(detail: String)
+  case BlessingError(detail: String)
+  case UnknownError(err: ErrorType)
+  // From SyncbaseCore
+  case NotAuthorized
+  case NotInDevMode
+  case UnknownBatch
+  case NotBoundToBatch
+  case ReadOnlyBatch
+  case ConcurrentBatch
+  case BlobNotCommitted
+  case SyncgroupJoinFailed
+  case BadExecStreamHeader
+  case InvalidPermissionsChange
+  case Exist
+  case NoExist
+  case InvalidName(name: String)
+  case CorruptDatabase(path: String)
+  case InvalidOperation(reason: String)
+  case InvalidUTF8(invalidUtf8: String)
+  case CastError(obj: Any)
+
+  init?(coreError: SyncbaseCore.SyncbaseError) {
+    switch coreError {
+    case .AlreadyConfigured: self = .AlreadyConfigured
+    case .NotConfigured: self = .NotConfigured
+    case .NotLoggedIn: self = .NotLoggedIn
+    case .NotAuthorized: self = .NotAuthorized
+    case .NotInDevMode: self = .NotInDevMode
+    case .UnknownBatch: self = .UnknownBatch
+    case .NotBoundToBatch: self = .NotBoundToBatch
+    case .ReadOnlyBatch: self = .ReadOnlyBatch
+    case .ConcurrentBatch: self = .ConcurrentBatch
+    case .BlobNotCommitted: self = .BlobNotCommitted
+    case .SyncgroupJoinFailed: self = .SyncgroupJoinFailed
+    case .BadExecStreamHeader: self = .BadExecStreamHeader
+    case .InvalidPermissionsChange: self = .InvalidPermissionsChange
+    case .Exist: self = .Exist
+    case .NoExist: self = .NoExist
+    case .InvalidName(let name): self = .InvalidName(name: name)
+    case .CorruptDatabase(let path): self = .CorruptDatabase(path: path)
+    case .InvalidOperation(let reason): self = .InvalidOperation(reason: reason)
+    case .InvalidUTF8(let invalidUtf8): self = .InvalidUTF8(invalidUtf8: invalidUtf8)
+    case .CastError(let obj): self = .CastError(obj: obj)
+    }
+  }
+
+  static func wrap<T>(block: Void throws -> T) throws -> T {
+    do {
+      return try block()
+    } catch let e as SyncbaseCore.SyncbaseError {
+      throw SyncbaseError(coreError: e)!
+    } catch let e {
+      throw SyncbaseError.UnknownError(err: e)
+    }
+  }
+}
+
+extension SyncbaseError: CustomStringConvertible {
+  public var description: String {
+    switch self {
+    case .IllegalArgument(let detail): return "Illegal argument: \(detail)"
+    case .BatchError(let detail): return "Batch error: \(detail)"
+    case .BlessingError(let detail): return "Blessing error: \(detail)"
+    case .UnknownError(let err): return "Unknown error: \(err)"
+      // From SyncbaseCore
+    case .AlreadyConfigured: return "Already configured"
+    case .NotConfigured: return "Not configured (via Syncbase.configure)"
+    case .NotLoggedIn: return "Not logged in (via Syncbase.login)"
+    case .NotAuthorized: return "No valid blessings; create new blessings using oauth"
+    case .NotInDevMode: return "Not running with --dev=true"
+    case .UnknownBatch: return "Unknown batch, perhaps the server restarted"
+    case .NotBoundToBatch: return "Not bound to batch"
+    case .ReadOnlyBatch: return "Batch is read-only"
+    case .ConcurrentBatch: return "Concurrent batch"
+    case .BlobNotCommitted: return "Blob is not yet committed"
+    case .SyncgroupJoinFailed: return "Syncgroup join failed"
+    case .BadExecStreamHeader: return "Exec stream header improperly formatted"
+    case .InvalidPermissionsChange: return "The sequence of permission changes is invalid"
+    case .Exist: return "Already exists"
+    case .NoExist: return "Does not exist"
+    case .InvalidName(let name): return "Invalid name: \(name)"
+    case .CorruptDatabase(let path):
+      return "Database corrupt, moved to path \(path); client must create a new database"
+    case .InvalidOperation(let reason): return "Invalid operation: \(reason)"
+    case .InvalidUTF8(let invalidUtf8): return "Unable to convert to utf8: \(invalidUtf8)"
+    case .CastError(let obj): return "Unable to convert to cast: \(obj)"
+    }
+  }
+}
diff --git a/Syncbase/Source/Id.swift b/Syncbase/Source/Id.swift
new file mode 100644
index 0000000..8d173c7
--- /dev/null
+++ b/Syncbase/Source/Id.swift
@@ -0,0 +1,63 @@
+// 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.
+
+import Foundation
+import SyncbaseCore
+
+/// Uniquely identifies a database, collection, or syncgroup.
+public struct Identifier: Hashable {
+  public let name: String
+  public let blessing: String
+
+  public init(name: String, blessing: String) {
+    self.name = name
+    self.blessing = blessing
+  }
+
+  init(coreId: SyncbaseCore.Identifier) {
+    self.name = coreId.name
+    self.blessing = coreId.blessing
+  }
+
+  public func encode() throws -> String {
+    var cStr = v23_syncbase_String()
+    let id = try v23_syncbase_Id(toCore())
+    v23_syncbase_EncodeId(id, &cStr)
+    // If there was a UTF-8 problem, it would have been thrown when UTF-8 encoding the id above.
+    // Therefore, we can be confident in unwrapping the conditional here.
+    return cStr.toString()!
+  }
+
+  // TODO(zinman): Replace decode method implementations with call to Cgo.
+  static let separator = ","
+  public static func decode(encodedId: String) throws -> Identifier {
+    let parts = encodedId.componentsSeparatedByString(separator)
+    if parts.count != 2 {
+      throw SyncbaseError.IllegalArgument(detail: "Invalid encoded id: \(encodedId)")
+    }
+    let (blessing, name) = (parts[0], parts[1])
+    return Identifier(name: name, blessing: blessing)
+  }
+
+  public var hashValue: Int {
+    // Note: Copied from VDL.
+    var result = 1
+    let prime = 31
+    result = prime * result + blessing.hashValue
+    result = prime * result + name.hashValue
+    return result
+  }
+
+  public var description: String {
+    return "Id(\(try? encode() ?? "<UTF-8 ERROR>"))"
+  }
+
+  func toCore() -> SyncbaseCore.Identifier {
+    return SyncbaseCore.Identifier(name: name, blessing: blessing)
+  }
+}
+
+public func == (d1: Identifier, d2: Identifier) -> Bool {
+  return d1.blessing == d2.blessing && d1.name == d2.name
+}
diff --git a/Syncbase/Source/Info.plist b/Syncbase/Source/Info.plist
new file mode 100644
index 0000000..d3de8ee
--- /dev/null
+++ b/Syncbase/Source/Info.plist
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>FMWK</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>$(CURRENT_PROJECT_VERSION)</string>
+	<key>NSPrincipalClass</key>
+	<string></string>
+</dict>
+</plist>
diff --git a/Syncbase/Source/OAuth.swift b/Syncbase/Source/OAuth.swift
new file mode 100644
index 0000000..799c23a
--- /dev/null
+++ b/Syncbase/Source/OAuth.swift
@@ -0,0 +1,29 @@
+// 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.
+
+import Foundation
+
+/// The provider of the oauth token, such as Google.
+public enum OAuthProvider: String {
+  /// Currently, Google is the only supported provider.
+  case Google = "google"
+}
+
+/// Represents a valid OAuth token obtained from an OAuth provider such as Google.
+public protocol OAuthCredentials {
+  /// The oauth provider, e.g. OAuthProvider.Google.
+  var provider: OAuthProvider { get }
+  /// The oauth token just received from the provider.
+  var token: String { get }
+}
+
+/// Shortcut for OAuthCredentials provided by Google.
+public struct GoogleOAuthCredentials: OAuthCredentials {
+  public let token: String
+  public let provider = OAuthProvider.Google
+
+  public init(token: String) {
+    self.token = token
+  }
+}
diff --git a/Syncbase/Source/Syncbase.h b/Syncbase/Source/Syncbase.h
new file mode 100644
index 0000000..09628aa
--- /dev/null
+++ b/Syncbase/Source/Syncbase.h
@@ -0,0 +1,11 @@
+// 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.
+
+#import <Foundation/Foundation.h>
+
+//! Project version number for Syncbase.
+FOUNDATION_EXPORT double SyncbaseVersionNumber;
+
+//! Project version string for Syncbase.
+FOUNDATION_EXPORT const unsigned char SyncbaseVersionString[];
\ No newline at end of file
diff --git a/Syncbase/Source/Syncbase.swift b/Syncbase/Source/Syncbase.swift
new file mode 100644
index 0000000..2ec27d4
--- /dev/null
+++ b/Syncbase/Source/Syncbase.swift
@@ -0,0 +1,192 @@
+// 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.
+
+import Foundation
+import SyncbaseCore
+
+/// Syncbase is a storage system for developers that makes it easy to synchronize app data between
+/// devices. It works even when devices are not connected to the Internet.
+public enum Syncbase {
+  // Constants
+  static let TAG = "syncbase",
+    DIR_NAME = "syncbase",
+    DB_NAME = "db",
+    USERDATA_SYNCGROUP_NAME = "userdata__"
+  // Initialization state
+  static var didCreateOrJoin = false
+  static var didInit = false
+  // Main database.
+  static var db: Database?
+  // Options for opening a database.
+  static var adminUserId = "alexfandrianto@google.com"
+  static var defaultBlessingStringPrefix = "dev.v.io:o:608941808256-43vtfndets79kf5hac8ieujto8837660.apps.googleusercontent.com:"
+  static var disableSyncgroupPublishing = false
+  static var disableUserdataSyncgroup = false
+  static var mountPoints = ["/ns.dev.v.io:8101/tmp/todos/users/"]
+  static var rootDir = Syncbase.defaultRootDir
+  /// Queue used to dispatch all asynchronous callbacks. Defaults to main.
+  public static var queue: dispatch_queue_t = dispatch_get_main_queue()
+
+  static public var defaultRootDir: String {
+    return NSFileManager.defaultManager()
+      .URLsForDirectory(.ApplicationSupportDirectory, inDomains: .UserDomainMask)[0]
+      .URLByAppendingPathComponent("Syncbase")
+      .absoluteString
+  }
+
+  static var publishSyncbaseName: String? {
+    if Syncbase.disableSyncgroupPublishing {
+      return nil
+    }
+    return mountPoints[0] + "cloud"
+  }
+
+  static var cloudBlessing: String {
+    return "dev.v.io:u:" + Syncbase.adminUserId
+  }
+
+  /// Starts Syncbase if needed; creates default database if needed; performs create-or-join for
+  /// "userdata" syncgroup if needed.
+  ///
+  /// The "userdata" collection is a per-user collection (and associated syncgroup) for data that
+  /// should automatically get synced across a given user's devices. It has the following schema:
+  /// - `/syncgroups/{encodedSyncgroupId}` -> `nil`
+  /// - `/ignoredInvites/{encodedSyncgroupId}` -> `nil`
+  ///
+  /// - parameter adminUserId:                 The email address for the administrator user.
+  /// - parameter rootDir:                     Where data should be persisted.
+  /// - parameter mountPoints:                 // TODO(zinman): Get appropriate documentation on mountPoints
+  /// - parameter defaultBlessingStringPrefix: // TODO(zinman): Figure out what this should default to.
+  /// - parameter disableSyncgroupPublishing:  **FOR ADVANCED USERS**. If true, syncgroups will not be published to the cloud peer.
+  /// - parameter disableUserdataSyncgroup:    **FOR ADVANCED USERS**. If true, the user's data will not be synced across their devices.
+  /// - parameter callback:                    Called on `Syncbase.queue` with either `Database` if successful, or an error if unsuccessful.
+  public static func configure(
+    adminUserId adminUserId: String,
+    // Default to Application Support/Syncbase.
+    rootDir: String = NSFileManager.defaultManager()
+      .URLsForDirectory(.ApplicationSupportDirectory, inDomains: .UserDomainMask)[0]
+      .URLByAppendingPathComponent("Syncbase")
+      .absoluteString,
+    mountPoints: [String] = ["/ns.dev.v.io:8101/tmp/todos/users/"],
+    defaultBlessingStringPrefix: String = "dev.v.io:o:608941808256-43vtfndets79kf5hac8ieujto8837660.apps.googleusercontent.com:",
+    disableSyncgroupPublishing: Bool = false,
+    disableUserdataSyncgroup: Bool = false,
+    queue: dispatch_queue_t = dispatch_get_main_queue()) throws {
+      if didInit {
+        throw SyncbaseError.AlreadyConfigured
+      }
+      Syncbase.adminUserId = adminUserId
+      Syncbase.rootDir = rootDir
+      Syncbase.mountPoints = mountPoints
+      Syncbase.defaultBlessingStringPrefix = defaultBlessingStringPrefix
+      Syncbase.disableSyncgroupPublishing = disableSyncgroupPublishing
+      Syncbase.disableUserdataSyncgroup = disableUserdataSyncgroup
+
+      // TODO(zinman): Reconfigure this logic once we have CL #23295 merged.
+      let database = try Syncbase.startSyncbaseAndInitDatabase()
+      if (Syncbase.disableUserdataSyncgroup) {
+        try database.collection(Syncbase.USERDATA_SYNCGROUP_NAME, withoutSyncgroup: true)
+      } else {
+        // This gets deferred to login as it's blocking. Once we've logged in we don't need it
+        // anyway.
+        didCreateOrJoin = false
+        // FIXME(zinman): Implement create-or-join (and watch) of userdata syncgroup.
+        throw SyncbaseError.IllegalArgument(detail: "Synced userdata collection is not yet supported")
+      }
+      Syncbase.db = database
+      Syncbase.didInit = true
+  }
+
+  private static func startSyncbaseAndInitDatabase() throws -> Database {
+    if Syncbase.rootDir == "" {
+      throw SyncbaseError.IllegalArgument(detail: "Missing rootDir")
+    }
+    if !NSFileManager.defaultManager().fileExistsAtPath(Syncbase.rootDir) {
+      try NSFileManager.defaultManager().createDirectoryAtPath(
+        Syncbase.rootDir,
+        withIntermediateDirectories: true,
+        attributes: [NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication])
+    }
+    return try SyncbaseError.wrap {
+      // TODO(zinman): Verify we should be using the user's blessing (by not explicitly passing
+      // the blessings in an Identifier).
+      try SyncbaseCore.Syncbase.configure(rootDir: Syncbase.rootDir, queue: Syncbase.queue)
+      let coreDb = try SyncbaseCore.Syncbase.database(Syncbase.DB_NAME)
+      let res = Database(coreDatabase: coreDb)
+      try res.createIfMissing()
+      return res
+    }
+  }
+
+  /// Returns the shared database handle. Must have already called `configure` and be logged in,
+  /// otherwise this will throw a `SyncbaseError.NotConfigured` or `SyncbaseError.NotLoggedIn`
+  /// error.
+  public static func database() throws -> Database {
+    guard let db = Syncbase.db where Syncbase.didInit else {
+      throw SyncbaseError.NotConfigured
+    }
+    if !SyncbaseCore.Syncbase.isLoggedIn {
+      throw SyncbaseError.NotLoggedIn
+    }
+    if !Syncbase.disableUserdataSyncgroup && !Syncbase.didCreateOrJoin {
+      // Create-or-join of userdata syncgroup occurs in login. We must have failed between
+      // login() and the create-or-join call.
+      throw SyncbaseError.NotLoggedIn
+    }
+    return db
+  }
+
+  public typealias LoginCallback = (err: ErrorType?) -> Void
+
+  /// Authorize using an oauth token. Right now only Google OAuth token is supported
+  /// (you should use the Google Sign In SDK to get this).
+  ///
+  /// You must login and have valid credentials before you can call `database()` to perform any
+  /// operation.
+  ///
+  /// Calls `callback` on `Syncbase.queue` with any error that occured, or nil on success.
+  public static func login(credentials: GoogleOAuthCredentials, callback: LoginCallback) {
+    SyncbaseCore.Syncbase.login(
+      SyncbaseCore.GoogleOAuthCredentials(token: credentials.token),
+      callback: { err in
+        guard err != nil else {
+          if let e = err as? SyncbaseCore.SyncbaseError {
+            callback(err: SyncbaseError(coreError: e))
+          } else {
+            callback(err: err)
+          }
+          return
+        }
+        if Syncbase.disableUserdataSyncgroup {
+          // Success
+          dispatch_async(Syncbase.queue) {
+            callback(err: nil)
+          }
+        } else {
+          dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
+            callback(err: SyncbaseError.IllegalArgument(detail:
+                "Synced userdata collection is not yet supported"))
+            return
+            // FIXME(zinman): Implement create-or-join (and watch) of userdata syncgroup.
+            // Syncbase.didCreateOrJoin = true
+            // dispatch_async(Syncbase.queue) {
+            // callback(nil)
+            // }
+          }
+        }
+    })
+  }
+
+  public static func isLoggedIn() throws -> Bool {
+    if !Syncbase.didInit {
+      throw SyncbaseError.NotConfigured
+    }
+    if !Syncbase.disableUserdataSyncgroup && !Syncbase.didCreateOrJoin {
+      // Create-or-join of userdata syncgroup occurs in login. We must have failed between
+      // login() and the create-or-join call.
+      throw SyncbaseError.NotLoggedIn
+    }
+    return SyncbaseCore.Syncbase.isLoggedIn
+  }
+}
diff --git a/Syncbase/Source/Syncgroup.swift b/Syncbase/Source/Syncgroup.swift
new file mode 100644
index 0000000..7645945
--- /dev/null
+++ b/Syncbase/Source/Syncgroup.swift
@@ -0,0 +1,128 @@
+// 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.
+
+import Foundation
+import SyncbaseCore
+
+/// Represents a set of collections, synced amongst a set of users.
+/// To get a Syncgroup handle, call `Database.syncgroup`.
+public class Syncgroup {
+  let database: Database
+  let coreSyncgroup: SyncbaseCore.Syncgroup
+
+  static var syncgroupMemberInfo: SyncgroupMemberInfo {
+    // TODO(zinman): Validate these are correct.
+    return SyncgroupMemberInfo(syncPriority: UInt8(3), blobDevType: BlobDevType.BlobDevTypeLeaf)
+  }
+
+  func createIfMissing(collections: [Collection]) throws {
+    let cxCoreIds = collections.map { $0.collectionId.toCore() }
+    let spec = SyncgroupSpec(
+      description: "",
+      collections: cxCoreIds,
+      permissions: try defaultSyncbasePerms(),
+      publishSyncbaseName: Syncbase.publishSyncbaseName,
+      mountTables: Syncbase.mountPoints,
+      isPrivate: false)
+    do {
+      try SyncbaseError.wrap {
+        try self.coreSyncgroup.create(spec, myInfo: Syncgroup.syncgroupMemberInfo)
+      }
+    } catch SyncbaseError.Exist {
+      // Syncgroup already exists.
+      // TODO(sadovsky): Verify that the existing syncgroup has the specified configuration,
+      // e.g. the specified collections?
+    }
+  }
+
+  init(coreSyncgroup: SyncbaseCore.Syncgroup, database: Database) {
+    self.coreSyncgroup = coreSyncgroup
+    self.database = database
+  }
+
+  /// Returns the id of this syncgroup.
+  public var syncbaseId: Identifier {
+    return Identifier(coreId: coreSyncgroup.syncgroupId)
+  }
+
+  /// Returns the `AccessList` for this syncgroup.
+  public func accessList() throws -> AccessList {
+    return try AccessList(perms: try coreSyncgroup.getSpec().spec.permissions)
+  }
+
+  /// **FOR ADVANCED USERS**. Adds the given users to the syncgroup, with the specified access level.
+  ///
+  /// - parameter users:          Users to add to the syncgroup.
+  /// - parameter level:          Access level for the specified `users`.
+  /// - parameter syncgroupOnly:  If false (the default), update the `AccessList` for the syncgroup
+  /// and its associated collections. If true, only update the `AccessList` for the syncgroup.
+  public func inviteUsers(users: [User], level: AccessList.AccessLevel, syncgroupOnly: Bool = false) throws {
+    var delta = AccessList()
+    for user in users {
+      delta.users[user.userId] = level
+    }
+    try updateAccessList(delta, syncgroupOnly: syncgroupOnly)
+  }
+
+  /// Adds the given user to the syncgroup, with the specified access level.
+  ///
+  /// - parameter user:           User to add to the syncgroup.
+  /// - parameter level:          Access level for the specified `user`.
+  /// - parameter syncgroupOnly:  If false (the default), update the `AccessList` for the syncgroup
+  /// and its associated collections. If true, only update the `AccessList` for the syncgroup.
+  public func inviteUser(user: User, level: AccessList.AccessLevel, syncgroupOnly: Bool = false) throws {
+    try inviteUsers([user], level: level, syncgroupOnly: syncgroupOnly)
+  }
+
+  /// **FOR ADVANCED USERS**. Removes the given users from the syncgroup.
+  ///
+  /// - parameter users:          Users to eject from the Syncgroup.
+  /// - parameter syncgroupOnly:  If false (the default), update the `AccessList` for the syncgroup
+  /// and its associated collections. If true, only update the `AccessList` for the syncgroup.
+  public func ejectUsers(users: [User], syncgroupOnly: Bool = false) throws {
+    var delta = AccessList()
+    for user in users {
+      delta.users[user.userId] = AccessList.AccessLevel.INTERNAL_ONLY_REMOVE
+    }
+    try updateAccessList(delta, syncgroupOnly: syncgroupOnly)
+  }
+
+  /// Removes the given user from the syncgroup.
+  ///
+  /// - parameter user:           User to eject from the Syncgroup.
+  /// - parameter syncgroupOnly:  If false (the default), update the `AccessList` for the syncgroup
+  /// and its associated collections. If true, only update the `AccessList` for the syncgroup.
+  public func ejectUser(user: User, syncgroupOnly: Bool = false) throws {
+    try ejectUsers([user], syncgroupOnly: syncgroupOnly)
+  }
+
+  /// **FOR ADVANCED USERS**. Applies `delta` to the `AccessList`.
+  ///
+  /// - parameter delta:          AccessList changes to the Syncgroup.
+  /// - parameter syncgroupOnly:  If false (the default), update the `AccessList` for the syncgroup
+  /// and its associated collections. If true, only update the `AccessList` for the syncgroup.
+  public func updateAccessList(delta: AccessList, syncgroupOnly: Bool = false) throws {
+    try SyncbaseError.wrap {
+      // TODO(sadovsky): Make it so SyncgroupSpec can be updated as part of a batch?
+      let versionedSpec = try self.coreSyncgroup.getSpec()
+      let permissions = AccessList.applyDelta(versionedSpec.spec.permissions, delta: delta)
+      let oldSpec = versionedSpec.spec
+      try self.coreSyncgroup.setSpec(VersionedSpec(
+        spec: SyncgroupSpec(description: oldSpec.description,
+          collections: oldSpec.collections,
+          permissions: permissions,
+          publishSyncbaseName: oldSpec.publishSyncbaseName,
+          mountTables: oldSpec.mountTables,
+          isPrivate: oldSpec.isPrivate),
+        version: versionedSpec.version))
+      // TODO(sadovsky): There's a race here - it's possible for a collection to get destroyed
+      // after spec.getCollections() but before db.getCollection().
+      try self.database.runInBatch { db in
+        for id in oldSpec.collections {
+          try db.collection(Identifier(coreId: id)).updateAccessList(delta)
+        }
+      }
+    }
+  }
+}
diff --git a/Syncbase/Source/SyncgroupInvite.swift b/Syncbase/Source/SyncgroupInvite.swift
new file mode 100644
index 0000000..42c04f6
--- /dev/null
+++ b/Syncbase/Source/SyncgroupInvite.swift
@@ -0,0 +1,19 @@
+// 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.
+
+import Foundation
+import SyncbaseCore
+
+/// Represents an invitation to join a syncgroup.
+public struct SyncgroupInvite {
+  public let syncgroupId: Identifier
+  public let remoteSyncbaseName: String
+  public let expectedSyncbaseBlessings: [String]
+
+  public init(syncgroupId: Identifier, remoteSyncbaseName: String, expectedSyncbaseBlessings: [String]) {
+    self.syncgroupId = syncgroupId
+    self.remoteSyncbaseName = remoteSyncbaseName
+    self.expectedSyncbaseBlessings = expectedSyncbaseBlessings
+  }
+}
\ No newline at end of file
diff --git a/Syncbase/Source/User.swift b/Syncbase/Source/User.swift
new file mode 100644
index 0000000..140fc8f
--- /dev/null
+++ b/Syncbase/Source/User.swift
@@ -0,0 +1,12 @@
+// 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.
+
+import Foundation
+import SyncbaseCore
+
+/// Represents a user.
+public struct User {
+  // TODO(zinman): Fill this in.
+  public let userId: String
+}
diff --git a/Syncbase/Source/WatchChange.swift b/Syncbase/Source/WatchChange.swift
new file mode 100644
index 0000000..7c84418
--- /dev/null
+++ b/Syncbase/Source/WatchChange.swift
@@ -0,0 +1,61 @@
+// 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.
+
+import Foundation
+import SyncbaseCore
+
+/// ResumeMarker provides a compact representation of all the messages that
+/// have been received by the caller for the given Watch call. It is not
+/// something that you would ever generate; it is always provided to you
+/// in a WatchChange.
+public typealias ResumeMarker = NSData
+
+/// Describes a change to a database.
+public class WatchChange {
+  public enum ChangeType: Int {
+    case Put
+    case Delete
+  }
+
+  /// Collection is the id of the collection that contains the changed row.
+  public let collectionId: Identifier
+
+  /// Row is the key of the changed row.
+  public let row: String
+
+  /// ChangeType describes the type of the change. If ChangeType is PutChange,
+  /// then the row exists in the collection, and Value can be called to obtain
+  /// the new value for this row. If ChangeType is DeleteChange, then the row was
+  /// removed from the collection.
+  public let changeType: ChangeType
+
+  /// value is the new value for the row if the ChangeType is PutChange, or nil
+  /// otherwise.
+  public let value: NSData?
+
+  /// ResumeMarker provides a compact representation of all the messages that
+  /// have been received by the caller for the given Watch call.
+  /// This marker can be provided in the Request message to allow the caller
+  /// to resume the stream watching at a specific point without fetching the
+  /// initial state.
+  public let resumeMarker: ResumeMarker
+
+  /// FromSync indicates whether the change came from sync. If FromSync is false,
+  /// then the change originated from the local device.
+  public let isFromSync: Bool
+
+  /// If true, this WatchChange is followed by more WatchChanges that are in the
+  /// same batch as this WatchChange.
+  public let isContinued: Bool
+
+  init(coreChange: SyncbaseCore.WatchChange) {
+    self.collectionId = Identifier(coreId: coreChange.collectionId)
+    self.row = coreChange.row
+    self.changeType = ChangeType(rawValue: coreChange.changeType.rawValue)!
+    self.value = coreChange.value
+    self.resumeMarker = coreChange.resumeMarker
+    self.isFromSync = coreChange.isFromSync
+    self.isContinued = coreChange.isContinued
+  }
+}
\ No newline at end of file
diff --git a/Syncbase/Syncbase.xcodeproj/project.pbxproj b/Syncbase/Syncbase.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..64dbfc9
--- /dev/null
+++ b/Syncbase/Syncbase.xcodeproj/project.pbxproj
@@ -0,0 +1,512 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 46;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		9374F67F1D010711004ECE59 /* Syncbase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9374F6741D010711004ECE59 /* Syncbase.framework */; };
+		9374F6921D01074B004ECE59 /* Syncbase.h in Headers */ = {isa = PBXBuildFile; fileRef = 9374F6901D01074B004ECE59 /* Syncbase.h */; settings = {ATTRIBUTES = (Public, ); }; };
+		9374F6AD1D01081D004ECE59 /* AccessList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9374F6A21D01081D004ECE59 /* AccessList.swift */; };
+		9374F6AE1D01081D004ECE59 /* BatchDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9374F6A31D01081D004ECE59 /* BatchDatabase.swift */; };
+		9374F6AF1D01081D004ECE59 /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9374F6A41D01081D004ECE59 /* Collection.swift */; };
+		9374F6B01D01081D004ECE59 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9374F6A51D01081D004ECE59 /* Database.swift */; };
+		9374F6B11D01081D004ECE59 /* DatabaseHandle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9374F6A61D01081D004ECE59 /* DatabaseHandle.swift */; };
+		9374F6B21D01081D004ECE59 /* Id.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9374F6A71D01081D004ECE59 /* Id.swift */; };
+		9374F6B31D01081D004ECE59 /* Syncbase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9374F6A81D01081D004ECE59 /* Syncbase.swift */; };
+		9374F6B41D01081D004ECE59 /* Syncgroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9374F6A91D01081D004ECE59 /* Syncgroup.swift */; };
+		9374F6B51D01081D004ECE59 /* SyncgroupInvite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9374F6AA1D01081D004ECE59 /* SyncgroupInvite.swift */; };
+		9374F6B61D01081D004ECE59 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9374F6AB1D01081D004ECE59 /* User.swift */; };
+		9374F6B71D01081D004ECE59 /* WatchChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9374F6AC1D01081D004ECE59 /* WatchChange.swift */; };
+		938DEA861D0BAE57003C9734 /* Blessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 938DEA851D0BAE57003C9734 /* Blessing.swift */; };
+		938DEA891D0F3FD4003C9734 /* BasicDatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 938DEA871D0F3FC7003C9734 /* BasicDatabaseTests.swift */; };
+		938DEA8B1D107078003C9734 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 938DEA8A1D107078003C9734 /* TestHelpers.swift */; };
+		938DEA9F1D12257E003C9734 /* OAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 938DEA9E1D12257E003C9734 /* OAuth.swift */; };
+		93D90C4D1D080E18004A8E72 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93D90C4C1D080E18004A8E72 /* Error.swift */; };
+		93D90C591D08BD6E004A8E72 /* SyncbaseCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 93D90C561D08BD62004A8E72 /* SyncbaseCore.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+		9374F6801D010711004ECE59 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 9374F66B1D010711004ECE59 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 9374F6731D010711004ECE59;
+			remoteInfo = Syncbase;
+		};
+		93D90C551D08BD62004A8E72 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 93D90C501D08BD61004A8E72 /* SyncbaseCore.xcodeproj */;
+			proxyType = 2;
+			remoteGlobalIDString = 30AD2DFF1CDD506700A28A0C;
+			remoteInfo = SyncbaseCore;
+		};
+		93D90C571D08BD62004A8E72 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 93D90C501D08BD61004A8E72 /* SyncbaseCore.xcodeproj */;
+			proxyType = 2;
+			remoteGlobalIDString = 30AD2E091CDD506700A28A0C;
+			remoteInfo = SyncbaseCoreTests;
+		};
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXFileReference section */
+		9374F6741D010711004ECE59 /* Syncbase.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Syncbase.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		9374F67E1D010711004ECE59 /* SyncbaseTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SyncbaseTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		9374F68F1D01074B004ECE59 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		9374F6901D01074B004ECE59 /* Syncbase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Syncbase.h; sourceTree = "<group>"; };
+		9374F6941D010751004ECE59 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		9374F6A21D01081D004ECE59 /* AccessList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessList.swift; sourceTree = "<group>"; };
+		9374F6A31D01081D004ECE59 /* BatchDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatchDatabase.swift; sourceTree = "<group>"; };
+		9374F6A41D01081D004ECE59 /* Collection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Collection.swift; sourceTree = "<group>"; };
+		9374F6A51D01081D004ECE59 /* Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = "<group>"; };
+		9374F6A61D01081D004ECE59 /* DatabaseHandle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseHandle.swift; sourceTree = "<group>"; };
+		9374F6A71D01081D004ECE59 /* Id.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Id.swift; sourceTree = "<group>"; };
+		9374F6A81D01081D004ECE59 /* Syncbase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Syncbase.swift; sourceTree = "<group>"; };
+		9374F6A91D01081D004ECE59 /* Syncgroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Syncgroup.swift; sourceTree = "<group>"; };
+		9374F6AA1D01081D004ECE59 /* SyncgroupInvite.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncgroupInvite.swift; sourceTree = "<group>"; };
+		9374F6AB1D01081D004ECE59 /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
+		9374F6AC1D01081D004ECE59 /* WatchChange.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchChange.swift; sourceTree = "<group>"; };
+		938DEA851D0BAE57003C9734 /* Blessing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Blessing.swift; sourceTree = "<group>"; };
+		938DEA871D0F3FC7003C9734 /* BasicDatabaseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasicDatabaseTests.swift; sourceTree = "<group>"; };
+		938DEA8A1D107078003C9734 /* TestHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = "<group>"; };
+		938DEA9E1D12257E003C9734 /* OAuth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth.swift; sourceTree = "<group>"; };
+		93D90C4C1D080E18004A8E72 /* Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = "<group>"; };
+		93D90C501D08BD61004A8E72 /* SyncbaseCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = SyncbaseCore.xcodeproj; path = ../SyncbaseCore/SyncbaseCore.xcodeproj; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		9374F6701D010711004ECE59 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				93D90C591D08BD6E004A8E72 /* SyncbaseCore.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		9374F67B1D010711004ECE59 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				9374F67F1D010711004ECE59 /* Syncbase.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		9374F66A1D010711004ECE59 = {
+			isa = PBXGroup;
+			children = (
+				93D90C501D08BD61004A8E72 /* SyncbaseCore.xcodeproj */,
+				9374F68E1D01074B004ECE59 /* Source */,
+				9374F6931D010751004ECE59 /* Tests */,
+				9374F6751D010711004ECE59 /* Products */,
+			);
+			sourceTree = "<group>";
+		};
+		9374F6751D010711004ECE59 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				9374F6741D010711004ECE59 /* Syncbase.framework */,
+				9374F67E1D010711004ECE59 /* SyncbaseTests.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		9374F68E1D01074B004ECE59 /* Source */ = {
+			isa = PBXGroup;
+			children = (
+				9374F6901D01074B004ECE59 /* Syncbase.h */,
+				9374F68F1D01074B004ECE59 /* Info.plist */,
+				9374F6A21D01081D004ECE59 /* AccessList.swift */,
+				9374F6A31D01081D004ECE59 /* BatchDatabase.swift */,
+				938DEA851D0BAE57003C9734 /* Blessing.swift */,
+				9374F6A41D01081D004ECE59 /* Collection.swift */,
+				9374F6A51D01081D004ECE59 /* Database.swift */,
+				9374F6A61D01081D004ECE59 /* DatabaseHandle.swift */,
+				93D90C4C1D080E18004A8E72 /* Error.swift */,
+				9374F6A71D01081D004ECE59 /* Id.swift */,
+				938DEA9E1D12257E003C9734 /* OAuth.swift */,
+				9374F6A81D01081D004ECE59 /* Syncbase.swift */,
+				9374F6A91D01081D004ECE59 /* Syncgroup.swift */,
+				9374F6AA1D01081D004ECE59 /* SyncgroupInvite.swift */,
+				9374F6AB1D01081D004ECE59 /* User.swift */,
+				9374F6AC1D01081D004ECE59 /* WatchChange.swift */,
+			);
+			path = Source;
+			sourceTree = "<group>";
+		};
+		9374F6931D010751004ECE59 /* Tests */ = {
+			isa = PBXGroup;
+			children = (
+				9374F6941D010751004ECE59 /* Info.plist */,
+				938DEA871D0F3FC7003C9734 /* BasicDatabaseTests.swift */,
+				938DEA8A1D107078003C9734 /* TestHelpers.swift */,
+			);
+			path = Tests;
+			sourceTree = "<group>";
+		};
+		93D90C511D08BD61004A8E72 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				93D90C561D08BD62004A8E72 /* SyncbaseCore.framework */,
+				93D90C581D08BD62004A8E72 /* SyncbaseCoreTests.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXHeadersBuildPhase section */
+		9374F6711D010711004ECE59 /* Headers */ = {
+			isa = PBXHeadersBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				9374F6921D01074B004ECE59 /* Syncbase.h in Headers */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXHeadersBuildPhase section */
+
+/* Begin PBXNativeTarget section */
+		9374F6731D010711004ECE59 /* Syncbase */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 9374F6881D010711004ECE59 /* Build configuration list for PBXNativeTarget "Syncbase" */;
+			buildPhases = (
+				9374F66F1D010711004ECE59 /* Sources */,
+				9374F6701D010711004ECE59 /* Frameworks */,
+				9374F6711D010711004ECE59 /* Headers */,
+				9374F6721D010711004ECE59 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = Syncbase;
+			productName = Syncbase;
+			productReference = 9374F6741D010711004ECE59 /* Syncbase.framework */;
+			productType = "com.apple.product-type.framework";
+		};
+		9374F67D1D010711004ECE59 /* SyncbaseTests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 9374F68B1D010711004ECE59 /* Build configuration list for PBXNativeTarget "SyncbaseTests" */;
+			buildPhases = (
+				9374F67A1D010711004ECE59 /* Sources */,
+				9374F67B1D010711004ECE59 /* Frameworks */,
+				9374F67C1D010711004ECE59 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				9374F6811D010711004ECE59 /* PBXTargetDependency */,
+			);
+			name = SyncbaseTests;
+			productName = SyncbaseTests;
+			productReference = 9374F67E1D010711004ECE59 /* SyncbaseTests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		9374F66B1D010711004ECE59 /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				LastSwiftUpdateCheck = 0730;
+				LastUpgradeCheck = 0730;
+				ORGANIZATIONNAME = "Google, Inc.";
+				TargetAttributes = {
+					9374F6731D010711004ECE59 = {
+						CreatedOnToolsVersion = 7.3.1;
+					};
+					9374F67D1D010711004ECE59 = {
+						CreatedOnToolsVersion = 7.3.1;
+					};
+				};
+			};
+			buildConfigurationList = 9374F66E1D010711004ECE59 /* Build configuration list for PBXProject "Syncbase" */;
+			compatibilityVersion = "Xcode 3.2";
+			developmentRegion = English;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+			);
+			mainGroup = 9374F66A1D010711004ECE59;
+			productRefGroup = 9374F6751D010711004ECE59 /* Products */;
+			projectDirPath = "";
+			projectReferences = (
+				{
+					ProductGroup = 93D90C511D08BD61004A8E72 /* Products */;
+					ProjectRef = 93D90C501D08BD61004A8E72 /* SyncbaseCore.xcodeproj */;
+				},
+			);
+			projectRoot = "";
+			targets = (
+				9374F6731D010711004ECE59 /* Syncbase */,
+				9374F67D1D010711004ECE59 /* SyncbaseTests */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXReferenceProxy section */
+		93D90C561D08BD62004A8E72 /* SyncbaseCore.framework */ = {
+			isa = PBXReferenceProxy;
+			fileType = wrapper.framework;
+			path = SyncbaseCore.framework;
+			remoteRef = 93D90C551D08BD62004A8E72 /* PBXContainerItemProxy */;
+			sourceTree = BUILT_PRODUCTS_DIR;
+		};
+		93D90C581D08BD62004A8E72 /* SyncbaseCoreTests.xctest */ = {
+			isa = PBXReferenceProxy;
+			fileType = wrapper.cfbundle;
+			path = SyncbaseCoreTests.xctest;
+			remoteRef = 93D90C571D08BD62004A8E72 /* PBXContainerItemProxy */;
+			sourceTree = BUILT_PRODUCTS_DIR;
+		};
+/* End PBXReferenceProxy section */
+
+/* Begin PBXResourcesBuildPhase section */
+		9374F6721D010711004ECE59 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		9374F67C1D010711004ECE59 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		9374F66F1D010711004ECE59 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				9374F6B71D01081D004ECE59 /* WatchChange.swift in Sources */,
+				938DEA9F1D12257E003C9734 /* OAuth.swift in Sources */,
+				9374F6AF1D01081D004ECE59 /* Collection.swift in Sources */,
+				9374F6B11D01081D004ECE59 /* DatabaseHandle.swift in Sources */,
+				9374F6B21D01081D004ECE59 /* Id.swift in Sources */,
+				93D90C4D1D080E18004A8E72 /* Error.swift in Sources */,
+				938DEA861D0BAE57003C9734 /* Blessing.swift in Sources */,
+				9374F6B51D01081D004ECE59 /* SyncgroupInvite.swift in Sources */,
+				9374F6AD1D01081D004ECE59 /* AccessList.swift in Sources */,
+				9374F6B41D01081D004ECE59 /* Syncgroup.swift in Sources */,
+				9374F6B31D01081D004ECE59 /* Syncbase.swift in Sources */,
+				9374F6B01D01081D004ECE59 /* Database.swift in Sources */,
+				9374F6B61D01081D004ECE59 /* User.swift in Sources */,
+				9374F6AE1D01081D004ECE59 /* BatchDatabase.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		9374F67A1D010711004ECE59 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				938DEA891D0F3FD4003C9734 /* BasicDatabaseTests.swift in Sources */,
+				938DEA8B1D107078003C9734 /* TestHelpers.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+		9374F6811D010711004ECE59 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 9374F6731D010711004ECE59 /* Syncbase */;
+			targetProxy = 9374F6801D010711004ECE59 /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
+/* Begin XCBuildConfiguration section */
+		9374F6861D010711004ECE59 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				CURRENT_PROJECT_VERSION = 1;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_BITCODE = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+				MTL_ENABLE_DEBUG_INFO = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALID_ARCHS = arm64;
+				VERSIONING_SYSTEM = "apple-generic";
+				VERSION_INFO_PREFIX = "";
+			};
+			name = Debug;
+		};
+		9374F6871D010711004ECE59 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				CURRENT_PROJECT_VERSION = 1;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_BITCODE = NO;
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+				VALID_ARCHS = arm64;
+				VERSIONING_SYSTEM = "apple-generic";
+				VERSION_INFO_PREFIX = "";
+			};
+			name = Release;
+		};
+		9374F6891D010711004ECE59 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CLANG_ENABLE_MODULES = YES;
+				DEFINES_MODULE = YES;
+				DYLIB_COMPATIBILITY_VERSION = 1;
+				DYLIB_CURRENT_VERSION = 1;
+				DYLIB_INSTALL_NAME_BASE = "@rpath";
+				INFOPLIST_FILE = Source/Info.plist;
+				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = io.v.Syncbase;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SKIP_INSTALL = YES;
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+			};
+			name = Debug;
+		};
+		9374F68A1D010711004ECE59 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CLANG_ENABLE_MODULES = YES;
+				DEFINES_MODULE = YES;
+				DYLIB_COMPATIBILITY_VERSION = 1;
+				DYLIB_CURRENT_VERSION = 1;
+				DYLIB_INSTALL_NAME_BASE = "@rpath";
+				INFOPLIST_FILE = Source/Info.plist;
+				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = io.v.Syncbase;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SKIP_INSTALL = YES;
+			};
+			name = Release;
+		};
+		9374F68C1D010711004ECE59 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				INFOPLIST_FILE = Tests/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = io.v.SyncbaseTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			name = Debug;
+		};
+		9374F68D1D010711004ECE59 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				INFOPLIST_FILE = Tests/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = io.v.SyncbaseTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		9374F66E1D010711004ECE59 /* Build configuration list for PBXProject "Syncbase" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				9374F6861D010711004ECE59 /* Debug */,
+				9374F6871D010711004ECE59 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		9374F6881D010711004ECE59 /* Build configuration list for PBXNativeTarget "Syncbase" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				9374F6891D010711004ECE59 /* Debug */,
+				9374F68A1D010711004ECE59 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		9374F68B1D010711004ECE59 /* Build configuration list for PBXNativeTarget "SyncbaseTests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				9374F68C1D010711004ECE59 /* Debug */,
+				9374F68D1D010711004ECE59 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 9374F66B1D010711004ECE59 /* Project object */;
+}
diff --git a/Syncbase/Syncbase.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Syncbase/Syncbase.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..74dcd88
--- /dev/null
+++ b/Syncbase/Syncbase.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "self:Syncbase.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/Syncbase/Tests/BasicDatabaseTests.swift b/Syncbase/Tests/BasicDatabaseTests.swift
new file mode 100644
index 0000000..c04b3cc
--- /dev/null
+++ b/Syncbase/Tests/BasicDatabaseTests.swift
@@ -0,0 +1,46 @@
+// 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.
+
+import XCTest
+@testable import Syncbase
+import SyncbaseCore
+
+class BasicDatabaseTests: XCTestCase {
+  override class func setUp() {
+    try! Syncbase.configure(adminUserId: "unittest@google.com")
+  }
+
+  override class func tearDown() {
+    SyncbaseCore.Syncbase.shutdown()
+  }
+
+  func testDatabaseInit() {
+    asyncDbTest() { db in
+      // Must be idempotent.
+      try db.createIfMissing()
+      try db.createIfMissing()
+    }
+  }
+
+  func testCollection() {
+    asyncDbTest() { db in
+      var collections = try db.collections()
+      XCTAssertEqual(collections.count, 0)
+
+      let collection = try db.collection("collection1")
+      // Must be idempotent.
+      try collection.createIfMissing()
+      try collection.createIfMissing()
+      // Should be empty.
+      XCTAssertFalse(try collection.exists("a"))
+
+      collections = try db.collections()
+      XCTAssertEqual(collections.count, 1)
+
+      // TODO(zinman): Delete collection.
+    }
+  }
+
+  // TODO(zinman): Add more unit tests.
+}
diff --git a/Syncbase/Tests/Info.plist b/Syncbase/Tests/Info.plist
new file mode 100644
index 0000000..ba72822
--- /dev/null
+++ b/Syncbase/Tests/Info.plist
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>BNDL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+</dict>
+</plist>
diff --git a/Syncbase/Tests/TestHelpers.swift b/Syncbase/Tests/TestHelpers.swift
new file mode 100644
index 0000000..bfc3e33
--- /dev/null
+++ b/Syncbase/Tests/TestHelpers.swift
@@ -0,0 +1,17 @@
+// 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.
+
+import XCTest
+import Syncbase
+
+extension XCTestCase {
+  func asyncDbTest(runBlock: Database throws -> Void) {
+    do {
+      let db = try Syncbase.database()
+      try runBlock(db)
+    } catch let e {
+      XCTFail("Unexpected error: \(e)")
+    }
+  }
+}
diff --git a/SyncbaseCore/Source/Batch.swift b/SyncbaseCore/Source/Batch.swift
index febd0f8..c81832c 100644
--- a/SyncbaseCore/Source/Batch.swift
+++ b/SyncbaseCore/Source/Batch.swift
@@ -24,8 +24,8 @@
     opts: BatchOptions?,
     op: Operation,
     completionHandler: BatchCompletionHandler) {
-      RunInBackgroundQueue {
-        for _ in 0...retries {
+      dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
+        for _ in 0 ... retries {
           // 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().
@@ -39,14 +39,51 @@
             log.warning("Unable to complete batch operation: \(e)")
             err = e
           }
-          RunInMainQueue { completionHandler(err) }
+          dispatch_async(Syncbase.queue) {
+            completionHandler(err)
+          }
           return
         }
         // We never were able to do it without error
-        RunInMainQueue { completionHandler(SyncbaseError.ConcurrentBatch) }
+        dispatch_async(Syncbase.queue) {
+          completionHandler(SyncbaseError.ConcurrentBatch)
+        }
       }
   }
 
+  /**
+   Runs the given batch operation, managing retries and BatchDatabase's commit() and abort()s.
+
+   This is run in a background thread and calls back on main.
+
+   - Parameter retries:      number of retries attempted before giving up. defaults to 3
+   - Parameter db:           database on which the batch operation is to be performed
+   - Parameter opts:         batch configuration
+   - Parameter op:           batch operation
+   - Parameter completionHandler:      future result called when runInBatch finishes
+   */
+  public static func runInBatchSync(retries: Int = 3,
+    db: Database,
+    opts: BatchOptions?,
+    op: Operation) throws {
+      for _ in 0 ... retries {
+        // 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().
+        do {
+          try attemptBatch(db, opts: opts, op: op)
+          return
+        } catch SyncbaseError.ConcurrentBatch {
+          continue
+        } catch let e {
+          log.warning("Unable to complete batch operation: \(e)")
+          throw e
+        }
+      }
+      // We never were able to do it without error.
+      throw SyncbaseError.ConcurrentBatch
+  }
+
   private static func attemptBatch(db: Database, opts: BatchOptions?, op: Operation) throws {
     let batchDb = try db.beginBatch(opts)
     // Use defer for abort to make sure it gets called in case op throws.
@@ -81,13 +118,18 @@
   /// Arbitrary string, typically used to describe the intent behind a batch.
   /// Hints are surfaced to clients during conflict resolution.
   /// TODO(sadovsky): Use "any" here?
-  public let hint: String? = nil
+  public let hint: String?
 
   /// ReadOnly specifies whether the batch should allow writes.
   /// If ReadOnly is set to true, Abort() should be used to release any resources
   /// associated with this batch (though it is not strictly required), and
   /// Commit() will always fail.
-  public let readOnly: Bool = false
+  public let readOnly: Bool
+
+  public init(hint: String? = nil, readOnly: Bool = false) {
+    self.hint = hint
+    self.readOnly = readOnly
+  }
 }
 
 public class BatchDatabase: Database {
diff --git a/SyncbaseCore/Source/Collection.swift b/SyncbaseCore/Source/Collection.swift
index 748a4b9..1c0f406 100644
--- a/SyncbaseCore/Source/Collection.swift
+++ b/SyncbaseCore/Source/Collection.swift
@@ -89,6 +89,19 @@
     }
   }
 
+  /// Returns true if there is a value associated with `key`.
+  public func exists(key: String) throws -> Bool {
+    var exists = v23_syncbase_Bool(false)
+    try VError.maybeThrow { errPtr in
+      v23_syncbase_RowExists(
+        try encodedRowName(key),
+        try cBatchHandle(),
+        &exists,
+        errPtr)
+    }
+    return exists.toBool()
+  }
+
   /**
    Get loads the value stored under the given key into inout parameter value.
 
@@ -133,11 +146,8 @@
           &cBytes,
           errPtr)
       }
-    } catch let e as VError {
-      if e.id == "v.io/v23/verror.NoExist" {
-        return nil
-      }
-      throw e
+    } catch SyncbaseError.NoExist {
+      return nil
     }
     // If we got here then we know that row exists, otherwise we would have gotten the NoExist
     // exception above. However, that row might also just be empty data. Because
diff --git a/SyncbaseCore/Source/Database.swift b/SyncbaseCore/Source/Database.swift
index 828d6b0..8df1c0e 100644
--- a/SyncbaseCore/Source/Database.swift
+++ b/SyncbaseCore/Source/Database.swift
@@ -16,7 +16,7 @@
   func collection(name: String) throws -> Collection
 
   /// CollectionForId returns the Collection with the given user blessing and name.
-  /// Throws if the id cannot be encoded into UTF8.
+  /// Throws if the id cannot be encoded into UTF-8.
   func collection(collectionId: Identifier) throws -> Collection
 
   /// ListCollections returns a list of all Collection ids that the caller is
@@ -67,7 +67,8 @@
     }
   }
 
-  /** BeginBatch creates a new batch. Instead of calling this function directly,
+  /**
+   BeginBatch creates a new batch. Instead of calling this function directly,
    clients are encouraged to use the RunInBatch() helper function, which
    detects "concurrent batch" errors and handles retries internally.
 
@@ -127,15 +128,27 @@
   }
 
   /// Syncgroup returns a handle to the syncgroup with the given name.
-  public func syncgroup(sgName: String) -> Syncgroup {
-    preconditionFailure("stub")
+  public func syncgroup(name: String) throws -> Syncgroup {
+    return Syncgroup(
+      encodedDatabaseName: encodedDatabaseName,
+      syncgroupId: Identifier(name: name, blessing: try Principal.userBlessing()))
   }
 
-  /// GetSyncgroupNames returns the names of all syncgroups attached to this
-  /// database.
-  /// TODO(sadovsky): Rename to ListSyncgroups, for parity with ListDatabases.
-  public func getSyncgroupNames() throws -> [String] {
-    preconditionFailure("stub")
+  /// Syncgroup returns a handle to the syncgroup with the given identifier.
+  public func syncgroup(syncgroupId: Identifier) -> Syncgroup {
+    return Syncgroup(encodedDatabaseName: encodedDatabaseName, syncgroupId: syncgroupId)
+  }
+
+  /// ListSyncgroups returns the identifiers of all syncgroups attached to this database.
+  public func listSyncgroups() throws -> [Identifier] {
+    var ids = v23_syncbase_Ids()
+    try VError.maybeThrow { errPtr in
+      v23_syncbase_DbListSyncgroups(
+        try encodedDatabaseName.toCgoString(),
+        &ids,
+        errPtr)
+    }
+    return ids.toIdentifiers()
   }
 
   /// CreateBlob creates a new blob and returns a handle to it.
@@ -216,7 +229,7 @@
         errPtr)
     }
     // TODO(zinman): Verify that permissions defaulting to zero-value is correct for Permissions.
-    // We force cast of cVersion because we know it can be UTF8 converted.
+    // We force cast of cVersion because we know it can be UTF-8 converted.
     return (try cPermissions.toPermissions() ?? Permissions(), cVersion.toString()!)
   }
 
diff --git a/SyncbaseCore/Source/Errors.swift b/SyncbaseCore/Source/Errors.swift
index 755cfd1..925a7e1 100644
--- a/SyncbaseCore/Source/Errors.swift
+++ b/SyncbaseCore/Source/Errors.swift
@@ -5,6 +5,9 @@
 import Foundation
 
 public enum SyncbaseError: ErrorType, CustomStringConvertible {
+  case AlreadyConfigured
+  case NotConfigured
+  case NotLoggedIn
   case NotAuthorized
   case NotInDevMode
   case UnknownBatch
@@ -15,6 +18,8 @@
   case SyncgroupJoinFailed
   case BadExecStreamHeader
   case InvalidPermissionsChange
+  case Exist
+  case NoExist
   case InvalidName(name: String)
   case CorruptDatabase(path: String)
   case InvalidOperation(reason: String)
@@ -36,12 +41,17 @@
     case "v.io/v23/services/syncbase.SyncgroupJoinFailed": self = SyncbaseError.SyncgroupJoinFailed
     case "v.io/v23/services/syncbase.BadExecStreamHeader": self = SyncbaseError.BadExecStreamHeader
     case "v.io/v23/services/syncbase.InvalidPermissionsChange": self = SyncbaseError.InvalidPermissionsChange
+    case "v.io/v23/verror.Exist": self = SyncbaseError.Exist
+    case "v.io/v23/verror.NoExist": self = SyncbaseError.NoExist
     default: return nil
     }
   }
 
   public var description: String {
     switch (self) {
+    case .AlreadyConfigured: return "Already configured"
+    case .NotConfigured: return "Not configured (via Syncbase.configure)"
+    case .NotLoggedIn: return "Not logged in (via Syncbase.login)"
     case .NotAuthorized: return "No valid blessings; create new blessings using oauth"
     case .NotInDevMode: return "Not running with --dev=true"
     case .UnknownBatch: return "Unknown batch, perhaps the server restarted"
@@ -52,11 +62,13 @@
     case .SyncgroupJoinFailed: return "Syncgroup join failed"
     case .BadExecStreamHeader: return "Exec stream header improperly formatted"
     case .InvalidPermissionsChange: return "The sequence of permission changes is invalid"
+    case .NoExist: return "Does not exist"
+    case .Exist: return "Already exists"
     case .InvalidName(let name): return "Invalid name: \(name)"
     case .CorruptDatabase(let path):
       return "Database corrupt, moved to path \(path); client must create a new database"
     case .InvalidOperation(let reason): return "Invalid operation: \(reason)"
-    case .InvalidUTF8(let invalidUtf8): return "Unable to convert to utf8: \(invalidUtf8)"
+    case .InvalidUTF8(let invalidUtf8): return "Unable to convert to UTF-8: \(invalidUtf8)"
     case .CastError(let obj): return "Unable to convert to cast: \(obj)"
     }
   }
diff --git a/SyncbaseCore/Source/Marshal.swift b/SyncbaseCore/Source/Marshal.swift
index f5b67c0..fbe344e 100644
--- a/SyncbaseCore/Source/Marshal.swift
+++ b/SyncbaseCore/Source/Marshal.swift
@@ -32,7 +32,7 @@
   /// Apple's limitation of requiring top
   static func hackSerializeAnyObject(obj: AnyObject) throws -> NSData {
     let data = try serialize([obj])
-    // Hack of first and last runes, which also happen to be single byte UTF8s
+    // Hack of first and last runes, which also happen to be single byte UTF-8s
     return data.subdataWithRange(NSMakeRange(1, data.length - 2))
   }
 
diff --git a/SyncbaseCore/Source/OAuth.swift b/SyncbaseCore/Source/OAuth.swift
index 5dbb390..3cb9240 100644
--- a/SyncbaseCore/Source/OAuth.swift
+++ b/SyncbaseCore/Source/OAuth.swift
@@ -19,10 +19,13 @@
 }
 
 /// Shortcut for OAuthCredentials provided by Google.
-public struct GoogleCredentials: OAuthCredentials {
+public struct GoogleOAuthCredentials: OAuthCredentials {
+  /// OAuth token, typically received from the Google Sign-In SDK.
   public let token: String
   public let provider = OAuthProvider.Google
 
+  /// Inits an GoogleOAuthCredentials struct with the oauth token recieved by Google, typically
+  /// from the Google Sign-In SDK.
   public init(token: String) {
     self.token = token
   }
diff --git a/SyncbaseCore/Source/Permissions.swift b/SyncbaseCore/Source/Permissions.swift
index 1553bb7..9ccb892 100644
--- a/SyncbaseCore/Source/Permissions.swift
+++ b/SyncbaseCore/Source/Permissions.swift
@@ -52,6 +52,11 @@
   /// "alice:friend:bob" or "alice:friend:bob:spouse" etc.
   public let notAllowed: [BlessingPattern]
 
+  public init(allowed: [BlessingPattern] = [], notAllowed: [BlessingPattern] = []) {
+    self.allowed = allowed
+    self.notAllowed = notAllowed
+  }
+
   func toJsonable() -> [String: AnyObject] {
     return ["In": allowed, "NotIn": notAllowed]
   }
diff --git a/SyncbaseCore/Source/Principal.swift b/SyncbaseCore/Source/Principal.swift
index a3ff607..70cc3f9 100644
--- a/SyncbaseCore/Source/Principal.swift
+++ b/SyncbaseCore/Source/Principal.swift
@@ -4,20 +4,12 @@
 
 import Foundation
 
-/// Principal gets the default app/user blessings from the default blessings store. This class is
-/// for internal use only. It is used for encoding Identifier and getting the blessing store
-/// debug string.
-enum Principal {
-  /// Returns a debug string that contains the current blessing store. For debug use only.
-  static var blessingsDebugDescription: String {
-    var cStr = v23_syncbase_String()
-    v23_syncbase_BlessingStoreDebugString(&cStr)
-    return cStr.toString() ?? "ERROR"
-  }
-
+/// Principal gets the default app/user blessings from the default blessings store. These methods
+/// are mainly for internal use only (for encoding Identifier), or advanced users only.
+public enum Principal {
   /// Returns the app blessing from the main context. This is used for encoding database ids.
   /// If no app blessing has been set, this throws an exception.
-  static func appBlessing() throws -> String {
+  public static func appBlessing() throws -> String {
     let cStr: v23_syncbase_String = try VError.maybeThrow { errPtr in
       var cStr = v23_syncbase_String()
       v23_syncbase_AppBlessingFromContext(&cStr, errPtr)
@@ -31,7 +23,7 @@
 
   /// Returns the user blessing from the main context. This is used for encoding collection ids.
   /// If no user blessing has been set, this throws an exception.
-  static func userBlessing() throws -> String {
+  public static func userBlessing() throws -> String {
     let cStr: v23_syncbase_String = try VError.maybeThrow { errPtr in
       var cStr = v23_syncbase_String()
       v23_syncbase_UserBlessingFromContext(&cStr, errPtr)
@@ -43,6 +35,13 @@
     return str
   }
 
+  /// Returns a debug string that contains the current blessing store. For debug use only.
+  static var blessingsDebugDescription: String {
+    var cStr = v23_syncbase_String()
+    v23_syncbase_BlessingStoreDebugString(&cStr)
+    return cStr.toString() ?? "ERROR"
+  }
+
   /// True if the blessings have been successfully retrieved via exchanging an oauth token.
   static func blessingsAreValid() -> Bool {
     do {
diff --git a/SyncbaseCore/Source/Service.swift b/SyncbaseCore/Source/Service.swift
deleted file mode 100644
index ccaaa74..0000000
--- a/SyncbaseCore/Source/Service.swift
+++ /dev/null
@@ -1,18 +0,0 @@
-// 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.
-
-import Foundation
-
-/// Service represents a Vanadium Syncbase service.
-public protocol Service: AccessController {
-  /// DatabaseForId returns the Database with the given app blessing and name (from the Id struct).
-  func database(databaseId: Identifier) throws -> Database
-
-  /// DatabaseForId returns the Database with the given relative name and default app blessing.
-  func database(name: String) throws -> Database
-
-  /// ListDatabases returns a list of all Database ids that the caller is allowed to see.
-  /// The list is sorted by blessing, then by name.
-  func listDatabases() throws -> [Identifier]
-}
diff --git a/SyncbaseCore/Source/Syncbase.swift b/SyncbaseCore/Source/Syncbase.swift
index 6147c86..2517111 100644
--- a/SyncbaseCore/Source/Syncbase.swift
+++ b/SyncbaseCore/Source/Syncbase.swift
@@ -4,33 +4,55 @@
 
 import Foundation
 
-//public let instance = Syncbase.instance
+/// The singleton instance of Syncbase that represents the local store.
+public enum Syncbase {
+  /// The dispatch queue to run callbacks on. Defaults to main.
+  public static var queue: dispatch_queue_t = dispatch_get_main_queue()
 
-public class Syncbase: Service {
-  /// The singleton instance of Syncbase that represents the local store.
-  /// You won't be able to sync with anybody unless you grant yourself a blessing via the authorize
-  /// method.
-  public static var instance: Syncbase = Syncbase()
+  static var didInit = false
 
-  /// Private constructor -- because this class is a singleton it should only be called once
-  /// and by the static instance method.
-  private init() {
-    v23_syncbase_Init(v23_syncbase_Bool(false), try! "root-dir".toCgoString())
+  public static func configure(
+    // Default to Application Support/Syncbase.
+    rootDir rootDir: String = NSFileManager.defaultManager()
+      .URLsForDirectory(.ApplicationSupportDirectory, inDomains: .UserDomainMask)[0]
+      .URLByAppendingPathComponent("Syncbase")
+      .absoluteString,
+    queue: dispatch_queue_t = dispatch_get_main_queue()) throws {
+      if didInit {
+        throw SyncbaseError.AlreadyConfigured
+      }
+      v23_syncbase_Init(v23_syncbase_Bool(false), try rootDir.toCgoString())
+      Syncbase.didInit = true
+  }
+
+  /// Shuts down the Syncbase service. You must call configure again before any calls will work.
+  public static func shutdown() {
+    v23_syncbase_Shutdown()
+    Syncbase.didInit = false
   }
 
   /// Create a database using the relative name and user's blessings.
-  public func database(name: String) throws -> Database {
+  public static func database(name: String) throws -> Database {
+    if !Syncbase.didInit {
+      throw SyncbaseError.NotConfigured
+    }
     return try database(Identifier(name: name, blessing: try Principal.appBlessing()))
   }
 
   /// DatabaseForId returns the Database with the given app blessing and name (from the Id struct).
-  public func database(databaseId: Identifier) throws -> Database {
+  public static func database(databaseId: Identifier) throws -> Database {
+    if !Syncbase.didInit {
+      throw SyncbaseError.NotConfigured
+    }
     return try Database(databaseId: databaseId, batchHandle: nil)
   }
 
   /// ListDatabases returns a list of all Database ids that the caller is allowed to see.
   /// The list is sorted by blessing, then by name.
-  public func listDatabases() throws -> [Identifier] {
+  public static func listDatabases() throws -> [Identifier] {
+    if !Syncbase.didInit {
+      throw SyncbaseError.NotConfigured
+    }
     var ids = v23_syncbase_Ids()
     return try VError.maybeThrow { err in
       v23_syncbase_ServiceListDatabases(&ids, err)
@@ -40,27 +62,34 @@
 
   /// Must return true before any Syncbase operation can work. Authorize using GoogleCredentials
   /// created from a Google OAuth token (you should use the Google Sign In SDK to get this).
-  public var isLoggedIn: Bool {
+  public static var isLoggedIn: Bool {
     log.debug("Blessings debug string is \(Principal.blessingsDebugDescription)")
     return Principal.blessingsAreValid()
   }
 
   /// For debugging the current Syncbase user blessings.
-  public var loggedInBlessingDebugDescription: String {
+  public static var loggedInBlessingDebugDescription: String {
     return Principal.blessingsDebugDescription
   }
 
-  /// Authorize using GoogleCredentials created from a Google OAuth token (you should use the
-  /// Google Sign In SDK to get this). You must login and have valid credentials before any
-  /// Syncbase operation will work.
+  public typealias LoginCallback = (err: ErrorType?) -> Void
+
+  /// Authorize using an oauth token. Right now only Google OAuth token is supported
+  /// (you should use the Google Sign In SDK to get this), and you should use the
+  /// GoogleOAuthCredentials struct to pass the token.
   ///
-  /// Calls callback on main with nil on success, or on failure a SyncbaseError or VError.
+  /// You must login and have valid credentials before any Syncbase operation will work.
+  ///
+  /// Calls `callback` on `Syncbase.queue` with any error that occured, or nil on success.
   ///
   /// TODO(zinman): Make sure the blessings cache works so we don't actually have to login
   /// every single time.
-  public func login(credentials: GoogleCredentials, callback: ErrorType? -> Void) {
+  public static func login(credentials: OAuthCredentials, callback: LoginCallback) {
+    if !Syncbase.didInit {
+      callback(err: SyncbaseError.NotConfigured)
+    }
     // Go's login is blocking, so call on a background concurrent queue.
-    RunInBackgroundQueue {
+    dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
       var err: ErrorType? = nil
       do {
         try VError.maybeThrow { errPtr in
@@ -72,13 +101,16 @@
       } catch (let e) {
         err = e
       }
-      RunInMainQueue { callback(err) }
+      dispatch_async(Syncbase.queue) {
+        callback(err: err)
+      }
     }
   }
-}
 
-extension Syncbase: AccessController {
-  public func getPermissions() throws -> (Permissions, PermissionsVersion) {
+  public static func getPermissions() throws -> (Permissions, PermissionsVersion) {
+    if !Syncbase.didInit {
+      throw SyncbaseError.NotConfigured
+    }
     var cPermissions = v23_syncbase_Permissions()
     var cVersion = v23_syncbase_String()
     try VError.maybeThrow { errPtr in
@@ -88,11 +120,14 @@
         errPtr)
     }
     // TODO(zinman): Verify that permissions defaulting to zero-value is correct for Permissions.
-    // We force cast of cVersion because we know it can be UTF8 converted.
+    // We force cast of cVersion because we know it can be UTF-8 converted.
     return (try cPermissions.toPermissions() ?? Permissions(), cVersion.toString()!)
   }
 
-  public func setPermissions(permissions: Permissions, version: PermissionsVersion) throws {
+  public static func setPermissions(permissions: Permissions, version: PermissionsVersion) throws {
+    if !Syncbase.didInit {
+      throw SyncbaseError.NotConfigured
+    }
     try VError.maybeThrow { errPtr in
       v23_syncbase_ServiceSetPermissions(
         try v23_syncbase_Permissions(permissions),
diff --git a/SyncbaseCore/Source/Syncgroup.swift b/SyncbaseCore/Source/Syncgroup.swift
index 3cb26a6..4604f87 100644
--- a/SyncbaseCore/Source/Syncgroup.swift
+++ b/SyncbaseCore/Source/Syncgroup.swift
@@ -5,8 +5,13 @@
 import Foundation
 
 public struct VersionedSpec {
-  let spec: SyncgroupSpec
-  let version: String
+  public let spec: SyncgroupSpec
+  public let version: String
+
+  public init(spec: SyncgroupSpec, version: String) {
+    self.spec = spec
+    self.version = version
+  }
 }
 
 public class Syncgroup {
@@ -23,7 +28,7 @@
   /// Requires: Client must have at least Read access on the Database; all
   /// Collections specified in prefixes must exist; Client must have at least
   /// Read access on each of the Collection ACLs.
-  func create(spec: SyncgroupSpec, myInfo: SyncgroupMemberInfo) throws {
+  public func create(spec: SyncgroupSpec, myInfo: SyncgroupMemberInfo) throws {
     try VError.maybeThrow { errPtr in
       v23_syncbase_DbCreateSyncgroup(
         try encodedDatabaseName.toCgoString(),
@@ -47,7 +52,7 @@
   ///
   /// Requires: Client must have at least Read access on the Database and on the
   /// syncgroup ACL.
-  func join(remoteSyncbaseName: String, expectedSyncbaseBlessings: [String], myInfo: SyncgroupMemberInfo) throws -> SyncgroupSpec {
+  public func join(remoteSyncbaseName: String, expectedSyncbaseBlessings: [String], myInfo: SyncgroupMemberInfo) throws -> SyncgroupSpec {
     var spec = v23_syncbase_SyncgroupSpec()
     try VError.maybeThrow { errPtr in
       v23_syncbase_DbJoinSyncgroup(
@@ -66,7 +71,7 @@
   /// to be available.
   ///
   /// Requires: Client must have at least Read access on the Database.
-  func leave() throws {
+  public func leave() throws {
     try VError.maybeThrow { errPtr in
       v23_syncbase_DbLeaveSyncgroup(
         try encodedDatabaseName.toCgoString(),
@@ -80,7 +85,7 @@
   ///
   /// Requires: Client must have at least Read access on the Database, and must
   /// have Admin access on the syncgroup ACL.
-  func destroy() throws {
+  public func destroy() throws {
     try VError.maybeThrow { errPtr in
       v23_syncbase_DbDestroySyncgroup(
         try encodedDatabaseName.toCgoString(),
@@ -95,7 +100,7 @@
   ///
   /// Requires: Client must have at least Read access on the Database, and must
   /// have Admin access on the syncgroup ACL.
-  func eject(member: String) throws {
+  public func eject(member: String) throws {
     try VError.maybeThrow { errPtr in
       v23_syncbase_DbEjectFromSyncgroup(
         try encodedDatabaseName.toCgoString(),
@@ -110,7 +115,7 @@
   ///
   /// Requires: Client must have at least Read access on the Database and on the
   /// syncgroup ACL.
-  func getSpec() throws -> VersionedSpec {
+  public func getSpec() throws -> VersionedSpec {
     var spec = v23_syncbase_SyncgroupSpec()
     var version = v23_syncbase_String()
     try VError.maybeThrow { errPtr in
@@ -132,7 +137,7 @@
   ///
   /// Requires: Client must have at least Read access on the Database, and must
   /// have Admin access on the syncgroup ACL.
-  func setSpec(versionedSpec: VersionedSpec) throws {
+  public func setSpec(versionedSpec: VersionedSpec) throws {
     try VError.maybeThrow { errPtr in
       v23_syncbase_DbSetSyncgroupSpec(
         try encodedDatabaseName.toCgoString(),
@@ -147,7 +152,7 @@
   ///
   /// Requires: Client must have at least Read access on the Database and on the
   /// syncgroup ACL.
-  func getMembers() throws -> [String: SyncgroupMemberInfo] {
+  public func getMembers() throws -> [String: SyncgroupMemberInfo] {
     var members = v23_syncbase_SyncgroupMemberInfoMap()
     try VError.maybeThrow { errPtr in
       v23_syncbase_DbGetSyncgroupMembers(
@@ -162,23 +167,28 @@
 
 /// SyncgroupMemberInfo contains per-member metadata.
 public struct SyncgroupMemberInfo {
-  let syncPriority: UInt8
-  let blobDevType: BlobDevType /// See BlobDevType* constants.
+  public let syncPriority: UInt8
+  public let blobDevType: BlobDevType /// See BlobDevType* constants.
+
+  public init(syncPriority: UInt8, blobDevType: BlobDevType) {
+    self.syncPriority = syncPriority
+    self.blobDevType = blobDevType
+  }
 }
 
 public struct SyncgroupSpec {
   /// Human-readable description of this syncgroup.
-  let description: String
+  public let description: String
 
   // Data (set of collectionIds) covered by this syncgroup.
-  let collections: [Identifier]
+  public let collections: [Identifier]
 
   /// Permissions governing access to this syncgroup.
-  let permissions: Permissions
+  public let permissions: Permissions
 
   // Optional. If present then any syncbase that is the admin of this syncgroup
   // is responsible for ensuring that the syncgroup is published to this syncbase instance.
-  let publishSyncbaseName: String?
+  public let publishSyncbaseName: String?
 
   /// Mount tables at which to advertise this syncgroup, for rendezvous purposes.
   /// (Note that in addition to these mount tables, Syncbase also uses
@@ -186,10 +196,25 @@
   /// We expect most clients to specify a single mount table, but we accept an
   /// array of mount tables to permit the mount table to be changed over time
   /// without disruption.
-  let mountTables: [String]
+  public let mountTables: [String]
 
   /// Specifies the privacy of this syncgroup. More specifically, specifies
   /// whether blobs in this syncgroup can be served to clients presenting
   /// blobrefs obtained from other syncgroups.
-  let isPrivate: Bool
+  public let isPrivate: Bool
+
+  public init(
+    description: String,
+    collections: [Identifier],
+    permissions: Permissions,
+    publishSyncbaseName: String?,
+    mountTables: [String],
+    isPrivate: Bool) {
+      self.description = description
+      self.collections = collections
+      self.permissions = permissions
+      self.publishSyncbaseName = publishSyncbaseName
+      self.mountTables = mountTables
+      self.isPrivate = isPrivate
+  }
 }
diff --git a/SyncbaseCore/Source/Watch.swift b/SyncbaseCore/Source/Watch.swift
index 84303ea..78e9be3 100644
--- a/SyncbaseCore/Source/Watch.swift
+++ b/SyncbaseCore/Source/Watch.swift
@@ -10,19 +10,23 @@
 public struct CollectionRowPattern {
   /// collectionName is a SQL LIKE-style glob pattern ('%' and '_' wildcards, '\' as escape
   /// character) for matching collections. May not be empty.
-  let collectionName: String
+  public let collectionName: String
 
   /// The blessing for collections.
-  let collectionBlessing: String
+  public let collectionBlessing: String
 
   /// rowKey is a SQL LIKE-style glob pattern ('%' and '_' wildcards, '\' as escape character)
   /// for matching rows. If empty then only the collectionId pattern is matched.
-  let rowKey: String?
+  public let rowKey: String?
+
+  public init(collectionName: String, collectionBlessing: String, rowKey: String?) {
+    self.collectionName = collectionName
+    self.collectionBlessing = collectionBlessing
+    self.rowKey = rowKey
+  }
 }
 
-public struct ResumeMarker {
-  let data: NSData
-}
+public typealias ResumeMarker = NSData
 
 public struct WatchChange {
   public enum ChangeType: Int {
@@ -31,35 +35,35 @@
   }
 
   /// Collection is the id of the collection that contains the changed row.
-  let collectionId: Identifier
+  public let collectionId: Identifier
 
   /// Row is the key of the changed row.
-  let row: String
+  public let row: String
 
   /// ChangeType describes the type of the change. If ChangeType is PutChange,
   /// then the row exists in the collection, and Value can be called to obtain
   /// the new value for this row. If ChangeType is DeleteChange, then the row was
   /// removed from the collection.
-  let changeType: ChangeType
+  public let changeType: ChangeType
 
   /// value is the new value for the row if the ChangeType is PutChange, or nil
   /// otherwise.
-  let value: NSData?
+  public let value: NSData?
 
   /// ResumeMarker provides a compact representation of all the messages that
   /// have been received by the caller for the given Watch call.
   /// This marker can be provided in the Request message to allow the caller
   /// to resume the stream watching at a specific point without fetching the
   /// initial state.
-  let resumeMarker: ResumeMarker
+  public let resumeMarker: ResumeMarker
 
   /// FromSync indicates whether the change came from sync. If FromSync is false,
   /// then the change originated from the local device.
-  let isFromSync: Bool
+  public let isFromSync: Bool
 
   /// If true, this WatchChange is followed by more WatchChanges that are in the
   /// same batch as this WatchChange.
-  let isContinued: Bool
+  public let isContinued: Bool
 }
 
 public typealias WatchStream = AnonymousStream<WatchChange>
@@ -147,7 +151,7 @@
       try VError.maybeThrow { errPtr in
         let oHandle = UnsafeMutablePointer<Void>(Unmanaged.passRetained(handle).toOpaque())
         let cPatterns = try v23_syncbase_CollectionRowPatterns(patterns)
-        let cResumeMarker = v23_syncbase_Bytes(resumeMarker?.data)
+        let cResumeMarker = v23_syncbase_Bytes(resumeMarker)
         let callbacks = v23_syncbase_DbWatchPatternsCallbacks(
           handle: v23_syncbase_Handle(oHandle),
           onChange: { Watch.onWatchChange($0, change: $1) },
diff --git a/SyncbaseCore/Source/util/Threads.swift b/SyncbaseCore/Source/util/Threads.swift
deleted file mode 100644
index 4e24d3c..0000000
--- a/SyncbaseCore/Source/util/Threads.swift
+++ /dev/null
@@ -1,57 +0,0 @@
-// 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.
-
-import Foundation
-
-func dispatch_maybe_async(queue: dispatch_queue_t?, block: dispatch_block_t) {
-  guard let queue = queue else {
-    block()
-    return
-  }
-  if (isCurrentQueue(queue)) {
-    block()
-  } else {
-    dispatch_async(queue, block)
-  }
-}
-
-func dispatch_maybe_sync(queue: dispatch_queue_t?, block: dispatch_block_t) {
-  guard let queue = queue else {
-    block()
-    return
-  }
-  if (isCurrentQueue(queue)) {
-    block()
-  } else {
-    dispatch_sync(queue, block)
-  }
-}
-
-func isCurrentQueue(queue: dispatch_queue_t) -> Bool {
-  let current = dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)
-  let queueName = dispatch_queue_get_label(queue)
-  return current != nil && queueName != nil && strcmp(current, queueName) == 0
-}
-
-func dispatch_after_delay(delay: NSTimeInterval, queue: dispatch_queue_t, block: dispatch_block_t) {
-  dispatch_after(dispatch_time_t.fromNSTimeInterval(delay), queue, block)
-}
-
-func RunInMainQueue(block: dispatch_block_t) {
-  dispatch_maybe_async(dispatch_get_main_queue(), block: block)
-}
-
-func RunInBackgroundQueue(block: dispatch_block_t) {
-  dispatch_maybe_async(dispatch_get_bg_queue(), block: block)
-}
-
-func dispatch_get_bg_queue() -> dispatch_queue_t {
-  return dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)
-}
-
-extension dispatch_time_t {
-  static func fromNSTimeInterval(t: NSTimeInterval) -> dispatch_time_t {
-    return dispatch_time(DISPATCH_TIME_NOW, Int64(t * Double(NSEC_PER_SEC)))
-  }
-}
\ No newline at end of file
diff --git a/SyncbaseCore/Source/util/Types.swift b/SyncbaseCore/Source/util/Types.swift
index 22ed131..110b488 100644
--- a/SyncbaseCore/Source/util/Types.swift
+++ b/SyncbaseCore/Source/util/Types.swift
@@ -7,7 +7,7 @@
 
 import Foundation
 
-extension v23_syncbase_BatchOptions {
+public extension v23_syncbase_BatchOptions {
   init (_ opts: BatchOptions?) throws {
     guard let o = opts else {
       self.hint = v23_syncbase_String()
@@ -19,7 +19,7 @@
   }
 }
 
-extension v23_syncbase_Bool {
+public extension v23_syncbase_Bool {
   init(_ bool: Bool) {
     switch bool {
     case true: self = 1
@@ -35,7 +35,7 @@
   }
 }
 
-extension v23_syncbase_Bytes {
+public extension v23_syncbase_Bytes {
   init(_ data: NSData?) {
     guard let data = data else {
       self.n = 0
@@ -61,13 +61,13 @@
   }
 }
 
-extension v23_syncbase_ChangeType {
+public extension v23_syncbase_ChangeType {
   func toChangeType() -> WatchChange.ChangeType? {
     return WatchChange.ChangeType(rawValue: Int(self.rawValue))
   }
 }
 
-extension v23_syncbase_CollectionRowPattern {
+public extension v23_syncbase_CollectionRowPattern {
   init(_ pattern: CollectionRowPattern) throws {
     self.collectionBlessing = try pattern.collectionBlessing.toCgoString()
     self.collectionName = try pattern.collectionName.toCgoString()
@@ -75,7 +75,7 @@
   }
 }
 
-extension v23_syncbase_CollectionRowPatterns {
+public extension v23_syncbase_CollectionRowPatterns {
   init(_ patterns: [CollectionRowPattern]) throws {
     if (patterns.isEmpty) {
       self.n = 0
@@ -103,7 +103,7 @@
   }
 }
 
-extension v23_syncbase_Id {
+public extension v23_syncbase_Id {
   init(_ id: Identifier) throws {
     self.name = try id.name.toCgoString()
     self.blessing = try id.blessing.toCgoString()
@@ -118,7 +118,7 @@
   }
 }
 
-extension v23_syncbase_Ids {
+public extension v23_syncbase_Ids {
   init(_ ids: [Identifier]) throws {
     let arrayBytes = ids.count * sizeof(v23_syncbase_Id)
     let p = unsafeBitCast(malloc(arrayBytes), UnsafeMutablePointer<v23_syncbase_Id>.self)
@@ -156,7 +156,7 @@
   }
 }
 
-extension v23_syncbase_Permissions {
+public extension v23_syncbase_Permissions {
   init(_ permissions: Permissions?) throws {
     guard let p = permissions where !p.isEmpty else {
       // Zero-value constructor.
@@ -190,7 +190,7 @@
   }
 }
 
-extension v23_syncbase_String {
+public extension v23_syncbase_String {
   init(_ string: String) throws {
     // TODO: If possible, make one copy instead of two, e.g. using s.getCString.
     guard let data = string.dataUsingEncoding(NSUTF8StringEncoding) else {
@@ -218,7 +218,7 @@
   }
 }
 
-extension v23_syncbase_Strings {
+public extension v23_syncbase_Strings {
   init(_ strings: [String]) throws {
     let arrayBytes = strings.count * sizeof(v23_syncbase_String)
     let p = unsafeBitCast(malloc(arrayBytes), UnsafeMutablePointer<v23_syncbase_String>.self)
@@ -269,14 +269,14 @@
   }
 }
 
-extension String {
+public extension String {
   /// Create a Cgo-passable string struct forceably (will crash if the string cannot be created).
   func toCgoString() throws -> v23_syncbase_String {
     return try v23_syncbase_String(self)
   }
 }
 
-extension v23_syncbase_SyncgroupMemberInfo {
+public extension v23_syncbase_SyncgroupMemberInfo {
   init(_ info: SyncgroupMemberInfo) {
     self.syncPriority = info.syncPriority
     self.blobDevType = UInt8(info.blobDevType.rawValue)
@@ -289,7 +289,7 @@
   }
 }
 
-extension v23_syncbase_SyncgroupMemberInfoMap {
+public extension v23_syncbase_SyncgroupMemberInfoMap {
   func toSyncgroupMemberInfoMap() -> [String: SyncgroupMemberInfo] {
     var ret = [String: SyncgroupMemberInfo]()
     for i in 0 ..< Int(n) {
@@ -303,7 +303,7 @@
   }
 }
 
-extension v23_syncbase_SyncgroupSpec {
+public extension v23_syncbase_SyncgroupSpec {
   init(_ spec: SyncgroupSpec) throws {
     self.collections = try v23_syncbase_Ids(spec.collections)
     self.description = try spec.description.toCgoString()
@@ -324,7 +324,7 @@
   }
 }
 
-extension v23_syncbase_WatchChange {
+public extension v23_syncbase_WatchChange {
   func toWatchChange() -> WatchChange {
     let resumeMarkerData = v23_syncbase_Bytes(
       p: unsafeBitCast(self.resumeMarker.p, UnsafeMutablePointer<UInt8>.self),
@@ -334,14 +334,14 @@
       row: self.row.toString()!,
       changeType: self.changeType.toChangeType()!,
       value: self.value.toNSData(),
-      resumeMarker: ResumeMarker(data: resumeMarkerData),
+      resumeMarker: resumeMarkerData,
       isFromSync: self.fromSync,
       isContinued: self.continued)
   }
 }
 
 // Note, we don't define init(VError) since we never pass Swift VError objects to Go.
-extension v23_syncbase_VError {
+public extension v23_syncbase_VError {
   // Return value takes ownership of the memory associated with this object.
   func toVError() -> VError? {
     if id.p == nil {
@@ -349,7 +349,7 @@
     }
     // Take ownership of all memory before checking optionals.
     let vId = id.toString(), vMsg = msg.toString(), vStack = stack.toString()
-    // TODO: Stop requiring id, msg, and stack to be valid UTF8?
+    // TODO: Stop requiring id, msg, and stack to be valid UTF-8?
     return VError(id: vId!, actionCode: actionCode, msg: vMsg ?? "", stack: vStack!)
   }
 }
diff --git a/SyncbaseCore/SyncbaseCore.xcodeproj/project.pbxproj b/SyncbaseCore/SyncbaseCore.xcodeproj/project.pbxproj
index 1620101..896da9a 100644
--- a/SyncbaseCore/SyncbaseCore.xcodeproj/project.pbxproj
+++ b/SyncbaseCore/SyncbaseCore.xcodeproj/project.pbxproj
@@ -18,7 +18,6 @@
 		30AD2E381CDD508700A28A0C /* Marshal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AD2E221CDD508700A28A0C /* Marshal.swift */; };
 		30AD2E391CDD508700A28A0C /* Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AD2E231CDD508700A28A0C /* Permissions.swift */; };
 		30AD2E3A1CDD508700A28A0C /* RowRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AD2E241CDD508700A28A0C /* RowRange.swift */; };
-		30AD2E3C1CDD508700A28A0C /* Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AD2E261CDD508700A28A0C /* Service.swift */; };
 		30AD2E3D1CDD508700A28A0C /* Stream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AD2E271CDD508700A28A0C /* Stream.swift */; };
 		30AD2E3E1CDD508700A28A0C /* SyncbaseCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 30AD2E281CDD508700A28A0C /* SyncbaseCore.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		30AD2E3F1CDD508700A28A0C /* Syncbase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AD2E291CDD508700A28A0C /* Syncbase.swift */; };
@@ -27,7 +26,6 @@
 		30AD2E451CDD508700A28A0C /* Watch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AD2E2F1CDD508700A28A0C /* Watch.swift */; };
 		30AD2E801CDD569200A28A0C /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AD2E791CDD569200A28A0C /* Logging.swift */; };
 		30AD2E811CDD569200A28A0C /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AD2E7A1CDD569200A28A0C /* Strings.swift */; };
-		30AD2E821CDD569200A28A0C /* Threads.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AD2E7B1CDD569200A28A0C /* Threads.swift */; };
 		30AD2E891CDD56B700A28A0C /* Exceptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 30AD2E861CDD56B700A28A0C /* Exceptions.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		30AD2E8A1CDD56B700A28A0C /* Exceptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 30AD2E871CDD56B700A28A0C /* Exceptions.m */; };
 		30AD2E911CDD593000A28A0C /* Principal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AD2E901CDD593000A28A0C /* Principal.swift */; };
@@ -69,7 +67,6 @@
 		30AD2E221CDD508700A28A0C /* Marshal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Marshal.swift; sourceTree = "<group>"; };
 		30AD2E231CDD508700A28A0C /* Permissions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Permissions.swift; sourceTree = "<group>"; };
 		30AD2E241CDD508700A28A0C /* RowRange.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowRange.swift; sourceTree = "<group>"; };
-		30AD2E261CDD508700A28A0C /* Service.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Service.swift; sourceTree = "<group>"; };
 		30AD2E271CDD508700A28A0C /* Stream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Stream.swift; sourceTree = "<group>"; };
 		30AD2E281CDD508700A28A0C /* SyncbaseCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SyncbaseCore.h; sourceTree = "<group>"; };
 		30AD2E291CDD508700A28A0C /* Syncbase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Syncbase.swift; sourceTree = "<group>"; };
@@ -79,7 +76,6 @@
 		30AD2E481CDD508D00A28A0C /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		30AD2E791CDD569200A28A0C /* Logging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Logging.swift; path = util/Logging.swift; sourceTree = "<group>"; };
 		30AD2E7A1CDD569200A28A0C /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Strings.swift; path = util/Strings.swift; sourceTree = "<group>"; };
-		30AD2E7B1CDD569200A28A0C /* Threads.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Threads.swift; path = util/Threads.swift; sourceTree = "<group>"; };
 		30AD2E861CDD56B700A28A0C /* Exceptions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Exceptions.h; path = util/Exceptions.h; sourceTree = "<group>"; };
 		30AD2E871CDD56B700A28A0C /* Exceptions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Exceptions.m; path = util/Exceptions.m; sourceTree = "<group>"; };
 		30AD2E901CDD593000A28A0C /* Principal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Principal.swift; sourceTree = "<group>"; };
@@ -159,7 +155,6 @@
 				30AD2E231CDD508700A28A0C /* Permissions.swift */,
 				30AD2E901CDD593000A28A0C /* Principal.swift */,
 				30AD2E241CDD508700A28A0C /* RowRange.swift */,
-				30AD2E261CDD508700A28A0C /* Service.swift */,
 				30AD2E271CDD508700A28A0C /* Stream.swift */,
 				30AD2E291CDD508700A28A0C /* Syncbase.swift */,
 				30AD2E2A1CDD508700A28A0C /* Syncgroup.swift */,
@@ -202,7 +197,6 @@
 				30AD2E871CDD56B700A28A0C /* Exceptions.m */,
 				30AD2E791CDD569200A28A0C /* Logging.swift */,
 				30AD2E7A1CDD569200A28A0C /* Strings.swift */,
-				30AD2E7B1CDD569200A28A0C /* Threads.swift */,
 				30AD2E921CDD60A600A28A0C /* Types.swift */,
 			);
 			name = Util;
@@ -337,7 +331,6 @@
 				30AD2E401CDD508700A28A0C /* Syncgroup.swift in Sources */,
 				30AD2E811CDD569200A28A0C /* Strings.swift in Sources */,
 				30AD2E451CDD508700A28A0C /* Watch.swift in Sources */,
-				30AD2E821CDD569200A28A0C /* Threads.swift in Sources */,
 				30AD2E801CDD569200A28A0C /* Logging.swift in Sources */,
 				30AD2E351CDD508700A28A0C /* Errors.swift in Sources */,
 				930DFCE21CEE46DE00738DB8 /* OAuth.swift in Sources */,
@@ -346,7 +339,6 @@
 				30AD2E3D1CDD508700A28A0C /* Stream.swift in Sources */,
 				30AD2E321CDD508700A28A0C /* Blob.swift in Sources */,
 				30AD2E441CDD508700A28A0C /* Vom.swift in Sources */,
-				30AD2E3C1CDD508700A28A0C /* Service.swift in Sources */,
 				30AD2E341CDD508700A28A0C /* Database.swift in Sources */,
 				30AD2E311CDD508700A28A0C /* Batch.swift in Sources */,
 			);
diff --git a/SyncbaseCore/Tests/BasicDatabaseTests.swift b/SyncbaseCore/Tests/BasicDatabaseTests.swift
index 1a7f79c..642ee66 100644
--- a/SyncbaseCore/Tests/BasicDatabaseTests.swift
+++ b/SyncbaseCore/Tests/BasicDatabaseTests.swift
@@ -7,6 +7,17 @@
 @testable import SyncbaseCore
 
 class BasicDatabaseTests: XCTestCase {
+  override class func setUp() {
+    let rootDir = NSFileManager.defaultManager()
+      .URLsForDirectory(.ApplicationSupportDirectory, inDomains: .UserDomainMask)[0]
+      .URLByAppendingPathComponent("SyncbaseUnitTest")
+      .absoluteString
+    try! Syncbase.configure(rootDir: rootDir)
+  }
+
+  override class func tearDown() {
+    Syncbase.shutdown()
+  }
 
   // MARK: Database & collection creation / destroying / listing
 
@@ -16,7 +27,7 @@
 
   func testListDatabases() {
     withTestDb { db in
-      let databases = try Syncbase.instance.listDatabases()
+      let databases = try Syncbase.listDatabases()
       XCTAssertFalse(databases.isEmpty)
       if !databases.isEmpty {
         XCTAssertTrue(databases.first! == db.databaseId)
@@ -48,6 +59,7 @@
     } else {
       XCTAssertEqual(value!, targetValue, "Value should be defined and \(targetValue)")
     }
+    XCTAssertTrue(try collection.exists(key))
     try collection.delete(key)
     value = try collection.get(key)
     XCTAssertNil(value, "Deleted row shouldn't exist")
@@ -239,6 +251,22 @@
       })
     }
     waitForExpectationsWithTimeout(2) { XCTAssertNil($0) }
+
+    // Test sync version
+    withTestDb { db in
+      try Batch.runInBatchSync(db: db, opts: nil) { batchDb in
+        let collection = try batchDb.collection(Identifier(name: "collection3", blessing: anyPermissions))
+        try collection.create(anyCollectionPermissions)
+        try collection.put("b", value: NSData())
+      }
+      do {
+        let collection = try db.collection(Identifier(name: "collection3", blessing: anyPermissions))
+        let value: NSData? = try collection.get("b")
+        XCTAssertNotNil(value)
+      } catch let e {
+        XCTFail("Unexpected error: \(e)")
+      }
+    }
   }
 
   func testRunInBatchAbort() {
@@ -267,6 +295,23 @@
       })
     }
     waitForExpectationsWithTimeout(2) { XCTAssertNil($0) }
+
+    withTestDb { db in
+      try Batch.runInBatchSync(db: db, opts: nil) { batchDb in
+        let collection = try batchDb.collection(Identifier(name: "collection4", blessing: anyPermissions))
+        try collection.create(anyCollectionPermissions)
+        try collection.put("a", value: NSData())
+        try batchDb.abort()
+      }
+
+      do {
+        let collection = try db.collection(Identifier(name: "collection4", blessing: anyPermissions))
+        let value: NSData? = try collection.get("a")
+        XCTAssertNil(value)
+      } catch let e {
+        XCTFail("Unexpected error: \(e)")
+      }
+    }
   }
 
   // MARK: Test watch
@@ -280,7 +325,7 @@
           collectionName: collection.collectionId.name,
           collectionBlessing: collection.collectionId.blessing,
           rowKey: nil)])
-        RunInBackgroundQueue {
+        dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
           XCTAssertNil(stream.next(timeout: 0.1))
           XCTAssertNil(stream.err())
           cleanup()
@@ -305,7 +350,7 @@
         collectionBlessing: collection.collectionId.blessing,
         rowKey: nil)])
       let semaphore = dispatch_semaphore_create(0)
-      RunInBackgroundQueue {
+      dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
         dispatch_semaphore_signal(semaphore)
         // Watch for changes in bg thread.
         for tup in data {
@@ -323,7 +368,7 @@
           XCTAssertEqual(change.collectionId.name, collection.collectionId.name)
           XCTAssertFalse(change.isContinued)
           XCTAssertFalse(change.isFromSync)
-          XCTAssertGreaterThan(change.resumeMarker.data.length, 0)
+          XCTAssertGreaterThan(change.resumeMarker.length, 0)
           XCTAssertEqual(change.row, key)
           XCTAssertEqual(change.value, value)
         }
@@ -364,7 +409,7 @@
         collectionBlessing: collection.collectionId.blessing,
         rowKey: nil)])
       let semaphore = dispatch_semaphore_create(0)
-      RunInBackgroundQueue {
+      dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
         dispatch_semaphore_signal(semaphore)
         // Watch for changes in bg thread.
         for tup in data {
@@ -381,7 +426,7 @@
           XCTAssertEqual(change.collectionId.name, collection.collectionId.name)
           XCTAssertFalse(change.isContinued)
           XCTAssertFalse(change.isFromSync)
-          XCTAssertGreaterThan(change.resumeMarker.data.length, 0)
+          XCTAssertGreaterThan(change.resumeMarker.length, 0)
           XCTAssertEqual(change.row, key)
           XCTAssertNil(change.value)
         }
diff --git a/SyncbaseCore/Tests/TestHelpers.swift b/SyncbaseCore/Tests/TestHelpers.swift
index 1f9726c..7729ae6 100644
--- a/SyncbaseCore/Tests/TestHelpers.swift
+++ b/SyncbaseCore/Tests/TestHelpers.swift
@@ -38,7 +38,7 @@
     do {
       // Randomize the name to prevent conflicts between tests
       let dbName = "test\(NSUUID().UUIDString)".stringByReplacingOccurrencesOfString("-", withString: "")
-      let db = try Syncbase.instance.database(Identifier(name: dbName, blessing: anyPermissions))
+      let db = try Syncbase.database(Identifier(name: dbName, blessing: anyPermissions))
       let cleanup = {
         do {
           print("Destroying db \(db)")