Merge branch 'master' of ../../../experimental
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8165887
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+# Build artifacts
+/bin
+/creds
+/gen
+/dart/lib/gen
+
+# Dart artifacts
+.packages
+.pub
+pubspec.lock
+packages
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..f51a72d
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,234 @@
+SHELL := /bin/bash -euo pipefail
+PWD := $(shell pwd)
+V23_GOPATH := $(shell echo `v23 run env | grep GOPATH | cut -d\= -f2`)
+DART_FILES := $(shell find dart/bin dart/lib dart/test sky_demo/lib -name "*.dart" -not -path "dart/lib/gen/*")
+GO_FILES := $(shell find go/src -name "*.go")
+V23_GO_FILES := $(shell find $(V23_ROOT) -name "*.go")
+
+# Flags for Syncbase service running as Mojo service.
+# See v.io/x/ref/runtime/internal/mojo_util.go for the wonderful magic that
+# makes this work.
+# If you change these flags, be sure to do a "make clean", since the flag values
+# get bound at compile time.
+SYNCBASED_ADDR := 127.0.0.1:4002
+V23_MOJO_FLAGS := --v=0 --v23.tcp.address=$(SYNCBASED_ADDR) --v23.permissions.literal={\"Admin\":{\"In\":[\"...\"]},\"Write\":{\"In\":[\"...\"]},\"Read\":{\"In\":[\"...\"]},\"Resolve\":{\"In\":[\"...\"]},\"Debug\":{\"In\":[\"...\"]}}
+
+# MOUNTTABLE_ADDR := 127.0.0.1:4001
+ifdef MOUNTTABLE_ADDR
+	V23_MOJO_FLAGS += --name=syncbase_mojo --v23.namespace.root=/$(MOUNTTABLE_ADDR)
+endif
+
+ifdef ANDROID
+	# Configure compiler and linker for Android.
+	export GOROOT := $(MOJO_DIR)/src/third_party/go/tool/android_arm
+	export CGO_ENABLED := 1
+	export GOOS := android
+	export GOARCH := arm
+	export GOARM := 7
+
+	ANDROID_NDK := $(V23_ROOT)/third_party/android/ndk-toolchain
+
+	export CC := $(ANDROID_NDK)/bin/arm-linux-androideabi-gcc
+	export CXX := $(ANDROID_NDK)/bin/arm-linux-androideabi-g++
+
+	MOJO_ANDROID_FLAGS := --android
+	MOJO_BUILD_DIR := $(MOJO_DIR)/src/out/android_Debug
+	MOJO_SHARED_LIB := $(PWD)/gen/lib/android/libsystem_thunk.a
+	MOJO_SHELL_PATH := $(MOJO_BUILD_DIR)/apks/MojoShell.apk
+	SKY_BUILD_DIR := $(SKY_DIR)/src/out/android_Debug
+	ETHER_BUILD_DIR := $(PWD)/gen/mojo/android
+
+	THIRD_PARTY_LIBS := $(V23_ROOT)/third_party/cout/android_arm
+
+	# NOTE(nlacasse): Trying to write to a directory that the app does not have
+	# permission to causes a crash with no stack trace.  Because of this, we
+	# set logtostderr=true to prevent vlog from writing logs to directories we
+	# don't have permissions on.  (Alternatively, we could set --log_dir to a
+	# directory inside APP_HOME_DIR.)  We set syncbase root-dir inside
+	# APP_HOME_DIR for the same reason.
+	APP_HOME_DIR = /data/data/org.chromium.mojo.shell/app_home
+	V23_MOJO_FLAGS += --logtostderr=true --root-dir=$(APP_HOME_DIR)/syncbase_data
+else
+	# Configure compiler and linker for Linux.
+	export GOROOT := $(MOJO_DIR)/src/third_party/go/tool/linux_amd64
+
+	MOJO_BUILD_DIR := $(MOJO_DIR)/src/out/Debug
+	MOJO_SHARED_LIB := $(PWD)/gen/lib/linux_amd64/libsystem_thunk.a
+	MOJO_SHELL_PATH := $(MOJO_BUILD_DIR)/mojo_shell
+	SKY_BUILD_DIR := $(SKY_DIR)/src/out/Debug
+	ETHER_BUILD_DIR := $(PWD)/gen/mojo/linux_amd64
+
+	THIRD_PARTY_LIBS := $(V23_ROOT)/third_party/cout/linux_amd64
+
+	V23_MOJO_FLAGS += --root-dir=/tmp/syncbase_data
+endif
+
+GOPATH := $(V23_GOPATH):$(MOJO_DIR):$(MOJO_DIR)/third_party/go:$(MOJO_BUILD_DIR)/gen/go:$(PWD)/go:$(PWD)/gen/go
+
+# NOTE(nlacasse): Running Go Mojo services requires passing the
+# --enable-multiprocess flag to mojo_shell.  This is because the Go runtime is
+# very large, and can interfere with C++ memory if they are in the same
+# process.
+MOJO_SHELL_FLAGS := -v --enable-multiprocess \
+	--config-alias MOJO_BUILD_DIR=$(MOJO_BUILD_DIR) \
+	--config-alias SKY_DIR=$(SKY_DIR) \
+	--config-alias SKY_BUILD_DIR=$(SKY_BUILD_DIR) \
+	--config-alias ETHER_DIR=$(PWD) \
+	--config-alias ETHER_BUILD_DIR=$(ETHER_BUILD_DIR)
+
+LDFLAGS := -shared
+
+# Compiles a Go program and links against the Mojo C library.
+# $1 is input filename.
+# $2 is output filename.
+define MOGO_BUILD
+	mkdir -p $(dir $2)
+	GOPATH="$(GOPATH)" \
+	CGO_CFLAGS="-I$(MOJO_DIR)/src $(CGO_CFLAGS)" \
+	CGO_CXXFLAGS="-I$(MOJO_DIR)/src $(CGO_CXXFLAGS)" \
+	CGO_LDFLAGS="-L$(dir $(MOJO_SHARED_LIB)) -lsystem_thunk $(CGO_LDFLAGS)" \
+	$(GOROOT)/bin/go build -o $2 -tags=mojo -ldflags="$(LDFLAGS)" -buildmode=c-shared $1
+	rm -f $(basename $2).h
+endef
+
+# Generates go bindings from .mojom file.
+# $1 is input filename.
+# $2 is output directory.
+# $3 is language (go, dart, ...).
+define MOJOM_GEN
+	mkdir -p $2
+	$(MOJO_DIR)/src/mojo/public/tools/bindings/mojom_bindings_generator.py $1 -d . -o $2 -g $3
+endef
+
+.DELETE_ON_ERROR:
+
+all: test
+
+.PHONY: build
+build: $(ETHER_BUILD_DIR)/echo_server.mojo  $(ETHER_BUILD_DIR)/syncbase_server.mojo
+
+# Builds mounttabled, principal, and syncbased.
+bin: $(V23_GO_FILES) | env-check
+	v23 go build -a -o $@/mounttabled v.io/x/ref/services/mounttable/mounttabled
+	v23 go build -a -o $@/principal v.io/x/ref/cmd/principal
+	v23 go build -a -o $@/syncbased v.io/syncbase/x/ref/services/syncbase/syncbased
+	touch $@
+
+# Mints credentials.
+creds: bin
+	./bin/principal seekblessings --v23.credentials creds
+	touch $@
+
+# Builds the library that Mojo services must be linked with.
+$(MOJO_SHARED_LIB): | env-check
+	mkdir -p $(dir $@)
+	ar rcs $@ $(MOJO_BUILD_DIR)/obj/mojo/public/platform/native/system.system_thunks.o
+
+.PHONY: gen-mojom
+gen-mojom: dart/lib/gen/dart-pkg/mojom/lib/mojo/echo.mojom.dart gen/go/src/mojom/echo/echo.mojom.go
+gen-mojom: dart/lib/gen/dart-pkg/mojom/lib/mojo/syncbase.mojom.dart gen/go/src/mojom/syncbase/syncbase.mojom.go
+
+dart/lib/gen/dart-pkg/mojom/lib/mojo/echo.mojom.dart: mojom/echo.mojom
+dart/lib/gen/dart-pkg/mojom/lib/mojo/syncbase.mojom.dart: mojom/syncbase.mojom
+dart/lib/gen/dart-pkg/mojom/lib/mojo/echo.mojom.dart dart/lib/gen/dart-pkg/mojom/lib/mojo/syncbase.mojom.dart: | env-check
+	$(call MOJOM_GEN,$<,dart/lib/gen,dart)
+	# TODO(nlacasse): mojom_bindings_generator creates bad symlinks on dart
+	# files, so we delete them.  Stop doing this once the generator is fixed.
+	# See https://github.com/domokit/mojo/issues/386
+	rm -f dart/lib/gen/mojom/$(notdir $@)
+
+gen/go/src/mojom/echo/echo.mojom.go: mojom/echo.mojom
+gen/go/src/mojom/syncbase/syncbase.mojom.go: mojom/syncbase.mojom
+gen/go/src/mojom/echo/echo.mojom.go gen/go/src/mojom/syncbase/syncbase.mojom.go: | env-check
+	$(call MOJOM_GEN,$<,gen,go)
+	gofmt -w $@
+
+$(ETHER_BUILD_DIR)/echo_server.mojo: $(GO_FILES) $(MOJO_SHARED_LIB) gen/go/src/mojom/echo/echo.mojom.go | env-check
+	$(call MOGO_BUILD,$(PWD)/go/src/echo_server.go,$@)
+
+# TODO(nlacasse): These target-specific variables will affect this task and all
+# pre-requisite tasks.  Luckily none of the prerequisites require that these
+# variables have their original value, so everything works.  Once we have a
+# prereq that requires the original value, we will need to re-work these
+# variables.
+$(ETHER_BUILD_DIR)/syncbase_server.mojo: CGO_CFLAGS := -I$(THIRD_PARTY_LIBS)/leveldb/include
+$(ETHER_BUILD_DIR)/syncbase_server.mojo: CGO_CXXFLAGS := -I$(THIRD_PARTY_LIBS)/leveldb/include
+$(ETHER_BUILD_DIR)/syncbase_server.mojo: CGO_LDFLAGS := -L$(THIRD_PARTY_LIBS)/leveldb/lib -lleveldb -L$(THIRD_PARTY_LIBS)/snappy/lib -lsnappy
+$(ETHER_BUILD_DIR)/syncbase_server.mojo: LDFLAGS := -X v.io/x/ref/runtime/internal.commandLineFlags '$(V23_MOJO_FLAGS)'
+$(ETHER_BUILD_DIR)/syncbase_server.mojo: $(GO_FILES) $(V23_GO_FILES) $(MOJO_SHARED_LIB) gen/go/src/mojom/syncbase/syncbase.mojom.go | env-check
+	$(call MOGO_BUILD,v.io/syncbase/x/ref/services/syncbase/syncbased,$@)
+
+# Formats dart files to follow dart style conventions.
+.PHONY: dartfmt
+dartfmt:
+	dartfmt --overwrite $(DART_FILES)
+
+# Lints src and test files with dartanalyzer. This takes a few seconds.
+.PHONY: dartanalyzer
+dartanalyzer: dart/packages sky_demo/packages gen-mojom
+	# TODO(nlacasse): Fix dart mojom binding generator so it does not produce
+	# files that violate dartanalyzer.  For now, we use "grep -v" to hide all
+	# hints and warnings from *.mojom.dart files.
+	cd dart && dartanalyzer bin/*.dart lib/*.dart test/*.dart | grep -v "\.mojom\.dart, line"
+	cd sky_demo && dartanalyzer lib/*.dart | grep -v "\.mojom\.dart, line"
+
+# Installs dart dependencies.
+dart/packages: dart/pubspec.yaml
+	cd dart && pub get
+
+# Installs dart dependencies.
+sky_demo/packages: sky_demo/pubspec.yaml
+	cd sky_demo && pub get
+
+.PHONY: run-syncbase-example
+run-syncbase-example: $(ETHER_BUILD_DIR)/syncbase_server.mojo dart/packages dart/lib/gen/dart-pkg/mojom/lib/mojo/syncbase.mojom.dart | env-check
+	$(MOJO_DIR)/src/mojo/devtools/common/mojo_run --config-file $(PWD)/mojoconfig $(MOJO_SHELL_FLAGS) $(MOJO_ANDROID_FLAGS) https://mojo.v.io/syncbase_example.dart
+
+.PHONY: run-echo-example
+run-echo-example: $(ETHER_BUILD_DIR)/echo_server.mojo dart/packages dart/lib/gen/dart-pkg/mojom/lib/mojo/echo.mojom.dart | env-check
+	$(MOJO_DIR)/src/mojo/devtools/common/mojo_run --config-file $(PWD)/mojoconfig $(MOJO_SHELL_FLAGS) $(MOJO_ANDROID_FLAGS) https://mojo.v.io/echo_example.dart
+
+.PHONY: run-sky-demo
+run-sky-demo: $(ETHER_BUILD_DIR)/echo_server.mojo sky_demo/packages $(ETHER_BUILD_DIR)/syncbase_server.mojo dart/lib/gen/dart-pkg/mojom/lib/mojo/echo.mojom.dart dart/lib/gen/dart-pkg/mojom/lib/mojo/syncbase.mojom.dart | env-check
+	$(MOJO_DIR)/src/mojo/devtools/common/mojo_run --config-file $(PWD)/sky_demo/mojoconfig $(MOJO_SHELL_FLAGS) $(MOJO_ANDROID_FLAGS) 'mojo:window_manager https://mojo.v.io/sky_demo/lib/main.dart'
+
+.PHONY: test
+test: dart/packages $(ETHER_BUILD_DIR)/echo_server.mojo $(ETHER_BUILD_DIR)/syncbase_server.mojo gen-mojom | env-check
+	$(MOJO_DIR)/src/mojo/devtools/common/mojo_test --config-file $(PWD)/mojoconfig $(MOJO_SHELL_FLAGS) $(MOJO_ANDROID_FLAGS) --shell-path $(MOJO_SHELL_PATH) tests
+
+.PHONY: env-check
+env-check:
+ifndef MOJO_DIR
+	$(error MOJO_DIR is not set)
+endif
+ifndef SKY_DIR
+	$(error SKY_DIR is not set)
+endif
+ifndef V23_ROOT
+	$(error V23_ROOT is not set)
+endif
+ifeq ($(wildcard $(MOJO_BUILD_DIR)),)
+	$(error ERROR: $(MOJO_BUILD_DIR) does not exist.  Please see README.md for instructions on compiling Mojo resources.)
+endif
+ifeq ($(wildcard $(THIRD_PARTY_LIBS)/*),)
+	ifdef ANDROID
+		$(error ERROR: $(THIRD_PARTY_LIBS) does not exist or is empty.  Please run "GOOS=android GOARCH=arm v23 profile install syncbase")
+	else
+		$(error ERROR: $(THIRD_PARTY_LIBS) does not exist or is empty.  Please run "v23 profile install syncbase")
+	endif
+endif
+ifdef ANDROID
+	ifeq ($(wildcard $(ANDROID_NDK)),)
+		$(error ERROR: $(ANDROID_NDK) does not exist.  Please install android profile with "v23 profile install android")
+	endif
+endif
+
+.PHONY: clean
+clean:
+	rm -rf gen/mojo gen/go
+	rm -rf dart/lib/gen
+
+.PHONY: veryclean
+veryclean: clean
+	rm -rf gen
+	rm -rf {dart,sky_demo}/{.packages,pubspec.lock,packages}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e21f655
--- /dev/null
+++ b/README.md
@@ -0,0 +1,100 @@
+# Ether
+
+This project exposes Syncbase as a Mojo service.
+
+Read the [architecture proposal].
+
+## Initial Mojo setup
+
+You must have the Mojo repo in `$MOJO_DIR`.
+
+This section only needs to be run once.
+
+See the [Mojo readme] for more comprehensive instructions.
+
+### Install Mojo prereqs
+
+1. Install [depot tools].
+2. Install [Goma][goma].
+3. Put the following in your `.bashrc`:
+
+       # NOTE: Actual locations depend on where you installed depot_tools and
+       # goma.
+       export PATH=${PATH}:${HOME}/dev/depot_tools
+       export GOMA_DIR=${HOME}/goma
+       export MOJO_DIR=${HOME}/mojo
+
+### Download Mojo repo
+
+    $ mkdir $MOJO_DIR && cd $MOJO_DIR
+
+    # NOTE: This step takes about 10 min.
+    $ fetch mojo --target_os=android,linux
+
+    # NOTE: This step also takes about 10 min.  Furthermore, the script uses
+    # 'sudo', so you will need to enter your password.
+    $ cd src && ./build/install-build-deps.sh
+
+    # Or, to include Android deps as well:
+    $ cd src && ./build/install-build-deps-android.sh
+
+## Update Mojo and compile resources
+
+This updates the Mojo repo to HEAD, and builds the Mojo resources needed to
+compile Ether.
+
+Run this while you grab your morning coffee.
+
+1. Start by updating the repo.
+
+       $ cd $MOJO_DIR/src
+       $ git checkout master
+       $ git pull
+       $ gclient sync
+
+2. Compile for Linux.  Built resources will be in `$MOJO_DIR/src/out/Debug`
+
+       $ ./mojo/tools/mojob.py gn
+       $ ./mojo/tools/mojob.py build # NOTE: This can take up to 10 minutes.
+
+3. Compile for Android.  Built resources will be in
+   `$MOJO_DIR/src/out/android_Debug`
+
+       $ ./mojo/tools/mojob.py gn --android
+       $ ./mojo/tools/mojob.py build --android # NOTE: This can take up to 10 minutes.
+
+## Sky setup
+
+You must have the Sky code in `$SKY_DIR`. Follow the instructions in the Sky
+[CONTRIBUTING.md][sky contrib] file for "Getting the code" and "Building the
+code".
+
+Also, you must patch your Mojo code (and rebuild Mojo):
+https://github.com/domokit/mojo/issues/370
+
+## Install Dart SDK
+
+To run Dart apps, you must install the Dart SDK.
+
+Googlers: http://go/install-dart
+External: https://www.dartlang.org/downloads/
+
+## Testing
+
+Run the tests:
+
+    make test
+
+This will run all tests listed in the `tests` file in the root directory of
+this repo.
+
+The following command will run a single test file.  This is useful when the
+full test suite hangs with no output.
+
+    $(MOJO_DIR)/src/mojo/devtools/common/mojo_run -v --enable-multiprocess --shell-path $(MOJO_DIR)/src/out/Debug/mojo_shell dart/test/<filename>
+
+[architecture proposal]: https://docs.google.com/document/d/1TyxPYIhj9VBCtY7eAXu_MEV9y0dtRx7n7UY4jm76Qq4/edit
+[depot tools]: http://www.chromium.org/developers/how-tos/install-depot-tools
+[goma]: https://sites.google.com/a/google.com/goma/how-to-use-goma/how-to-use-goma-for-chrome-team
+[mojo readme]: https://github.com/domokit/mojo/blob/master/README.md
+[sky contrib]: https://github.com/domokit/sky_engine/blob/master/CONTRIBUTING.md
diff --git a/dart/bin/echo_example.dart b/dart/bin/echo_example.dart
new file mode 100755
index 0000000..226f1fb
--- /dev/null
+++ b/dart/bin/echo_example.dart
@@ -0,0 +1,18 @@
+#!mojo mojo:dart_content_handler
+import 'package:ether/initialized_application.dart' show InitializedApplication;
+import 'package:ether/echo_client.dart' show EchoClient;
+
+main(List args) async {
+  InitializedApplication app = new InitializedApplication.fromHandle(args[0]);
+  await app.initialized;
+
+  EchoClient c = new EchoClient(
+      app.connectToService, 'https://mojo.v.io/echo_server.mojo');
+
+  String input = 'foobar';
+  String output = await c.echo(input);
+  print('in=$input out=$output match=${input == output}');
+
+  await c.close();
+  await app.close();
+}
diff --git a/dart/bin/syncbase_example.dart b/dart/bin/syncbase_example.dart
new file mode 100644
index 0000000..21a6627
--- /dev/null
+++ b/dart/bin/syncbase_example.dart
@@ -0,0 +1,17 @@
+#!mojo mojo:dart_content_handler
+import 'package:ether/initialized_application.dart' show InitializedApplication;
+import 'package:ether/syncbase_client.dart' show SyncbaseClient;
+
+main(List args) async {
+  InitializedApplication app = new InitializedApplication.fromHandle(args[0]);
+  await app.initialized;
+
+  SyncbaseClient c = new SyncbaseClient(
+      app.connectToService, 'https://mojo.v.io/syncbase_server.mojo');
+
+  bool exists = await c.app('foo').exists();
+  print('app(foo).exists(): $exists');
+
+  await c.close();
+  await app.close();
+}
diff --git a/dart/lib/echo_client.dart b/dart/lib/echo_client.dart
new file mode 100644
index 0000000..64869be
--- /dev/null
+++ b/dart/lib/echo_client.dart
@@ -0,0 +1,34 @@
+library echo_client;
+
+import 'dart:async';
+
+import 'package:mojo/bindings.dart' as bindings;
+
+import 'gen/dart-gen/mojom/lib/mojo/echo.mojom.dart' as mojom;
+
+typedef void ConnectToServiceFn(String url, bindings.ProxyBase proxy);
+
+class EchoClient {
+  final mojom.EchoProxy _proxy;
+
+  EchoClient(ConnectToServiceFn cts, String url)
+      : _proxy = new mojom.EchoProxy.unbound() {
+    print('connecting to $url');
+    cts(url, _proxy);
+    print('connected');
+  }
+
+  // TODO(nlacasse): Is this necessary?
+  Future close({bool immediate: false}) {
+    return _proxy.close(immediate: immediate);
+  }
+
+  Future<String> echo(String s) async {
+    print('calling echoString($s)');
+    mojom.EchoEchoStringResponseParams v = await _proxy.ptr.echoString(s);
+
+    String output = v.value;
+    print('got echo result: $output');
+    return output;
+  }
+}
diff --git a/dart/lib/initialized_application.dart b/dart/lib/initialized_application.dart
new file mode 100644
index 0000000..4447e6f
--- /dev/null
+++ b/dart/lib/initialized_application.dart
@@ -0,0 +1,20 @@
+library initialized_application;
+
+import 'dart:async';
+
+import 'package:mojo/application.dart' show Application;
+import 'package:mojo/core.dart' show MojoHandle;
+
+// InitializedApplication is an Application with a future 'initialized' that is
+// resolved after the 'initialize' method finishes.
+class InitializedApplication extends Application {
+  final _initializeCompleter = new Completer();
+  Future get initialized => _initializeCompleter.future;
+
+  InitializedApplication.fromHandle(int handle)
+      : super.fromHandle(new MojoHandle(handle));
+
+  void initialize(List<String> args, String url) {
+    _initializeCompleter.complete();
+  }
+}
diff --git a/dart/lib/src/app.dart b/dart/lib/src/app.dart
new file mode 100644
index 0000000..6eb61ab
--- /dev/null
+++ b/dart/lib/src/app.dart
@@ -0,0 +1,41 @@
+part of syncbase_client;
+
+class SyncbaseApp extends NamedResource {
+  SyncbaseApp._internal(_proxy, fullName)
+      : super._internal(_proxy, null, fullName);
+
+  // noSqlDatabase returns the noSqlDatabase with the given relativeName.
+  // relativeName must not contain slashes.
+  SyncbaseNoSqlDatabase noSqlDatabase(String relativeName) {
+    return new SyncbaseNoSqlDatabase._internal(_proxy, fullName, relativeName);
+  }
+
+  Future create(mojom.Perms perms) async {
+    var v = await _proxy.ptr.appCreate(fullName, perms);
+    if (isError(v.err)) throw v.err;
+  }
+
+  Future delete() async {
+    var v = await _proxy.ptr.appDelete(fullName);
+    if (isError(v.err)) throw v.err;
+  }
+
+  Future<bool> exists() async {
+    var v = await _proxy.ptr.appExists(fullName);
+    if (isError(v.err)) throw v.err;
+    return v.exists;
+  }
+
+  Future<mojom.Perms> getPermissions() async {
+    var v = await _proxy.ptr.appGetPermissions(fullName);
+    if (isError(v.err)) throw v.err;
+    // TODO(nlacasse): We need to return the version too.  Create a struct type
+    // that combines perms and version?
+    return v.perms;
+  }
+
+  Future setPermissions(mojom.Perms perms, String version) async {
+    var v = await _proxy.ptr.appSetPermissions(fullName, perms, version);
+    if (isError(v.err)) throw v.err;
+  }
+}
diff --git a/dart/lib/src/named_resource.dart b/dart/lib/src/named_resource.dart
new file mode 100644
index 0000000..9b51356
--- /dev/null
+++ b/dart/lib/src/named_resource.dart
@@ -0,0 +1,19 @@
+part of syncbase_client;
+
+// NamedResource is the superclass of resources with names.
+class NamedResource {
+  final String fullName;
+  final String relativeName;
+  final mojom.SyncbaseProxy _proxy;
+
+  NamedResource._internal(
+      mojom.SyncbaseProxy _proxy, String _parentFullName, String relativeName)
+      : this._proxy = _proxy,
+        this.relativeName = relativeName,
+        this.fullName = (_parentFullName == null ? '' : _parentFullName + '/') +
+            relativeName {
+    if (relativeName.contains('/')) {
+      throw 'relativeName cannot contain "/": $relativeName';
+    }
+  }
+}
diff --git a/dart/lib/src/nosql/database.dart b/dart/lib/src/nosql/database.dart
new file mode 100644
index 0000000..afb76ea
--- /dev/null
+++ b/dart/lib/src/nosql/database.dart
@@ -0,0 +1,104 @@
+part of syncbase_client;
+
+class SyncbaseNoSqlDatabase extends NamedResource {
+  SyncbaseNoSqlDatabase._internal(_proxy, _parentFullName, relativeName)
+      : super._internal(_proxy, _parentFullName, relativeName);
+
+  // table returns a table with the given relativeName.  relativeName must not
+  // contain slashes.
+  SyncbaseTable table(String relativeName) {
+    return new SyncbaseTable._internal(_proxy, fullName, relativeName);
+  }
+
+  // syncGroup returns a syncGroup with the given name.
+  SyncbaseSyncGroup syncGroup(String name) {
+    return new SyncbaseSyncGroup._internal(_proxy, this.fullName, name);
+  }
+
+  Future<List<String>> getSyncGroupNames() async {
+    var v = await _proxy.ptr.dbGetSyncGroupNames(fullName);
+    if (isError(v.err)) throw v.err;
+    return v.names;
+  }
+
+  Future create(mojom.Perms perms) async {
+    var v = await _proxy.ptr.dbCreate(fullName, perms);
+    if (isError(v.err)) throw v.err;
+  }
+
+  Future delete() async {
+    var v = await _proxy.ptr.dbDelete(fullName);
+    if (isError(v.err)) throw v.err;
+  }
+
+  Future<bool> exists() async {
+    var v = await _proxy.ptr.dbExists(fullName);
+    if (isError(v.err)) throw v.err;
+    return v.exists;
+  }
+
+  Stream<mojom.Result> exec(String query) {
+    StreamController<mojom.Result> sc = new StreamController();
+    mojom.ExecStream execStream = new ExecStreamImpl._fromStreamController(sc);
+
+    // Call dbExec asynchronously.
+    _proxy.ptr.dbExec(fullName, query, execStream).then((v) {
+      // TODO(nlacasse): Is throwing the correct behavior here?  Consider
+      // returning a tuple (Stream<mojom.Result>, Future) and resolve the
+      // Future at the end of the RPC (with an error if applicable).  Then
+      // errors will be handled the same in this method as in all the other
+      // methods that return Futures.  (Even though the other methods seem to
+      // "throw", they are actually resolving a Future since the function is
+      // declared with "async".)
+      if (isError(v.err)) throw v.err;
+    });
+
+    return sc.stream;
+  }
+
+  // TODO(nlacasse): Make a BatchDatabase class similar to what we did in JS.
+  Future<String> beginBatch(mojom.BatchOptions opts) async {
+    var v = await _proxy.ptr.dbBeginBatch(fullName, opts);
+    if (isError(v.err)) throw v.err;
+    return v.batchDn;
+  }
+
+  Future commit() async {
+    var v = await _proxy.ptr.dbCommit(fullName);
+    if (isError(v.err)) throw v.err;
+  }
+
+  Future abort() async {
+    var v = await _proxy.ptr.dbAbort(fullName);
+    if (isError(v.err)) throw v.err;
+  }
+
+  Future<mojom.Perms> getPermissions() async {
+    var v = await _proxy.ptr.dbGetPermissions(fullName);
+    if (isError(v.err)) throw v.err;
+    // TODO(nlacasse): We need to return the version too.  Create a struct type
+    // that combines perms and version?
+    return v.perms;
+  }
+
+  Future setPermissions(mojom.Perms perms, String version) async {
+    var v = await _proxy.ptr.dbSetPermissions(fullName, perms, version);
+    if (isError(v.err)) throw v.err;
+  }
+}
+
+class ExecStreamImpl implements mojom.ExecStream {
+  final StreamController<mojom.Result> sc;
+  ExecStreamImpl._fromStreamController(this.sc);
+
+  onResult(mojom.Result result) {
+    sc.add(result);
+  }
+
+  onDone(mojom.Error err) {
+    if (isError(err)) {
+      sc.addError(err);
+    }
+    sc.close();
+  }
+}
diff --git a/dart/lib/src/nosql/row.dart b/dart/lib/src/nosql/row.dart
new file mode 100644
index 0000000..882f0b5
--- /dev/null
+++ b/dart/lib/src/nosql/row.dart
@@ -0,0 +1,28 @@
+part of syncbase_client;
+
+class SyncbaseRow extends NamedResource {
+  SyncbaseRow._internal(_proxy, _parentFullName, relativeName)
+      : super._internal(_proxy, _parentFullName, relativeName);
+
+  Future<bool> exists() async {
+    var v = await _proxy.ptr.rowExists(fullName);
+    if (isError(v.err)) throw v.err;
+    return v.exists;
+  }
+
+  Future<List<int>> get() async {
+    var v = await _proxy.ptr.rowGet(fullName);
+    if (isError(v.err)) throw v.err;
+    return v.value;
+  }
+
+  Future put(List<int> value) async {
+    var v = await _proxy.ptr.rowPut(fullName, value);
+    if (isError(v.err)) throw v.err;
+  }
+
+  Future delete() async {
+    var v = await _proxy.ptr.rowDelete(fullName);
+    if (isError(v.err)) throw v.err;
+  }
+}
diff --git a/dart/lib/src/nosql/syncgroup.dart b/dart/lib/src/nosql/syncgroup.dart
new file mode 100644
index 0000000..65ecc68
--- /dev/null
+++ b/dart/lib/src/nosql/syncgroup.dart
@@ -0,0 +1,54 @@
+part of syncbase_client;
+
+class SyncbaseSyncGroup {
+  final String name;
+  final String _dbName;
+  final mojom.SyncbaseProxy _proxy;
+
+  SyncbaseSyncGroup._internal(this._proxy, this._dbName, this.name);
+
+  Future create(
+      mojom.SyncGroupSpec spec, mojom.SyncGroupMemberInfo myInfo) async {
+    var v = await _proxy.ptr.dbCreateSyncGroup(_dbName, name, spec, myInfo);
+    if (isError(v.err)) throw v.err;
+  }
+
+  Future join(mojom.SyncGroupMemberInfo myInfo) async {
+    var v = await _proxy.ptr.dbJoinSyncGroup(_dbName, name, myInfo);
+    if (isError(v.err)) throw v.err;
+  }
+
+  Future leave() async {
+    var v = await _proxy.ptr.dbLeaveSyncGroup(_dbName, name);
+    if (isError(v.err)) throw v.err;
+  }
+
+  Future destroy() async {
+    var v = await _proxy.ptr.dbDestroySyncGroup(_dbName, name);
+    if (isError(v.err)) throw v.err;
+  }
+
+  Future eject(String memberName) async {
+    var v = await _proxy.ptr.dbEjectFromSyncGroup(_dbName, name, memberName);
+    if (isError(v.err)) throw v.err;
+  }
+
+  Future<mojom.SyncGroupSpec> getSpec() async {
+    var v = await _proxy.ptr.dbGetSyncGroupSpec(_dbName, name);
+    if (isError(v.err)) throw v.err;
+    // TODO(nlacasse): We need to return the version too.  Create a struct type
+    // that combines spec and version?
+    return v.spec;
+  }
+
+  Future setSpec(mojom.SyncGroupSpec spec, String version) async {
+    var v = await _proxy.ptr.dbSetSyncGroupSpec(_dbName, name, spec, version);
+    if (isError(v.err)) throw v.err;
+  }
+
+  Future<Map<String, mojom.SyncGroupMemberInfo>> getMembers() async {
+    var v = await _proxy.ptr.dbGetSyncGroupMembers(_dbName, name);
+    if (isError(v.err)) throw v.err;
+    return v.infos;
+  }
+}
diff --git a/dart/lib/src/nosql/table.dart b/dart/lib/src/nosql/table.dart
new file mode 100644
index 0000000..d3409ec
--- /dev/null
+++ b/dart/lib/src/nosql/table.dart
@@ -0,0 +1,84 @@
+part of syncbase_client;
+
+class SyncbaseTable extends NamedResource {
+  SyncbaseTable._internal(_proxy, _parentFullName, relativeName)
+      : super._internal(_proxy, _parentFullName, relativeName);
+
+  SyncbaseRow row(String key) {
+    return new SyncbaseRow._internal(_proxy, fullName, key);
+  }
+
+  Future create(mojom.Perms perms) async {
+    var v = await _proxy.ptr.tableCreate(fullName, perms);
+    if (isError(v.err)) throw v.err;
+  }
+
+  Future delete() async {
+    var v = await _proxy.ptr.tableDelete(fullName);
+    if (isError(v.err)) throw v.err;
+  }
+
+  Future<bool> exists() async {
+    var v = await _proxy.ptr.tableExists(fullName);
+    if (isError(v.err)) throw v.err;
+    return v.exists;
+  }
+
+  Future deleteRowRange(List<int> start, List<int> limit) async {
+    var v = await _proxy.ptr.tableDeleteRowRange(fullName, start, limit);
+    if (isError(v.err)) throw v.err;
+  }
+
+  Stream<mojom.KeyValue> scan(List<int> start, List<int> limit) {
+    StreamController<mojom.KeyValue> sc = new StreamController();
+
+    mojom.ScanStreamStub stub = new mojom.ScanStreamStub.unbound();
+    stub.impl = new ScanStreamImpl._fromStreamController(sc);
+
+    // Call tableScan asynchronously.
+    _proxy.ptr.tableScan(fullName, start, limit, stub).then((v) {
+      // TODO(nlacasse): Is throwing the correct behavior here?  Consider
+      // returning a tuple (Stream<mojom.KeyValue>, Future) and resolve the
+      // Future at the end of the RPC (with an error if applicable).  Then
+      // errors will be handled the same in this method as in all the other
+      // methods that return Futures.  (Even though the other methods seem to
+      // "throw", they are actually resolving a Future since the function is
+      // declared with "async".)
+      if (isError(v.err)) throw v.err;
+    });
+
+    return sc.stream;
+  }
+
+  Future<List<mojom.PrefixPerms>> getPermissions(String key) async {
+    var v = await _proxy.ptr.tableGetPermissions(fullName, key);
+    if (isError(v.err)) throw v.err;
+    return v.permsArr;
+  }
+
+  Future setPermissions(String prefix, mojom.Perms perms) async {
+    var v = await _proxy.ptr.tableSetPermissions(fullName, prefix, perms);
+    if (isError(v.err)) throw v.err;
+  }
+
+  Future deletePermissions(String prefix) async {
+    var v = await _proxy.ptr.tableDeletePermissions(fullName, prefix);
+    if (isError(v.err)) throw v.err;
+  }
+}
+
+class ScanStreamImpl implements mojom.ScanStream {
+  final StreamController<mojom.KeyValue> sc;
+  ScanStreamImpl._fromStreamController(this.sc);
+
+  onKeyValue(mojom.KeyValue keyValue) {
+    sc.add(keyValue);
+  }
+
+  onDone(mojom.Error err) {
+    if (isError(err)) {
+      sc.addError(err);
+    }
+    sc.close();
+  }
+}
diff --git a/dart/lib/syncbase_client.dart b/dart/lib/syncbase_client.dart
new file mode 100644
index 0000000..a61f687
--- /dev/null
+++ b/dart/lib/syncbase_client.dart
@@ -0,0 +1,60 @@
+library syncbase_client;
+
+import 'dart:async';
+
+import 'package:mojo/bindings.dart' as bindings;
+
+import 'gen/dart-gen/mojom/lib/mojo/syncbase.mojom.dart' as mojom;
+
+// Export struct types from syncbase.mojom.
+// TODO(nlacasse): Create wrapper around Perms, and possibly other struct
+// constructors, since the default constructors are not user-friendly.  They
+// take zero arguments, so all fields must be populated with assignments.
+export 'gen/dart-gen/mojom/lib/mojo/syncbase.mojom.dart'
+    show BatchOptions, Perms, SyncGroupMemberInfo, SyncGroupSpec;
+
+part 'src/app.dart';
+part 'src/named_resource.dart';
+part 'src/nosql/database.dart';
+part 'src/nosql/row.dart';
+part 'src/nosql/syncgroup.dart';
+part 'src/nosql/table.dart';
+
+typedef void ConnectToServiceFn(String url, bindings.ProxyBase proxy);
+
+bool isError(mojom.Error err) {
+  return err != null && err.id != '';
+}
+
+class SyncbaseClient {
+  final mojom.SyncbaseProxy _proxy;
+
+  SyncbaseClient(ConnectToServiceFn cts, String url)
+      : _proxy = new mojom.SyncbaseProxy.unbound() {
+    print('connecting to $url');
+    cts(url, _proxy);
+    print('connected');
+  }
+
+  // Closes the connection to the syncbase server.
+  // TODO(nlacasse): Is this necessary?
+  Future close({bool immediate: false}) {
+    return _proxy.close(immediate: immediate);
+  }
+
+  // app returns the app with the given name, which should not contain slashes.
+  SyncbaseApp app(String name) => new SyncbaseApp._internal(_proxy, name);
+
+  Future<mojom.Perms> getPermissions() async {
+    var v = await _proxy.ptr.serviceGetPermissions();
+    if (isError(v.err)) throw v.err;
+    // TODO(nlacasse): We need to return the version too.  Create a struct type
+    // that combines perms and version?
+    return v.perms;
+  }
+
+  Future setPermissions(mojom.Perms perms, String version) async {
+    var v = await _proxy.ptr.serviceSetPermissions(perms, version);
+    if (isError(v.err)) throw v.err;
+  }
+}
diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml
new file mode 100644
index 0000000..bfef881
--- /dev/null
+++ b/dart/pubspec.yaml
@@ -0,0 +1,6 @@
+name: ether
+dependencies:
+  mojo: any
+dev_dependencies:
+  dart_style: any
+  test: any
diff --git a/dart/test/echo_test.dart b/dart/test/echo_test.dart
new file mode 100644
index 0000000..00bbf0c
--- /dev/null
+++ b/dart/test/echo_test.dart
@@ -0,0 +1,48 @@
+#!mojo mojo:dart_content_handler
+import 'dart:async';
+
+import 'package:mojo/core.dart' show MojoHandle;
+import 'package:test/test.dart';
+
+import 'package:ether/echo_client.dart' show EchoClient;
+import 'package:ether/initialized_application.dart' show InitializedApplication;
+
+main(List args) async {
+  InitializedApplication app = new InitializedApplication.fromHandle(args[0]);
+  await app.initialized;
+
+  EchoClient c = new EchoClient(
+      app.connectToService, 'https://mojo.v.io/echo_server.mojo');
+
+  tearDown(() {
+    app.resetConnections();
+  });
+
+  test('echo string returns correct response', () {
+    String input = 'foobee';
+    Future<String> got = c.echo(input);
+    expect(got, completion(equals(input)));
+  });
+
+  test('echo empty string returns correct response', () {
+    String input = '';
+    Future<String> got = c.echo(input);
+    expect(got, completion(equals(input)));
+  });
+
+  // Append a final test to terminate shell connection.
+  // TODO(nlacasse): Remove this once package 'test' supports a global tearDown
+  // callback.  See https://github.com/dart-lang/test/issues/18.
+  test('terminate shell connection', () async {
+    await c.close();
+    expect(MojoHandle.reportLeakedHandles(), isTrue);
+
+    // TODO(nlacasse): When running mojo_shell with --enable-multiprocess,
+    // closing the application causes a non-graceful shutdown.  To avoid this,
+    // we sleep for a second so the test reporter can finish and print the
+    // results before we close the app.  Once mojo_shell can shut down more
+    // gracefully, we should call app.close directly in the test and not in
+    // this Timer.
+    new Timer(new Duration(seconds: 1), app.close);
+  });
+}
diff --git a/dart/test/syncbase_app_test.dart b/dart/test/syncbase_app_test.dart
new file mode 100644
index 0000000..97ef1e5
--- /dev/null
+++ b/dart/test/syncbase_app_test.dart
@@ -0,0 +1,29 @@
+library syncbase_app_test;
+
+import 'package:test/test.dart';
+
+import 'package:ether/syncbase_client.dart' show SyncbaseClient;
+
+import './utils.dart' as utils;
+
+runAppTests(SyncbaseClient c) {
+  test('getting a handle to an app', () {
+    var appName = utils.uniqueName('app');
+    var app = c.app(appName);
+    expect(app.relativeName, equals(appName));
+    expect(app.fullName, equals(appName));
+  });
+
+  test('creating and deleting an app', () async {
+    var appName = utils.uniqueName('app');
+    var app = c.app(appName);
+
+    expect(await app.exists(), equals(false));
+    await app.create(utils.emptyPerms());
+    expect(await app.exists(), equals(true));
+    await app.delete();
+    expect(await app.exists(), equals(false));
+  });
+
+  // TODO(nlacasse): Test app.get/setPermissions.
+}
diff --git a/dart/test/syncbase_database_test.dart b/dart/test/syncbase_database_test.dart
new file mode 100644
index 0000000..dd81c0f
--- /dev/null
+++ b/dart/test/syncbase_database_test.dart
@@ -0,0 +1,32 @@
+library syncbase_database_test;
+
+import 'package:test/test.dart';
+
+import 'package:ether/syncbase_client.dart' show SyncbaseClient;
+
+import './utils.dart' as utils;
+
+runDatabaseTests(SyncbaseClient c) {
+  test('getting a handle to a database', () {
+    var app = c.app(utils.uniqueName('app'));
+    var dbName = utils.uniqueName('db');
+    var db = app.noSqlDatabase(dbName);
+    expect(db.relativeName, equals(dbName));
+    expect(db.fullName, equals(app.fullName + '/' + dbName));
+  });
+
+  test('creating and deleting a database', () async {
+    var app = c.app(utils.uniqueName('app'));
+    await app.create(utils.emptyPerms());
+
+    var db = app.noSqlDatabase(utils.uniqueName('db'));
+
+    expect(await db.exists(), equals(false));
+    await db.create(utils.emptyPerms());
+    expect(await db.exists(), equals(true));
+    await db.delete();
+    expect(await db.exists(), equals(false));
+  });
+
+  // TODO(nlacasse): Test database.get/setPermissions.
+}
diff --git a/dart/test/syncbase_row_test.dart b/dart/test/syncbase_row_test.dart
new file mode 100644
index 0000000..1ce6643
--- /dev/null
+++ b/dart/test/syncbase_row_test.dart
@@ -0,0 +1,51 @@
+library syncbase_row_test;
+
+import 'dart:convert' show UTF8;
+
+import 'package:test/test.dart';
+
+import 'package:ether/syncbase_client.dart' show SyncbaseClient;
+
+import './utils.dart' as utils;
+
+runRowTests(SyncbaseClient c) {
+  test('getting a handle to a row', () {
+    var app = c.app(utils.uniqueName('app'));
+    var db = app.noSqlDatabase(utils.uniqueName('db'));
+    var table = db.table(utils.uniqueName('table'));
+
+    var rowName = utils.uniqueName('row');
+    var row = table.row(rowName);
+
+    expect(row.relativeName, equals(rowName));
+    expect(row.fullName, equals(table.fullName + '/' + rowName));
+  });
+
+  test('putting and getting a row', () async {
+    var app = c.app(utils.uniqueName('app'));
+    await app.create(utils.emptyPerms());
+    var db = app.noSqlDatabase(utils.uniqueName('db'));
+    await db.create(utils.emptyPerms());
+    var table = db.table(utils.uniqueName('table'));
+    await table.create(utils.emptyPerms());
+
+    var row = table.row(utils.uniqueName('row'));
+
+    expect(await row.exists(), equals(false));
+
+    var value1 = UTF8.encode("foo");
+    await row.put(value1);
+
+    expect(await row.exists(), equals(true));
+    expect(await row.get(), equals(value1));
+
+    var value2 = UTF8.encode("bar");
+    await row.put(value2);
+
+    expect(await row.exists(), equals(true));
+    expect(await row.get(), equals(value2));
+
+    await row.delete();
+    expect(await row.exists(), equals(false));
+  });
+}
diff --git a/dart/test/syncbase_table_test.dart b/dart/test/syncbase_table_test.dart
new file mode 100644
index 0000000..7aea3fe
--- /dev/null
+++ b/dart/test/syncbase_table_test.dart
@@ -0,0 +1,63 @@
+library syncbase_table_test;
+
+import 'dart:convert' show UTF8;
+
+import 'package:test/test.dart';
+
+import 'package:ether/syncbase_client.dart' show SyncbaseClient;
+
+import './utils.dart' as utils;
+
+runTableTests(SyncbaseClient c) {
+  test('getting a handle to a table', () {
+    var app = c.app(utils.uniqueName('app'));
+    var db = app.noSqlDatabase(utils.uniqueName('db'));
+    var tableName = utils.uniqueName('table');
+    var table = db.table(tableName);
+    expect(table.relativeName, equals(tableName));
+    expect(table.fullName, equals(db.fullName + '/' + tableName));
+  });
+
+  test('creating and deleting a table', () async {
+    var app = c.app(utils.uniqueName('app'));
+    await app.create(utils.emptyPerms());
+    var db = app.noSqlDatabase(utils.uniqueName('db'));
+    await db.create(utils.emptyPerms());
+
+    var table = db.table(utils.uniqueName('table'));
+
+    expect(await table.exists(), equals(false));
+    await table.create(utils.emptyPerms());
+    expect(await table.exists(), equals(true));
+    await table.delete();
+    expect(await table.exists(), equals(false));
+  });
+
+  test('scanning rows', () async {
+    var app = c.app(utils.uniqueName('app'));
+    await app.create(utils.emptyPerms());
+    var db = app.noSqlDatabase(utils.uniqueName('db'));
+    await db.create(utils.emptyPerms());
+    var table = db.table(utils.uniqueName('table'));
+    await table.create(utils.emptyPerms());
+
+    // Put some rows.
+    var rowNames = [utils.uniqueName('rowA'), utils.uniqueName('rowB')];
+
+    for (var rowName in rowNames) {
+      var row = table.row(rowName);
+      await row.put(UTF8.encode(utils.uniqueName('value')));
+    }
+
+    // Scan!
+    var stream = table.scan(UTF8.encode(''), UTF8.encode('z'));
+
+    var gotRows = await stream.toList();
+    expect(gotRows, hasLength(rowNames.length));
+    expect(gotRows[0].key, equals(rowNames[0]));
+    expect(gotRows[1].key, equals(rowNames[1]));
+
+    // TODO(nlacasse): Write tests that check that 'start' and 'limit' are
+    // working properly.
+  });
+}
diff --git a/dart/test/syncbase_test.dart b/dart/test/syncbase_test.dart
new file mode 100755
index 0000000..f55ae4a
--- /dev/null
+++ b/dart/test/syncbase_test.dart
@@ -0,0 +1,48 @@
+#!mojo mojo:dart_content_handler
+import 'dart:async';
+
+import 'package:mojo/core.dart' show MojoHandle;
+import 'package:test/test.dart';
+
+import 'package:ether/initialized_application.dart' show InitializedApplication;
+import 'package:ether/syncbase_client.dart' show SyncbaseClient;
+
+// Import other test files.
+import './syncbase_app_test.dart' show runAppTests;
+import './syncbase_database_test.dart' show runDatabaseTests;
+import './syncbase_row_test.dart' show runRowTests;
+import './syncbase_table_test.dart' show runTableTests;
+
+main(List args) async {
+  InitializedApplication app = new InitializedApplication.fromHandle(args[0]);
+  await app.initialized;
+
+  SyncbaseClient c = new SyncbaseClient(
+      app.connectToService, 'https://mojo.v.io/syncbase_server.mojo');
+
+  tearDown(() {
+    app.resetConnections();
+  });
+
+  // Run imported tests.
+  runAppTests(c);
+  runDatabaseTests(c);
+  runTableTests(c);
+  runRowTests(c);
+
+  // Append a final test to terminate shell connection.
+  // TODO(nlacasse): Remove this once package 'test' supports a global tearDown
+  // callback.  See https://github.com/dart-lang/test/issues/18.
+  test('terminate shell connection', () async {
+    await c.close();
+    expect(MojoHandle.reportLeakedHandles(), isTrue);
+
+    // TODO(nlacasse): When running mojo_shell with --enable-multiprocess,
+    // closing the application causes a non-graceful shutdown.  To avoid this,
+    // we sleep for a second so the test reporter can finish and print the
+    // results before we close the app.  Once mojo_shell can shut down more
+    // gracefully, we should call app.close directly in the test and not in
+    // this Timer.
+    new Timer(new Duration(seconds: 1), app.close);
+  });
+}
diff --git a/dart/test/utils.dart b/dart/test/utils.dart
new file mode 100644
index 0000000..37c4e8a
--- /dev/null
+++ b/dart/test/utils.dart
@@ -0,0 +1,18 @@
+library utils;
+
+import 'package:ether/syncbase_client.dart' show Perms;
+
+// Returns an empty Perms object.
+Perms emptyPerms() => new Perms()..json = '{}';
+
+// Returns the current timestamp in ms since epoch.
+int timestamp() => new DateTime.now().millisecondsSinceEpoch;
+
+int _nameCounter = timestamp();
+
+// Returns a new unique name.
+String uniqueName(String type) {
+  type ??= 'unknown';
+  _nameCounter++;
+  return type + '-' + _nameCounter.toString();
+}
diff --git a/go/src/echo_server.go b/go/src/echo_server.go
new file mode 100644
index 0000000..f07e438
--- /dev/null
+++ b/go/src/echo_server.go
@@ -0,0 +1,69 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// NOTE(nlacasse): This file was taken from $MOJO_DIR/src/examples/go.  The
+// only changes are the mojom import path.
+
+package main
+
+import (
+	"log"
+
+	"mojo/public/go/application"
+	"mojo/public/go/bindings"
+	"mojo/public/go/system"
+
+	"mojom/echo"
+)
+
+//#include "mojo/public/c/system/types.h"
+import "C"
+
+type echoImpl struct {
+}
+
+func (e *echoImpl) EchoString(in *string) (out *string, err error) {
+	log.Printf("server: %s\n", *in)
+	return in, nil
+}
+
+type delegate struct {
+	stubs []*bindings.Stub
+}
+
+func (d *delegate) Initialize(ctx application.Context) {}
+
+func (d *delegate) Create(req echo.Echo_Request) {
+	stub := echo.NewEchoStub(req, &echoImpl{}, bindings.GetAsyncWaiter())
+	d.stubs = append(d.stubs, stub)
+	go func() {
+		for {
+			if err := stub.ServeRequest(); err != nil {
+				connErr, ok := err.(*bindings.ConnectionError)
+				if !ok || !connErr.Closed() {
+					log.Println(err)
+				}
+				break
+			}
+		}
+	}()
+}
+
+func (d *delegate) AcceptConnection(conn *application.Connection) {
+	conn.ProvideServices(&echo.Echo_ServiceFactory{d})
+}
+
+func (d *delegate) Quit() {
+	for _, stub := range d.stubs {
+		stub.Close()
+	}
+}
+
+//export MojoMain
+func MojoMain(handle C.MojoHandle) C.MojoResult {
+	application.Run(&delegate{}, system.MojoHandle(handle))
+	return C.MOJO_RESULT_OK
+}
+
+func main() {}
diff --git a/mojoconfig b/mojoconfig
new file mode 100644
index 0000000..8fb6c9c
--- /dev/null
+++ b/mojoconfig
@@ -0,0 +1,23 @@
+# Derived from $MOJO_DIR/mojoconfig.
+
+{
+  'dev_servers': [
+    {
+      'host': 'https://mojo.v.io/',
+      'mappings': [
+        ('', [
+          # For echo_server.mojo and syncbase_server.mojo.
+          '@{ETHER_BUILD_DIR}',
+          # For echo_example.dart and syncbase_example.dart.
+          '@{ETHER_DIR}/dart/bin'
+        ]),
+      ],
+    },
+    {
+      'host': 'https://test.mojo.v.io/',
+      'mappings': [
+        ('', ['@{ETHER_DIR}/dart/test']),
+      ],
+    },
+  ],
+}
diff --git a/mojom/echo.mojom b/mojom/echo.mojom
new file mode 100644
index 0000000..14ca169
--- /dev/null
+++ b/mojom/echo.mojom
@@ -0,0 +1,9 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+module mojo;
+
+interface Echo {
+  EchoString(string? value) => (string? value);
+};
diff --git a/mojom/syncbase.mojom b/mojom/syncbase.mojom
new file mode 100644
index 0000000..00a22c5
--- /dev/null
+++ b/mojom/syncbase.mojom
@@ -0,0 +1,143 @@
+// TODO(sadovsky): Copy struct and interface comments from VDL files.
+
+// TODO(sadovsky): This should probably be something else.
+module mojo;
+
+// TODO(sadovsky): Put structs in a separate types.mojom file.
+// TODO(sadovsky): Loose representation of verror. Improve this.
+struct Error {
+  string id;  // empty string means no error
+  uint32 action_code;
+  string msg;
+};
+
+// TODO(sadovsky): Decide how to represent perms.
+struct Perms {
+  string json;
+};
+
+struct BatchOptions {
+  string hint;
+  bool read_only;
+};
+
+struct PrefixPerms {
+  string prefix;
+  Perms perms;
+};
+
+struct SyncGroupSpec {
+  string description;
+  Perms perms;
+  array<string> prefixes;
+  array<string> mount_tables;
+  bool is_private;
+};
+
+struct SyncGroupMemberInfo {
+  uint8 sync_priority;
+};
+
+struct Result {
+  array<array<uint8>> values;
+};
+
+interface ExecStream {
+  OnResult(Result result);
+  OnDone(Error err);
+};
+
+struct KeyValue {
+  string key;
+  array<uint8> value;
+};
+
+interface ScanStream {
+  OnKeyValue(KeyValue key_value);
+  OnDone(Error err);
+};
+
+// TODO(sadovsky): Add schema version to all RPCs. See v.io/c/13734.
+// All 'name' params are service-relative object names.
+
+// Error handling modeled after:
+// https://github.com/domokit/mojo/blob/master/mojo/services/files/public/interfaces/file.mojom
+interface Syncbase {
+  ////////////////////////////////////////
+  // Service
+
+  ServiceGetPermissions() => (Error err, Perms perms, string version);
+  ServiceSetPermissions(Perms perms, string version) => (Error err);
+
+  ////////////////////////////////////////
+  // App
+
+  AppCreate(string name, Perms perms) => (Error err);
+  AppDelete(string name) => (Error err);
+  AppExists(string name) => (Error err, bool exists);
+  AppGetPermissions(string name) => (Error err, Perms perms, string version);
+  AppSetPermissions(string name, Perms perms, string version) => (Error err);
+
+  ////////////////////////////////////////
+  // nosql.Database
+
+  // TODO(sadovsky): Add SchemaMetadata argument.
+  DbCreate(string name, Perms perms) => (Error err);
+  DbDelete(string name) => (Error err);
+  DbExists(string name) => (Error err, bool exists);
+  DbExec(string name, string query, ExecStream stream) => (Error err);
+  DbBeginBatch(string name, BatchOptions? bo) => (Error err, string batch_dn);
+  DbCommit(string name) => (Error err);
+  DbAbort(string name) => (Error err);
+  DbGetPermissions(string name) => (Error err, Perms perms, string version);
+  DbSetPermissions(string name, Perms perms, string version) => (Error err);
+
+  // TODO(sadovsky): Add DatabaseWatcher, BlobManager, and SchemaManager
+  // methods.
+
+  ////////////////////////////////////////
+  // nosql.Database:SyncGroupManager
+
+  DbGetSyncGroupNames(string name) => (Error err, array<string> names);
+  DbCreateSyncGroup(
+      string name, string sg_name, SyncGroupSpec spec,
+      SyncGroupMemberInfo my_info)
+      => (Error err);
+  DbJoinSyncGroup(string name, string sg_name, SyncGroupMemberInfo my_info)
+      => (Error err);
+  DbLeaveSyncGroup(string name, string sg_name) => (Error err);
+  DbDestroySyncGroup(string name, string sg_name) => (Error err);
+  DbEjectFromSyncGroup(string name, string sg_name, string member)
+      => (Error err);
+  DbGetSyncGroupSpec(string name, string sg_name)
+      => (Error err, SyncGroupSpec spec, string version);
+  DbSetSyncGroupSpec(
+      string name, string sg_name, SyncGroupSpec spec, string version)
+      => (Error err);
+  DbGetSyncGroupMembers(string name, string sg_name)
+      => (Error err, map<string, SyncGroupMemberInfo> infos);
+
+  ////////////////////////////////////////
+  // nosql.Table
+
+  TableCreate(string name, Perms perms) => (Error err);
+  TableDelete(string name) => (Error err);
+  TableExists(string name) => (Error err, bool exists);
+  TableDeleteRowRange(string name, array<uint8> start, array<uint8> limit)
+      => (Error err);
+  TableScan(
+      string name, array<uint8> start, array<uint8> limit, ScanStream stream)
+      => (Error err);
+  TableGetPermissions(string name, string key)
+      => (Error err, array<PrefixPerms> perms_arr);
+  TableSetPermissions(string name, string prefix, Perms perms) => (Error err);
+  TableDeletePermissions(string name, string prefix) => (Error err);
+
+  ////////////////////////////////////////
+  // nosql.Row
+
+  RowExists(string name) => (Error err, bool exists);
+  RowGet(string name) => (Error err, array<uint8> value);
+  RowPut(string name, array<uint8> value) => (Error err);
+  RowDelete(string name) => (Error err);
+};
diff --git a/sky_demo/lib/main.dart b/sky_demo/lib/main.dart
new file mode 100644
index 0000000..003f819
--- /dev/null
+++ b/sky_demo/lib/main.dart
@@ -0,0 +1,113 @@
+import 'dart:async';
+import 'dart:convert' show UTF8;
+
+import 'package:sky/mojo/embedder.dart' show embedder;
+import 'package:sky/widgets.dart';
+
+import 'package:ether/echo_client.dart' show EchoClient;
+import 'package:ether/syncbase_client.dart'
+    show Perms, SyncbaseClient, SyncbaseTable;
+
+log(String msg) {
+  DateTime now = new DateTime.now();
+  print('$now $msg');
+}
+
+Perms emptyPerms() => new Perms()..json = '{}';
+
+class DemoApp extends App {
+  final EchoClient _echoClient;
+  final SyncbaseClient _syncbaseClient;
+
+  DemoApp()
+      : _echoClient = new EchoClient(
+            embedder.connectToService, 'https://mojo.v.io/echo_server.mojo'),
+        _syncbaseClient = new SyncbaseClient(embedder.connectToService,
+            'https://mojo.v.io/syncbase_server.mojo');
+
+  int seq = 0;
+  SyncbaseTable tb;
+  String sendMsg, recvMsg, putStr, getStr;
+
+  Future doEcho() async {
+    log('DemoApp.doEcho');
+
+    setState(() {
+      sendMsg = seq.toString();
+      recvMsg = '';
+    });
+    seq++;
+    log('setState sendMsg done');
+
+    String recvMsgAsync = await _echoClient.echo(sendMsg);
+
+    setState(() {
+      recvMsg = recvMsgAsync;
+    });
+    log('setState recvMsg done');
+  }
+
+  Future doSyncbaseInit() async {
+    log('DemoApp.doSyncbaseInit');
+    if (tb != null) {
+      log('syncbase already initialized');
+      return;
+    }
+    // TODO(sadovsky): Handle "already exists" case.
+    var app = _syncbaseClient.app('app');
+    await app.create(emptyPerms());
+    var db = app.noSqlDatabase('db');
+    await db.create(emptyPerms());
+    var table = db.table('table');
+    await table.create(emptyPerms());
+    tb = table;
+    log('syncbase is now initialized');
+  }
+
+  Future doPutGet() async {
+    log('DemoApp.doPutGet');
+    await doSyncbaseInit();
+
+    setState(() {
+      putStr = seq.toString();
+      getStr = '';
+    });
+    seq++;
+    log('setState putStr done');
+
+    // TODO(sadovsky): Switch to tb.put/get once they exist.
+    var row = tb.row('key');
+    await row.put(UTF8.encode(putStr));
+    var getBytes = await row.get();
+
+    setState(() {
+      getStr = UTF8.decode(getBytes);
+    });
+    log('setState getStr done');
+  }
+
+  // TODO(sadovsky): I don't think Sky calls App.close().
+  Future close({bool immediate: false}) async {
+    log('DemoApp.close');
+    await _echoClient.close(immediate: immediate);
+    await _syncbaseClient.close(immediate: immediate);
+  }
+
+  Widget build() {
+    return new Container(
+        decoration:
+            const BoxDecoration(backgroundColor: const Color(0xFF00ACC1)),
+        child: new Flex([
+          new RaisedButton(child: new Text('doEcho'), onPressed: doEcho),
+          new Text('sendMsg: $sendMsg'),
+          new Text('recvMsg: $recvMsg'),
+          new RaisedButton(child: new Text('doPutGet'), onPressed: doPutGet),
+          new Text('putStr: $putStr'),
+          new Text('getStr: $getStr')
+        ], direction: FlexDirection.vertical));
+  }
+}
+
+void main() {
+  runApp(new DemoApp());
+}
diff --git a/sky_demo/mojoconfig b/sky_demo/mojoconfig
new file mode 100644
index 0000000..ab18775
--- /dev/null
+++ b/sky_demo/mojoconfig
@@ -0,0 +1,34 @@
+# Combination of ../mojoconfig and $MOJO_DIR/src/mojo/tools/configs/sky.
+
+{
+  'dev_servers': [
+    {
+      'host': 'https://mojo.v.io/',
+      'mappings': [
+        ('packages/', [
+          # For sky_demo packages.
+          '@{ETHER_DIR}/sky_demo/packages',
+        ]),
+        ('', [
+          # For echo_server.mojo and syncbase_server.mojo.
+          '@{ETHER_BUILD_DIR}',
+          # For sky_demo/lib/main.dart.
+          '@{ETHER_DIR}',
+        ]),
+      ],
+    },
+    {
+      'host': 'https://sky/',
+      'mappings': [
+        ('', [
+          # For sky_viewer.mojo.
+          '@{SKY_BUILD_DIR}'
+        ]),
+      ],
+    }
+  ],
+
+  'content_handlers': {
+    'application/dart': 'https://sky/sky_viewer.mojo',
+  }
+}
diff --git a/sky_demo/pubspec.yaml b/sky_demo/pubspec.yaml
new file mode 100644
index 0000000..ca5894e
--- /dev/null
+++ b/sky_demo/pubspec.yaml
@@ -0,0 +1,8 @@
+name: sky_demo
+dependencies:
+  sky: any
+  sky_tools: any
+  ether: any
+dependency_overrides:
+  ether:
+    path: ../dart
diff --git a/tests b/tests
new file mode 100644
index 0000000..03a494f
--- /dev/null
+++ b/tests
@@ -0,0 +1,14 @@
+# See $MOJO_DIR/src/mojo/devtools/common/mojo_test for the format of this file.
+
+tests = [
+	{
+		"test": "https://test.mojo.v.io/echo_test.dart",
+		"type": "dart",
+		"timeout": 10
+	},
+	{
+		"test": "https://test.mojo.v.io/syncbase_test.dart",
+		"type": "dart",
+		"timeout": 30
+	}
+]
diff --git a/tools/sync_repos.sh b/tools/sync_repos.sh
new file mode 100755
index 0000000..51c69b1
--- /dev/null
+++ b/tools/sync_repos.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+DESKTOP="${DESKTOP:-1}"
+ANDROID="${ANDROID:-1}"
+
+####################
+# Mojo
+
+cd $MOJO_DIR/src
+git pull
+gclient sync
+if [[ "${DESKTOP}" = "1" ]]; then
+  ./mojo/tools/mojob.py gn
+  ./mojo/tools/mojob.py build
+fi
+if [[ "${ANDROID}" = "1" ]]; then
+  ./mojo/tools/mojob.py gn --android
+  ./mojo/tools/mojob.py build --android
+fi
+
+####################
+# Sky
+
+cd $SKY_DIR/src
+git pull
+gclient sync
+if [[ "${DESKTOP}" = "1" ]]; then
+  ./sky/tools/gn
+  ninja -C out/Debug
+fi
+if [[ "${ANDROID}" = "1" ]]; then
+  ./sky/tools/gn --android
+  ninja -C out/android_Debug
+fi