Merge branch 'master' of /tmp/staging2/roadmap.go.syncbase into sb
diff --git a/cmd/sb51/doc.go b/cmd/sb51/doc.go
new file mode 100644
index 0000000..0cd9c36
--- /dev/null
+++ b/cmd/sb51/doc.go
@@ -0,0 +1,55 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Antimony (sb51) is a Syncbase general-purpose client and management utility.
+// It currently supports experimenting with the Syncbase query language.
+//
+// The 'sh' command connects to a specified database on a Syncbase instance,
+// creating it if it does not exist if -create-missing is specified.
+// The user can then enter the following at the command line:
+//     1. dump - to get a dump of the database
+//     2. a syncbase select statement - which is executed and results printed to stdout
+//     3. make-demo - to create demo tables in the database to experiment with, equivalent to -make-demo flag
+//     4. exit (or quit) - to exit the program
+//
+// When the shell is running non-interactively (stdin not connected to a tty),
+// errors cause the shell to exit with a non-zero status.
+//
+// To build client:
+//     v23 go install v.io/syncbase/x/ref/syncbase/sb51
+//
+// To run client:
+//     $V23_ROOT/roadmap/go/bin/sb51 sh <appname> <dbname>
+//
+// Sample run (assuming a syncbase service is mounted at '/:8101/syncbase',
+// otherwise specify using -service flag):
+//     > $V23_ROOT/roadmap/go/bin/sb51 sh -create-missing -make-demo -format=csv demoapp demodb
+//     ? select v.Name, v.Address.State from DemoCustomers where Type(v) = "Customer";
+//     v.Name,v.Address.State
+//     John Smith,CA
+//     Bat Masterson,IA
+//     ? select v.CustId, v.InvoiceNum, v.ShipTo.Zip, v.Amount from DemoCustomers where Type(v) = "Invoice" and v.Amount > 100;
+//     v.CustId,v.InvoiceNum,v.ShipTo.Zip,v.Amount
+//     2,1001,50055,166
+//     2,1002,50055,243
+//     2,1004,50055,787
+//     ? select k, v fro DemoCustomers;
+//     Error:
+//     select k, v fro DemoCustomers
+//                 ^
+//     13: Expected 'from', found fro.
+//     ? select k, v from DemoCustomers;
+//     k,v
+//     001,"{Name: ""John Smith"", Id: 1, Active: true, Address: {Street: ""1 Main St."", City: ""Palo Alto"", State: ""CA"", Zip: ""94303""}, Credit: {Agency: Equifax, Report: EquifaxReport: {Rating: 65}}}"
+//     001001,"{CustId: 1, InvoiceNum: 1000, Amount: 42, ShipTo: {Street: ""1 Main St."", City: ""Palo Alto"", State: ""CA"", Zip: ""94303""}}"
+//     001002,"{CustId: 1, InvoiceNum: 1003, Amount: 7, ShipTo: {Street: ""2 Main St."", City: ""Palo Alto"", State: ""CA"", Zip: ""94303""}}"
+//     001003,"{CustId: 1, InvoiceNum: 1005, Amount: 88, ShipTo: {Street: ""3 Main St."", City: ""Palo Alto"", State: ""CA"", Zip: ""94303""}}"
+//     002,"{Name: ""Bat Masterson"", Id: 2, Active: true, Address: {Street: ""777 Any St."", City: ""Collins"", State: ""IA"", Zip: ""50055""}, Credit: {Agency: TransUnion, Report: TransUnionReport: {Rating: 80}}}"
+//     002001,"{CustId: 2, InvoiceNum: 1001, Amount: 166, ShipTo: {Street: ""777 Any St."", City: ""Collins"", State: ""IA"", Zip: ""50055""}}"
+//     002002,"{CustId: 2, InvoiceNum: 1002, Amount: 243, ShipTo: {Street: ""888 Any St."", City: ""Collins"", State: ""IA"", Zip: ""50055""}}"
+//     002003,"{CustId: 2, InvoiceNum: 1004, Amount: 787, ShipTo: {Street: ""999 Any St."", City: ""Collins"", State: ""IA"", Zip: ""50055""}}"
+//     002004,"{CustId: 2, InvoiceNum: 1006, Amount: 88, ShipTo: {Street: ""101010 Any St."", City: ""Collins"", State: ""IA"", Zip: ""50055""}}"
+//     ? exit;
+//     >
+package main
diff --git a/cmd/sb51/internal/demodb/db.go b/cmd/sb51/internal/demodb/db.go
new file mode 100644
index 0000000..2674361
--- /dev/null
+++ b/cmd/sb51/internal/demodb/db.go
@@ -0,0 +1,138 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package demodb
+
+import (
+	"fmt"
+	"time"
+
+	wire "v.io/syncbase/v23/services/syncbase/nosql"
+	"v.io/syncbase/v23/syncbase/nosql"
+	"v.io/v23/context"
+	"v.io/v23/vdl"
+)
+
+type kv struct {
+	key   string
+	value *vdl.Value
+}
+
+type table struct {
+	name string
+	rows []kv
+}
+
+const demoPrefix = "Demo"
+
+var demoTables = []table{
+	table{
+		name: "Customers",
+		rows: []kv{
+			kv{
+				"001",
+				vdl.ValueOf(Customer{"John Smith", 1, true, AddressInfo{"1 Main St.", "Palo Alto", "CA", "94303"}, CreditReport{Agency: CreditAgencyEquifax, Report: AgencyReportEquifaxReport{EquifaxCreditReport{'A'}}}}),
+			},
+			kv{
+				"001001",
+				vdl.ValueOf(Invoice{1, 1000, 42, AddressInfo{"1 Main St.", "Palo Alto", "CA", "94303"}}),
+			},
+			kv{
+				"001002",
+				vdl.ValueOf(Invoice{1, 1003, 7, AddressInfo{"2 Main St.", "Palo Alto", "CA", "94303"}}),
+			},
+			kv{
+				"001003",
+				vdl.ValueOf(Invoice{1, 1005, 88, AddressInfo{"3 Main St.", "Palo Alto", "CA", "94303"}}),
+			},
+			kv{
+				"002",
+				vdl.ValueOf(Customer{"Bat Masterson", 2, true, AddressInfo{"777 Any St.", "Collins", "IA", "50055"}, CreditReport{Agency: CreditAgencyTransUnion, Report: AgencyReportTransUnionReport{TransUnionCreditReport{80}}}}),
+			},
+			kv{
+				"002001",
+				vdl.ValueOf(Invoice{2, 1001, 166, AddressInfo{"777 Any St.", "Collins", "IA", "50055"}}),
+			},
+			kv{
+				"002002",
+				vdl.ValueOf(Invoice{2, 1002, 243, AddressInfo{"888 Any St.", "Collins", "IA", "50055"}}),
+			},
+			kv{
+				"002003",
+				vdl.ValueOf(Invoice{2, 1004, 787, AddressInfo{"999 Any St.", "Collins", "IA", "50055"}}),
+			},
+			kv{
+				"002004",
+				vdl.ValueOf(Invoice{2, 1006, 88, AddressInfo{"101010 Any St.", "Collins", "IA", "50055"}}),
+			},
+		},
+	},
+	table{
+		name: "Numbers",
+		rows: []kv{
+			kv{
+				"001",
+				vdl.ValueOf(Numbers{byte(12), uint16(1234), uint32(5678), uint64(999888777666), int16(9876), int32(876543), int64(128), float32(3.14159), float64(2.71828182846), complex64(123.0 + 7.0i), complex128(456.789 + 10.1112i)}),
+			},
+			kv{
+				"002",
+				vdl.ValueOf(Numbers{byte(9), uint16(99), uint32(999), uint64(9999999), int16(9), int32(99), int64(88), float32(1.41421356237), float64(1.73205080757), complex64(9.87 + 7.65i), complex128(4.32 + 1.0i)}),
+			},
+			kv{
+				"003",
+				vdl.ValueOf(Numbers{byte(210), uint16(210), uint32(210), uint64(210), int16(210), int32(210), int64(210), float32(210.0), float64(210.0), complex64(210.0 + 0.0i), complex128(210.0 + 0.0i)}),
+			},
+		},
+	},
+	table{
+		name: "Composites",
+		rows: []kv{
+			kv{
+				"uno",
+				vdl.ValueOf(Composite{Array2String{"foo", "bar"}, []int32{1, 2}, map[int32]struct{}{1: struct{}{}, 2: struct{}{}}, map[string]int32{"foo": 1, "bar": 2}}),
+			},
+		},
+	},
+	table{
+		name: "Recursives",
+		rows: []kv{
+			kv{
+				"alpha",
+				vdl.ValueOf(Recursive{nil, &Times{time.Unix(123456789, 42244224), time.Duration(1337)}, map[Array2String]Recursive{
+					Array2String{"a", "b"}: Recursive{},
+					Array2String{"x", "y"}: Recursive{vdl.ValueOf(CreditReport{Agency: CreditAgencyExperian, Report: AgencyReportExperianReport{ExperianCreditReport{ExperianRatingGood}}}), nil, map[Array2String]Recursive{
+						Array2String{"alpha", "beta"}: Recursive{vdl.ValueOf(FooType{Bar: BarType{Baz: BazType{Name: "hello", TitleOrValue: TitleOrValueTypeValue{Value: 42}}}}), nil, nil},
+					}},
+					Array2String{"u", "v"}: Recursive{vdl.ValueOf(vdl.TypeOf(Recursive{})), nil, nil},
+				}}),
+			},
+		},
+	},
+}
+
+// Creates demo tables in the provided database. Tables are deleted and
+// recreated if they already exist.
+func PopulateDemoDB(ctx *context.T, db nosql.Database) error {
+	for i, t := range demoTables {
+		tn := demoPrefix + t.name
+		if err := db.DeleteTable(ctx, tn); err != nil {
+			return fmt.Errorf("failed deleting table %s (%d/%d): %v", tn, i+1, len(demoTables), err)
+		}
+		if err := db.CreateTable(ctx, tn, nil); err != nil {
+			return fmt.Errorf("failed creating table %s (%d/%d): %v", tn, i+1, len(demoTables), err)
+		}
+		if err := nosql.RunInBatch(ctx, db, wire.BatchOptions{}, func(db nosql.BatchDatabase) error {
+			dt := db.Table(tn)
+			for _, kv := range t.rows {
+				if err := dt.Put(ctx, kv.key, kv.value); err != nil {
+					return err
+				}
+			}
+			return nil
+		}); err != nil {
+			return fmt.Errorf("failed populating table %s (%d/%d): %v", tn, i+1, len(demoTables), err)
+		}
+	}
+	return nil
+}
diff --git a/cmd/sb51/internal/demodb/db_objects.vdl b/cmd/sb51/internal/demodb/db_objects.vdl
new file mode 100644
index 0000000..cbf119a
--- /dev/null
+++ b/cmd/sb51/internal/demodb/db_objects.vdl
@@ -0,0 +1,115 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package demodb
+
+import "time"
+
+type AddressInfo struct {
+	Street string
+	City   string
+	State  string
+	Zip    string
+}
+
+type CreditAgency enum {
+	Equifax
+	Experian
+	TransUnion
+}
+
+type ExperianRating enum {
+	Good
+	Bad
+}
+
+type EquifaxCreditReport struct {
+	Rating byte
+}
+
+type ExperianCreditReport struct {
+	Rating ExperianRating
+}
+
+type TransUnionCreditReport struct {
+	Rating	int16
+}
+
+type AgencyReport union {
+	EquifaxReport    EquifaxCreditReport
+	ExperianReport   ExperianCreditReport
+	TransUnionReport TransUnionCreditReport
+}
+
+type CreditReport struct {
+	Agency	CreditAgency
+	Report	AgencyReport
+}
+
+type Customer struct {
+	Name    string
+	Id      int64
+	Active  bool
+	Address AddressInfo
+	Credit  CreditReport
+}
+
+type Invoice struct {
+	CustId     int64
+	InvoiceNum int64
+	Amount     int64
+	ShipTo     AddressInfo
+}
+
+type Numbers struct {
+	B    byte
+	Ui16 uint16
+	Ui32 uint32
+	Ui64 uint64
+	I16  int16
+	I32  int32
+	I64  int64
+	F32  float32
+	F64  float64
+	C64  complex64
+	C128 complex128
+}
+
+type FooType struct {
+	Bar BarType
+}
+
+type BarType struct {
+	Baz BazType
+}
+
+type TitleOrValueType union {
+	Title string
+	Value int64
+}
+
+type BazType struct {
+	Name         string
+	TitleOrValue TitleOrValueType
+}
+
+type Array2String [2]string
+
+type Composite struct {
+	Arr     Array2String
+	ListInt []int32
+	MySet   set[int32]
+	Map     map[string]int32
+}
+
+type Times struct {
+	Stamp    time.Time
+	Interval time.Duration
+}
+
+type Recursive struct {
+	Any   any
+	Maybe ?Times
+	Rec   map[Array2String]Recursive
+}
diff --git a/cmd/sb51/internal/demodb/db_objects.vdl.go b/cmd/sb51/internal/demodb/db_objects.vdl.go
new file mode 100644
index 0000000..23481f8
--- /dev/null
+++ b/cmd/sb51/internal/demodb/db_objects.vdl.go
@@ -0,0 +1,383 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file was auto-generated by the vanadium vdl tool.
+// Source: db_objects.vdl
+
+package demodb
+
+import (
+	// VDL system imports
+	"fmt"
+	"v.io/v23/vdl"
+
+	// VDL user imports
+	"time"
+	_ "v.io/v23/vdlroot/time"
+)
+
+type AddressInfo struct {
+	Street string
+	City   string
+	State  string
+	Zip    string
+}
+
+func (AddressInfo) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.AddressInfo"`
+}) {
+}
+
+type CreditAgency int
+
+const (
+	CreditAgencyEquifax CreditAgency = iota
+	CreditAgencyExperian
+	CreditAgencyTransUnion
+)
+
+// CreditAgencyAll holds all labels for CreditAgency.
+var CreditAgencyAll = [...]CreditAgency{CreditAgencyEquifax, CreditAgencyExperian, CreditAgencyTransUnion}
+
+// CreditAgencyFromString creates a CreditAgency from a string label.
+func CreditAgencyFromString(label string) (x CreditAgency, err error) {
+	err = x.Set(label)
+	return
+}
+
+// Set assigns label to x.
+func (x *CreditAgency) Set(label string) error {
+	switch label {
+	case "Equifax", "equifax":
+		*x = CreditAgencyEquifax
+		return nil
+	case "Experian", "experian":
+		*x = CreditAgencyExperian
+		return nil
+	case "TransUnion", "transunion":
+		*x = CreditAgencyTransUnion
+		return nil
+	}
+	*x = -1
+	return fmt.Errorf("unknown label %q in demodb.CreditAgency", label)
+}
+
+// String returns the string label of x.
+func (x CreditAgency) String() string {
+	switch x {
+	case CreditAgencyEquifax:
+		return "Equifax"
+	case CreditAgencyExperian:
+		return "Experian"
+	case CreditAgencyTransUnion:
+		return "TransUnion"
+	}
+	return ""
+}
+
+func (CreditAgency) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.CreditAgency"`
+	Enum struct{ Equifax, Experian, TransUnion string }
+}) {
+}
+
+type ExperianRating int
+
+const (
+	ExperianRatingGood ExperianRating = iota
+	ExperianRatingBad
+)
+
+// ExperianRatingAll holds all labels for ExperianRating.
+var ExperianRatingAll = [...]ExperianRating{ExperianRatingGood, ExperianRatingBad}
+
+// ExperianRatingFromString creates a ExperianRating from a string label.
+func ExperianRatingFromString(label string) (x ExperianRating, err error) {
+	err = x.Set(label)
+	return
+}
+
+// Set assigns label to x.
+func (x *ExperianRating) Set(label string) error {
+	switch label {
+	case "Good", "good":
+		*x = ExperianRatingGood
+		return nil
+	case "Bad", "bad":
+		*x = ExperianRatingBad
+		return nil
+	}
+	*x = -1
+	return fmt.Errorf("unknown label %q in demodb.ExperianRating", label)
+}
+
+// String returns the string label of x.
+func (x ExperianRating) String() string {
+	switch x {
+	case ExperianRatingGood:
+		return "Good"
+	case ExperianRatingBad:
+		return "Bad"
+	}
+	return ""
+}
+
+func (ExperianRating) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.ExperianRating"`
+	Enum struct{ Good, Bad string }
+}) {
+}
+
+type EquifaxCreditReport struct {
+	Rating byte
+}
+
+func (EquifaxCreditReport) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.EquifaxCreditReport"`
+}) {
+}
+
+type ExperianCreditReport struct {
+	Rating ExperianRating
+}
+
+func (ExperianCreditReport) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.ExperianCreditReport"`
+}) {
+}
+
+type TransUnionCreditReport struct {
+	Rating int16
+}
+
+func (TransUnionCreditReport) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.TransUnionCreditReport"`
+}) {
+}
+
+type (
+	// AgencyReport represents any single field of the AgencyReport union type.
+	AgencyReport interface {
+		// Index returns the field index.
+		Index() int
+		// Interface returns the field value as an interface.
+		Interface() interface{}
+		// Name returns the field name.
+		Name() string
+		// __VDLReflect describes the AgencyReport union type.
+		__VDLReflect(__AgencyReportReflect)
+	}
+	// AgencyReportEquifaxReport represents field EquifaxReport of the AgencyReport union type.
+	AgencyReportEquifaxReport struct{ Value EquifaxCreditReport }
+	// AgencyReportExperianReport represents field ExperianReport of the AgencyReport union type.
+	AgencyReportExperianReport struct{ Value ExperianCreditReport }
+	// AgencyReportTransUnionReport represents field TransUnionReport of the AgencyReport union type.
+	AgencyReportTransUnionReport struct{ Value TransUnionCreditReport }
+	// __AgencyReportReflect describes the AgencyReport union type.
+	__AgencyReportReflect struct {
+		Name  string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.AgencyReport"`
+		Type  AgencyReport
+		Union struct {
+			EquifaxReport    AgencyReportEquifaxReport
+			ExperianReport   AgencyReportExperianReport
+			TransUnionReport AgencyReportTransUnionReport
+		}
+	}
+)
+
+func (x AgencyReportEquifaxReport) Index() int                         { return 0 }
+func (x AgencyReportEquifaxReport) Interface() interface{}             { return x.Value }
+func (x AgencyReportEquifaxReport) Name() string                       { return "EquifaxReport" }
+func (x AgencyReportEquifaxReport) __VDLReflect(__AgencyReportReflect) {}
+
+func (x AgencyReportExperianReport) Index() int                         { return 1 }
+func (x AgencyReportExperianReport) Interface() interface{}             { return x.Value }
+func (x AgencyReportExperianReport) Name() string                       { return "ExperianReport" }
+func (x AgencyReportExperianReport) __VDLReflect(__AgencyReportReflect) {}
+
+func (x AgencyReportTransUnionReport) Index() int                         { return 2 }
+func (x AgencyReportTransUnionReport) Interface() interface{}             { return x.Value }
+func (x AgencyReportTransUnionReport) Name() string                       { return "TransUnionReport" }
+func (x AgencyReportTransUnionReport) __VDLReflect(__AgencyReportReflect) {}
+
+type CreditReport struct {
+	Agency CreditAgency
+	Report AgencyReport
+}
+
+func (CreditReport) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.CreditReport"`
+}) {
+}
+
+type Customer struct {
+	Name    string
+	Id      int64
+	Active  bool
+	Address AddressInfo
+	Credit  CreditReport
+}
+
+func (Customer) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.Customer"`
+}) {
+}
+
+type Invoice struct {
+	CustId     int64
+	InvoiceNum int64
+	Amount     int64
+	ShipTo     AddressInfo
+}
+
+func (Invoice) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.Invoice"`
+}) {
+}
+
+type Numbers struct {
+	B    byte
+	Ui16 uint16
+	Ui32 uint32
+	Ui64 uint64
+	I16  int16
+	I32  int32
+	I64  int64
+	F32  float32
+	F64  float64
+	C64  complex64
+	C128 complex128
+}
+
+func (Numbers) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.Numbers"`
+}) {
+}
+
+type FooType struct {
+	Bar BarType
+}
+
+func (FooType) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.FooType"`
+}) {
+}
+
+type BarType struct {
+	Baz BazType
+}
+
+func (BarType) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.BarType"`
+}) {
+}
+
+type (
+	// TitleOrValueType represents any single field of the TitleOrValueType union type.
+	TitleOrValueType interface {
+		// Index returns the field index.
+		Index() int
+		// Interface returns the field value as an interface.
+		Interface() interface{}
+		// Name returns the field name.
+		Name() string
+		// __VDLReflect describes the TitleOrValueType union type.
+		__VDLReflect(__TitleOrValueTypeReflect)
+	}
+	// TitleOrValueTypeTitle represents field Title of the TitleOrValueType union type.
+	TitleOrValueTypeTitle struct{ Value string }
+	// TitleOrValueTypeValue represents field Value of the TitleOrValueType union type.
+	TitleOrValueTypeValue struct{ Value int64 }
+	// __TitleOrValueTypeReflect describes the TitleOrValueType union type.
+	__TitleOrValueTypeReflect struct {
+		Name  string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.TitleOrValueType"`
+		Type  TitleOrValueType
+		Union struct {
+			Title TitleOrValueTypeTitle
+			Value TitleOrValueTypeValue
+		}
+	}
+)
+
+func (x TitleOrValueTypeTitle) Index() int                             { return 0 }
+func (x TitleOrValueTypeTitle) Interface() interface{}                 { return x.Value }
+func (x TitleOrValueTypeTitle) Name() string                           { return "Title" }
+func (x TitleOrValueTypeTitle) __VDLReflect(__TitleOrValueTypeReflect) {}
+
+func (x TitleOrValueTypeValue) Index() int                             { return 1 }
+func (x TitleOrValueTypeValue) Interface() interface{}                 { return x.Value }
+func (x TitleOrValueTypeValue) Name() string                           { return "Value" }
+func (x TitleOrValueTypeValue) __VDLReflect(__TitleOrValueTypeReflect) {}
+
+type BazType struct {
+	Name         string
+	TitleOrValue TitleOrValueType
+}
+
+func (BazType) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.BazType"`
+}) {
+}
+
+type Array2String [2]string
+
+func (Array2String) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.Array2String"`
+}) {
+}
+
+type Composite struct {
+	Arr     Array2String
+	ListInt []int32
+	MySet   map[int32]struct{}
+	Map     map[string]int32
+}
+
+func (Composite) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.Composite"`
+}) {
+}
+
+type Times struct {
+	Stamp    time.Time
+	Interval time.Duration
+}
+
+func (Times) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.Times"`
+}) {
+}
+
+type Recursive struct {
+	Any   *vdl.Value
+	Maybe *Times
+	Rec   map[Array2String]Recursive
+}
+
+func (Recursive) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.Recursive"`
+}) {
+}
+
+func init() {
+	vdl.Register((*AddressInfo)(nil))
+	vdl.Register((*CreditAgency)(nil))
+	vdl.Register((*ExperianRating)(nil))
+	vdl.Register((*EquifaxCreditReport)(nil))
+	vdl.Register((*ExperianCreditReport)(nil))
+	vdl.Register((*TransUnionCreditReport)(nil))
+	vdl.Register((*AgencyReport)(nil))
+	vdl.Register((*CreditReport)(nil))
+	vdl.Register((*Customer)(nil))
+	vdl.Register((*Invoice)(nil))
+	vdl.Register((*Numbers)(nil))
+	vdl.Register((*FooType)(nil))
+	vdl.Register((*BarType)(nil))
+	vdl.Register((*TitleOrValueType)(nil))
+	vdl.Register((*BazType)(nil))
+	vdl.Register((*Array2String)(nil))
+	vdl.Register((*Composite)(nil))
+	vdl.Register((*Times)(nil))
+	vdl.Register((*Recursive)(nil))
+}
diff --git a/cmd/sb51/internal/demodb/doc.go b/cmd/sb51/internal/demodb/doc.go
new file mode 100644
index 0000000..3daf52f
--- /dev/null
+++ b/cmd/sb51/internal/demodb/doc.go
@@ -0,0 +1,7 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package demodb supports loading an example database into Syncbase for
+// experimentation and testing purposes.
+package demodb
diff --git a/cmd/sb51/internal/reader/reader.go b/cmd/sb51/internal/reader/reader.go
new file mode 100644
index 0000000..930c140
--- /dev/null
+++ b/cmd/sb51/internal/reader/reader.go
@@ -0,0 +1,130 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package reader provides an object that reads queries from various input
+// sources (e.g. stdin, pipe).
+package reader
+
+import (
+	"bufio"
+	"os"
+	"strings"
+	"text/scanner"
+
+	"github.com/peterh/liner"
+)
+
+type T struct {
+	s      scanner.Scanner
+	prompt prompter
+}
+
+func newT(prompt prompter) *T {
+	t := &T{prompt: prompt}
+	t.initScanner("")
+	return t
+}
+
+// Close frees any resources acquired by this reader.
+func (t *T) Close() {
+	t.prompt.Close()
+}
+
+func (t *T) initScanner(input string) {
+	t.s.Init(strings.NewReader(input))
+	// Keep all whitespace.
+	t.s.Whitespace = 0
+}
+
+// GetQuery returns an entire query where queries are delimited by semicolons.
+// GetQuery returns the error io.EOF when there is no more input.
+func (t *T) GetQuery() (string, error) {
+	if t.s.Peek() == scanner.EOF {
+		input, err := t.prompt.InitialPrompt()
+		if err != nil {
+			return "", err
+		}
+		t.initScanner(input)
+	}
+	var query string
+WholeQuery:
+	for true {
+		for tok := t.s.Scan(); tok != scanner.EOF; tok = t.s.Scan() {
+			if tok == ';' {
+				break WholeQuery
+			}
+			query += t.s.TokenText()
+		}
+		input, err := t.prompt.ContinuePrompt()
+		if err != nil {
+			return "", err
+		}
+		t.initScanner(input)
+		query += "\n" // User started a new line.
+	}
+	t.prompt.AppendHistory(query + ";")
+	return query, nil
+}
+
+type prompter interface {
+	Close()
+	InitialPrompt() (string, error)
+	ContinuePrompt() (string, error)
+	AppendHistory(query string)
+}
+
+// noninteractive prompter just blindly reads from stdin.
+type noninteractive struct {
+	input *bufio.Reader
+}
+
+// NewNonInteractive returns a T that simply reads input from stdin. Useful
+// for when the user is piping input from a file or another program.
+func NewNonInteractive() *T {
+	return newT(&noninteractive{bufio.NewReader(os.Stdin)})
+}
+
+func (i *noninteractive) Close() {
+}
+
+func (i *noninteractive) InitialPrompt() (string, error) {
+	return i.input.ReadString('\n')
+}
+
+func (i *noninteractive) ContinuePrompt() (string, error) {
+	return i.input.ReadString('\n')
+}
+
+func (i *noninteractive) AppendHistory(query string) {
+}
+
+// interactive prompter provides a nice prompt for a user to input queries.
+type interactive struct {
+	line *liner.State
+}
+
+// NewInteractive returns a T that prompts the user for input.
+func NewInteractive() *T {
+	i := &interactive{
+		line: liner.NewLiner(),
+	}
+	i.line.SetCtrlCAborts(true)
+	return newT(i)
+}
+
+func (i *interactive) Close() {
+	i.line.Close()
+}
+
+func (i *interactive) InitialPrompt() (string, error) {
+	return i.line.Prompt("? ")
+}
+
+func (i *interactive) ContinuePrompt() (string, error) {
+	return i.line.Prompt("  > ")
+}
+
+func (i *interactive) AppendHistory(query string) {
+	i.line.AppendHistory(query)
+}
diff --git a/cmd/sb51/internal/reader/reader_test.go b/cmd/sb51/internal/reader/reader_test.go
new file mode 100644
index 0000000..de4332c
--- /dev/null
+++ b/cmd/sb51/internal/reader/reader_test.go
@@ -0,0 +1,87 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package reader
+
+import (
+	"io"
+	"reflect"
+	"testing"
+)
+
+type stringPrompter struct {
+	lines []string
+	curr  int
+}
+
+func (s *stringPrompter) Close() {
+}
+
+func (s *stringPrompter) InitialPrompt() (string, error) {
+	if s.curr >= len(s.lines) {
+		return "", io.EOF
+	}
+	q := s.lines[s.curr]
+	s.curr++
+	return q, nil
+}
+
+func (s *stringPrompter) ContinuePrompt() (string, error) {
+	return s.InitialPrompt()
+}
+
+func (s *stringPrompter) AppendHistory(query string) {
+}
+
+func TestGetQuery(t *testing.T) {
+	type testCase struct {
+		lines   []string
+		queries []string
+	}
+
+	tests := []testCase{
+		{ // Single query.
+			[]string{"select k from C;"},
+			[]string{"select k from C"},
+		},
+		{ // Multiple queries.
+			[]string{"select k from C;", "select bar from C;"},
+			[]string{"select k from C", "select bar from C"},
+		},
+		{ // Multiple queries on one line.
+			[]string{"select k from C; select bar from C;"},
+			[]string{"select k from C", " select bar from C"},
+		},
+		{ // Multiple queries without a ; are just one query.
+			[]string{"select k from C select bar from C;"},
+			[]string{"select k from C select bar from C"},
+		},
+		{ // Multiple queries without a ; are just one query.
+			[]string{"select k from C", "select bar from C;"},
+			[]string{"select k from C\nselect bar from C"},
+		},
+		{
+			[]string{"select\tfoo.bar from\nC;"},
+			[]string{"select\tfoo.bar from\nC"},
+		},
+	}
+	for _, test := range tests {
+		r := newT(&stringPrompter{lines: test.lines})
+		var queries []string
+		for true {
+			if q, err := r.GetQuery(); err != nil {
+				if err == io.EOF {
+					break
+				}
+				t.Errorf("test %v: unexpected error: %v", test.lines, err)
+				break
+			} else {
+				queries = append(queries, q)
+			}
+		}
+		if got, want := queries, test.queries; !reflect.DeepEqual(got, want) {
+			t.Errorf("test %#v: got %#v, want %#v", test.lines, got, want)
+		}
+	}
+}
diff --git a/cmd/sb51/internal/writer/doc.go b/cmd/sb51/internal/writer/doc.go
new file mode 100644
index 0000000..315ba57
--- /dev/null
+++ b/cmd/sb51/internal/writer/doc.go
@@ -0,0 +1,8 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package writer provides functions for formatting query results.
+//
+// TODO(ivanpi): Export as VDL formatter library.
+package writer
diff --git a/cmd/sb51/internal/writer/writer.go b/cmd/sb51/internal/writer/writer.go
new file mode 100644
index 0000000..c7900b6
--- /dev/null
+++ b/cmd/sb51/internal/writer/writer.go
@@ -0,0 +1,404 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package writer
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"strconv"
+	"strings"
+	"unicode/utf8"
+
+	"v.io/syncbase/v23/syncbase/nosql"
+	"v.io/v23/vdl"
+	vtime "v.io/v23/vdlroot/time"
+)
+
+type Justification int
+
+const (
+	Unknown Justification = iota
+	Left
+	Right
+)
+
+// WriteTable formats the results as ASCII tables.
+func WriteTable(out io.Writer, columnNames []string, rs nosql.ResultStream) error {
+	// Buffer the results so we can compute the column widths.
+	columnWidths := make([]int, len(columnNames))
+	for i, cName := range columnNames {
+		columnWidths[i] = utf8.RuneCountInString(cName)
+	}
+	justification := make([]Justification, len(columnNames))
+	var results [][]string
+	for rs.Advance() {
+		row := make([]string, len(columnNames))
+		for i, column := range rs.Result() {
+			if i >= len(columnNames) {
+				return errors.New("more columns in result than in columnNames")
+			}
+			if justification[i] == Unknown {
+				justification[i] = getJustification(column)
+			}
+			columnStr := toString(column, false)
+			row[i] = columnStr
+			columnLen := utf8.RuneCountInString(columnStr)
+			if columnLen > columnWidths[i] {
+				columnWidths[i] = columnLen
+			}
+		}
+		results = append(results, row)
+	}
+	if rs.Err() != nil {
+		return rs.Err()
+	}
+
+	writeBorder(out, columnWidths)
+	sep := "| "
+	for i, cName := range columnNames {
+		io.WriteString(out, fmt.Sprintf("%s%*s", sep, columnWidths[i], cName))
+		sep = " | "
+	}
+	io.WriteString(out, " |\n")
+	writeBorder(out, columnWidths)
+	for _, result := range results {
+		sep = "| "
+		for i, column := range result {
+			if justification[i] == Right {
+				io.WriteString(out, fmt.Sprintf("%s%*s", sep, columnWidths[i], column))
+			} else {
+				io.WriteString(out, fmt.Sprintf("%s%-*s", sep, columnWidths[i], column))
+			}
+			sep = " | "
+		}
+		io.WriteString(out, " |\n")
+	}
+	writeBorder(out, columnWidths)
+	return nil
+}
+
+func writeBorder(out io.Writer, columnWidths []int) {
+	sep := "+-"
+	for _, width := range columnWidths {
+		io.WriteString(out, fmt.Sprintf("%s%s", sep, strings.Repeat("-", width)))
+		sep = "-+-"
+	}
+	io.WriteString(out, "-+\n")
+}
+
+func getJustification(val *vdl.Value) Justification {
+	switch val.Kind() {
+	// TODO(kash): Floating point numbers should have the decimal point line up.
+	case vdl.Bool, vdl.Byte, vdl.Uint16, vdl.Uint32, vdl.Uint64, vdl.Int16, vdl.Int32, vdl.Int64,
+		vdl.Float32, vdl.Float64, vdl.Complex64, vdl.Complex128:
+		return Right
+	// TODO(kash): Leave nil values as unknown.
+	default:
+		return Left
+	}
+}
+
+// WriteCSV formats the results as CSV as specified by https://tools.ietf.org/html/rfc4180.
+func WriteCSV(out io.Writer, columnNames []string, rs nosql.ResultStream, delimiter string) error {
+	delim := ""
+	for _, cName := range columnNames {
+		str := doubleQuoteForCSV(cName, delimiter)
+		io.WriteString(out, fmt.Sprintf("%s%s", delim, str))
+		delim = delimiter
+	}
+	io.WriteString(out, "\n")
+	for rs.Advance() {
+		delim := ""
+		for _, column := range rs.Result() {
+			str := doubleQuoteForCSV(toString(column, false), delimiter)
+			io.WriteString(out, fmt.Sprintf("%s%s", delim, str))
+			delim = delimiter
+		}
+		io.WriteString(out, "\n")
+	}
+	return rs.Err()
+}
+
+// doubleQuoteForCSV follows the escaping rules from
+// https://tools.ietf.org/html/rfc4180. In particular, values containing
+// newlines, double quotes, and the delimiter must be enclosed in double
+// quotes.
+func doubleQuoteForCSV(str, delimiter string) string {
+	doubleQuote := strings.Index(str, delimiter) != -1 || strings.Index(str, "\n") != -1
+	if strings.Index(str, "\"") != -1 {
+		str = strings.Replace(str, "\"", "\"\"", -1)
+		doubleQuote = true
+	}
+	if doubleQuote {
+		str = "\"" + str + "\""
+	}
+	return str
+}
+
+// WriteJson formats the result as a JSON array of arrays (rows) of values.
+func WriteJson(out io.Writer, columnNames []string, rs nosql.ResultStream) error {
+	io.WriteString(out, "[")
+	jsonColNames := make([][]byte, len(columnNames))
+	for i, cName := range columnNames {
+		jsonCName, err := json.Marshal(cName)
+		if err != nil {
+			panic(fmt.Sprintf("JSON marshalling failed for column name: %v", err))
+		}
+		jsonColNames[i] = jsonCName
+	}
+	bOpen := "{"
+	for rs.Advance() {
+		io.WriteString(out, bOpen)
+		linestart := "\n  "
+		for i, column := range rs.Result() {
+			str := toJson(column)
+			io.WriteString(out, fmt.Sprintf("%s%s: %s", linestart, jsonColNames[i], str))
+			linestart = ",\n  "
+		}
+		io.WriteString(out, "\n}")
+		bOpen = ", {"
+	}
+	io.WriteString(out, "]\n")
+	return rs.Err()
+}
+
+// Converts VDL value to readable yet parseable string representation.
+// If nested is not set, strings outside composites are left unquoted.
+// TODO(ivanpi): Handle cycles and improve non-tree DAG handling.
+func toString(val *vdl.Value, nested bool) string {
+	switch val.Type() {
+	case vdl.TypeOf(vtime.Time{}), vdl.TypeOf(vtime.Duration{}):
+		s, err := toStringNative(val)
+		if err != nil {
+			panic(fmt.Sprintf("toStringNative failed for builtin time type: %v", err))
+		}
+		if nested {
+			s = strconv.Quote(s)
+		}
+		return s
+	default:
+		// fall through to Kind switch
+	}
+	switch val.Kind() {
+	case vdl.Bool:
+		return fmt.Sprint(val.Bool())
+	case vdl.Byte:
+		return fmt.Sprint(val.Byte())
+	case vdl.Uint16, vdl.Uint32, vdl.Uint64:
+		return fmt.Sprint(val.Uint())
+	case vdl.Int16, vdl.Int32, vdl.Int64:
+		return fmt.Sprint(val.Int())
+	case vdl.Float32, vdl.Float64:
+		return fmt.Sprint(val.Float())
+	case vdl.Complex64, vdl.Complex128:
+		c := val.Complex()
+		return fmt.Sprintf("%v+%vi", real(c), imag(c))
+	case vdl.String:
+		s := val.RawString()
+		if nested {
+			s = strconv.Quote(s)
+		}
+		return s
+	case vdl.Enum:
+		return val.EnumLabel()
+	case vdl.Array, vdl.List:
+		return listToString("[", ", ", "]", val.Len(), func(i int) string {
+			return toString(val.Index(i), true)
+		})
+	case vdl.Any, vdl.Optional:
+		if val.IsNil() {
+			if nested {
+				return "nil"
+			}
+			// TODO(ivanpi): Blank is better for CSV, but <nil> might be better for table and TSV.
+			return ""
+		}
+		return toString(val.Elem(), nested)
+	case vdl.Struct:
+		return listToString("{", ", ", "}", val.Type().NumField(), func(i int) string {
+			field := toString(val.StructField(i), true)
+			return fmt.Sprintf("%s: %s", val.Type().Field(i).Name, field)
+		})
+	case vdl.Union:
+		ui, uv := val.UnionField()
+		field := toString(uv, true)
+		return fmt.Sprintf("%s: %s", val.Type().Field(ui).Name, field)
+	case vdl.Set:
+		// TODO(ivanpi): vdl.SortValuesAsString() used for predictable output ordering.
+		// Use a more sensible sort for numbers etc.
+		keys := vdl.SortValuesAsString(val.Keys())
+		return listToString("{", ", ", "}", len(keys), func(i int) string {
+			return toString(keys[i], true)
+		})
+	case vdl.Map:
+		// TODO(ivanpi): vdl.SortValuesAsString() used for predictable output ordering.
+		// Use a more sensible sort for numbers etc.
+		keys := vdl.SortValuesAsString(val.Keys())
+		return listToString("{", ", ", "}", len(keys), func(i int) string {
+			k := toString(keys[i], true)
+			v := toString(val.MapIndex(keys[i]), true)
+			return fmt.Sprintf("%s: %s", k, v)
+		})
+	case vdl.TypeObject:
+		return val.String()
+	default:
+		panic(fmt.Sprintf("unknown Kind %s", val.Kind()))
+	}
+}
+
+// Converts a VDL value to string using the corresponding native type String()
+// method.
+func toStringNative(val *vdl.Value) (string, error) {
+	var natVal interface{}
+	if err := vdl.Convert(&natVal, val); err != nil {
+		return "", fmt.Errorf("failed converting %s to native value: %v", val.Type().String(), err)
+	}
+	if _, ok := natVal.(*vdl.Value); ok {
+		return "", fmt.Errorf("failed converting %s to native value: got vdl.Value", val.Type().String())
+	}
+	if strNatVal, ok := natVal.(fmt.Stringer); !ok {
+		return "", fmt.Errorf("native value of %s doesn't implement String()", val.Type().String())
+	} else {
+		return strNatVal.String(), nil
+	}
+}
+
+// Stringifies a sequence of n elements, where element i string representation
+// is obtained using elemToString(i),
+func listToString(begin, sep, end string, n int, elemToString func(i int) string) string {
+	elems := make([]string, n)
+	for i, _ := range elems {
+		elems[i] = elemToString(i)
+	}
+	return begin + strings.Join(elems, sep) + end
+}
+
+// Converts VDL value to JSON representation.
+func toJson(val *vdl.Value) string {
+	jf := toJsonFriendly(val)
+	jOut, err := json.Marshal(jf)
+	if err != nil {
+		panic(fmt.Sprintf("JSON marshalling failed: %v", err))
+	}
+	return string(jOut)
+}
+
+// Converts VDL value to Go type compatible with json.Marshal().
+func toJsonFriendly(val *vdl.Value) interface{} {
+	switch val.Type() {
+	case vdl.TypeOf(vtime.Time{}), vdl.TypeOf(vtime.Duration{}):
+		s, err := toStringNative(val)
+		if err != nil {
+			panic(fmt.Sprintf("toStringNative failed for builtin time type: %v", err))
+		}
+		return s
+	default:
+		// fall through to Kind switch
+	}
+	switch val.Kind() {
+	case vdl.Bool:
+		return val.Bool()
+	case vdl.Byte:
+		return val.Byte()
+	case vdl.Uint16, vdl.Uint32, vdl.Uint64:
+		return val.Uint()
+	case vdl.Int16, vdl.Int32, vdl.Int64:
+		return val.Int()
+	case vdl.Float32, vdl.Float64:
+		return val.Float()
+	case vdl.Complex64, vdl.Complex128:
+		// Go doesn't support marshalling complex values, we need to stringify.
+		c := val.Complex()
+		return fmt.Sprintf("%v+%vi", real(c), imag(c))
+	case vdl.String:
+		return val.RawString()
+	case vdl.Enum:
+		return val.EnumLabel()
+	case vdl.Array, vdl.List:
+		arr := make([]interface{}, val.Len())
+		for i, _ := range arr {
+			arr[i] = toJsonFriendly(val.Index(i))
+		}
+		return arr
+	case vdl.Any, vdl.Optional:
+		if val.IsNil() {
+			return nil
+		}
+		return toJsonFriendly(val.Elem())
+	case vdl.Struct:
+		// TODO(ivanpi): Consider lowercasing field names.
+		return toOrderedMap(val.Type().NumField(), func(i int) (string, interface{}) {
+			return val.Type().Field(i).Name, toJsonFriendly(val.StructField(i))
+		})
+	case vdl.Union:
+		// TODO(ivanpi): Consider lowercasing field name.
+		ui, uv := val.UnionField()
+		return toOrderedMap(1, func(_ int) (string, interface{}) {
+			return val.Type().Field(ui).Name, toJsonFriendly(uv)
+		})
+	case vdl.Set:
+		// TODO(ivanpi): vdl.SortValuesAsString() used for predictable output ordering.
+		// Use a more sensible sort for numbers etc.
+		keys := vdl.SortValuesAsString(val.Keys())
+		return toOrderedMap(len(keys), func(i int) (string, interface{}) {
+			return toString(keys[i], false), true
+		})
+	case vdl.Map:
+		// TODO(ivanpi): vdl.SortValuesAsString() used for predictable output ordering.
+		// Use a more sensible sort for numbers etc.
+		keys := vdl.SortValuesAsString(val.Keys())
+		return toOrderedMap(len(keys), func(i int) (string, interface{}) {
+			return toString(keys[i], false), toJsonFriendly(val.MapIndex(keys[i]))
+		})
+	case vdl.TypeObject:
+		return val.String()
+	default:
+		panic(fmt.Sprintf("unknown Kind %s", val.Kind()))
+	}
+}
+
+// Serializes to JSON object, preserving key order.
+// Native Go map will serialize to JSON object with sorted keys, which is
+// unexpected behaviour for a struct.
+type orderedMap []orderedMapElem
+
+type orderedMapElem struct {
+	Key string
+	Val interface{}
+}
+
+var _ json.Marshaler = (*orderedMap)(nil)
+
+// Builds an orderedMap with n elements, obtaining the key and value of element
+// i using elemToKeyVal(i).
+func toOrderedMap(n int, elemToKeyVal func(i int) (string, interface{})) orderedMap {
+	om := make(orderedMap, n)
+	for i, _ := range om {
+		om[i].Key, om[i].Val = elemToKeyVal(i)
+	}
+	return om
+}
+
+// Serializes orderedMap to JSON object, preserving key order.
+func (om orderedMap) MarshalJSON() (_ []byte, rerr error) {
+	defer func() {
+		if r := recover(); r != nil {
+			rerr = fmt.Errorf("orderedMap: %v", r)
+		}
+	}()
+	return []byte(listToString("{", ",", "}", len(om), func(i int) string {
+		keyJson, err := json.Marshal(om[i].Key)
+		if err != nil {
+			panic(err)
+		}
+		valJson, err := json.Marshal(om[i].Val)
+		if err != nil {
+			panic(err)
+		}
+		return fmt.Sprintf("%s:%s", keyJson, valJson)
+	})), nil
+}
diff --git a/cmd/sb51/internal/writer/writer_test.go b/cmd/sb51/internal/writer/writer_test.go
new file mode 100644
index 0000000..3d7c1f2
--- /dev/null
+++ b/cmd/sb51/internal/writer/writer_test.go
@@ -0,0 +1,564 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package writer_test
+
+import (
+	"bytes"
+	"encoding/json"
+	"testing"
+	"time"
+
+	"v.io/syncbase/v23/syncbase/nosql"
+	db "v.io/syncbase/x/ref/syncbase/sb51/internal/demodb"
+	"v.io/syncbase/x/ref/syncbase/sb51/internal/writer"
+	"v.io/v23/vdl"
+)
+
+type fakeResultStream struct {
+	rows [][]*vdl.Value
+	curr int
+}
+
+var (
+	customer = db.Customer{
+		Name:   "John Smith",
+		Id:     1,
+		Active: true,
+		Address: db.AddressInfo{
+			Street: "1 Main St.",
+			City:   "Palo Alto",
+			State:  "CA",
+			Zip:    "94303",
+		},
+		Credit: db.CreditReport{
+			Agency: db.CreditAgencyEquifax,
+			Report: db.AgencyReportEquifaxReport{Value: db.EquifaxCreditReport{Rating: 'A'}},
+		},
+	}
+	invoice = db.Invoice{
+		CustId:     1,
+		InvoiceNum: 1000,
+		Amount:     42,
+		ShipTo: db.AddressInfo{
+			Street: "1 Main St.",
+			City:   "Palo Alto",
+			State:  "CA",
+			Zip:    "94303",
+		},
+	}
+)
+
+func array2String(s1, s2 string) db.Array2String {
+	a := [2]string{s1, s2}
+	return db.Array2String(a)
+}
+
+func newResultStream(iRows [][]interface{}) nosql.ResultStream {
+	vRows := make([][]*vdl.Value, len(iRows))
+	for i, iRow := range iRows {
+		vRow := make([]*vdl.Value, len(iRow))
+		for j, iCol := range iRow {
+			vRow[j] = vdl.ValueOf(iCol)
+		}
+		vRows[i] = vRow
+	}
+	return &fakeResultStream{
+		rows: vRows,
+		curr: -1,
+	}
+}
+
+func (f *fakeResultStream) Advance() bool {
+	f.curr++
+	return f.curr < len(f.rows)
+}
+
+func (f *fakeResultStream) Result() []*vdl.Value {
+	if f.curr == -1 {
+		panic("call advance first")
+	}
+	return f.rows[f.curr]
+}
+
+func (f *fakeResultStream) Err() error {
+	return nil
+}
+
+func (f *fakeResultStream) Cancel() {
+	// Nothing to do.
+}
+
+func TestWriteTable(t *testing.T) {
+	type testCase struct {
+		columns []string
+		rows    [][]interface{}
+		// To make the test cases easier to read, output should have a leading
+		// newline.
+		output string
+	}
+	tests := []testCase{
+		{
+			[]string{"c1", "c2"},
+			[][]interface{}{
+				{5, "foo"},
+				{6, "bar"},
+			},
+			`
++----+-----+
+| c1 |  c2 |
++----+-----+
+|  5 | foo |
+|  6 | bar |
++----+-----+
+`,
+		},
+		{
+			[]string{"c1", "c2"},
+			[][]interface{}{
+				{500, "foo"},
+				{6, "barbaz"},
+			},
+			`
++-----+--------+
+|  c1 |     c2 |
++-----+--------+
+| 500 | foo    |
+|   6 | barbaz |
++-----+--------+
+`,
+		},
+		{
+			[]string{"c1", "reallylongcolumnheader"},
+			[][]interface{}{
+				{5, "foo"},
+				{6, "bar"},
+			},
+			`
++----+------------------------+
+| c1 | reallylongcolumnheader |
++----+------------------------+
+|  5 | foo                    |
+|  6 | bar                    |
++----+------------------------+
+`,
+		},
+		{ // Numbers.
+			[]string{"byte", "uint16", "uint32", "uint64", "int16", "int32", "int64",
+				"float32", "float64", "complex64", "complex128"},
+			[][]interface{}{
+				{
+					byte(12), uint16(1234), uint32(5678), uint64(999888777666), int16(9876), int32(876543), int64(128),
+					float32(3.14159), float64(2.71828182846), complex64(123.0 + 7.0i), complex128(456.789 + 10.1112i),
+				},
+				{
+					byte(9), uint16(99), uint32(999), uint64(9999999), int16(9), int32(99), int64(88),
+					float32(1.41421356237), float64(1.73205080757), complex64(9.87 + 7.65i), complex128(4.32 + 1.0i),
+				},
+			},
+			`
++------+--------+--------+--------------+-------+--------+-------+--------------------+---------------+--------------------------------------+------------------+
+| byte | uint16 | uint32 |       uint64 | int16 |  int32 | int64 |            float32 |       float64 |                            complex64 |       complex128 |
++------+--------+--------+--------------+-------+--------+-------+--------------------+---------------+--------------------------------------+------------------+
+|   12 |   1234 |   5678 | 999888777666 |  9876 | 876543 |   128 |  3.141590118408203 | 2.71828182846 |                               123+7i | 456.789+10.1112i |
+|    9 |     99 |    999 |      9999999 |     9 |     99 |    88 | 1.4142135381698608 | 1.73205080757 | 9.869999885559082+7.650000095367432i |          4.32+1i |
++------+--------+--------+--------------+-------+--------+-------+--------------------+---------------+--------------------------------------+------------------+
+`,
+		},
+		{ // Strings with whitespace should be printed literally.
+			[]string{"c1", "c2"},
+			[][]interface{}{
+				{"foo\tbar", "foo\nbar"},
+			},
+			`
++---------+---------+
+|      c1 |      c2 |
++---------+---------+
+| foo	bar | foo
+bar |
++---------+---------+
+`,
+		},
+		{ // nil is shown as blank.
+			[]string{"c1"},
+			[][]interface{}{
+				{nil},
+			},
+			`
++----+
+| c1 |
++----+
+|    |
++----+
+`,
+		},
+		{
+			[]string{"c1"},
+			[][]interface{}{{customer}, {invoice}},
+			`
++------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+|                                                                                                                                                                                       c1 |
++------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+| {Name: "John Smith", Id: 1, Active: true, Address: {Street: "1 Main St.", City: "Palo Alto", State: "CA", Zip: "94303"}, Credit: {Agency: Equifax, Report: EquifaxReport: {Rating: 65}}} |
+| {CustId: 1, InvoiceNum: 1000, Amount: 42, ShipTo: {Street: "1 Main St.", City: "Palo Alto", State: "CA", Zip: "94303"}}                                                                  |
++------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+`,
+		},
+		{
+			[]string{"c1"},
+			[][]interface{}{
+				{db.Composite{array2String("foo", "棎鶊鵱"), []int32{1, 2}, map[int32]struct{}{1: struct{}{}, 2: struct{}{}}, map[string]int32{"foo": 1, "bar": 2}}},
+			},
+			`
++----------------------------------------------------------------------------------+
+|                                                                               c1 |
++----------------------------------------------------------------------------------+
+| {Arr: ["foo", "棎鶊鵱"], ListInt: [1, 2], MySet: {1, 2}, Map: {"bar": 2, "foo": 1}} |
++----------------------------------------------------------------------------------+
+`,
+		},
+		{ // Types not built in to Go.
+			[]string{"time", "type", "union", "enum", "set"},
+			[][]interface{}{
+				{time.Unix(13377331, 0), vdl.TypeOf(map[float32]struct{ B bool }{}), db.TitleOrValueTypeTitle{"dahar master"}, db.ExperianRatingBad, map[int32]struct{}{47: struct{}{}}},
+			},
+			`
++-------------------------------+----------------------------------------+-----------------------+------+------+
+|                          time |                                   type |                 union | enum |  set |
++-------------------------------+----------------------------------------+-----------------------+------+------+
+| 1970-06-04 19:55:31 +0000 UTC | typeobject(map[float32]struct{B bool}) | Title: "dahar master" | Bad  | {47} |
++-------------------------------+----------------------------------------+-----------------------+------+------+
+`,
+		},
+		{
+			[]string{"c1"},
+			[][]interface{}{
+				{
+					db.Recursive{
+						Any: nil,
+						Maybe: &db.Times{
+							Stamp:    time.Unix(123456789, 42244224),
+							Interval: time.Duration(13377331),
+						},
+						Rec: map[db.Array2String]db.Recursive{
+							array2String("a", "b"): db.Recursive{},
+							array2String("x\nx", "y\"y"): db.Recursive{
+								Any:   vdl.ValueOf(db.AgencyReportExperianReport{Value: db.ExperianCreditReport{Rating: db.ExperianRatingGood}}),
+								Maybe: nil,
+								Rec:   nil,
+							},
+						},
+					},
+				},
+			},
+			`
++----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+|                                                                                                                                                                                                                               c1 |
++----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+| {Any: nil, Maybe: {Stamp: "1973-11-29 21:33:09.042244224 +0000 UTC", Interval: "13.377331ms"}, Rec: {["a", "b"]: {Any: nil, Maybe: nil, Rec: {}}, ["x\nx", "y\"y"]: {Any: ExperianReport: {Rating: Good}, Maybe: nil, Rec: {}}}} |
++----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+`,
+		},
+	}
+	for _, test := range tests {
+		var b bytes.Buffer
+		if err := writer.WriteTable(&b, test.columns, newResultStream(test.rows)); err != nil {
+			t.Errorf("Unexpected error: %v", err)
+			continue
+		}
+		// Add a leading newline to the output to match the leading newline
+		// in our test cases.
+		if got, want := "\n"+b.String(), test.output; got != want {
+			t.Errorf("Wrong output:\nGOT:%s\nWANT:%s", got, want)
+		}
+	}
+}
+
+func TestWriteCSV(t *testing.T) {
+	type testCase struct {
+		columns   []string
+		rows      [][]interface{}
+		delimiter string
+		// To make the test cases easier to read, output should have a leading
+		// newline.
+		output string
+	}
+	tests := []testCase{
+		{ // Basic.
+			[]string{"c1", "c2"},
+			[][]interface{}{
+				{5, "foo"},
+				{6, "bar"},
+			},
+			",",
+			`
+c1,c2
+5,foo
+6,bar
+`,
+		},
+		{ // Numbers.
+			[]string{"byte", "uint16", "uint32", "uint64", "int16", "int32", "int64",
+				"float32", "float64", "complex64", "complex128"},
+			[][]interface{}{
+				{
+					byte(12), uint16(1234), uint32(5678), uint64(999888777666), int16(9876), int32(876543), int64(128),
+					float32(3.14159), float64(2.71828182846), complex64(123.0 + 7.0i), complex128(456.789 + 10.1112i),
+				},
+				{
+					byte(9), uint16(99), uint32(999), uint64(9999999), int16(9), int32(99), int64(88),
+					float32(1.41421356237), float64(1.73205080757), complex64(9.87 + 7.65i), complex128(4.32 + 1.0i),
+				},
+			},
+			",",
+			`
+byte,uint16,uint32,uint64,int16,int32,int64,float32,float64,complex64,complex128
+12,1234,5678,999888777666,9876,876543,128,3.141590118408203,2.71828182846,123+7i,456.789+10.1112i
+9,99,999,9999999,9,99,88,1.4142135381698608,1.73205080757,9.869999885559082+7.650000095367432i,4.32+1i
+`,
+		},
+		{
+			// Values containing newlines, double quotes, and the delimiter must be
+			// enclosed in double quotes.
+			[]string{"c1", "c2"},
+			[][]interface{}{
+				{"foo\tbar", "foo\nbar"},
+				{"foo\"bar\"", "foo,bar"},
+			},
+			",",
+			`
+c1,c2
+foo	bar,"foo
+bar"
+"foo""bar""","foo,bar"
+`,
+		},
+		{ // Delimiters other than comma should be supported.
+			[]string{"c1", "c2"},
+			[][]interface{}{
+				{"foo\tbar", "foo\nbar"},
+				{"foo\"bar\"", "foo,bar"},
+			},
+			"\t",
+			`
+c1	c2
+"foo	bar"	"foo
+bar"
+"foo""bar"""	foo,bar
+`,
+		},
+		{ // Column names should be escaped properly.
+			[]string{"foo\tbar", "foo,bar"},
+			[][]interface{}{},
+			",",
+			`
+foo	bar,"foo,bar"
+`,
+		},
+		{ // Same as above but use a non-default delimiter.
+			[]string{"foo\tbar", "foo,棎鶊鵱"},
+			[][]interface{}{},
+			"\t",
+			`
+"foo	bar"	foo,棎鶊鵱
+`,
+		},
+		{
+			[]string{"c1"},
+			[][]interface{}{{customer}, {invoice}},
+			",",
+			`
+c1
+"{Name: ""John Smith"", Id: 1, Active: true, Address: {Street: ""1 Main St."", City: ""Palo Alto"", State: ""CA"", Zip: ""94303""}, Credit: {Agency: Equifax, Report: EquifaxReport: {Rating: 65}}}"
+"{CustId: 1, InvoiceNum: 1000, Amount: 42, ShipTo: {Street: ""1 Main St."", City: ""Palo Alto"", State: ""CA"", Zip: ""94303""}}"
+`,
+		},
+	}
+	for _, test := range tests {
+		var b bytes.Buffer
+		if err := writer.WriteCSV(&b, test.columns, newResultStream(test.rows), test.delimiter); err != nil {
+			t.Errorf("Unexpected error: %v", err)
+			continue
+		}
+		// Add a leading newline to the output to match the leading newline
+		// in our test cases.
+		if got, want := "\n"+b.String(), test.output; got != want {
+			t.Errorf("Wrong output:\nGOT: %q\nWANT:%q", got, want)
+		}
+	}
+}
+
+func TestWriteJson(t *testing.T) {
+	type testCase struct {
+		columns []string
+		rows    [][]interface{}
+		// To make the test cases easier to read, output should have a leading
+		// newline.
+		output string
+	}
+	tests := []testCase{
+		{ // Basic.
+			[]string{"c\n1", "c鶊2"},
+			[][]interface{}{
+				{5, "foo\nbar"},
+				{6, "bar\tfoo"},
+			},
+			`
+[{
+  "c\n1": 5,
+  "c鶊2": "foo\nbar"
+}, {
+  "c\n1": 6,
+  "c鶊2": "bar\tfoo"
+}]
+`,
+		},
+		{ // Numbers.
+			[]string{"byte", "uint16", "uint32", "uint64", "int16", "int32", "int64",
+				"float32", "float64", "complex64", "complex128"},
+			[][]interface{}{
+				{
+					byte(12), uint16(1234), uint32(5678), uint64(999888777666), int16(9876), int32(876543), int64(128),
+					float32(3.14159), float64(2.71828182846), complex64(123.0 + 7.0i), complex128(456.789 + 10.1112i),
+				},
+				{
+					byte(9), uint16(99), uint32(999), uint64(9999999), int16(9), int32(99), int64(88),
+					float32(1.41421356237), float64(1.73205080757), complex64(9.87 + 7.65i), complex128(4.32 + 1.0i),
+				},
+			},
+			`
+[{
+  "byte": 12,
+  "uint16": 1234,
+  "uint32": 5678,
+  "uint64": 999888777666,
+  "int16": 9876,
+  "int32": 876543,
+  "int64": 128,
+  "float32": 3.141590118408203,
+  "float64": 2.71828182846,
+  "complex64": "123+7i",
+  "complex128": "456.789+10.1112i"
+}, {
+  "byte": 9,
+  "uint16": 99,
+  "uint32": 999,
+  "uint64": 9999999,
+  "int16": 9,
+  "int32": 99,
+  "int64": 88,
+  "float32": 1.4142135381698608,
+  "float64": 1.73205080757,
+  "complex64": "9.869999885559082+7.650000095367432i",
+  "complex128": "4.32+1i"
+}]
+`,
+		},
+		{ // Empty result.
+			[]string{"nothing", "nada", "zilch"},
+			[][]interface{}{},
+			`
+[]
+`,
+		},
+		{ // Empty column set.
+			[]string{},
+			[][]interface{}{
+				{},
+				{},
+			},
+			`
+[{
+}, {
+}]
+`,
+		},
+		{ // Empty values.
+			[]string{"blank", "empty", "nil"},
+			[][]interface{}{
+				{struct{}{}, []string{}, nil},
+			},
+			`
+[{
+  "blank": {},
+  "empty": [],
+  "nil": null
+}]
+`,
+		},
+		{
+			[]string{"c1"},
+			[][]interface{}{{customer}, {invoice}},
+			`
+[{
+  "c1": {"Name":"John Smith","Id":1,"Active":true,"Address":{"Street":"1 Main St.","City":"Palo Alto","State":"CA","Zip":"94303"},"Credit":{"Agency":"Equifax","Report":{"EquifaxReport":{"Rating":65}}}}
+}, {
+  "c1": {"CustId":1,"InvoiceNum":1000,"Amount":42,"ShipTo":{"Street":"1 Main St.","City":"Palo Alto","State":"CA","Zip":"94303"}}
+}]
+`,
+		},
+		{
+			[]string{"nil", "composite", "typeobj"},
+			[][]interface{}{
+				{
+					nil,
+					db.Composite{array2String("foo", "bar"), []int32{1, 2}, map[int32]struct{}{1: struct{}{}, 2: struct{}{}}, map[string]int32{"foo": 1, "bar": 2}},
+					vdl.TypeOf(map[string]struct{}{}),
+				},
+			},
+			`
+[{
+  "nil": null,
+  "composite": {"Arr":["foo","bar"],"ListInt":[1,2],"MySet":{"1":true,"2":true},"Map":{"bar":2,"foo":1}},
+  "typeobj": "typeobject(set[string])"
+}]
+`,
+		},
+		{
+			[]string{"c1"},
+			[][]interface{}{
+				{
+					db.Recursive{
+						Any: nil,
+						Maybe: &db.Times{
+							Stamp:    time.Unix(123456789, 42244224),
+							Interval: time.Duration(1337),
+						},
+						Rec: map[db.Array2String]db.Recursive{
+							array2String("a", "棎鶊鵱"): db.Recursive{},
+							array2String("x", "y"): db.Recursive{
+								Any: vdl.ValueOf(db.CreditReport{
+									Agency: db.CreditAgencyExperian,
+									Report: db.AgencyReportExperianReport{Value: db.ExperianCreditReport{Rating: db.ExperianRatingGood}},
+								}),
+								Maybe: nil,
+								Rec:   nil,
+							},
+						}},
+				},
+			},
+			`
+[{
+  "c1": {"Any":null,"Maybe":{"Stamp":"1973-11-29 21:33:09.042244224 +0000 UTC","Interval":"1.337µs"},"Rec":{"[\"a\", \"棎鶊鵱\"]":{"Any":null,"Maybe":null,"Rec":{}},"[\"x\", \"y\"]":{"Any":{"Agency":"Experian","Report":{"ExperianReport":{"Rating":"Good"}}},"Maybe":null,"Rec":{}}}}
+}]
+`,
+		},
+	}
+	for _, test := range tests {
+		var b bytes.Buffer
+		if err := writer.WriteJson(&b, test.columns, newResultStream(test.rows)); err != nil {
+			t.Errorf("Unexpected error: %v", err)
+			continue
+		}
+		var decoded interface{}
+		if err := json.Unmarshal(b.Bytes(), &decoded); err != nil {
+			t.Errorf("Got invalid JSON: %v", err)
+		}
+		// Add a leading newline to the output to match the leading newline
+		// in our test cases.
+		if got, want := "\n"+b.String(), test.output; got != want {
+			t.Errorf("Wrong output:\nGOT: %q\nWANT:%q", got, want)
+		}
+	}
+}
diff --git a/cmd/sb51/main.go b/cmd/sb51/main.go
new file mode 100644
index 0000000..9968722
--- /dev/null
+++ b/cmd/sb51/main.go
@@ -0,0 +1,34 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Antimony (sb51) - Syncbase general-purpose client and management utility.
+// Currently supports SyncQL select queries.
+
+package main
+
+import (
+	"flag"
+
+	"v.io/x/lib/cmdline"
+	_ "v.io/x/ref/runtime/factories/generic"
+)
+
+func main() {
+	cmdline.Main(cmdSb51)
+}
+
+var cmdSb51 = &cmdline.Command{
+	Name:  "sb51",
+	Short: "Antimony - Vanadium Syncbase client and management utility",
+	Long: `
+Syncbase general-purpose client and management utility.
+Currently supports starting a SyncQL shell.
+`,
+	Children: []*cmdline.Command{cmdSbShell},
+}
+
+var (
+	// TODO(ivanpi): Decide on convention for local syncbase service name.
+	flagSbService = flag.String("service", "/:8101/syncbase", "Location of the Syncbase service to connect to. Can be absolute or relative to the namespace root.")
+)
diff --git a/cmd/sb51/shell.go b/cmd/sb51/shell.go
new file mode 100644
index 0000000..b5f18c0
--- /dev/null
+++ b/cmd/sb51/shell.go
@@ -0,0 +1,240 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Syncbase client shell. Currently supports SyncQL select queries.
+
+package main
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"strconv"
+	"strings"
+
+	isatty "github.com/mattn/go-isatty"
+
+	"v.io/syncbase/v23/syncbase"
+	"v.io/syncbase/v23/syncbase/nosql"
+	"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb"
+	"v.io/syncbase/x/ref/syncbase/sb51/internal/reader"
+	"v.io/syncbase/x/ref/syncbase/sb51/internal/writer"
+	"v.io/v23/context"
+	"v.io/x/lib/cmdline"
+	"v.io/x/ref/lib/v23cmd"
+)
+
+var cmdSbShell = &cmdline.Command{
+	Runner: v23cmd.RunnerFunc(runSbShell),
+	Name:   "sh",
+	Short:  "Start a SyncQL shell",
+	Long: `
+Connect to a database on the Syncbase service and start a SyncQL shell.
+`,
+	ArgsName: "<app_name> <db_name>",
+	ArgsLong: `
+<app_name> and <db_name> specify the database to execute queries against.
+The database must exist unless -create-missing is specified.
+`,
+}
+
+var (
+	flagFormat            string
+	flagCSVDelimiter      string
+	flagCreateIfNotExists bool
+	flagMakeDemoTables    bool
+)
+
+func init() {
+	cmdSbShell.Flags.StringVar(&flagFormat, "format", "table", "Output format. 'table': human-readable table; 'csv': comma-separated values, use -csv-delimiter to control the delimiter; 'json': JSON objects.")
+	cmdSbShell.Flags.StringVar(&flagCSVDelimiter, "csv-delimiter", ",", "Delimiter to use when printing data as CSV (e.g. \"\t\", \",\")")
+	cmdSbShell.Flags.BoolVar(&flagCreateIfNotExists, "create-missing", false, "Create the app and/or database if they do not exist yet.")
+	cmdSbShell.Flags.BoolVar(&flagMakeDemoTables, "make-demo", false, "(Re)create demo tables in the database.")
+}
+
+func validateFlags() error {
+	if flagFormat != "table" && flagFormat != "csv" && flagFormat != "json" {
+		return fmt.Errorf("Unsupported -format %q.  Must be one of 'table', 'csv', or 'json'.", flagFormat)
+	}
+	if len(flagCSVDelimiter) == 0 {
+		return fmt.Errorf("-csv-delimiter cannot be empty.")
+	}
+	return nil
+}
+
+// Starts a SyncQL shell against the specified database.
+// Runs in interactive or batch mode depending on stdin.
+func runSbShell(ctx *context.T, env *cmdline.Env, args []string) error {
+	// TODO(ivanpi): Add 'use' statement, default to no app/database selected.
+	if len(args) != 2 {
+		return env.UsageErrorf("exactly two arguments expected")
+	}
+	appName, dbName := args[0], args[1]
+	if err := validateFlags(); err != nil {
+		return env.UsageErrorf("%v", err)
+	}
+
+	sbs := syncbase.NewService(*flagSbService)
+	d, err := openAppDB(ctx, sbs, appName, dbName, flagCreateIfNotExists)
+	if err != nil {
+		return err
+	}
+
+	if flagMakeDemoTables {
+		if err := makeDemoDB(ctx, d); err != nil {
+			return err
+		}
+	}
+
+	var input *reader.T
+	// TODO(ivanpi): This is hacky, it would be better for lib/cmdline to support IsTerminal.
+	stdinFile, ok := env.Stdin.(*os.File)
+	isTerminal := ok && isatty.IsTerminal(stdinFile.Fd())
+	if isTerminal {
+		input = reader.NewInteractive()
+	} else {
+		input = reader.NewNonInteractive()
+	}
+	defer input.Close()
+
+stmtLoop:
+	for true {
+		if q, err := input.GetQuery(); err != nil {
+			if err == io.EOF {
+				if isTerminal {
+					// ctrl-d
+					fmt.Println()
+				}
+				break
+			} else {
+				// ctrl-c
+				break
+			}
+		} else {
+			var err error
+			tq := strings.Fields(q)
+			if len(tq) > 0 {
+				switch strings.ToLower(tq[0]) {
+				case "exit", "quit":
+					break stmtLoop
+				case "dump":
+					err = dumpDB(ctx, env.Stdout, d)
+				case "make-demo":
+					err = makeDemoDB(ctx, d)
+				case "select":
+					err = queryExec(ctx, env.Stdout, d, q)
+				default:
+					err = fmt.Errorf("unknown statement: '%s'; expected one of: 'select', 'make-demo', 'dump', 'exit', 'quit'", strings.ToLower(tq[0]))
+				}
+			}
+			if err != nil {
+				if isTerminal {
+					fmt.Fprintln(env.Stderr, "Error:", err)
+				} else {
+					// If running non-interactively, errors stop execution.
+					return err
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+func openAppDB(ctx *context.T, sbs syncbase.Service, appName, dbName string, createIfNotExists bool) (nosql.Database, error) {
+	app := sbs.App(appName)
+	if exists, err := app.Exists(ctx); err != nil {
+		return nil, fmt.Errorf("failed checking for app %q: %v", app.FullName(), err)
+	} else if !exists {
+		if !createIfNotExists {
+			return nil, fmt.Errorf("app %q does not exist", app.FullName())
+		}
+		if err := app.Create(ctx, nil); err != nil {
+			return nil, err
+		}
+	}
+	d := app.NoSQLDatabase(dbName, nil)
+	if exists, err := d.Exists(ctx); err != nil {
+		return nil, fmt.Errorf("failed checking for db %q: %v", d.FullName(), err)
+	} else if !exists {
+		if !createIfNotExists {
+			return nil, fmt.Errorf("db %q does not exist", d.FullName())
+		}
+		if err := d.Create(ctx, nil); err != nil {
+			return nil, err
+		}
+	}
+	return d, nil
+}
+
+func dumpDB(ctx *context.T, w io.Writer, d nosql.Database) error {
+	tables, err := d.ListTables(ctx)
+	if err != nil {
+		return fmt.Errorf("failed listing tables: %v", err)
+	}
+	var errs []error
+	for _, table := range tables {
+		fmt.Fprintf(w, "table: %s\n", table)
+		if err := queryExec(ctx, w, d, fmt.Sprintf("select k, v from %s", table)); err != nil {
+			errs = append(errs, fmt.Errorf("> %s: %v", table, err))
+		}
+	}
+	if len(errs) > 0 {
+		err := fmt.Errorf("failed dumping %d of %d tables:", len(errs), len(tables))
+		for _, e := range errs {
+			err = fmt.Errorf("%v\n%v", err, e)
+		}
+		return err
+	}
+	return nil
+}
+
+func makeDemoDB(ctx *context.T, d nosql.Database) error {
+	if err := demodb.PopulateDemoDB(ctx, d); err != nil {
+		return fmt.Errorf("failed making demo tables: %v", err)
+	}
+	return nil
+}
+
+// Split an error message into an offset and the remaining (i.e., rhs of offset) message.
+// The convention for syncql is "<module><optional-rpc>[offset]<remaining-message>".
+func splitError(err error) (int64, string) {
+	errMsg := err.Error()
+	idx1 := strings.Index(errMsg, "[")
+	idx2 := strings.Index(errMsg, "]")
+	if idx1 == -1 || idx2 == -1 {
+		return 0, errMsg
+	}
+	offsetString := errMsg[idx1+1 : idx2]
+	offset, err := strconv.ParseInt(offsetString, 10, 64)
+	if err != nil {
+		return 0, errMsg
+	}
+	return offset, errMsg[idx2+1:]
+}
+
+func queryExec(ctx *context.T, w io.Writer, d nosql.Database, q string) error {
+	if columnNames, rs, err := d.Exec(ctx, q); err != nil {
+		off, msg := splitError(err)
+		return fmt.Errorf("\n%s\n%s^\n%d: %s", q, strings.Repeat(" ", int(off)), off+1, msg)
+	} else {
+		switch flagFormat {
+		case "table":
+			if err := writer.WriteTable(w, columnNames, rs); err != nil {
+				return err
+			}
+		case "csv":
+			if err := writer.WriteCSV(w, columnNames, rs, flagCSVDelimiter); err != nil {
+				return err
+			}
+		case "json":
+			if err := writer.WriteJson(w, columnNames, rs); err != nil {
+				return err
+			}
+		default:
+			panic(fmt.Sprintf("invalid format flag value: %v", flagFormat))
+		}
+	}
+	return nil
+}
diff --git a/services/syncbase/clock/clock_darwin.go b/services/syncbase/clock/clock_darwin.go
new file mode 100644
index 0000000..4c801cb
--- /dev/null
+++ b/services/syncbase/clock/clock_darwin.go
@@ -0,0 +1,51 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package clock
+
+import (
+	"bytes"
+	"encoding/binary"
+	"syscall"
+	"time"
+	"unsafe"
+)
+
+// This file contains darwin specific implementations of functions for clock
+// package.
+
+// ElapsedTime returns the time elapsed since last boot.
+// Darwin provides a system call "kern.boottime" which returns a Timeval32
+// object containing the boot time for the system. Darwin calculates this
+// boottime based on the current clock and the internal tracking of elapsed
+// time since boot. Hence if the clock is changed, the boot time changes along
+// with it. So the difference between the current time and boot time will always
+// give us the correct elapsed time since boot.
+func (sc *systemClockImpl) ElapsedTime() (time.Duration, error) {
+	tv := syscall.Timeval32{}
+
+	if err := sysctlbyname("kern.boottime", &tv); err != nil {
+		return 0, err
+	}
+	return time.Since(time.Unix(int64(tv.Sec), int64(tv.Usec)*1000)), nil
+}
+
+// Generic Sysctl buffer unmarshalling.
+func sysctlbyname(name string, data interface{}) (err error) {
+	val, err := syscall.Sysctl(name)
+	if err != nil {
+		return err
+	}
+
+	buf := []byte(val)
+
+	switch v := data.(type) {
+	case *uint64:
+		*v = *(*uint64)(unsafe.Pointer(&buf[0]))
+		return
+	}
+
+	bbuf := bytes.NewBuffer([]byte(val))
+	return binary.Read(bbuf, binary.LittleEndian, data)
+}
diff --git a/services/syncbase/clock/clock_linux.go b/services/syncbase/clock/clock_linux.go
new file mode 100644
index 0000000..dabaab4
--- /dev/null
+++ b/services/syncbase/clock/clock_linux.go
@@ -0,0 +1,26 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package clock
+
+import (
+	"syscall"
+	"time"
+)
+
+// This file contains linux specific implementations of functions for clock
+// package.
+
+// Linux System stores this information in /proc/uptime as seconds
+// since boot with a precision up to 2 decimal points.
+// NOTE: Go system call returns elapsed time in seconds and removes the decimal
+// points by rounding to the closest second. Be careful in using this value as
+// it can introduce a compounding error.
+func (sc *systemClockImpl) ElapsedTime() (time.Duration, error) {
+	var sysInfo syscall.Sysinfo_t
+	if err := syscall.Sysinfo(&sysInfo); err != nil {
+		return 0, err
+	}
+	return time.Duration(sysInfo.Uptime) * time.Second, nil
+}
diff --git a/services/syncbase/clock/clockservice.go b/services/syncbase/clock/clockservice.go
new file mode 100644
index 0000000..fed9a02
--- /dev/null
+++ b/services/syncbase/clock/clockservice.go
@@ -0,0 +1,127 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package clock
+
+import (
+	"math"
+	"time"
+
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/v23/context"
+	"v.io/v23/verror"
+	"v.io/x/lib/vlog"
+)
+
+// This file contains code related to checking current system clock to see
+// if it has been changed by any external action.
+
+// runClockCheck estimates the current system time based on saved boottime
+// and elapsed time since boot and checks if the system clock shows the same
+// time. This involves the following steps:
+// 1) Check if system was rebooted since last run. If so update the saved
+// ClockData.
+// 2) Fetch stored ClockData. If none exists, this is the first time
+// runClockCheck has been run. Write new ClockData.
+// 3) Estimate current system clock time and check if the actual system clock
+// agrees with the estimation. If not update the skew value appropriately.
+// 4) Update saved elapsed time since boot. This is used to check if the system
+// was rebooted or not. TODO(jlodhia): work with device manager to provide a
+// way to notify syncbase if the system was just rebooted.
+func (c *VClock) runClockCheck(ctx *context.T) {
+	checkSystemRebooted(ctx, c)
+
+	clockData := &ClockData{}
+	if err := c.sa.GetClockData(ctx, clockData); err != nil {
+		if verror.ErrorID(err) == verror.ErrNoExist.ID {
+			// VClock's cron job to setup UTC time at boot is being run for the
+			// first time. Skew is not known, hence assigning 0.
+			writeNewClockData(ctx, c, 0)
+		} else {
+			vlog.Errorf("Error while fetching clock data: %v", err)
+		}
+		return
+	}
+
+	systemTime := c.clock.Now()
+	elapsedTime, err := c.clock.ElapsedTime()
+	if err != nil {
+		vlog.Errorf("Error while fetching elapsed time: %v", err)
+		return
+	}
+
+	newClockData := &ClockData{
+		SystemTimeAtBoot:     clockData.SystemTimeAtBoot,
+		Skew:                 clockData.Skew,
+		ElapsedTimeSinceBoot: elapsedTime.Nanoseconds(),
+	}
+
+	estimatedClockTime := clockData.SystemBootTime().Add(elapsedTime)
+	diff := estimatedClockTime.Sub(systemTime)
+	if math.Abs(float64(diff.Nanoseconds())) > util.LocalClockDriftThreshold {
+		newClockData.Skew = newClockData.Skew + diff.Nanoseconds()
+		newSystemTimeAtBoot := systemTime.Add(-elapsedTime)
+		newClockData.SystemTimeAtBoot = newSystemTimeAtBoot.UnixNano()
+	}
+
+	if err := c.sa.SetClockData(ctx, newClockData); err != nil {
+		vlog.Errorf("Error while setting clock data: %v", err)
+	}
+}
+
+func writeNewClockData(ctx *context.T, c *VClock, skew time.Duration) {
+	systemTime := c.clock.Now()
+	elapsedTime, err := c.clock.ElapsedTime()
+	if err != nil {
+		vlog.Errorf("Error while fetching elapsed time: %v", err)
+		return
+	}
+	systemTimeAtBoot := systemTime.Add(-elapsedTime)
+	clockData := &ClockData{
+		SystemTimeAtBoot:     systemTimeAtBoot.UnixNano(),
+		Skew:                 skew.Nanoseconds(),
+		ElapsedTimeSinceBoot: elapsedTime.Nanoseconds(),
+	}
+	if err := c.sa.SetClockData(ctx, clockData); err != nil {
+		vlog.Errorf("Error while setting clock data: %v", err)
+	}
+}
+
+// checkSystemRebooted compares the elapsed time stored during the last
+// run of runClockCheck() to the current elapsed time since boot provided
+// by system clock. Since elapsed time is monotonically increasing and cannot
+// be changed unless a reboot happens, if the current value is lower than the
+// previous value then a reboot has happened since last run. If so, update
+// the boot time and elapsed time since boot appropriately.
+func checkSystemRebooted(ctx *context.T, c *VClock) bool {
+	currentSysTime := c.clock.Now()
+	elapsedTime, err := c.clock.ElapsedTime()
+	if err != nil {
+		vlog.Errorf("Error while fetching elapsed time: %v", err)
+		return false
+	}
+
+	clockData := &ClockData{}
+	if err := c.sa.GetClockData(ctx, clockData); err != nil {
+		if verror.ErrorID(err) != verror.ErrNoExist.ID {
+			vlog.Errorf("Error while fetching clock delta: %v", err)
+		}
+		// In case of verror.ErrNoExist no clock data present. Nothing needed to
+		// be done. writeNewClockData() will write new clock data to storage.
+		return false
+	}
+
+	if elapsedTime.Nanoseconds() < clockData.ElapsedTimeSinceBoot {
+		// Since the elapsed time since last boot provided by the system is
+		// less than the elapsed time since boot seen the last time clockservice
+		// ran, the system must have rebooted in between.
+		clockData.SystemTimeAtBoot = currentSysTime.Add(-elapsedTime).UnixNano()
+		clockData.ElapsedTimeSinceBoot = elapsedTime.Nanoseconds()
+		if err := c.sa.SetClockData(ctx, clockData); err != nil {
+			vlog.Errorf("Error while setting clock data: %v", err)
+		}
+		return true
+	}
+	return false
+}
diff --git a/services/syncbase/clock/clockservice_test.go b/services/syncbase/clock/clockservice_test.go
new file mode 100644
index 0000000..86650c1
--- /dev/null
+++ b/services/syncbase/clock/clockservice_test.go
@@ -0,0 +1,191 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package clock
+
+import (
+	"testing"
+	"time"
+)
+
+const (
+	constElapsedTime int64 = 50
+)
+
+func TestWriteNewClockData(t *testing.T) {
+	sysTs := time.Now()
+	sysClock := MockSystemClock(sysTs, time.Duration(constElapsedTime))
+	stAdapter := MockStorageAdapter()
+
+	clock := NewVClockWithMockServices(stAdapter, sysClock, nil)
+
+	writeNewClockData(nil, clock, 0)
+
+	expectedSystemTimeAtBoot := sysTs.UnixNano() - constElapsedTime
+	verifyClockData(t, stAdapter, 0, expectedSystemTimeAtBoot, constElapsedTime)
+}
+
+// This test runs the following scenarios
+// 1) Run checkSystemRebooted() with no ClockData stored
+// Result: no op.
+// 2) Run checkSystemRebooted() with ClockData that has SystemTimeAtBoot higher
+// than the current elapsed time.
+// Result: A new ClockData is written with updated SystemTimeAtBoot and
+// elapsed time.
+// 3) Run checkSystemRebooted() again after moving the sysClock forward
+// Result: no op.
+func TestCheckSystemRebooted(t *testing.T) {
+	sysTs := time.Now()
+	sysClock := MockSystemClock(sysTs, time.Duration(constElapsedTime))
+	stAdapter := MockStorageAdapter()
+
+	clock := NewVClockWithMockServices(stAdapter, sysClock, nil)
+
+	// stAdapter will return ErrNoExist while fetching ClockData
+	// checkSystemRebooted should return false.
+	if checkSystemRebooted(nil, clock) {
+		t.Error("Unexpected return value")
+	}
+
+	// Set clock data with elapsed time greater than constElapsedTime
+	clockData := &ClockData{25003, 25, 34569}
+	stAdapter.SetClockData(nil, clockData)
+
+	if !checkSystemRebooted(nil, clock) {
+		t.Error("Unexpected return value")
+	}
+	expectedSystemTimeAtBoot := sysTs.UnixNano() - constElapsedTime
+	verifyClockData(t, stAdapter, 25, expectedSystemTimeAtBoot, constElapsedTime)
+
+	// move clock forward without reboot and run checkSystemRebooted again
+	var timePassed int64 = 200
+	newSysTs := sysTs.Add(time.Duration(timePassed))
+	sysClock.SetNow(newSysTs)
+	sysClock.SetElapsedTime(time.Duration(constElapsedTime + timePassed))
+
+	if checkSystemRebooted(nil, clock) {
+		t.Error("Unexpected return value")
+	}
+	expectedSystemTimeAtBoot = sysTs.UnixNano() - constElapsedTime
+	verifyClockData(t, stAdapter, 25, expectedSystemTimeAtBoot, constElapsedTime)
+}
+
+// Setup: No prior ClockData present.
+// Result: A new ClockData value gets set.
+func TestRunClockCheck1(t *testing.T) {
+	sysTs := time.Now()
+	sysClock := MockSystemClock(sysTs, time.Duration(constElapsedTime))
+	stAdapter := MockStorageAdapter()
+
+	clock := NewVClockWithMockServices(stAdapter, sysClock, nil)
+
+	clock.runClockCheck(nil)
+	expectedSystemTimeAtBoot := sysTs.UnixNano() - constElapsedTime
+	verifyClockData(t, stAdapter, 0, expectedSystemTimeAtBoot, constElapsedTime)
+}
+
+// Setup: ClockData present, system clock elapsed time is lower than whats
+// stored in clock data.
+// Result: A new ClockData value gets set with new system boot time and elapsed
+// time, skew remains the same.
+func TestRunClockCheck2(t *testing.T) {
+	sysTs := time.Now()
+	sysClock := MockSystemClock(sysTs, time.Duration(constElapsedTime))
+	stAdapter := MockStorageAdapter()
+	// Set clock data with elapsed time greater than constElapsedTime
+	clockData := &ClockData{25003, 25, 34569}
+	stAdapter.SetClockData(nil, clockData)
+
+	clock := NewVClockWithMockServices(stAdapter, sysClock, nil)
+
+	clock.runClockCheck(nil)
+	expectedSystemTimeAtBoot := sysTs.UnixNano() - constElapsedTime
+	verifyClockData(t, stAdapter, 25, expectedSystemTimeAtBoot, constElapsedTime)
+}
+
+// Setup: ClockData present, system clock gets a skew of 10 seconds
+// Result: A new ClockData value gets set with new elapsed time and skew,
+// system boot time remains the same.
+func TestRunClockCheck3(t *testing.T) {
+	sysTs := time.Now()
+	sysClock := MockSystemClock(sysTs, time.Duration(constElapsedTime))
+	stAdapter := MockStorageAdapter()
+
+	bootTs := sysTs.Add(time.Duration(-constElapsedTime))
+	oldSkew := 25 * time.Second
+	clockData := &ClockData{bootTs.UnixNano(), oldSkew.Nanoseconds(), 40}
+	stAdapter.SetClockData(nil, clockData)
+
+	clock := NewVClockWithMockServices(stAdapter, sysClock, nil)
+
+	// introduce a change in sys clock
+	extraSkew := 10 * time.Second // moves clock closer to UTC
+	changedSysTs := sysTs.Add(extraSkew)
+	sysClock.SetNow(changedSysTs)
+	newSkew := 15 * time.Second
+
+	clock.runClockCheck(nil)
+	expectedSystemTimeAtBoot := bootTs.UnixNano() + extraSkew.Nanoseconds()
+	verifyClockData(t, stAdapter, newSkew.Nanoseconds(), expectedSystemTimeAtBoot, constElapsedTime)
+}
+
+func TestWithRealSysClock(t *testing.T) {
+	stAdapter := MockStorageAdapter()
+	clock := NewVClockWithMockServices(stAdapter, nil, nil)
+
+	writeNewClockData(nil, clock, 0)
+
+	// Verify if clock data was written to StorageAdapter
+	clockData := &ClockData{}
+	if err := stAdapter.GetClockData(nil, clockData); err != nil {
+		t.Errorf("Expected to find clockData, received error: %v", err)
+	}
+
+	// Verify that calling checkSystemRebooted() does nothing
+	if checkSystemRebooted(nil, clock) {
+		t.Error("Unexpected return value")
+	}
+
+	// sleep for 1 second more than the skew threshold
+	time.Sleep(1800 * time.Millisecond)
+
+	// Verify that calling runClockCheck() only updates elapsed time
+	clock.runClockCheck(nil)
+	newClockData := &ClockData{}
+	if err := stAdapter.GetClockData(nil, newClockData); err != nil {
+		t.Errorf("Expected to find clockData, received error: %v", err)
+	}
+	if newClockData.Skew != clockData.Skew {
+		t.Errorf("Unexpected value for skew: %d", newClockData.Skew)
+	}
+	if newClockData.ElapsedTimeSinceBoot <= clockData.ElapsedTimeSinceBoot {
+		t.Errorf("Unexpected value for elapsed time: %d",
+			newClockData.ElapsedTimeSinceBoot)
+	}
+	if newClockData.SystemTimeAtBoot != clockData.SystemTimeAtBoot {
+		t.Errorf("SystemTimeAtBoot expected: %d, found: %d",
+			clockData.SystemTimeAtBoot, newClockData.SystemTimeAtBoot)
+	}
+}
+
+func verifyClockData(t *testing.T, stAdapter StorageAdapter, skew int64,
+	sysTimeAtBoot int64, elapsedTime int64) {
+	// verify ClockData
+	clockData := &ClockData{}
+	if err := stAdapter.GetClockData(nil, clockData); err != nil {
+		t.Errorf("Expected to find clockData, found error: %v", err)
+	}
+
+	if clockData.Skew != skew {
+		t.Errorf("Expected value for skew: %d, found: %d", skew, clockData.Skew)
+	}
+	if clockData.ElapsedTimeSinceBoot != elapsedTime {
+		t.Errorf("Expected value for elapsed time: %d, found: %d", elapsedTime,
+			clockData.ElapsedTimeSinceBoot)
+	}
+	if clockData.SystemTimeAtBoot != sysTimeAtBoot {
+		t.Errorf("Expected value for SystemTimeAtBoot: %d, found: %d",
+			sysTimeAtBoot, clockData.SystemTimeAtBoot)
+	}
+}
diff --git a/services/syncbase/clock/ntp.go b/services/syncbase/clock/ntp.go
new file mode 100644
index 0000000..ba55322
--- /dev/null
+++ b/services/syncbase/clock/ntp.go
@@ -0,0 +1,151 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package clock
+
+import (
+	"fmt"
+	"net"
+	"time"
+
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+)
+
+const (
+	udp  = "udp"
+	port = "123"
+)
+
+var _ NtpSource = (*ntpSourceImpl)(nil)
+
+func NewNtpSource(clock SystemClock) NtpSource {
+	return &ntpSourceImpl{util.NtpServerPool, clock}
+}
+
+type ntpSourceImpl struct {
+	ntpHost string
+	sc      SystemClock
+}
+
+// NtpSync samples data from NTP server and returns the one which has the lowest
+// network delay. The sample with lowest network delay will have the least error
+// in computation of the offset.
+// Param sampleCount is the number of samples this method will fetch.
+func (ns *ntpSourceImpl) NtpSync(sampleCount int) (*NtpData, error) {
+	var canonicalSample *NtpData = nil
+	for i := 0; i < sampleCount; i++ {
+		if sample, err := ns.sample(); err == nil {
+			if (canonicalSample == nil) || (sample.delay < canonicalSample.delay) {
+				canonicalSample = sample
+			}
+		}
+	}
+	if canonicalSample == nil {
+		err := fmt.Errorf("Failed to get any sample from NTP server: %s", ns.ntpHost)
+		return nil, err
+	}
+	return canonicalSample, nil
+}
+
+// Sample connects to an NTP server and returns NtpData containing the clock
+// offset and the network delay experienced while talking to the server.
+//
+// NTP protocol involves sending a request of size 48 bytes with the first
+// byte containing protocol version and mode and the last 8 bytes containing
+// transmit timestamp. The current NTP version is 4. A response from NTP server
+// contains original timestamp (client's transmit timestamp from request) from
+// bytes 24 to 31, server's receive timestamp from byte 32 to 39 and server's
+// transmit time from byte 40 to 47. Client can register the response receive
+// time as soon it receives a response from server.
+// Based on the 4 timestamps the client can compute the offset between the
+// two clocks and the roundtrip network delay for the request.
+func (ns *ntpSourceImpl) sample() (*NtpData, error) {
+	raddr, err := net.ResolveUDPAddr(udp, ns.ntpHost+":"+port)
+	if err != nil {
+		return nil, err
+	}
+
+	con, err := net.DialUDP("udp", nil, raddr)
+	if err != nil {
+		return nil, err
+	}
+	defer con.Close()
+
+	msg := ns.createRequest()
+	_, err = con.Write(msg)
+	if err != nil {
+		return nil, err
+	}
+
+	con.SetDeadline(time.Now().Add(5 * time.Second))
+	_, err = con.Read(msg)
+	if err != nil {
+		return nil, err
+	}
+
+	clientReceiveTs := ns.sc.Now()
+	clientTransmitTs := extractTime(msg[24:32])
+	serverReceiveTs := extractTime(msg[32:40])
+	serverTransmitTs := extractTime(msg[40:48])
+
+	// Following code extracts the clock offset and network delay based on the
+	// transmit and receive timestamps on the client and the server as per
+	// the formula explained at http://www.eecis.udel.edu/~mills/time.html
+	data := NtpData{}
+	data.offset = (serverReceiveTs.Sub(clientTransmitTs) + serverTransmitTs.Sub(clientReceiveTs)) / 2
+	data.delay = clientReceiveTs.Sub(clientTransmitTs) - serverTransmitTs.Sub(serverReceiveTs)
+
+	return &data, nil
+}
+
+func (ns *ntpSourceImpl) createRequest() []byte {
+	data := make([]byte, 48)
+	data[0] = 0x23 // protocol version = 4, mode = 3 (Client)
+
+	// For NTP the prime epoch, or base date of era 0, is 0 h 1 January 1900 UTC
+	t0 := time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC)
+	tnow := ns.sc.Now()
+	d := tnow.Sub(t0)
+	nsec := d.Nanoseconds()
+
+	// The encoding of timestamp below is an exact opposite of the decoding
+	// being done in extractTime(). Refer extractTime() for more explaination.
+	sec := nsec / 1e9                  // Integer part of seconds since epoch
+	frac := ((nsec % 1e9) << 32) / 1e9 // fractional part of seconds since epoch
+
+	// write the timestamp to Transmit Timestamp section of request.
+	data[43] = byte(sec)
+	data[42] = byte(sec >> 8)
+	data[41] = byte(sec >> 16)
+	data[40] = byte(sec >> 24)
+
+	data[47] = byte(frac)
+	data[46] = byte(frac >> 8)
+	data[45] = byte(frac >> 16)
+	data[44] = byte(frac >> 24)
+	return data
+}
+
+// ExtractTime takes a byte array which contains encoded timestamp from NTP
+// server starting at the 0th byte and is 8 bytes long. The encoded timestamp is
+// in seconds since 1900. The first 4 bytes contain the integer part of of the
+// seconds while the last 4 bytes contain the fractional part of the seconds
+// where (FFFFFFFF + 1) represents 1 second while 00000001 represents 2^(-32) of
+// a second.
+func extractTime(data []byte) time.Time {
+	var sec, frac uint64
+	sec = uint64(data[3]) | uint64(data[2])<<8 | uint64(data[1])<<16 | uint64(data[0])<<24
+	frac = uint64(data[7]) | uint64(data[6])<<8 | uint64(data[5])<<16 | uint64(data[4])<<24
+
+	// multiply the integral second part with 1Billion to convert to nanoseconds
+	nsec := sec * 1e9
+	// multiply frac part with 2^(-32) to get the correct value in seconds and
+	// then multiply with 1Billion to convert to nanoseconds. The multiply by
+	// Billion is done first to make sure that we dont loose precision.
+	nsec += (frac * 1e9) >> 32
+
+	t := time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC).Add(time.Duration(nsec)).Local()
+
+	return t
+}
diff --git a/services/syncbase/clock/ntpservice.go b/services/syncbase/clock/ntpservice.go
new file mode 100644
index 0000000..79045f3
--- /dev/null
+++ b/services/syncbase/clock/ntpservice.go
@@ -0,0 +1,46 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package clock
+
+import (
+	"math"
+
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/v23/context"
+	"v.io/v23/verror"
+	"v.io/x/lib/vlog"
+)
+
+// runNtpCheck talks to an NTP server, fetches the current UTC time from it
+// and corrects VClock time.
+func (c *VClock) runNtpCheck(ctx *context.T) error {
+	ntpData, err := c.ntpSource.NtpSync(util.NtpSampleCount)
+	if err != nil {
+		vlog.Errorf("Error while fetching ntp time: %v", err)
+		return err
+	}
+	offset := ntpData.offset
+
+	data := &ClockData{}
+	if err := c.sa.GetClockData(ctx, data); err != nil {
+		if verror.ErrorID(err) == verror.ErrNoExist.ID {
+			// No ClockData found, write a new one.
+			writeNewClockData(ctx, c, offset)
+			return nil
+		}
+		vlog.Info("Error while fetching clock data: %v", err)
+		vlog.Info("Overwriting clock data with NTP")
+		writeNewClockData(ctx, c, offset)
+		return nil
+	}
+
+	// Update clock skew if the difference between offset and skew is larger
+	// than NtpDiffThreshold. NtpDiffThreshold helps avoid constant tweaking of
+	// the syncbase clock.
+	if math.Abs(float64(offset.Nanoseconds() - data.Skew)) > util.NtpDiffThreshold {
+		writeNewClockData(ctx, c, offset)
+	}
+	return nil
+}
diff --git a/services/syncbase/clock/ntpservice_test.go b/services/syncbase/clock/ntpservice_test.go
new file mode 100644
index 0000000..e505e01
--- /dev/null
+++ b/services/syncbase/clock/ntpservice_test.go
@@ -0,0 +1,192 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package clock
+
+import (
+	"net"
+	"testing"
+	"time"
+)
+
+func TestWithMockNtpForErr(t *testing.T) {
+	sysClock := MockSystemClock(time.Now(), 0)
+	stAdapter := MockStorageAdapter()
+	ntpSource := MockNtpSource()
+	ntpSource.Err = net.UnknownNetworkError("network err")
+
+	vclock := NewVClockWithMockServices(stAdapter, sysClock, ntpSource)
+
+	if err := vclock.runNtpCheck(nil); err == nil {
+		t.Error("Network error expected but not found")
+	}
+
+	if stAdapter.clockData != nil {
+		t.Error("Non-nil clock data found.")
+	}
+}
+
+func TestWithMockNtpForDiffBelowThreshold(t *testing.T) {
+	sysClock := MockSystemClock(time.Now(), 0) // not used
+	stAdapter := MockStorageAdapter()
+	originalData := NewClockData(0)
+	stAdapter.SetClockData(nil, &originalData)
+
+	ntpSource := MockNtpSource()
+	offset := 1800 * time.Millisecond // error threshold is 2 seconds
+	ntpSource.Data = &NtpData{offset: offset, delay: 5 * time.Millisecond}
+
+	vclock := NewVClockWithMockServices(stAdapter, sysClock, ntpSource)
+	if err := vclock.runNtpCheck(nil); err != nil {
+		t.Errorf("Unexpected err: %v", err)
+	}
+	if isClockDataChanged(stAdapter, &originalData) {
+		t.Error("ClockData expected to be unchanged but found updated")
+	}
+}
+
+func TestWithMockNtpForDiffAboveThreshold(t *testing.T) {
+	sysTs := time.Now()
+	elapsedTime := 10 * time.Minute
+	sysClock := MockSystemClock(sysTs, elapsedTime)
+
+	stAdapter := MockStorageAdapter()
+	originalData := NewClockData(0)
+	stAdapter.SetClockData(nil, &originalData)
+
+	ntpSource := MockNtpSource()
+	skew := 2100 * time.Millisecond // error threshold is 2 seconds
+	ntpSource.Data = &NtpData{offset: skew, delay: 5 * time.Millisecond}
+
+	vclock := NewVClockWithMockServices(stAdapter, sysClock, ntpSource)
+	if err := vclock.runNtpCheck(nil); err != nil {
+		t.Errorf("Unexpected err: %v", err)
+	}
+	if !isClockDataChanged(stAdapter, &originalData) {
+		t.Error("ClockData expected to be updated but found unchanged")
+	}
+	expectedBootTime := sysTs.Add(-elapsedTime).UnixNano()
+	if stAdapter.clockData.Skew != skew.Nanoseconds() {
+		t.Errorf("Skew expected to be %d but found %d",
+			skew.Nanoseconds(), stAdapter.clockData.Skew)
+	}
+	if stAdapter.clockData.ElapsedTimeSinceBoot != elapsedTime.Nanoseconds() {
+		t.Errorf("ElapsedTime expected to be %d but found %d",
+			elapsedTime.Nanoseconds(), stAdapter.clockData.ElapsedTimeSinceBoot)
+	}
+	if stAdapter.clockData.SystemTimeAtBoot != expectedBootTime {
+		t.Errorf("Skew expected to be %d but found %d",
+			expectedBootTime, stAdapter.clockData.SystemTimeAtBoot)
+	}
+}
+
+func TestWithMockNtpForDiffBelowThresholdAndExistingLargeSkew(t *testing.T) {
+	sysTs := time.Now()
+	elapsedTime := 10 * time.Minute
+	sysClock := MockSystemClock(sysTs, elapsedTime)
+
+	stAdapter := MockStorageAdapter()
+	originalData := NewClockData(2300 * time.Millisecond.Nanoseconds()) // large skew
+	stAdapter.SetClockData(nil, &originalData)
+
+	ntpSource := MockNtpSource()
+	skew := 200 * time.Millisecond // error threshold is 2 seconds
+	ntpSource.Data = &NtpData{offset: skew, delay: 5 * time.Millisecond}
+
+	vclock := NewVClockWithMockServices(stAdapter, sysClock, ntpSource)
+	if err := vclock.runNtpCheck(nil); err != nil {
+		t.Errorf("Unexpected err: %v", err)
+	}
+	if !isClockDataChanged(stAdapter, &originalData) {
+		t.Error("ClockData expected to be updated but found unchanged")
+	}
+	expectedBootTime := sysTs.Add(-elapsedTime).UnixNano()
+	if stAdapter.clockData.Skew != skew.Nanoseconds() {
+		t.Errorf("Skew expected to be %d but found %d",
+			skew.Nanoseconds(), stAdapter.clockData.Skew)
+	}
+	if stAdapter.clockData.ElapsedTimeSinceBoot != elapsedTime.Nanoseconds() {
+		t.Errorf("ElapsedTime expected to be %d but found %d",
+			elapsedTime.Nanoseconds(), stAdapter.clockData.ElapsedTimeSinceBoot)
+	}
+	if stAdapter.clockData.SystemTimeAtBoot != expectedBootTime {
+		t.Errorf("Skew expected to be %d but found %d",
+			expectedBootTime, stAdapter.clockData.SystemTimeAtBoot)
+	}
+}
+
+func TestWithMockNtpForDiffBelowThresholdWithNoStoredClockData(t *testing.T) {
+	sysTs := time.Now()
+	elapsedTime := 10 * time.Minute
+	sysClock := MockSystemClock(sysTs, elapsedTime)
+
+	stAdapter := MockStorageAdapter() // no skew data stored
+
+	ntpSource := MockNtpSource()
+	skew := 200 * time.Millisecond // error threshold is 2 seconds
+	ntpSource.Data = &NtpData{offset: skew, delay: 5 * time.Millisecond}
+
+	vclock := NewVClockWithMockServices(stAdapter, sysClock, ntpSource)
+	if err := vclock.runNtpCheck(nil); err != nil {
+		t.Errorf("Unexpected err: %v", err)
+	}
+	if !isClockDataChanged(stAdapter, nil) {
+		t.Error("ClockData expected to be updated but found unchanged")
+	}
+	expectedBootTime := sysTs.Add(-elapsedTime).UnixNano()
+	if stAdapter.clockData.Skew != skew.Nanoseconds() {
+		t.Errorf("Skew expected to be %d but found %d",
+			skew.Nanoseconds(), stAdapter.clockData.Skew)
+	}
+	if stAdapter.clockData.ElapsedTimeSinceBoot != elapsedTime.Nanoseconds() {
+		t.Errorf("ElapsedTime expected to be %d but found %d",
+			elapsedTime.Nanoseconds(), stAdapter.clockData.ElapsedTimeSinceBoot)
+	}
+	if stAdapter.clockData.SystemTimeAtBoot != expectedBootTime {
+		t.Errorf("Skew expected to be %d but found %d",
+			expectedBootTime, stAdapter.clockData.SystemTimeAtBoot)
+	}
+}
+
+/*
+Following two tests are commented out as they hit the real NTP servers
+and can resut into being flaky if the clock of the machine running continuous
+test has a skew more than 2 seconds.
+
+func TestWithRealNtp(t *testing.T) {
+	stAdapter := MockStorageAdapter()
+	originalData := NewClockData(100 * time.Millisecond.Nanoseconds())  // small skew
+	stAdapter.SetClockData(nil, &originalData)
+	vclock := NewVClockWithMockServices(stAdapter, nil, nil)
+	if err := vclock.runNtpCheck(nil); err != nil {
+		t.Errorf("Unexpected err: %v", err)
+	}
+	if isClockDataChanged(stAdapter, &originalData) {
+		t.Error("ClockData expected to be unchanged but found updated")
+	}
+}
+
+func TestWithRealNtpForNoClockData(t *testing.T) {
+	stAdapter := MockStorageAdapter()
+	vclock := NewVClockWithMockServices(stAdapter, nil, nil)
+	if err := vclock.runNtpCheck(nil); err != nil {
+		t.Errorf("Unexpected err: %v", err)
+	}
+	if !isClockDataChanged(stAdapter, nil) {
+		t.Error("ClockData expected to be updated but found unchanged")
+	}
+}
+*/
+
+func NewClockData(skew int64) ClockData {
+	return ClockData{
+		SystemTimeAtBoot:     0,
+		Skew:                 skew,
+		ElapsedTimeSinceBoot: 0,
+	}
+}
+
+func isClockDataChanged(stAdapter *storageAdapterMockImpl, originalData *ClockData) bool {
+	return stAdapter.clockData != originalData // check for same pointer
+}
diff --git a/services/syncbase/clock/storage_adapter.go b/services/syncbase/clock/storage_adapter.go
new file mode 100644
index 0000000..33b4cda
--- /dev/null
+++ b/services/syncbase/clock/storage_adapter.go
@@ -0,0 +1,33 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package clock
+
+import (
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/context"
+)
+
+var _ StorageAdapter = (*storageAdapterImpl)(nil)
+
+func NewStorageAdapter(st store.Store) StorageAdapter {
+	return &storageAdapterImpl{st}
+}
+
+type storageAdapterImpl struct {
+	st store.Store
+}
+
+func (sa *storageAdapterImpl) GetClockData(ctx *context.T, data *ClockData) error {
+	return util.Get(ctx, sa.st, clockDataKey(), data)
+}
+
+func (sa *storageAdapterImpl) SetClockData(ctx *context.T, data *ClockData) error {
+	return util.Put(ctx, sa.st, clockDataKey(), data)
+}
+
+func clockDataKey() string {
+	return util.ClockPrefix
+}
diff --git a/services/syncbase/clock/test_util.go b/services/syncbase/clock/test_util.go
new file mode 100644
index 0000000..6027f49
--- /dev/null
+++ b/services/syncbase/clock/test_util.go
@@ -0,0 +1,119 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package clock
+
+// Utilities for testing clock.
+
+import (
+	"time"
+
+	"v.io/v23/context"
+	"v.io/v23/verror"
+)
+
+/////////////////////////////////////////////////
+// Mock for StorageAdapter
+
+var _ StorageAdapter = (*storageAdapterMockImpl)(nil)
+
+func MockStorageAdapter() *storageAdapterMockImpl {
+	return &storageAdapterMockImpl{}
+}
+
+type storageAdapterMockImpl struct {
+	clockData *ClockData
+	err       error
+}
+
+func (sa *storageAdapterMockImpl) GetClockData(ctx *context.T, data *ClockData) error {
+	if sa.err != nil {
+		return sa.err
+	}
+	if sa.clockData == nil {
+		return verror.NewErrNoExist(ctx)
+	}
+	*data = *sa.clockData
+	return nil
+}
+
+func (sa *storageAdapterMockImpl) SetClockData(ctx *context.T, data *ClockData) error {
+	if sa.err != nil {
+		return sa.err
+	}
+	sa.clockData = data
+	return nil
+}
+
+func (sa *storageAdapterMockImpl) SetError(err error) {
+	sa.err = err
+}
+
+/////////////////////////////////////////////////
+// Mock for SystemClock
+
+var _ SystemClock = (*systemClockMockImpl)(nil)
+
+func MockSystemClock(now time.Time, elapsedTime time.Duration) *systemClockMockImpl {
+	return &systemClockMockImpl{
+		now: now,
+		elapsedTime: elapsedTime,
+	}
+}
+
+type systemClockMockImpl struct {
+	now         time.Time
+	elapsedTime time.Duration
+}
+
+func (sc *systemClockMockImpl) Now() time.Time {
+	return sc.now
+}
+
+func (sc *systemClockMockImpl) SetNow(now time.Time) {
+	sc.now = now
+}
+
+func (sc *systemClockMockImpl) ElapsedTime() (time.Duration, error) {
+	return sc.elapsedTime, nil
+}
+
+func (sc *systemClockMockImpl) SetElapsedTime(elapsed time.Duration) {
+	sc.elapsedTime = elapsed
+}
+
+/////////////////////////////////////////////////
+// Mock for NtpSource
+
+var _ NtpSource = (*ntpSourceMockImpl)(nil)
+
+func MockNtpSource() *ntpSourceMockImpl {
+	return &ntpSourceMockImpl{}
+}
+
+type ntpSourceMockImpl struct {
+	Err  error
+	Data *NtpData
+}
+
+func (ns *ntpSourceMockImpl) NtpSync(sampleCount int) (*NtpData, error) {
+	if ns.Err != nil {
+		return nil, ns.Err
+	}
+	return ns.Data, nil
+}
+
+func NewVClockWithMockServices(sa StorageAdapter, sc SystemClock, ns NtpSource) *VClock {
+	if sc == nil {
+		sc = newSystemClock()
+	}
+	if ns == nil {
+		ns = NewNtpSource(sc)
+	}
+	return &VClock{
+		clock:     sc,
+		sa:        sa,
+		ntpSource: ns,
+	}
+}
diff --git a/services/syncbase/clock/types.go b/services/syncbase/clock/types.go
new file mode 100644
index 0000000..6934f11
--- /dev/null
+++ b/services/syncbase/clock/types.go
@@ -0,0 +1,53 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package clock
+
+import (
+	"time"
+
+	"v.io/v23/context"
+)
+
+// This interface provides a wrapper over system clock to allow easy testing
+// of VClock and other code that uses timestamps.
+type SystemClock interface {
+	// Now returns the current UTC time as known by the system.
+	// This may not reflect the NTP time if the system clock is out of
+	// sync with NTP.
+	Now() time.Time
+
+	// ElapsedTime returns a duration representing the time elapsed since the device
+	// rebooted.
+	ElapsedTime() (time.Duration, error)
+}
+
+type StorageAdapter interface {
+	GetClockData(ctx *context.T, data *ClockData) error
+	SetClockData(ctx *context.T, data *ClockData) error
+}
+
+type NtpSource interface {
+	// NtpSync obtains NtpData samples from an NTP server and returns the one
+	// which has the lowest network delay.
+	// Param sampleCount is the number of samples this method will fetch.
+	// NtpData contains the clock offset and the network delay experienced while
+	// talking to the server.
+	NtpSync(sampleCount int) (*NtpData, error)
+}
+
+type NtpData struct {
+	// Offset is the difference between the NTP time and the system clock.
+	// Adding offset to system clock will give estimated NTP time.
+	offset time.Duration
+
+	// Delay is the round trip network delay experienced while talking to NTP
+	// server. The smaller the delay, the more accurate the offset is.
+	delay  time.Duration
+}
+
+func (cd *ClockData) SystemBootTime() time.Time {
+	ns := time.Second.Nanoseconds()
+	return time.Unix(cd.SystemTimeAtBoot/ns, cd.SystemTimeAtBoot%ns)
+}
diff --git a/services/syncbase/clock/types.vdl b/services/syncbase/clock/types.vdl
new file mode 100644
index 0000000..b12f8f9
--- /dev/null
+++ b/services/syncbase/clock/types.vdl
@@ -0,0 +1,20 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package clock
+
+// ClockData is the persistent state of syncbase clock used to estimate current
+// NTP time and catch any unexpected changes to system clock.
+type ClockData struct {
+	// UTC time in unix nano seconds obtained from system clock at boot.
+	SystemTimeAtBoot int64
+
+	// Skew between the system clock and NTP time.
+	Skew int64
+
+	// The elapsed time since boot as last seen during a run of clockservice.
+	// This is used to determine if the device rebooted since the last run of
+	// clockservice.
+	ElapsedTimeSinceBoot int64
+}
\ No newline at end of file
diff --git a/services/syncbase/clock/types.vdl.go b/services/syncbase/clock/types.vdl.go
new file mode 100644
index 0000000..4749d42
--- /dev/null
+++ b/services/syncbase/clock/types.vdl.go
@@ -0,0 +1,35 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file was auto-generated by the vanadium vdl tool.
+// Source: types.vdl
+
+package clock
+
+import (
+	// VDL system imports
+	"v.io/v23/vdl"
+)
+
+// ClockData is the persistent state of syncbase clock used to estimate current
+// NTP time and catch any unexpected changes to system clock.
+type ClockData struct {
+	// UTC time in unix nano seconds obtained from system clock at boot.
+	SystemTimeAtBoot int64
+	// Skew between the system clock and NTP time.
+	Skew int64
+	// The elapsed time since boot as last seen during a run of clockservice.
+	// This is used to determine if the device rebooted since the last run of
+	// clockservice.
+	ElapsedTimeSinceBoot int64
+}
+
+func (ClockData) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/clock.ClockData"`
+}) {
+}
+
+func init() {
+	vdl.Register((*ClockData)(nil))
+}
diff --git a/services/syncbase/clock/vclock.go b/services/syncbase/clock/vclock.go
new file mode 100644
index 0000000..95719f4
--- /dev/null
+++ b/services/syncbase/clock/vclock.go
@@ -0,0 +1,73 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package clock
+
+import (
+	"time"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/context"
+	"v.io/v23/verror"
+	"v.io/x/lib/vlog"
+)
+
+// VClock holds data required to provide an estimate of the UTC time at any
+// given point. The fields contained in here are
+// - systemTimeAtBoot : the time shown by the system clock at boot.
+// - skew             : the difference between the system clock and UTC time.
+// - clock            : Instance of clock.SystemClock interface providing access
+//                      to the system time.
+// - sa               : adapter for storage of clock data.
+// - ntpSource        : source for fetching NTP data.
+type VClock struct {
+	systemTimeAtBoot time.Time
+	skew             time.Duration
+	clock            SystemClock
+	sa               StorageAdapter
+	ntpSource        NtpSource
+}
+
+func NewVClock(st store.Store) *VClock {
+	sysClock := newSystemClock()
+	return &VClock{
+		clock:     sysClock,
+		sa:        NewStorageAdapter(st),
+		ntpSource: NewNtpSource(sysClock),
+	}
+}
+
+// Now returns current UTC time based on the estimation of skew that
+// the system clock has with respect to NTP time.
+func (c *VClock) Now(ctx *context.T) time.Time {
+	clockData := &ClockData{}
+	if err := c.sa.GetClockData(ctx, clockData); err != nil {
+		if verror.ErrorID(err) == verror.ErrNoExist.ID {
+			// VClock's cron job to setup UTC time at boot has not been run yet.
+			vlog.Error("No ClockData found while creating a timestamp")
+		} else {
+			vlog.Errorf("Error while fetching clock data: %v", err)
+		}
+		vlog.Error("Returning current system clock time")
+		return c.clock.Now()
+	}
+	skew := time.Duration(clockData.Skew)
+	return c.clock.Now().Add(skew)
+}
+
+///////////////////////////////////////////////////
+// Implementation for SystemClock.
+
+type systemClockImpl struct{}
+
+// Returns system time in UTC.
+func (sc *systemClockImpl) Now() time.Time {
+	return time.Now().UTC()
+}
+
+var _ SystemClock = (*systemClockImpl)(nil)
+
+func newSystemClock() SystemClock {
+	return &systemClockImpl{}
+}
diff --git a/services/syncbase/clock/vclock_test.go b/services/syncbase/clock/vclock_test.go
new file mode 100644
index 0000000..bf92f5c
--- /dev/null
+++ b/services/syncbase/clock/vclock_test.go
@@ -0,0 +1,69 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package clock
+
+import (
+	"testing"
+	"time"
+
+	"v.io/v23/verror"
+)
+
+func TestVClock(t *testing.T) {
+	sysTs := time.Now()
+	sysClock := MockSystemClock(sysTs, 0)
+	stAdapter := MockStorageAdapter()
+	stAdapter.SetClockData(nil, &ClockData{0, 0, 0})
+	clock := NewVClockWithMockServices(stAdapter, sysClock, nil)
+
+	ts := clock.Now(nil)
+	if ts != sysTs {
+		t.Errorf("timestamp expected to be %q but found to be %q", sysTs, ts)
+	}
+}
+
+func TestVClockWithSkew(t *testing.T) {
+	// test with positive skew
+	checkSkew(t, 5)
+	// test with negative skew
+	checkSkew(t, -5)
+}
+
+func checkSkew(t *testing.T, skew int64) {
+	sysTs := time.Now()
+	sysClock := MockSystemClock(sysTs, 0)
+
+	var elapsedTime int64 = 100
+	stAdapter := MockStorageAdapter()
+	bootTime := sysTs.UnixNano() - elapsedTime
+	clockData := ClockData{bootTime, skew, elapsedTime}
+	stAdapter.SetClockData(nil, &clockData)
+
+	clock := NewVClockWithMockServices(stAdapter, sysClock, nil)
+
+	ts := clock.Now(nil)
+	if ts == sysTs {
+		t.Errorf("timestamp expected to be %q but found to be %q", sysTs, ts)
+	}
+	if ts.UnixNano() != (sysTs.UnixNano() + skew) {
+		t.Errorf("Unexpected vclock timestamp. vclock: %v, sysclock: %v, skew: %v", ts, sysTs, skew)
+	}
+}
+
+func TestVClockWithInternalErr(t *testing.T) {
+	sysTs := time.Now()
+	sysClock := MockSystemClock(sysTs, 0)
+
+	stAdapter := MockStorageAdapter()
+	stAdapter.SetError(verror.NewErrInternal(nil))
+
+	clock := NewVClockWithMockServices(stAdapter, sysClock, nil)
+
+	// Internal err should result in vclock falling back to the system clock.
+	ts := clock.Now(nil)
+	if ts != sysTs {
+		t.Errorf("timestamp expected to be %q but found to be %q", sysTs, ts)
+	}
+}
diff --git a/services/syncbase/localblobstore/blobmap/blobmap.go b/services/syncbase/localblobstore/blobmap/blobmap.go
new file mode 100644
index 0000000..c674f68
--- /dev/null
+++ b/services/syncbase/localblobstore/blobmap/blobmap.go
@@ -0,0 +1,480 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package blobmap implements a map from chunk checksums to chunk locations
+// and vice versa, using a store.Store (currently, one implemented with
+// leveldb).
+package blobmap
+
+import "encoding/binary"
+import "sync"
+
+import "v.io/syncbase/x/ref/services/syncbase/store"
+import "v.io/syncbase/x/ref/services/syncbase/store/leveldb"
+import "v.io/v23/context"
+import "v.io/v23/verror"
+
+const pkgPath = "v.io/syncbase/x/ref/services/syncbase/localblobstore/blobmap"
+
+var (
+	errBadBlobIDLen        = verror.Register(pkgPath+".errBadBlobIDLen", verror.NoRetry, "{1:}{2:} blobmap {3}: bad blob length {4} should be {5}{:_}")
+	errBadChunkHashLen     = verror.Register(pkgPath+".errBadChunkHashLen", verror.NoRetry, "{1:}{2:} blobmap {3}: bad chunk hash length {4} should be {5}{:_}")
+	errNoSuchBlob          = verror.Register(pkgPath+".errNoSuchBlob", verror.NoRetry, "{1:}{2:} blobmap {3}: no such blob{:_}")
+	errMalformedChunkEntry = verror.Register(pkgPath+".errMalformedChunkEntry", verror.NoRetry, "{1:}{2:} blobmap {3}: malfored chunk entry{:_}")
+	errNoSuchChunk         = verror.Register(pkgPath+".errNoSuchChunk", verror.NoRetry, "{1:}{2:} blobmap {3}: no such chunk{:_}")
+	errMalformedBlobEntry  = verror.Register(pkgPath+".errMalformedBlobEntry", verror.NoRetry, "{1:}{2:} blobmap {3}: malfored blob entry{:_}")
+)
+
+// There are two tables: chunk-to-location, and blob-to-chunk.
+// Each chunk is represented by one entry in each table.
+// On deletion, the latter is used to find the former, so the latter is added
+// first, and deleted last.
+//
+// chunk-to-location:
+//    Key:    1-byte containing chunkPrefix, 16-byte chunk hash, 16-byte blob ID
+//    Value:  Varint offset, Varint length.
+// The chunk with the specified 16-byte hash had the specified length, and is
+// (or was) found at the specified offset in the blob.
+//
+// blob-to-chunk:
+//    Key:    1-byte containing blobPrefix, 16-byte blob ID, 8-byte bigendian offset
+//    Value:  16-byte chunk hash, Varint length.
+//
+// The varint encoded fields are written/read with
+// encoding/binary.{Put,Read}Varint.  The blob-to-chunk keys encode the offset
+// as raw big-endian (encoding/binary.{Put,}Uint64) so that it will sort in
+// increasing offset order.
+
+const chunkHashLen = 16 // length of chunk hash
+const blobIDLen = 16    // length of blob ID
+const offsetLen = 8     // length of offset in blob-to-chunk key
+
+const maxKeyLen = 64 // conservative maximum key length
+const maxValLen = 64 // conservative maximum value length
+
+var chunkPrefix []byte = []byte{0} // key prefix for chunk-to-location
+var blobPrefix []byte = []byte{1}  // key prefix for blob-to-chunk
+
+// offsetLimit is an offset that's greater than, and one byte longer than, any
+// real offset.
+var offsetLimit []byte = []byte{
+	0xff, 0xff, 0xff, 0xff,
+	0xff, 0xff, 0xff, 0xff,
+	0xff,
+}
+
+// blobLimit is a blobID that's greater than, and one byte longer than, any
+// real blob ID
+var blobLimit []byte = []byte{
+	0xff, 0xff, 0xff, 0xff,
+	0xff, 0xff, 0xff, 0xff,
+	0xff, 0xff, 0xff, 0xff,
+	0xff, 0xff, 0xff, 0xff,
+	0xff,
+}
+
+// A Location describes chunk's location within a blob.
+type Location struct {
+	BlobID []byte // ID of blob
+	Offset int64  // byte offset of chunk within blob
+	Size   int64  // size of chunk
+}
+
+// A BlobMap maps chunk checksums to Locations, and vice versa.
+type BlobMap struct {
+	dir string      // the directory where the store is held
+	st  store.Store // private store that holds the mapping.
+}
+
+// New() returns a pointer to a BlobMap, backed by storage in directory dir.
+func New(ctx *context.T, dir string) (bm *BlobMap, err error) {
+	bm = new(BlobMap)
+	bm.dir = dir
+	bm.st, err = leveldb.Open(dir, leveldb.OpenOptions{CreateIfMissing: true, ErrorIfExists: false})
+	return bm, err
+}
+
+// Close() closes any files or other resources associated with *bm.
+// No other methods on bm may be called after Close().
+func (bm *BlobMap) Close() error {
+	return bm.st.Close()
+}
+
+// AssociateChunkWithLocation() remembers that the specified chunk hash is
+// associated with the specified Location.
+func (bm *BlobMap) AssociateChunkWithLocation(ctx *context.T, chunk []byte, loc Location) (err error) {
+	// Check of expected lengths explicitly in routines that modify the database.
+	if len(loc.BlobID) != blobIDLen {
+		err = verror.New(errBadBlobIDLen, ctx, bm.dir, len(loc.BlobID), blobIDLen)
+	} else if len(chunk) != chunkHashLen {
+		err = verror.New(errBadChunkHashLen, ctx, bm.dir, len(chunk), chunkHashLen)
+	} else {
+		var key [maxKeyLen]byte
+		var val [maxValLen]byte
+
+		// Put the blob-to-chunk entry first, since it's used
+		// to garbage collect the other.
+		keyLen := copy(key[:], blobPrefix)
+		keyLen += copy(key[keyLen:], loc.BlobID)
+		binary.BigEndian.PutUint64(key[keyLen:], uint64(loc.Offset))
+		keyLen += offsetLen
+
+		valLen := copy(val[:], chunk)
+		valLen += binary.PutVarint(val[valLen:], loc.Size)
+		err = bm.st.Put(key[:keyLen], val[:valLen])
+
+		if err == nil {
+			keyLen = copy(key[:], chunkPrefix)
+			keyLen += copy(key[keyLen:], chunk)
+			keyLen += copy(key[keyLen:], loc.BlobID)
+
+			valLen = binary.PutVarint(val[:], loc.Offset)
+			valLen += binary.PutVarint(val[valLen:], loc.Size)
+
+			err = bm.st.Put(key[:keyLen], val[:valLen])
+		}
+	}
+
+	return err
+}
+
+// DeleteBlob() deletes any of the chunk associations previously added with
+// AssociateChunkWithLocation(..., chunk, ...).
+func (bm *BlobMap) DeleteBlob(ctx *context.T, blob []byte) (err error) {
+	// Check of expected lengths explicitly in routines that modify the database.
+	if len(blob) != blobIDLen {
+		err = verror.New(errBadBlobIDLen, ctx, bm.dir, len(blob), blobIDLen)
+	} else {
+		var start [maxKeyLen]byte
+		var limit [maxKeyLen]byte
+
+		startLen := copy(start[:], blobPrefix)
+		startLen += copy(start[startLen:], blob)
+
+		limitLen := copy(limit[:], start[:startLen])
+		limitLen += copy(limit[limitLen:], offsetLimit)
+
+		var keyBuf [maxKeyLen]byte    // buffer for keys returned by stream
+		var valBuf [maxValLen]byte    // buffer for values returned by stream
+		var deleteKey [maxKeyLen]byte // buffer to construct chunk-to-location keys to delete
+
+		deletePrefixLen := copy(deleteKey[:], chunkPrefix)
+
+		seenAValue := false
+
+		s := bm.st.Scan(start[:startLen], limit[:limitLen])
+		for s.Advance() && err == nil {
+			seenAValue = true
+
+			key := s.Key(keyBuf[:])
+			value := s.Value(valBuf[:])
+
+			if len(value) >= chunkHashLen {
+				deleteKeyLen := deletePrefixLen
+				deleteKeyLen += copy(deleteKey[deleteKeyLen:], value[:chunkHashLen])
+				deleteKeyLen += copy(deleteKey[deleteKeyLen:], blob)
+				err = bm.st.Delete(deleteKey[:deleteKeyLen])
+			}
+
+			if err == nil {
+				// Delete the blob-to-chunk entry last, as it's
+				// used to find the chunk-to-location entry.
+				err = bm.st.Delete(key)
+			}
+		}
+
+		if err != nil {
+			s.Cancel()
+		} else {
+			err = s.Err()
+			if err == nil && !seenAValue {
+				err = verror.New(errNoSuchBlob, ctx, bm.dir, blob)
+			}
+		}
+	}
+
+	return err
+}
+
+// LookupChunk() returns a Location for the specified chunk.  Only one Location
+// is returned, even if several are available in the database.  If the client
+// finds that the Location is not available, perhaps because its blob has
+// been deleted, the client should remove the blob from the BlobMap using
+// DeleteBlob(loc.Blob), and try again.  (The client may also wish to
+// arrange at some point to call GC() on the blob store.)
+func (bm *BlobMap) LookupChunk(ctx *context.T, chunkHash []byte) (loc Location, err error) {
+	var start [maxKeyLen]byte
+	var limit [maxKeyLen]byte
+
+	startLen := copy(start[:], chunkPrefix)
+	startLen += copy(start[startLen:], chunkHash)
+
+	limitLen := copy(limit[:], start[:startLen])
+	limitLen += copy(limit[limitLen:], blobLimit)
+
+	var keyBuf [maxKeyLen]byte // buffer for keys returned by stream
+	var valBuf [maxValLen]byte // buffer for values returned by stream
+
+	s := bm.st.Scan(start[:startLen], limit[:limitLen])
+	if s.Advance() {
+		var n int
+		key := s.Key(keyBuf[:])
+		value := s.Value(valBuf[:])
+		loc.BlobID = key[len(chunkPrefix)+chunkHashLen:]
+		loc.Offset, n = binary.Varint(value)
+		if n > 0 {
+			loc.Size, n = binary.Varint(value[n:])
+		}
+		if n <= 0 {
+			err = verror.New(errMalformedChunkEntry, ctx, bm.dir, chunkHash, key, value)
+		}
+		s.Cancel()
+	} else {
+		if err == nil {
+			err = s.Err()
+		}
+		if err == nil {
+			err = verror.New(errNoSuchChunk, ctx, bm.dir, chunkHash)
+		}
+	}
+
+	return loc, err
+}
+
+// A ChunkStream allows the client to iterate over the chunks in a blob:
+//	cs := bm.NewChunkStream(ctx, blob)
+//	for cs.Advance() {
+//		chunkHash := cs.Value()
+//		...process chunkHash...
+//	}
+//	if cs.Err() != nil {
+//		...there was an error...
+//	}
+type ChunkStream struct {
+	bm     *BlobMap
+	ctx    *context.T
+	stream store.Stream
+
+	keyBuf [maxKeyLen]byte // buffer for keys
+	valBuf [maxValLen]byte // buffer for values
+	key    []byte          // key for current element
+	value  []byte          // value of current element
+	loc    Location        // location of current element
+	err    error           // error encountered.
+	more   bool            // whether stream may be consulted again
+}
+
+// NewChunkStream() returns a pointer to a new ChunkStream that allows the client
+// to enumerate the chunk hashes in a blob, in order.
+func (bm *BlobMap) NewChunkStream(ctx *context.T, blob []byte) *ChunkStream {
+	var start [maxKeyLen]byte
+	var limit [maxKeyLen]byte
+
+	startLen := copy(start[:], blobPrefix)
+	startLen += copy(start[startLen:], blob)
+
+	limitLen := copy(limit[:], start[:startLen])
+	limitLen += copy(limit[limitLen:], offsetLimit)
+
+	cs := new(ChunkStream)
+	cs.bm = bm
+	cs.ctx = ctx
+	cs.stream = bm.st.Scan(start[:startLen], limit[:limitLen])
+	cs.more = true
+
+	return cs
+}
+
+// Advance() stages an element so the client can retrieve the chunk hash with
+// Value(), or its Location with Location().  Advance() returns true iff there
+// is an element to retrieve.  The client must call Advance() before calling
+// Value() or Location() The client must call Cancel if it does not iterate
+// through all elements (i.e. until Advance() returns false).  Advance() may
+// block if an element is not immediately available.
+func (cs *ChunkStream) Advance() (ok bool) {
+	if cs.more && cs.err == nil {
+		if !cs.stream.Advance() {
+			cs.err = cs.stream.Err()
+			cs.more = false // no more stream, even if no error
+		} else {
+			cs.key = cs.stream.Key(cs.keyBuf[:])
+			cs.value = cs.stream.Value(cs.valBuf[:])
+			ok = (len(cs.value) >= chunkHashLen) &&
+				(len(cs.key) == len(blobPrefix)+blobIDLen+offsetLen)
+			if ok {
+				var n int
+				cs.loc.BlobID = make([]byte, blobIDLen)
+				copy(cs.loc.BlobID, cs.key[len(blobPrefix):len(blobPrefix)+blobIDLen])
+				cs.loc.Offset = int64(binary.BigEndian.Uint64(cs.key[len(blobPrefix)+blobIDLen:]))
+				cs.loc.Size, n = binary.Varint(cs.value[chunkHashLen:])
+				ok = (n > 0)
+			}
+			if !ok {
+				cs.err = verror.New(errMalformedBlobEntry, cs.ctx, cs.bm.dir, cs.key, cs.value)
+				cs.stream.Cancel()
+			}
+		}
+	}
+	return ok
+}
+
+// Value() returns the content hash of the chunk staged by
+// Advance().  The returned slice may be a sub-slice of buf if buf is large
+// enough to hold the entire value.  Otherwise, a newly allocated slice will be
+// returned.  It is valid to pass a nil buf.  Value() may panic if Advance()
+// returned false or was not called at all.  Value() does not block.
+func (cs *ChunkStream) Value(buf []byte) (result []byte) {
+	if len(buf) < chunkHashLen {
+		buf = make([]byte, chunkHashLen)
+	}
+	copy(buf, cs.value[:chunkHashLen])
+	return buf[:chunkHashLen]
+}
+
+// Location() returns the Location associated with the chunk staged by
+// Advance().  Location() may panic if Advance() returned false or was not
+// called at all.  Location() does not block.
+func (cs *ChunkStream) Location() Location {
+	return cs.loc
+}
+
+// Err() returns a non-nil error iff the stream encountered any errors.  Err()
+// does not block.
+func (cs *ChunkStream) Err() error {
+	return cs.err
+}
+
+// Cancel() notifies the stream provider that it can stop producing elements.
+// The client must call Cancel() if it does not iterate through all elements
+// (i.e. until Advance() returns false).  Cancel() is idempotent and can be
+// called concurrently with a goroutine that is iterating via Advance() and
+// Value().  Cancel() causes Advance() to subsequently return false.
+// Cancel() does not block.
+func (cs *ChunkStream) Cancel() {
+	cs.stream.Cancel()
+}
+
+// A BlobStream allows the client to iterate over the blobs in BlobMap:
+//	bs := bm.NewBlobStream(ctx)
+//	for bs.Advance() {
+//		blobID := bs.Value()
+//		...process blobID...
+//	}
+//	if bs.Err() != nil {
+//		...there was an error...
+//	}
+type BlobStream struct {
+	bm  *BlobMap
+	ctx *context.T
+
+	key    []byte          // key for current element
+	keyBuf [maxKeyLen]byte // buffer for keys
+	err    error           // error encountered.
+	mu     sync.Mutex      // protects "more", which may be written in Cancel()
+	more   bool            // whether stream may be consulted again
+}
+
+// keyLimit is the key for limit in store.Scan() calls within a BlobStream.
+var keyLimit []byte
+
+func init() {
+	// The limit key is the maximum length key, all ones after the blobPrefix.
+	keyLimit = make([]byte, maxKeyLen)
+	for i := copy(keyLimit, blobPrefix); i != len(keyLimit); i++ {
+		keyLimit[i] = 0xff
+	}
+}
+
+// NewBlobStream() returns a pointer to a new BlobStream that allows the client
+// to enumerate the blobs BlobMap, in lexicographic order.
+func (bm *BlobMap) NewBlobStream(ctx *context.T) *BlobStream {
+	bs := new(BlobStream)
+	bs.bm = bm
+	bs.ctx = ctx
+	bs.more = true
+	return bs
+}
+
+// Advance() stages an element so the client can retrieve the next blob ID with
+// Value().  Advance() returns true iff there is an element to retrieve.  The
+// client must call Advance() before calling Value().  The client must call
+// Cancel if it does not iterate through all elements (i.e. until Advance()
+// returns false).  Advance() may block if an element is not immediately
+// available.
+func (bs *BlobStream) Advance() (ok bool) {
+	bs.mu.Lock()
+	ok = bs.more
+	bs.mu.Unlock()
+	if ok {
+		prefixAndKeyLen := len(blobPrefix) + blobIDLen
+		// Compute the next key to search for.
+		if len(bs.key) == 0 { // First time through: anything starting with blobPrefix.
+			n := copy(bs.keyBuf[:], blobPrefix)
+			bs.key = bs.keyBuf[:n]
+		} else {
+			// Increment the blobID to form the next possible key.
+			i := prefixAndKeyLen - 1
+			for ; i != len(blobPrefix)-1 && bs.keyBuf[i] == 0xff; i-- {
+				bs.keyBuf[i] = 0
+			}
+			if i == len(blobPrefix)-1 { // End of database
+				ok = false
+			} else {
+				bs.keyBuf[i]++
+			}
+			bs.key = bs.keyBuf[:prefixAndKeyLen]
+		}
+		if ok {
+			stream := bs.bm.st.Scan(bs.key, keyLimit)
+			if !stream.Advance() {
+				bs.err = stream.Err()
+				ok = false // no more stream, even if no error
+			} else {
+				bs.key = stream.Key(bs.keyBuf[:])
+				if len(bs.key) < prefixAndKeyLen {
+					bs.err = verror.New(errMalformedBlobEntry, bs.ctx, bs.bm.dir, bs.key, stream.Value(nil))
+					ok = false
+				}
+				stream.Cancel() // We get at most one element from each stream.
+			}
+		}
+		if !ok {
+			bs.mu.Lock()
+			bs.more = false
+			bs.mu.Unlock()
+		}
+	}
+	return ok
+}
+
+// Value() returns the blob ID staged by Advance().  The returned slice may be
+// a sub-slice of buf if buf is large enough to hold the entire value.
+// Otherwise, a newly allocated slice will be returned.  It is valid to pass a
+// nil buf.  Value() may panic if Advance() returned false or was not called at
+// all.  Value() does not block.
+func (bs *BlobStream) Value(buf []byte) (result []byte) {
+	if len(buf) < blobIDLen {
+		buf = make([]byte, blobIDLen)
+	}
+	copy(buf, bs.key[len(blobPrefix):len(blobPrefix)+blobIDLen])
+	return buf[:blobIDLen]
+}
+
+// Err() returns a non-nil error iff the stream encountered any errors.  Err()
+// does not block.
+func (bs *BlobStream) Err() error {
+	return bs.err
+}
+
+// Cancel() notifies the stream provider that it can stop producing elements.
+// The client must call Cancel() if it does not iterate through all elements
+// (i.e. until Advance() returns false).  Cancel() is idempotent and can be
+// called concurrently with a goroutine that is iterating via Advance() and
+// Value().  Cancel() causes Advance() to subsequently return false.
+// Cancel() does not block.
+func (bs *BlobStream) Cancel() {
+	bs.mu.Lock()
+	bs.more = false
+	bs.mu.Unlock()
+}
diff --git a/services/syncbase/localblobstore/blobmap/blobmap_test.go b/services/syncbase/localblobstore/blobmap/blobmap_test.go
new file mode 100644
index 0000000..450049a
--- /dev/null
+++ b/services/syncbase/localblobstore/blobmap/blobmap_test.go
@@ -0,0 +1,278 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// A test for blobmap.
+package blobmap_test
+
+import "bytes"
+import "io/ioutil"
+import "math/rand"
+import "os"
+import "runtime"
+import "testing"
+
+import "v.io/syncbase/x/ref/services/syncbase/localblobstore/blobmap"
+import "v.io/v23/context"
+
+import "v.io/x/ref/test"
+import _ "v.io/x/ref/runtime/factories/generic"
+
+// id() returns a new random 16-byte byte vector.
+func id() []byte {
+	v := make([]byte, 16)
+	for i := 0; i != len(v); i++ {
+		v[i] = byte(rand.Int31n(256))
+	}
+	return v
+}
+
+// verifyBlobs() tests that the blobs in *bm are those in b[], as revealed via
+// the BlobStream() interface.
+func verifyBlobs(t *testing.T, ctx *context.T, bm *blobmap.BlobMap, b [][]byte) {
+	_, _, callerLine, _ := runtime.Caller(1)
+	seen := make([]bool, len(b)) // seen[i] == whether b[i] seen in *bm
+	bs := bm.NewBlobStream(ctx)
+	var i int
+	for i = 0; bs.Advance(); i++ {
+		blob := bs.Value(nil)
+		var j int
+		for j = 0; j != len(b) && bytes.Compare(b[j], blob) != 0; j++ {
+		}
+		if j == len(b) {
+			t.Errorf("blobmap_test: line %d: unexpected blob %v present in BlobMap",
+				callerLine, blob)
+		} else if seen[j] {
+			t.Errorf("blobmap_test: line %d: blob %v seen twice in BlobMap",
+				callerLine, blob)
+		} else {
+			seen[j] = true
+		}
+	}
+	if i != len(b) {
+		t.Errorf("blobmap_test: line %d: found %d blobs in BlobMap, but expected %d",
+			callerLine, i, len(b))
+	}
+	for j := range seen {
+		if !seen[j] {
+			t.Errorf("blobmap_test: line %d: blob %v not seen un BlobMap",
+				callerLine, b[j])
+		}
+	}
+	if bs.Err() != nil {
+		t.Errorf("blobmap_test: line %d: BlobStream.Advance: unexpected error %v",
+			callerLine, bs.Err())
+	}
+}
+
+// verifyNoChunksInBlob() tests that blob b[blobi] has no chunks in *bm, as
+// revealed by the ChunkStream interface.
+func verifyNoChunksInBlob(t *testing.T, ctx *context.T, bm *blobmap.BlobMap, blobi int, b [][]byte) {
+	_, _, callerLine, _ := runtime.Caller(1)
+	cs := bm.NewChunkStream(ctx, b[blobi])
+	for i := 0; cs.Advance(); i++ {
+		t.Errorf("blobmap_test: line %d: blob %d: chunk %d: %v",
+			callerLine, blobi, i, cs.Value(nil))
+	}
+	if cs.Err() != nil {
+		t.Errorf("blobmap_test: line %d: blob %d: ChunkStream.Advance: unexpected error %v",
+			callerLine, blobi, cs.Err())
+	}
+}
+
+// verifyChunksInBlob() tests that blob b[blobi] in *bm contains the expected
+// chunks from c[].  Each blob is expected to have 8 chunks, 0...7, except that
+// b[1] has c[8] instead of c[4] for chunk 4.
+func verifyChunksInBlob(t *testing.T, ctx *context.T, bm *blobmap.BlobMap, blobi int, b [][]byte, c [][]byte) {
+	_, _, callerLine, _ := runtime.Caller(1)
+	var err error
+	var i int
+	cs := bm.NewChunkStream(ctx, b[blobi])
+	for i = 0; cs.Advance(); i++ {
+		chunk := cs.Value(nil)
+		chunki := i
+		if blobi == 1 && i == 4 { // In blob 1, c[4] is replaced by c[8]
+			chunki = 8
+		}
+		if bytes.Compare(c[chunki], chunk) != 0 {
+			t.Errorf("blobmap_test: line %d: blob %d: chunk %d: got %v, expected %v",
+				callerLine, blobi, i, chunk, c[chunki])
+		}
+
+		var loc blobmap.Location
+		loc, err = bm.LookupChunk(ctx, chunk)
+		if err != nil {
+			t.Errorf("blobmap_test: line %d: blob %d: chunk %d: LookupChunk got unexpected error: %v",
+				callerLine, blobi, i, err)
+		} else {
+			if i == 4 {
+				if bytes.Compare(loc.BlobID, b[blobi]) != 0 {
+					t.Errorf("blobmap_test: line %d: blob %d: chunk %d: Location.BlobID got %v, expected %v",
+						callerLine, blobi, i, loc.BlobID, b[blobi])
+				}
+			} else {
+				if bytes.Compare(loc.BlobID, b[0]) != 0 && bytes.Compare(loc.BlobID, b[1]) != 0 {
+					t.Errorf("blobmap_test: line %d: blob %d: chunk %d: Location.BlobID got %v, expected %v",
+						callerLine, blobi, i, loc.BlobID, b[blobi])
+				}
+			}
+			if loc.Offset != int64(i) {
+				t.Errorf("blobmap_test: line %d: blob %d: chunk %d: Location.Offset got %d, expected %d",
+					callerLine, blobi, i, loc.Offset, i)
+			}
+			if loc.Size != 1 {
+				t.Errorf("blobmap_test: line %d: blob %d: chunk %d: Location.Size got %d, expected 1",
+					callerLine, blobi, i, loc.Size)
+			}
+
+			// The offsets and sizes will match, between the result
+			// from the stream and the result from LookupChunk(),
+			// because for all chunks written to both, they are
+			// written to the same places.  However, the blob need
+			// not match, since LookupChunk() will return an
+			// arbitrary Location in the store that contains the
+			// chunk.
+			loc2 := cs.Location()
+			if loc.Offset != loc2.Offset || loc.Size != loc2.Size {
+				t.Errorf("blobmap_test: line %d: blob %d: chunk %d: disagreement about location: LookupChunk %v vs ChunkStream %v",
+					callerLine, blobi, i, loc, loc2)
+			}
+		}
+	}
+	if cs.Err() != nil {
+		t.Errorf("blobmap_test: line %d: blob %d: ChunkStream.Err() unepxected error %v",
+			callerLine, blobi, cs.Err())
+	}
+	if i != 8 {
+		t.Errorf("blobmap_test: line %d: blob %d: ChunkStream.Advance unexpectedly saw %d chunks, expected 8",
+			callerLine, blobi, i)
+	}
+}
+
+// TestAddRetrieveAndDelete() tests insertion, retrieval, and deletion of blobs
+// from a BlobMap.  It's all done in one test case, because one cannot retrieve
+// or delete blobs that have not been inserted.
+func TestAddRetrieveAndDelete(t *testing.T) {
+	ctx, shutdown := test.V23Init()
+	defer shutdown()
+
+	// Make a temporary directory.
+	var err error
+	var testDirName string
+	testDirName, err = ioutil.TempDir("", "blobmap_test")
+	if err != nil {
+		t.Fatalf("blobmap_test: can't make tmp directory: %v", err)
+	}
+	defer os.RemoveAll(testDirName)
+
+	// Create a blobmap.
+	var bm *blobmap.BlobMap
+	bm, err = blobmap.New(ctx, testDirName)
+	if err != nil {
+		t.Fatalf("blobmap_test: blobmap.New failed: %v", err)
+	}
+
+	// Two blobs: b[0] and b[1].
+	b := [][]byte{id(), id()}
+
+	// Nine chunks: c[0 .. 8]
+	c := [][]byte{id(), id(), id(), id(), id(), id(), id(), id(), id()}
+
+	// Verify that there are no blobs, or chunks in blobs initially.
+	verifyBlobs(t, ctx, bm, nil)
+	verifyNoChunksInBlob(t, ctx, bm, 0, b)
+	verifyNoChunksInBlob(t, ctx, bm, 1, b)
+
+	// Verify that all chunks have no locations initially.
+	for chunki := range c {
+		_, err = bm.LookupChunk(ctx, c[chunki])
+		if err == nil {
+			t.Errorf("blobmap_test: chunk %d: LookupChunk: unexpected lack of error", chunki)
+		}
+	}
+
+	// Put chunks 0..7 into blob 0, and chunks 0..3, 8, 5..7 into blob 1.
+	// Each blob is treated as size 1.
+	for blobi := 0; blobi != 2; blobi++ {
+		for i := 0; i != 8; i++ {
+			chunki := i
+			if blobi == 1 && i == 4 { // In blob 1, c[4] 4 is replaced by c[8]
+				chunki = 8
+			}
+			err = bm.AssociateChunkWithLocation(ctx, c[chunki],
+				blobmap.Location{BlobID: b[blobi], Offset: int64(i), Size: 1})
+			if err != nil {
+				t.Errorf("blobmap_test: blob %d: AssociateChunkWithLocation: unexpected error: %v",
+					blobi, err)
+			}
+		}
+	}
+
+	// Verify that the blobs are present, with the chunks specified.
+	verifyBlobs(t, ctx, bm, b)
+	verifyChunksInBlob(t, ctx, bm, 0, b, c)
+	verifyChunksInBlob(t, ctx, bm, 1, b, c)
+
+	// Verify that all chunks now have locations.
+	for chunki := range c {
+		_, err = bm.LookupChunk(ctx, c[chunki])
+		if err != nil {
+			t.Errorf("blobmap_test: chunk %d: LookupChunk: unexpected error: %v",
+				chunki, err)
+		}
+	}
+
+	// Delete b[0].
+	err = bm.DeleteBlob(ctx, b[0])
+	if err != nil {
+		t.Errorf("blobmap_test: blob 0: DeleteBlob: unexpected error: %v", err)
+	}
+
+	// Verify that all chunks except chunk 4 (which was in only blob 0)
+	// still have locations.
+	for chunki := range c {
+		_, err = bm.LookupChunk(ctx, c[chunki])
+		if chunki == 4 {
+			if err == nil {
+				t.Errorf("blobmap_test: chunk %d: LookupChunk: expected lack of error",
+					chunki)
+			}
+		} else {
+			if err != nil {
+				t.Errorf("blobmap_test: chunk %d: LookupChunk: unexpected error: %v",
+					chunki, err)
+			}
+		}
+	}
+
+	// Verify that blob 0 is gone, but blob 1 remains.
+	verifyBlobs(t, ctx, bm, b[1:])
+	verifyNoChunksInBlob(t, ctx, bm, 0, b)
+	verifyChunksInBlob(t, ctx, bm, 1, b, c)
+
+	// Delete b[1].
+	err = bm.DeleteBlob(ctx, b[1])
+	if err != nil {
+		t.Errorf("blobmap_test: blob 1: DeleteBlob: unexpected error: %v",
+			err)
+	}
+
+	// Verify that there are no blobs, or chunks in blobs once more.
+	verifyBlobs(t, ctx, bm, nil)
+	verifyNoChunksInBlob(t, ctx, bm, 0, b)
+	verifyNoChunksInBlob(t, ctx, bm, 1, b)
+
+	// Verify that all chunks have no locations once more.
+	for chunki := range c {
+		_, err = bm.LookupChunk(ctx, c[chunki])
+		if err == nil {
+			t.Errorf("blobmap_test: chunk %d: LookupChunk: unexpected lack of error",
+				chunki)
+		}
+	}
+
+	err = bm.Close()
+	if err != nil {
+		t.Errorf("blobmap_test: unexpected error closing BlobMap: %v", err)
+	}
+}
diff --git a/services/syncbase/localblobstore/chunker/chunker.go b/services/syncbase/localblobstore/chunker/chunker.go
new file mode 100644
index 0000000..cb07533
--- /dev/null
+++ b/services/syncbase/localblobstore/chunker/chunker.go
@@ -0,0 +1,284 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package chunker breaks a stream of bytes into context-defined chunks whose
+// boundaries are chosen based on content checksums of a window that slides
+// over the data.  An edited sequence with insertions and removals can share
+// many chunks with the original sequence.
+//
+// The intent is that when a sequence of bytes is to be transmitted to a
+// recipient that may have much of the data, the sequence can be broken down
+// into chunks.  The checksums of the resulting chunks may then be transmitted
+// to the recipient, which can then discover which of the chunks it has, and
+// which it needs.
+//
+// Example:
+//      var s *chunker.Stream = chunker.New(&chunker.DefaultParam, anIOReader)
+//      for s.Advance() {
+//		var chunk []byte := s.Value()
+//              // process chunk
+//	}
+//	if s.Err() != nil {
+//		// anIOReader generated an error.
+//	}
+package chunker
+
+// The design is from:
+// "A Framework for Analyzing and Improving Content-Based Chunking Algorithms";
+// Kave Eshghi, Hsiu Khuern Tang; HPL-2005-30(R.1); Sep, 2005;
+// http://www.hpl.hp.com/techreports/2005/HPL-2005-30R1.pdf
+
+import "io"
+import "sync"
+
+import "v.io/syncbase/x/ref/services/syncbase/localblobstore/crc64window"
+import "v.io/v23/context"
+import "v.io/v23/verror"
+
+const pkgPath = "v.io/syncbase/x/ref/services/syncbase/localblobstore/chunker"
+
+var (
+	errStreamCancelled = verror.Register(pkgPath+".errStreamCancelled", verror.NoRetry, "{1:}{2:} Advance() called on cancelled stream{:_}")
+)
+
+// A Param contains the parameters for chunking.
+//
+// Chunks are broken based on a hash of a sliding window of width WindowWidth
+// bytes.
+// Each chunk is at most MaxChunk bytes long, and, unless end-of-file or an
+// error is reached, at least MinChunk bytes long.
+//
+// Subject to those constaints, a chunk boundary introduced at the first point
+// where the hash of the sliding window is 1 mod Primary, or if that doesn't
+// occur before MaxChunk bytes, at the last position where the hash is 1 mod
+// Secondary, or if that does not occur, after MaxChunk bytes.
+// Normally, MinChunk < Primary < MaxChunk.
+// Primary is the expected chunk size.
+// The Secondary divisor exists to make it more likely that a chunk boundary is
+// selected based on the local data when the Primary divisor by chance does not
+// find a match for a long distance.  It should be a few times smaller than
+// Primary.
+//
+// Using primes for Primary and Secondary is not essential, but recommended
+// because it guarantees mixing of the checksum bits should their distribution
+// be non-uniform.
+type Param struct {
+	WindowWidth int    // the window size to use when looking for chunk boundaries
+	MinChunk    int64  // minimum chunk size
+	MaxChunk    int64  // maximum chunk size
+	Primary     uint64 // primary divisor; the expected chunk size
+	Secondary   uint64 // secondary divisor
+}
+
+// DefaultParam contains default chunking parameters.
+var DefaultParam Param = Param{WindowWidth: 48, MinChunk: 512, MaxChunk: 3072, Primary: 601, Secondary: 307}
+
+// A Stream allows a client to iterate over the chunks within an io.Reader byte
+// stream.
+type Stream struct {
+	param        Param               // chunking parameters
+	ctx          *context.T          // context of creator
+	window       *crc64window.Window // sliding window for computing the hash
+	buf          []byte              // buffer of data
+	rd           io.Reader           // source of data
+	err          error               // error from rd
+	mu           sync.Mutex          // protects cancelled
+	cancelled    bool                // whether the stream has been cancelled
+	bufferChunks bool                // whether to buffer entire chunks
+	// Invariant:  bufStart <= chunkStart <= chunkEnd <= bufEnd
+	bufStart   int64  // offset in rd of first byte in buf[]
+	bufEnd     int64  // offset in rd of next byte after those in buf[]
+	chunkStart int64  // offset in rd of first byte of current chunk
+	chunkEnd   int64  // offset in rd of next byte after current chunk
+	windowEnd  int64  // offset in rd of next byte to be given to window
+	hash       uint64 // hash of sliding window
+}
+
+// newStream() returns a pointer to a new Stream instance, with the
+// parameters in *param.  This internal version of NewStream() allows the caller
+// to specify via bufferChunks whether entire chunks should be buffered.
+func newStream(ctx *context.T, param *Param, rd io.Reader, bufferChunks bool) *Stream {
+	s := new(Stream)
+	s.param = *param
+	s.ctx = ctx
+	s.window = crc64window.New(crc64window.ECMA, s.param.WindowWidth)
+	bufSize := int64(8192)
+	if bufferChunks {
+		// If we must buffer entire chunks, arrange that the buffer
+		// size is considerably larger than the max chunk size to avoid
+		// copying data repeatedly.
+		for bufSize < 4*s.param.MaxChunk {
+			bufSize *= 2
+		}
+	}
+	s.buf = make([]byte, bufSize)
+	s.rd = rd
+	s.bufferChunks = bufferChunks
+	return s
+}
+
+// NewStream() returns a pointer to a new Stream instance, with the
+// parameters in *param.
+func NewStream(ctx *context.T, param *Param, rd io.Reader) *Stream {
+	return newStream(ctx, param, rd, true)
+}
+
+// isCancelled() returns whether s.Cancel() has been called.
+func (s *Stream) isCancelled() (cancelled bool) {
+	s.mu.Lock()
+	cancelled = s.cancelled
+	s.mu.Unlock()
+	return cancelled
+}
+
+// Advance() stages the next chunk so that it may be retrieved via Value().
+// Returns true iff there is an item to retrieve.  Advance() must be called
+// before Value() is called.
+func (s *Stream) Advance() bool {
+	// Remember that s.{bufStart,bufEnd,chunkStart,chunkEnd,windowEnd}
+	// are all relative to the offset in it.rd, not it.buf.
+	// Therefore, these starts and ends can easily be compared
+	// with each other, but we must subtract bufStart when
+	// indexing into buf.  (Other schemes were considered, but
+	// nothing seems uniformly better.)
+
+	// If buffering entire chunks, ensure there's enough data in the buffer
+	// for the next chunk.
+	if s.bufferChunks && s.bufEnd < s.chunkEnd+s.param.MaxChunk && s.err == nil {
+		// Next chunk might need more data.
+		if s.bufStart < s.chunkEnd {
+			// Move any remaining buffered data to start of buffer.
+			copy(s.buf, s.buf[s.chunkEnd-s.bufStart:s.bufEnd-s.bufStart])
+			s.bufStart = s.chunkEnd
+		}
+		// Fill buffer with data, unless error/EOF.
+		for s.err == nil && s.bufEnd < s.bufStart+int64(len(s.buf)) && !s.isCancelled() {
+			var n int
+			n, s.err = s.rd.Read(s.buf[s.bufEnd-s.bufStart:])
+			s.bufEnd += int64(n)
+		}
+	}
+
+	// Make the next chunk current.
+	s.chunkStart = s.chunkEnd
+	minChunk := s.chunkStart + s.param.MinChunk
+	maxChunk := s.chunkStart + s.param.MaxChunk
+	lastSecondaryBreak := maxChunk
+
+	// While not end of chunk...
+	for s.windowEnd != maxChunk &&
+		(s.windowEnd < minChunk || (s.hash%s.param.Primary) != 1) &&
+		(s.windowEnd != s.bufEnd || s.err == nil) && !s.isCancelled() {
+
+		// Fill the buffer if empty, and there's more data to read.
+		if s.windowEnd == s.bufEnd && s.err == nil {
+			if s.bufferChunks {
+				panic("chunker.Advance had to fill buffer in bufferChunks mode")
+			}
+			s.bufStart = s.bufEnd
+			var n int
+			n, s.err = s.rd.Read(s.buf)
+			s.bufEnd += int64(n)
+		}
+
+		// bufLimit is the minimum of the maximum possible chunk size and the buffer length.
+		bufLimit := maxChunk
+		if s.bufEnd < bufLimit {
+			bufLimit = s.bufEnd
+		}
+		// Advance window until both MinChunk reached and primary boundary found.
+		for s.windowEnd != bufLimit &&
+			(s.windowEnd < minChunk || (s.hash%s.param.Primary) != 1) &&
+			!s.isCancelled() {
+
+			// Advance the window by one byte.
+			s.hash = s.window.Advance(s.buf[s.windowEnd-s.bufStart])
+			s.windowEnd++
+			if (s.hash % s.param.Secondary) == 1 {
+				lastSecondaryBreak = s.windowEnd
+			}
+		}
+	}
+
+	if s.windowEnd == maxChunk && (s.hash%s.param.Primary) != 1 && lastSecondaryBreak != maxChunk {
+		// The primary break point was not found in the maximum chunk
+		// size, and a secondary break point was found; use it.
+		s.chunkEnd = lastSecondaryBreak
+	} else {
+		s.chunkEnd = s.windowEnd
+	}
+
+	return !s.isCancelled() && s.chunkStart != s.chunkEnd // We have a non-empty chunk to return.
+}
+
+// Value() returns the chunk that was staged by Advance().  May panic if
+// Advance() returned false or was not called.  Never blocks.
+func (s *Stream) Value() []byte {
+	return s.buf[s.chunkStart-s.bufStart : s.chunkEnd-s.bufStart]
+}
+
+// Err() returns any error encountered by Advance().  Never blocks.
+func (s *Stream) Err() (err error) {
+	s.mu.Lock()
+	if s.cancelled && (s.err == nil || s.err == io.EOF) {
+		s.err = verror.New(errStreamCancelled, s.ctx)
+	}
+	s.mu.Unlock()
+	if s.err != io.EOF { // Do not consider EOF to be an error.
+		err = s.err
+	}
+	return err
+}
+
+// Cancel() causes the next call to Advance() to return false.
+// It should be used when the client does not wish to iterate to the end of the stream.
+// Never blocks.  May be called concurrently with other method calls on s.
+func (s *Stream) Cancel() {
+	s.mu.Lock()
+	s.cancelled = true
+	s.mu.Unlock()
+}
+
+// ----------------------------------
+
+// A PosStream is just like a Stream, except that the Value() method returns only
+// the byte offsets of the ends of chunks, rather than the chunks themselves.
+// It can be used when chunks are too large to buffer a small number
+// comfortably in memory.
+type PosStream struct {
+	s *Stream
+}
+
+// NewPosStream() returns a pointer to a new PosStream instance, with the
+// parameters in *param.
+func NewPosStream(ctx *context.T, param *Param, rd io.Reader) *PosStream {
+	ps := new(PosStream)
+	ps.s = newStream(ctx, param, rd, false)
+	return ps
+}
+
+// Advance() stages the offset of the end of the next chunk so that it may be
+// retrieved via Value().  Returns true iff there is an item to retrieve.
+// Advance() must be called before Value() is called.
+func (ps *PosStream) Advance() bool {
+	return ps.s.Advance()
+}
+
+// Value() returns the chunk that was staged by Advance().  May panic if
+// Advance() returned false or was not called.  Never blocks.
+func (ps *PosStream) Value() int64 {
+	return ps.s.chunkEnd
+}
+
+// Err() returns any error encountered by Advance().  Never blocks.
+func (ps *PosStream) Err() error {
+	return ps.s.Err()
+}
+
+// Cancel() causes the next call to Advance() to return false.
+// It should be used when the client does not wish to iterate to the end of the stream.
+// Never blocks.  May be called concurrently with other method calls on ps.
+func (ps *PosStream) Cancel() {
+	ps.s.Cancel()
+}
diff --git a/services/syncbase/localblobstore/chunker/chunker_test.go b/services/syncbase/localblobstore/chunker/chunker_test.go
new file mode 100644
index 0000000..57c6fed
--- /dev/null
+++ b/services/syncbase/localblobstore/chunker/chunker_test.go
@@ -0,0 +1,197 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// A test for the chunker package.
+package chunker_test
+
+import "bytes"
+import "crypto/md5"
+import "fmt"
+import "io"
+import "testing"
+
+import "v.io/syncbase/x/ref/services/syncbase/localblobstore/chunker"
+import "v.io/syncbase/x/ref/services/syncbase/localblobstore/localblobstore_testlib"
+import "v.io/v23/context"
+import "v.io/x/ref/test"
+import _ "v.io/x/ref/runtime/factories/generic"
+
+// TestChunksPartitionStream() tests that the chunker partitions its input
+// stream into reasonable sized chunks, which when concatenated form the
+// original stream.
+func TestChunksPartitionStream(t *testing.T) {
+	ctx, shutdown := test.V23Init()
+	defer shutdown()
+
+	var err error
+	totalLength := 1024 * 1024
+
+	// Compute the md5 of an arbiotrary stream.  We will later compare this
+	// with the md5 of the concanenation of chunks from an equivalent
+	// stream.
+	r := localblobstore_testlib.NewRandReader(1, totalLength, 0, io.EOF)
+	hStream := md5.New()
+	buf := make([]byte, 8192)
+	for err == nil {
+		var n int
+		n, err = r.Read(buf)
+		hStream.Write(buf[0:n])
+	}
+	checksumStream := hStream.Sum(nil)
+
+	// Using an equivalent stream, break it into chunks.
+	r = localblobstore_testlib.NewRandReader(1, totalLength, 0, io.EOF)
+	param := &chunker.DefaultParam
+	hChunked := md5.New()
+
+	length := 0
+	s := chunker.NewStream(ctx, param, r)
+	for s.Advance() {
+		chunk := s.Value()
+		length += len(chunk)
+		// The last chunk is permitted to be short, hence the second
+		// conjunct in the following predicate.
+		if int64(len(chunk)) < param.MinChunk && length != totalLength {
+			t.Errorf("chunker_test: chunk length %d below minimum %d", len(chunk), param.MinChunk)
+		}
+		if int64(len(chunk)) > param.MaxChunk {
+			t.Errorf("chunker_test: chunk length %d above maximum %d", len(chunk), param.MaxChunk)
+		}
+		hChunked.Write(chunk)
+	}
+	if s.Err() != nil {
+		t.Errorf("chunker_test: got error from chunker: %v\n", err)
+	}
+
+	if length != totalLength {
+		t.Errorf("chunker_test: chunk lengths summed to %d, expected %d", length, totalLength)
+	}
+
+	checksumChunked := hChunked.Sum(nil)
+	if bytes.Compare(checksumStream, checksumChunked) != 0 {
+		t.Errorf("chunker_test: md5 of stream is %v, but md5 of chunks is %v", checksumStream, checksumChunked)
+	}
+}
+
+// TestPosStream() tests that a PosStream leads to the same chunks as an Stream.
+func TestPosStream(t *testing.T) {
+	ctx, shutdown := test.V23Init()
+	defer shutdown()
+
+	totalLength := 1024 * 1024
+
+	s := chunker.NewStream(ctx, &chunker.DefaultParam,
+		localblobstore_testlib.NewRandReader(1, totalLength, 0, io.EOF))
+	ps := chunker.NewPosStream(ctx, &chunker.DefaultParam,
+		localblobstore_testlib.NewRandReader(1, totalLength, 0, io.EOF))
+
+	itReady := s.Advance()
+	pitReady := ps.Advance()
+	it_pos := 0
+	chunk_count := 0
+	for itReady && pitReady {
+		it_pos += len(s.Value())
+		if int64(it_pos) != ps.Value() {
+			t.Fatalf("chunker_test: Stream and PosStream positions diverged at chunk %d: %d vs %d", chunk_count, it_pos, ps.Value())
+		}
+		chunk_count++
+		itReady = s.Advance()
+		pitReady = ps.Advance()
+	}
+	if itReady {
+		t.Error("chunker_test: Stream ended before PosStream")
+	}
+	if pitReady {
+		t.Error("chunker_test: PosStream ended before Stream")
+	}
+	if s.Err() != nil {
+		t.Errorf("chunker_test: Stream got unexpected error: %v", s.Err())
+	}
+	if ps.Err() != nil {
+		t.Errorf("chunker_test: PosStream got unexpected error: %v", ps.Err())
+	}
+}
+
+// chunkSums() returns a vector of md5 checksums for the chunks of the
+// specified Reader, using the default chunking parameters.
+func chunkSums(ctx *context.T, r io.Reader) (sums [][md5.Size]byte) {
+	s := chunker.NewStream(ctx, &chunker.DefaultParam, r)
+	for s.Advance() {
+		sums = append(sums, md5.Sum(s.Value()))
+	}
+	return sums
+}
+
+// TestInsertions() tests the how chunk sequences differ when bytes are
+// periodically inserted into a stream.
+func TestInsertions(t *testing.T) {
+	ctx, shutdown := test.V23Init()
+	defer shutdown()
+
+	totalLength := 1024 * 1024
+	insertionInterval := 20 * 1024
+	bytesInserted := totalLength / insertionInterval
+
+	// Get the md5 sums of the chunks of two similar streams, where the
+	// second has an extra bytes every 20k bytes.
+	sums0 := chunkSums(ctx, localblobstore_testlib.NewRandReader(1, totalLength, 0, io.EOF))
+	sums1 := chunkSums(ctx, localblobstore_testlib.NewRandReader(1, totalLength, insertionInterval, io.EOF))
+
+	// Iterate over chunks of second stream, counting which are in common
+	// with first stream.  We expect to find common chunks within 10 of the
+	// last chunk in common, since insertions are single bytes, widely
+	// separated.
+	same := 0 // Number of chunks in sums1 that are the same as chunks in sums0.
+	i0 := 0   // Where to search for a match in sums0.
+	for i1 := 0; i1 != len(sums1); i1++ {
+		// Be prepared to search up to the next 10 elements of sums0 from the most recent match.
+		limit := len(sums0) - i0
+		if limit > 10 {
+			limit = 10
+		}
+		var d int
+		for d = 0; d != limit && bytes.Compare(sums0[i0+d][:], sums1[i1][:]) != 0; d++ {
+		}
+		if d != limit { // found
+			same++
+			i0 += d // Advance i0 to the most recent match.
+		}
+	}
+	// The number of chunks that aren't the same as one in the original stream should be at least as large
+	// as the number of bytes inserted, and not too many more.
+	different := len(sums1) - same
+	if different < bytesInserted {
+		t.Errorf("chunker_test: saw %d different chunks, but expected at least %d", different, bytesInserted)
+	}
+	if bytesInserted+(bytesInserted/2) < different {
+		t.Errorf("chunker_test: saw %d different chunks, but expected at most %d", different, bytesInserted+(bytesInserted/2))
+	}
+	// Require that most chunks are the same, by a substantial margin.
+	if same < 5*different {
+		t.Errorf("chunker_test: saw %d different chunks, and %d same, but expected at least a factor of 5 more same than different", different, same)
+	}
+}
+
+// TestError() tests the behaviour of a chunker when given an error by its
+// reader.
+func TestError(t *testing.T) {
+	ctx, shutdown := test.V23Init()
+	defer shutdown()
+
+	notEOF := fmt.Errorf("not EOF")
+	totalLength := 50 * 1024
+	r := localblobstore_testlib.NewRandReader(1, totalLength, 0, notEOF)
+	s := chunker.NewStream(ctx, &chunker.DefaultParam, r)
+	length := 0
+	for s.Advance() {
+		chunk := s.Value()
+		length += len(chunk)
+	}
+	if s.Err() != notEOF {
+		t.Errorf("chunker_test: chunk stream ended with error %v, expected %v", s.Err(), notEOF)
+	}
+	if length != totalLength {
+		t.Errorf("chunker_test: chunk lengths summed to %d, expected %d", length, totalLength)
+	}
+}
diff --git a/services/syncbase/localblobstore/crc64window/crc64window.go b/services/syncbase/localblobstore/crc64window/crc64window.go
new file mode 100644
index 0000000..b27dbb9
--- /dev/null
+++ b/services/syncbase/localblobstore/crc64window/crc64window.go
@@ -0,0 +1,153 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package crc64window provides CRCs over fixed-sized, rolling windows of bytes.
+//
+// It uses the same polynomial representation and CRC conditioning as
+// hash/crc64, so the results are the same as computing hash/crc64 over the
+// window of the last sz bytes added, where sz is the window size.  Thus, in
+// this code, rolling and nonRolling will receive the same value.
+//     w := crc64window.New(crc64window.ECMA, 3)  // Window size is 3 bytes.
+//     w.Advance(0x17)
+//     w.Advance(0x92)
+//     w.Advance(0x04)
+//     rolling := w.Advance(0x28)   // Rolls 0x17 out, and 0x28 in.
+//
+//     nonRolling := crc64.Update(0, crc64.MakeTable(crc64.ECMA), []byte{0x92, 0x04, 0x28})
+//
+// Strangely, hash/crc64's specification does not mention which of the many
+// possible bit representations and conditioning choices it uses.  We assume it
+// will not change from the following, which was gleaned from the hash/crc64
+// source code:
+//
+//  - All messages to be processed, CRC values, and CRC polynomials are
+//    polynomials in x whose coefficients are in Z(2).
+//  - CRC values are represented by uint64 values in which bit i of the integer
+//    represents the coefficient of x**(63-i) in the polynomial.
+//  - CRC polynomials are represented like CRC values, except that the x**64
+//    coefficient of the CRC polynomial is implicitly 1, and not stored.
+//  - Messages to be processed are represented by byte vectors in which the
+//    lowest-order bit of the first byte is the highest-degree polynomial
+//    coefficient.
+//  - For a CRC polynomial p and a message m, the CRC value:
+//        CRC(p, m) = c + ((c * (x**len(m)) + (m * x**64)) mod p)
+//    where the conditioning constant c = x**63 + x**62 + x**61 + ... + x + 1,
+//    and len(m) is the number of bits in m.
+package crc64window
+
+import "sync"
+
+// The ECMA-64 polynomial, defined in ECMA 182.
+// This polynomial is recommended for use with this package, though other
+// polynomials found in hash/crc64 will also work.
+const ECMA = 0xc96c5795d7870f42
+
+// A Window contains the state needed to compute a CRC over a fixed-sized,
+// rolling window of data.
+type Window struct {
+	crc     uint64   // CRC of window, unconditioned (i.e., just the window mod the CRC polynomial).
+	window  []byte   // The bytes in the window.
+	pos     int      // Index in window[] of first byte, which is the next byte to be overwritten.
+	crcData *crcData // Pointer to the immutable CRC tables for the CRC.
+}
+
+// A crcData is immutable after initialization, and contains tables for
+// computing a particular CRC over a particular window size.  Pre-computed
+// copies of crcData are stored in tables[] so that CRC tables need be computed
+// only once per (polynomial, window size) pair.
+type crcData struct {
+	conditioning  uint64
+	crcTableFront [256]uint64
+	crcTableRear  [256]uint64
+}
+
+var mu sync.Mutex // Protects "tables", the cache of CRC tables already computed.
+
+// A polySize represents a pair of a CRC polynomial and a window size.
+type polySize struct {
+	poly uint64
+	size int
+}
+
+// tables[] maps (polynomial,window size) pairs to computed tables, so tables
+// are computed only once.  It's accessed only under mu.
+var tables map[polySize]*crcData
+
+// getCRCData() returns a pointer to a crcData for the given CRC polynomial
+// and window size, either by cache lookup on by calculating it.  Requires
+// size > 0.
+func getCRCData(poly uint64, size int) *crcData {
+	mu.Lock()
+	// Use cached CRC tables if available.
+	if tables == nil {
+		tables = make(map[polySize]*crcData)
+	}
+	ps := polySize{poly: poly, size: size}
+	c, found := tables[ps]
+	if !found { // Compute and save the CRC tables.
+		c = new(crcData)
+		// Loop ensures:  c.crcTableFront[m & 0xff] ^ (m >> 8)==CRC(m * x**8)
+		zeroOrPoly := []uint64{0, poly}
+		for i := 1; i != 256; i <<= 1 {
+			crc := uint64(i)
+			for j := 0; j != 8; j++ {
+				crc = (crc >> 1) ^ zeroOrPoly[crc&1]
+			}
+			for j := 0; j != i; j++ {
+				c.crcTableFront[j+i] = crc ^ c.crcTableFront[j]
+			}
+		}
+		// Loop ensures: c.crcTableRear[b] == CRC(b * x**(size*8))
+		for i := 1; i != 256; i <<= 1 {
+			crc := c.crcTableFront[i]
+			for j := 1; j != size; j++ {
+				crc = c.crcTableFront[byte(crc)] ^ (crc >> 8)
+			}
+			for j := 0; j != i; j++ {
+				c.crcTableRear[j+i] = crc ^ c.crcTableRear[j]
+			}
+		}
+
+		// Loop ensures: c.conditioning == CRC(all-ones * x**(size*8))
+		conditioning := ^uint64(0)
+		for i := 0; i != size; i++ {
+			conditioning = c.crcTableFront[byte(conditioning)] ^ (conditioning >> 8)
+		}
+		c.conditioning = conditioning
+
+		tables[ps] = c
+	}
+	mu.Unlock()
+	return c
+}
+
+// New() returns a Window with the given size and CRC polynomial.
+// Initially, all the bytes in the window are zero.  Requires size > 0.
+func New(poly uint64, size int) *Window {
+	if size <= 0 {
+		panic("crc64window.New() called with size <= 0")
+	}
+	w := new(Window)
+	w.window = make([]byte, size)
+	w.crc = 0
+	w.crcData = getCRCData(poly, size)
+	return w
+}
+
+// Advance() removes the first byte from window *w, adds b as the new last
+// byte, and returns the CRC of the window.
+func (w *Window) Advance(b byte) uint64 {
+	c := w.crcData
+	pos := w.pos
+	crc := w.crc
+	crc ^= c.crcTableRear[w.window[pos]]
+	w.crc = c.crcTableFront[byte(crc)^b] ^ (crc >> 8)
+	w.window[pos] = b
+	pos++
+	if pos == len(w.window) {
+		pos = 0
+	}
+	w.pos = pos
+	return ^(c.conditioning ^ w.crc)
+}
diff --git a/services/syncbase/localblobstore/crc64window/crc64window_test.go b/services/syncbase/localblobstore/crc64window/crc64window_test.go
new file mode 100644
index 0000000..9969125
--- /dev/null
+++ b/services/syncbase/localblobstore/crc64window/crc64window_test.go
@@ -0,0 +1,51 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// A test for crc64window.
+package crc64window_test
+
+import "hash/crc64"
+import "math/rand"
+import "testing"
+
+import "v.io/syncbase/x/ref/services/syncbase/localblobstore/crc64window"
+
+// A test for the example given in the package's specification.
+func TestCRC64WindowExample(t *testing.T) {
+	w := crc64window.New(crc64.ECMA, 3)
+	w.Advance(0x17)
+	w.Advance(0x92)
+	w.Advance(0x04)
+	rolling := w.Advance(0x28) // Rolls 0x17 out, and 0x28 in.
+	nonRolling := crc64.Update(0, crc64.MakeTable(crc64.ECMA), []byte{0x92, 0x04, 0x28})
+	if rolling != nonRolling {
+		t.Errorf("crc64window: rolling(0x92, 0x04, 0x28)==%x nonRolling(0x92, 0x04, 0x28)==%x\n", rolling, nonRolling)
+	}
+}
+
+func TestCRC64Window(t *testing.T) {
+	winSize := 16
+	iterations := 1000
+
+	w := crc64window.New(crc64.ECMA, winSize)
+
+	table := crc64.MakeTable(crc64.ECMA)
+	block := make([]byte, winSize-1+iterations)
+
+	for i := 0; i != len(block); i++ {
+		block[i] = byte(rand.Int31n(256))
+	}
+
+	i := 0
+	for ; i != winSize-1; i++ {
+		w.Advance(block[i])
+	}
+	for ; i != len(block); i++ {
+		expect := crc64.Update(0, table, block[i+1-winSize:i+1])
+		got := w.Advance(block[i])
+		if expect != got {
+			t.Errorf("crc64window: i %d   winSize %d  got %x, expect %x\n", i, winSize, got, expect)
+		}
+	}
+}
diff --git a/services/syncbase/localblobstore/fs_cablobstore/fs_cablobstore.go b/services/syncbase/localblobstore/fs_cablobstore/fs_cablobstore.go
new file mode 100644
index 0000000..4fb7500
--- /dev/null
+++ b/services/syncbase/localblobstore/fs_cablobstore/fs_cablobstore.go
@@ -0,0 +1,1521 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package fs_cablobstore implements a content addressable blob store
+// on top of a file system.  It assumes that either os.Link() or
+// os.Rename() is available.
+package fs_cablobstore
+
+// Internals:
+// Blobs are partitioned into two types of unit: "fragments" and "chunks".
+// A fragment is stored in a single file on disc.  A chunk is a unit of network
+// transmission.
+//
+//   The blobstore consists of a directory with "blob", "cas", "chunk", and
+//   "tmp" subdirectories.
+//   - "tmp" is used for temporary files that are moved into place via
+//     link()/unlink() or rename(), depending on what's available.
+//   - "cas" contains files whose names are content hashes of the files being
+//     named.  A few slashes are thrown into the name near the front so that no
+//     single directory gets too large.  These files are called "fragments".
+//   - "blob" contains files whose names are random numbers.  These names are
+//     visible externally as "blob names".  Again, a few slashes are thrown
+//     into the name near the front so that no single directory gets too large.
+//     Each of these files contains a series of lines of the form:
+//        d <size> <offset> <cas-fragment>
+//     followed optionally by a line of the form:
+//        f <md5-hash>
+//     Each "d" line indicates that the next <size> bytes of the blob appear at
+//     <offset> bytes into <cas-fragment>, which is in the "cas" subtree.  The
+//     "f" line indicates that the blob is "finalized" and gives its complete
+//     md5 hash.  No fragments may be appended to a finalized blob.
+//   - "chunk" contains a store (currently implemented with leveldb) that
+//     maps chunks of blobs to content hashes and vice versa.
+
+import "bufio"
+import "bytes"
+import "crypto/md5"
+import "fmt"
+import "hash"
+import "io"
+import "io/ioutil"
+import "math"
+import "math/rand"
+import "os"
+import "path/filepath"
+import "strconv"
+import "strings"
+import "sync"
+import "time"
+
+import "v.io/syncbase/x/ref/services/syncbase/localblobstore"
+import "v.io/syncbase/x/ref/services/syncbase/localblobstore/chunker"
+import "v.io/syncbase/x/ref/services/syncbase/localblobstore/blobmap"
+import "v.io/v23/context"
+import "v.io/v23/verror"
+
+const pkgPath = "v.io/syncbase/x/ref/services/syncbase/localblobstore/fs_cablobstore"
+
+var (
+	errNotADir                = verror.Register(pkgPath+".errNotADir", verror.NoRetry, "{1:}{2:} Not a directory{:_}")
+	errAppendFailed           = verror.Register(pkgPath+".errAppendFailed", verror.NoRetry, "{1:}{2:} fs_cablobstore.Append failed{:_}")
+	errMalformedField         = verror.Register(pkgPath+".errMalformedField", verror.NoRetry, "{1:}{2:} Malformed field in blob specification{:_}")
+	errAlreadyClosed          = verror.Register(pkgPath+".errAlreadyClosed", verror.NoRetry, "{1:}{2:} BlobWriter is already closed{:_}")
+	errBlobAlreadyFinalized   = verror.Register(pkgPath+".errBlobAlreadyFinalized", verror.NoRetry, "{1:}{2:} Blob is already finalized{:_}")
+	errIllegalPositionForRead = verror.Register(pkgPath+".errIllegalPositionForRead", verror.NoRetry, "{1:}{2:} BlobReader: illegal position {3} on Blob of size {4}{:_}")
+	errBadSeekWhence          = verror.Register(pkgPath+".errBadSeekWhence", verror.NoRetry, "{1:}{2:} BlobReader: Bad value for 'whence' in Seek{:_}")
+	errNegativeSeekPosition   = verror.Register(pkgPath+".errNegativeSeekPosition", verror.NoRetry, "{1:}{2:} BlobReader: negative position for Seek: offset {3}, whence {4}{:_}")
+	errBadSizeOrOffset        = verror.Register(pkgPath+".errBadSizeOrOffset", verror.NoRetry, "{1:}{2:} Bad size ({3}) or offset ({4}) in blob {5} (size {6}){:_}")
+	errMalformedBlobHash      = verror.Register(pkgPath+".errMalformedBlobHash", verror.NoRetry, "{1:}{2:} Blob {3} hash malformed hash{:_}")
+	errInvalidBlobName        = verror.Register(pkgPath+".errInvalidBlobName", verror.NoRetry, "{1:}{2:} Invalid blob name {3}{:_}")
+	errCantDeleteBlob         = verror.Register(pkgPath+".errCantDeleteBlob", verror.NoRetry, "{1:}{2:} Can't delete blob {3}{:_}")
+	errBlobDeleted            = verror.Register(pkgPath+".errBlobDeleted", verror.NoRetry, "{1:}{2:} Blob is deleted{:_}")
+	errSizeTooBigForFragment  = verror.Register(pkgPath+".errSizeTooBigForFragment", verror.NoRetry, "{1:}{2:} writing blob {1}, size too big for fragment{:1}")
+	errStreamCancelled        = verror.Register(pkgPath+".errStreamCancelled", verror.NoRetry, "{1:}{2:} Advance() called on cancelled stream{:_}")
+)
+
+// For the moment, we disallow others from accessing the tree where blobs are
+// stored.  We could in the future relax this to 0711/0755, and 0644.
+const dirPermissions = 0700
+const filePermissions = 0600
+
+// Subdirectories of the blobstore's tree
+const (
+	blobDir  = "blob"  // Subdirectory where blobs are indexed by blob id.
+	casDir   = "cas"   // Subdirectory where fragments are indexed by content hash.
+	chunkDir = "chunk" // Subdirectory where chunks are indexed by content hash.
+	tmpDir   = "tmp"   // Subdirectory where temporary files are created.
+)
+
+// An FsCaBlobStore represents a simple, content-addressable store.
+type FsCaBlobStore struct {
+	rootName string           // The name of the root of the store.
+	bm       *blobmap.BlobMap // Mapping from chunks to blob locations and vice versa.
+
+	// mu protects fields below, plus most fields in each blobDesc when used from a BlobWriter.
+	mu         sync.Mutex
+	activeDesc []*blobDesc        // The blob descriptors in use by active BlobReaders and BlobWriters.
+	toDelete   []*map[string]bool // Sets of items that active GC threads are about to delete. (Pointers to maps, to allow pointer comparison.)
+}
+
+// hashToFileName() returns the name of the binary ID with the specified
+// prefix.  Requires len(id)==16.  An md5 hash is suitable.
+func hashToFileName(prefix string, hash []byte) string {
+	return filepath.Join(prefix,
+		fmt.Sprintf("%02x", hash[0]),
+		fmt.Sprintf("%02x", hash[1]),
+		fmt.Sprintf("%02x", hash[2]),
+		fmt.Sprintf("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
+			hash[3],
+			hash[4], hash[5], hash[6], hash[7],
+			hash[8], hash[9], hash[10], hash[11],
+			hash[12], hash[13], hash[14], hash[15]))
+}
+
+// fileNameToHash() converts a file name in the format generated by
+// hashToFileName(prefix, ...) to a vector of 16 bytes.  If the string is
+// malformed, the nil slice is returned.
+func fileNameToHash(prefix string, s string) []byte {
+	idStr := strings.TrimPrefix(filepath.ToSlash(s), prefix+"/")
+	hash := make([]byte, 16, 16)
+	n, err := fmt.Sscanf(idStr, "%02x/%02x/%02x/%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
+		&hash[0], &hash[1], &hash[2], &hash[3],
+		&hash[4], &hash[5], &hash[6], &hash[7],
+		&hash[8], &hash[9], &hash[10], &hash[11],
+		&hash[12], &hash[13], &hash[14], &hash[15])
+	if n != 16 || err != nil {
+		hash = nil
+	}
+	return hash
+}
+
+// newBlobName() returns a new random name for a blob.
+func newBlobName() string {
+	return filepath.Join(blobDir,
+		fmt.Sprintf("%02x", rand.Int31n(256)),
+		fmt.Sprintf("%02x", rand.Int31n(256)),
+		fmt.Sprintf("%02x", rand.Int31n(256)),
+		fmt.Sprintf("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
+			rand.Int31n(256),
+			rand.Int31n(256), rand.Int31n(256), rand.Int31n(256), rand.Int31n(256),
+			rand.Int31n(256), rand.Int31n(256), rand.Int31n(256), rand.Int31n(256),
+			rand.Int31n(256), rand.Int31n(256), rand.Int31n(256), rand.Int31n(256)))
+}
+
+// hashToString() returns a string representation of the hash.
+// Requires len(hash)==16.  An md5 hash is suitable.
+func hashToString(hash []byte) string {
+	return fmt.Sprintf("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
+		hash[0], hash[1], hash[2], hash[3],
+		hash[4], hash[5], hash[6], hash[7],
+		hash[8], hash[9], hash[10], hash[11],
+		hash[12], hash[13], hash[14], hash[15])
+}
+
+// stringToHash() converts a string in the format generated by hashToString()
+// to a vector of 16 bytes.  If the string is malformed, the nil slice is
+// returned.
+func stringToHash(s string) []byte {
+	hash := make([]byte, 16, 16)
+	n, err := fmt.Sscanf(s, "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
+		&hash[0], &hash[1], &hash[2], &hash[3],
+		&hash[4], &hash[5], &hash[6], &hash[7],
+		&hash[8], &hash[9], &hash[10], &hash[11],
+		&hash[12], &hash[13], &hash[14], &hash[15])
+	if n != 16 || err != nil {
+		hash = nil
+	}
+	return hash
+}
+
+// Create() returns a pointer to an FsCaBlobStore stored in the file system at
+// "rootName".  If the directory rootName does not exist, it is created.
+func Create(ctx *context.T, rootName string) (fscabs *FsCaBlobStore, err error) {
+	dir := []string{tmpDir, casDir, chunkDir, blobDir}
+	for i := 0; i != len(dir) && err == nil; i++ {
+		fullName := filepath.Join(rootName, dir[i])
+		os.MkdirAll(fullName, dirPermissions)
+		var fi os.FileInfo
+		fi, err = os.Stat(fullName)
+		if err == nil && !fi.IsDir() {
+			err = verror.New(errNotADir, ctx, fullName)
+		}
+	}
+	var bm *blobmap.BlobMap
+	if err == nil {
+		bm, err = blobmap.New(ctx, filepath.Join(rootName, chunkDir))
+	}
+	if err == nil {
+		fscabs = new(FsCaBlobStore)
+		fscabs.rootName = rootName
+		fscabs.bm = bm
+	}
+	return fscabs, err
+}
+
+// Close() closes the FsCaBlobStore. {
+func (fscabs *FsCaBlobStore) Close() error {
+	return fscabs.bm.Close()
+}
+
+// Root() returns the name of the root directory where *fscabs is stored.
+func (fscabs *FsCaBlobStore) Root() string {
+	return fscabs.rootName
+}
+
+// DeleteBlob() deletes the named blob from *fscabs.
+func (fscabs *FsCaBlobStore) DeleteBlob(ctx *context.T, blobName string) (err error) {
+	// Disallow deletions of things outside the blob tree, or that may contain "..".
+	// For simplicity, the code currently disallows '.'.
+	blobID := fileNameToHash(blobDir, blobName)
+	if blobID == nil || strings.IndexByte(blobName, '.') != -1 {
+		err = verror.New(errInvalidBlobName, ctx, blobName)
+	} else {
+		err = os.Remove(filepath.Join(fscabs.rootName, blobName))
+		if err != nil {
+			err = verror.New(errCantDeleteBlob, ctx, blobName, err)
+		} else {
+			err = fscabs.bm.DeleteBlob(ctx, blobID)
+		}
+	}
+	return err
+}
+
+// -----------------------------------------------------------
+
+// A file encapsulates both an os.File and a bufio.Writer on that file.
+type file struct {
+	fh     *os.File
+	writer *bufio.Writer
+}
+
+// newFile() returns a *file containing fh and a bufio.Writer on that file, if
+// err is nil.
+func newFile(fh *os.File, err error) (*file, error) {
+	var f *file
+	if err == nil {
+		f = new(file)
+		f.fh = fh
+		f.writer = bufio.NewWriter(f.fh)
+	}
+	return f, err
+}
+
+// newTempFile() returns a *file on a new temporary file created in the
+// directory dir.
+func newTempFile(ctx *context.T, dir string) (*file, error) {
+	return newFile(ioutil.TempFile(dir, "newfile"))
+}
+
+// close() flushes buffers (if err==nil initially) and closes the file,
+// returning its name.
+func (f *file) close(ctx *context.T, err error) (string, error) {
+	name := f.fh.Name()
+	// Flush the data out to disc and close the file.
+	if err == nil {
+		err = f.writer.Flush()
+	}
+	if err == nil {
+		err = f.fh.Sync()
+	}
+	err2 := f.fh.Close()
+	if err == nil {
+		err = err2
+	}
+	return name, err
+}
+
+// closeAndRename() calls f.close(), and if err==nil initially and no new
+// errors are seen, renames the file to newName.
+func (f *file) closeAndRename(ctx *context.T, newName string, err error) error {
+	var oldName string
+	oldName, err = f.close(ctx, err)
+	if err == nil { // if temp file written successfully...
+		// Link or rename the file into place, hoping at least one is
+		// supported on this file system.
+		os.MkdirAll(filepath.Dir(newName), dirPermissions)
+		err = os.Link(oldName, newName)
+		if err == nil {
+			os.Remove(oldName)
+		} else {
+			err = os.Rename(oldName, newName)
+		}
+	}
+	if err != nil {
+		os.Remove(oldName)
+	}
+	return err
+}
+
+// -----------------------------------------------------------
+
+// addFragment() ensures that the store *fscabs contains a fragment comprising
+// the catenation of the byte vectors named by item[..].block and the contents
+// of the files named by item[..].fileName.  The block field is ignored if
+// fileName!="".  The fragment is not physically added if already present.
+// The fragment is added to the fragment list of the descriptor *desc.
+func (fscabs *FsCaBlobStore) addFragment(ctx *context.T, extHasher hash.Hash,
+	desc *blobDesc, item ...localblobstore.BlockOrFile) (fileName string, size int64, err error) {
+
+	hasher := md5.New()
+	var buf []byte
+	var fileHandleList []*os.File
+
+	// Hash the inputs.
+	for i := 0; i != len(item) && err == nil; i++ {
+		if len(item[i].FileName) != 0 {
+			if buf == nil {
+				buf = make([]byte, 8192, 8192)
+				fileHandleList = make([]*os.File, 0, len(item))
+			}
+			var fileHandle *os.File
+			fileHandle, err = os.Open(filepath.Join(fscabs.rootName, item[i].FileName))
+			if err == nil {
+				fileHandleList = append(fileHandleList, fileHandle)
+				at := item[i].Offset
+				toRead := item[i].Size
+				var haveRead int64
+				for err == nil && (toRead == -1 || haveRead < toRead) {
+					var n int
+					n, err = fileHandle.ReadAt(buf, at)
+					if err == nil {
+						if toRead != -1 && int64(n)+haveRead > toRead {
+							n = int(toRead - haveRead)
+						}
+						haveRead += int64(n)
+						at += int64(n)
+						size += int64(n)
+						hasher.Write(buf[0:n]) // Cannot fail; see Hash interface.
+						extHasher.Write(buf[0:n])
+					}
+				}
+				if err == io.EOF {
+					if toRead == -1 || haveRead == toRead {
+						err = nil // The loop read all that was asked; EOF is a possible outcome.
+					} else { // The loop read less than was asked; request must have been too big.
+						err = verror.New(errSizeTooBigForFragment, ctx, desc.name, item[i].FileName)
+					}
+				}
+			}
+		} else {
+			hasher.Write(item[i].Block) // Cannot fail; see Hash interface.
+			extHasher.Write(item[i].Block)
+			size += int64(len(item[i].Block))
+		}
+	}
+
+	// Compute the hash, and form the file name in the respository.
+	hash := hasher.Sum(nil)
+	relFileName := hashToFileName(casDir, hash)
+	absFileName := filepath.Join(fscabs.rootName, relFileName)
+
+	// Add the fragment's name to *desc's fragments so the garbage
+	// collector will not delete it.
+	fscabs.mu.Lock()
+	desc.fragment = append(desc.fragment, blobFragment{
+		pos:      desc.size,
+		size:     size,
+		offset:   0,
+		fileName: relFileName})
+	fscabs.mu.Unlock()
+
+	// If the file does not already exist, ...
+	if _, statErr := os.Stat(absFileName); err == nil && os.IsNotExist(statErr) {
+		// ... try to create it by writing to a temp file and renaming.
+		var t *file
+		t, err = newTempFile(ctx, filepath.Join(fscabs.rootName, tmpDir))
+		if err == nil {
+			// Copy the byte-sequences and input files to the temp file.
+			j := 0
+			for i := 0; i != len(item) && err == nil; i++ {
+				if len(item[i].FileName) != 0 {
+					at := item[i].Offset
+					toRead := item[i].Size
+					var haveRead int64
+					for err == nil && (toRead == -1 || haveRead < toRead) {
+						var n int
+						n, err = fileHandleList[j].ReadAt(buf, at)
+						if err == nil {
+							if toRead != -1 && int64(n)+haveRead > toRead {
+								n = int(toRead - haveRead)
+							}
+							haveRead += int64(n)
+							at += int64(n)
+							_, err = t.writer.Write(buf[0:n])
+						}
+					}
+					if err == io.EOF { // EOF is the expected outcome.
+						err = nil
+					}
+					j++
+				} else {
+					_, err = t.writer.Write(item[i].Block)
+				}
+			}
+			err = t.closeAndRename(ctx, absFileName, err)
+		}
+	} // else file already exists, nothing more to do.
+
+	for i := 0; i != len(fileHandleList); i++ {
+		fileHandleList[i].Close()
+	}
+
+	if err != nil {
+		err = verror.New(errAppendFailed, ctx, fscabs.rootName, err)
+		// Remove the entry added to fragment list above.
+		fscabs.mu.Lock()
+		desc.fragment = desc.fragment[0 : len(desc.fragment)-1]
+		fscabs.mu.Unlock()
+	} else { // commit the change by updating the size
+		fscabs.mu.Lock()
+		desc.size += size
+		desc.cv.Broadcast() // Tell blobmap BlobReader there's more to read.
+		fscabs.mu.Unlock()
+	}
+
+	return relFileName, size, err
+}
+
+// A blobFragment represents a vector of bytes and its position within a blob.
+type blobFragment struct {
+	pos      int64  // position of this fragment within its containing blob.
+	size     int64  // size of this fragment.
+	offset   int64  // offset within fileName.
+	fileName string // name of file describing this fragment.
+}
+
+// A blobDesc is the in-memory representation of a blob.
+type blobDesc struct {
+	activeDescIndex int // Index into fscabs.activeDesc if refCount>0; under fscabs.mu.
+	refCount        int // Reference count; under fscabs.mu.
+
+	name string // Name of the blob.
+
+	// The following fields are modified under fscabs.mu and in BlobWriter
+	// owner's thread; they may be read by GC (when obtained from
+	// fscabs.activeDesc) and the chunk writer under fscabs.mu.  In the
+	// BlobWriter owner's thread, reading does not require a lock, but
+	// writing does.  In other contexts (BlobReader, or a desc that has
+	// just been allocated by getBlob()), no locking is needed.
+
+	fragment  []blobFragment // All the fragments in this blob
+	size      int64          // Total size of the blob.
+	finalized bool           // Whether the blob has been finalized.
+	// A finalized blob has a valid hash field, and no new bytes may be added
+	// to it.  A well-formed hash has 16 bytes.
+	hash []byte
+
+	openWriter bool       // Whether this descriptor is being written by an open BlobWriter.
+	cv         *sync.Cond // signalled when a BlobWriter writes or closes.
+}
+
+// isBeingDeleted() returns whether fragment fragName is about to be deleted
+// by the garbage collector.   Requires fscabs.mu held.
+func (fscabs *FsCaBlobStore) isBeingDeleted(fragName string) (beingDeleted bool) {
+	for i := 0; i != len(fscabs.toDelete) && !beingDeleted; i++ {
+		_, beingDeleted = (*(fscabs.toDelete[i]))[fragName]
+	}
+	return beingDeleted
+}
+
+// descRef() increments the reference count of *desc and returns whether
+// successful.  It may fail if the fragments referenced by the descriptor are
+// being deleted by the garbage collector.
+func (fscabs *FsCaBlobStore) descRef(desc *blobDesc) bool {
+	beingDeleted := false
+	fscabs.mu.Lock()
+	if desc.refCount == 0 {
+		// On the first reference, check whether the fragments are
+		// being deleted, and if not, add *desc to the
+		// fscabs.activeDesc vector.
+		for i := 0; i != len(desc.fragment) && !beingDeleted; i++ {
+			beingDeleted = fscabs.isBeingDeleted(desc.fragment[i].fileName)
+		}
+		if !beingDeleted {
+			desc.activeDescIndex = len(fscabs.activeDesc)
+			fscabs.activeDesc = append(fscabs.activeDesc, desc)
+		}
+	}
+	if !beingDeleted {
+		desc.refCount++
+	}
+	fscabs.mu.Unlock()
+	return !beingDeleted
+}
+
+// descUnref() decrements the reference count of *desc if desc!=nil; if that
+// removes the last reference, *desc is removed from the fscabs.activeDesc
+// vector.
+func (fscabs *FsCaBlobStore) descUnref(desc *blobDesc) {
+	if desc != nil {
+		fscabs.mu.Lock()
+		desc.refCount--
+		if desc.refCount < 0 {
+			panic("negative reference count")
+		} else if desc.refCount == 0 {
+			// Remove desc from fscabs.activeDesc by moving the
+			// last entry in fscabs.activeDesc to desc's slot.
+			n := len(fscabs.activeDesc)
+			lastDesc := fscabs.activeDesc[n-1]
+			lastDesc.activeDescIndex = desc.activeDescIndex
+			fscabs.activeDesc[desc.activeDescIndex] = lastDesc
+			fscabs.activeDesc = fscabs.activeDesc[0 : n-1]
+			desc.activeDescIndex = -1
+		}
+		fscabs.mu.Unlock()
+	}
+}
+
+// getBlob() returns the in-memory blob descriptor for the named blob.
+func (fscabs *FsCaBlobStore) getBlob(ctx *context.T, blobName string) (desc *blobDesc, err error) {
+	slashBlobName := filepath.ToSlash(blobName)
+	if !strings.HasPrefix(slashBlobName, blobDir+"/") || strings.IndexByte(blobName, '.') != -1 {
+		err = verror.New(errInvalidBlobName, ctx, blobName)
+	} else {
+		absBlobName := filepath.Join(fscabs.rootName, blobName)
+		var fh *os.File
+		fh, err = os.Open(absBlobName)
+		if err == nil {
+			var line string
+			desc = new(blobDesc)
+			desc.activeDescIndex = -1
+			desc.name = blobName
+			desc.cv = sync.NewCond(&fscabs.mu)
+			scanner := bufio.NewScanner(fh)
+			for scanner.Scan() {
+				field := strings.Split(scanner.Text(), " ")
+				if len(field) == 4 && field[0] == "d" {
+					var fragSize int64
+					var fragOffset int64
+					fragSize, err = strconv.ParseInt(field[1], 0, 64)
+					if err == nil {
+						fragOffset, err = strconv.ParseInt(field[2], 0, 64)
+					}
+					if err == nil {
+						// No locking needed here because desc
+						// is newly allocated and not yet passed to descRef().
+						desc.fragment = append(desc.fragment,
+							blobFragment{
+								fileName: field[3],
+								pos:      desc.size,
+								size:     fragSize,
+								offset:   fragOffset})
+					}
+					desc.size += fragSize
+				} else if len(field) == 2 && field[0] == "f" {
+					desc.hash = stringToHash(field[1])
+					desc.finalized = true
+					if desc.hash == nil {
+						err = verror.New(errMalformedBlobHash, ctx, blobName, field[1])
+					}
+				} else if len(field) > 0 && len(field[0]) == 1 && "a" <= field[0] && field[0] <= "z" {
+					// unrecognized line, reserved for extensions: ignore.
+				} else {
+					err = verror.New(errMalformedField, ctx, line)
+				}
+			}
+			err = scanner.Err()
+			fh.Close()
+		}
+	}
+	// Ensure that we return either a properly referenced desc, or nil.
+	if err != nil {
+		desc = nil
+	} else if !fscabs.descRef(desc) {
+		err = verror.New(errBlobDeleted, ctx, blobName)
+		desc = nil
+	}
+	return desc, err
+}
+
+// -----------------------------------------------------------
+
+// A BlobWriter allows a blob to be written.  If a blob has not yet been
+// finalized, it also allows that blob to be extended.  A BlobWriter may be
+// created with NewBlobWriter(), and should be closed with Close() or
+// CloseWithoutFinalize().
+type BlobWriter struct {
+	// The BlobWriter exists within a particular FsCaBlobStore and context.T
+	fscabs *FsCaBlobStore
+	ctx    *context.T
+
+	desc   *blobDesc // Description of the blob being written.
+	f      *file     // The file being written.
+	hasher hash.Hash // Running hash of blob.
+
+	// Fields to allow the BlobMap to be written.
+	csBr  *BlobReader     // Reader over the blob that's currently being written.
+	cs    *chunker.Stream // Stream of chunks derived from csBr
+	csErr chan error      // writeBlobMap() sends its result here; Close/CloseWithoutFinalize receives it.
+}
+
+// NewBlobWriter() returns a pointer to a newly allocated BlobWriter on
+// a newly created blob.  If "name" is non-empty, it is used to name
+// the blob, and it must be in the format of a name returned by this
+// interface (probably by another instance on another device).
+// Otherwise, a new name is created, which can be found using
+// the Name() method.  It is an error to attempt to overwrite a blob
+// that already exists in this blob store.  BlobWriters should not be
+// used concurrently by multiple threads.  The returned handle should
+// be closed with either the Close() or CloseWithoutFinalize() method
+// to avoid leaking file handles.
+func (fscabs *FsCaBlobStore) NewBlobWriter(ctx *context.T, name string) (localblobstore.BlobWriter, error) {
+	var bw *BlobWriter
+	if name == "" {
+		name = newBlobName()
+	}
+	fileName := filepath.Join(fscabs.rootName, name)
+	os.MkdirAll(filepath.Dir(fileName), dirPermissions)
+	f, err := newFile(os.OpenFile(fileName, os.O_RDWR|os.O_CREATE|os.O_EXCL, filePermissions))
+	if err == nil {
+		bw = new(BlobWriter)
+		bw.fscabs = fscabs
+		bw.ctx = ctx
+		bw.desc = new(blobDesc)
+		bw.desc.activeDescIndex = -1
+		bw.desc.name = name
+		bw.desc.cv = sync.NewCond(&fscabs.mu)
+		bw.desc.openWriter = true
+		bw.f = f
+		bw.hasher = md5.New()
+		if !fscabs.descRef(bw.desc) {
+			// Can't happen; descriptor refers to no fragments.
+			panic(verror.New(errBlobDeleted, ctx, bw.desc.name))
+		}
+		// Write the chunks of this blob into the BlobMap, as they are
+		// written by this writer.
+		bw.forkWriteBlobMap()
+	}
+	return bw, err
+}
+
+// ResumeBlobWriter() returns a pointer to a newly allocated BlobWriter on an
+// old, but unfinalized blob name.
+func (fscabs *FsCaBlobStore) ResumeBlobWriter(ctx *context.T, blobName string) (localblobstore.BlobWriter, error) {
+	var err error
+	var bw *BlobWriter
+	var desc *blobDesc
+	desc, err = fscabs.getBlob(ctx, blobName)
+	if err == nil && desc.finalized {
+		err = verror.New(errBlobAlreadyFinalized, ctx, blobName)
+	} else if err == nil {
+		bw = new(BlobWriter)
+		bw.fscabs = fscabs
+		bw.ctx = ctx
+		bw.desc = desc
+		bw.desc.openWriter = true
+		fileName := filepath.Join(fscabs.rootName, bw.desc.name)
+		bw.f, err = newFile(os.OpenFile(fileName, os.O_WRONLY|os.O_APPEND, 0666))
+		bw.hasher = md5.New()
+		// Add the existing fragments to the running hash.
+		// The descRef's ref count is incremented here to compensate
+		// for the decrement it will receive in br.Close(), below.
+		if !fscabs.descRef(bw.desc) {
+			// Can't happen; descriptor's ref count was already
+			// non-zero.
+			panic(verror.New(errBlobDeleted, ctx, fileName))
+		}
+		br := fscabs.blobReaderFromDesc(ctx, bw.desc, dontWaitForWriter)
+		buf := make([]byte, 8192, 8192)
+		for err == nil {
+			var n int
+			n, err = br.Read(buf)
+			bw.hasher.Write(buf[0:n])
+		}
+		br.Close()
+		if err == io.EOF { // EOF is expected.
+			err = nil
+		}
+		if err == nil {
+			// Write the chunks of this blob into the BlobMap, as
+			// they are written by this writer.
+			bw.forkWriteBlobMap()
+		}
+	}
+	return bw, err
+}
+
+// forkWriteBlobMap() creates a new thread to run writeBlobMap().  It adds
+// the chunks written to *bw to the blob store's BlobMap.  The caller is
+// expected to call joinWriteBlobMap() at some later point.
+func (bw *BlobWriter) forkWriteBlobMap() {
+	// The descRef's ref count is incremented here to compensate
+	// for the decrement it will receive in br.Close() in joinWriteBlobMap.
+	if !bw.fscabs.descRef(bw.desc) {
+		// Can't happen; descriptor's ref count was already non-zero.
+		panic(verror.New(errBlobDeleted, bw.ctx, bw.desc.name))
+	}
+	bw.csBr = bw.fscabs.blobReaderFromDesc(bw.ctx, bw.desc, waitForWriter)
+	bw.cs = chunker.NewStream(bw.ctx, &chunker.DefaultParam, bw.csBr)
+	bw.csErr = make(chan error)
+	go bw.writeBlobMap()
+}
+
+// insertChunk() inserts chunk into the blob store's BlobMap, associating it
+// with the specified byte offset in the blob blobID being written by *bw.  The byte
+// offset of the next chunk is returned.
+func (bw *BlobWriter) insertChunk(blobID []byte, chunkHash []byte, offset int64, size int64) (int64, error) {
+	err := bw.fscabs.bm.AssociateChunkWithLocation(bw.ctx, chunkHash[:],
+		blobmap.Location{BlobID: blobID, Offset: offset, Size: size})
+	if err != nil {
+		bw.cs.Cancel()
+	}
+	return offset + size, err
+}
+
+// writeBlobMap() iterates over the chunk in stream bw.cs, and associates each
+// one with the blob being written.
+func (bw *BlobWriter) writeBlobMap() {
+	var err error
+	var offset int64
+	blobID := fileNameToHash(blobDir, bw.desc.name)
+	// Associate each chunk only after the next chunk has been seen (or
+	// the blob finalized), to avoid recording an artificially short chunk
+	// at the end of a partial transfer.
+	var chunkHash [md5.Size]byte
+	var chunkLen int64
+	if bw.cs.Advance() {
+		chunk := bw.cs.Value()
+		// Record the hash and size, since chunk's underlying buffer
+		// may be reused by the next call to Advance().
+		chunkHash = md5.Sum(chunk)
+		chunkLen = int64(len(chunk))
+		for bw.cs.Advance() {
+			offset, err = bw.insertChunk(blobID, chunkHash[:], offset, chunkLen)
+			chunk = bw.cs.Value()
+			chunkHash = md5.Sum(chunk)
+			chunkLen = int64(len(chunk))
+		}
+	}
+	if err == nil {
+		err = bw.cs.Err()
+	}
+	bw.fscabs.mu.Lock()
+	if err == nil && chunkLen != 0 && bw.desc.finalized {
+		offset, err = bw.insertChunk(blobID, chunkHash[:], offset, chunkLen)
+	}
+	bw.fscabs.mu.Unlock()
+	bw.csErr <- err // wake joinWriteBlobMap()
+}
+
+// joinWriteBlobMap waits for the completion of the thread forked by forkWriteBlobMap().
+// It returns when the chunks in the blob have been written to the blob store's BlobMap.
+func (bw *BlobWriter) joinWriteBlobMap(err error) error {
+	err2 := <-bw.csErr // read error from end of writeBlobMap()
+	if err == nil {
+		err = err2
+	}
+	bw.csBr.Close()
+	return err
+}
+
+// Close() finalizes *bw, and indicates that the client will perform no further
+// append operations on *bw.  Any internal open file handles are closed.
+func (bw *BlobWriter) Close() (err error) {
+	if bw.f == nil {
+		err = verror.New(errAlreadyClosed, bw.ctx, bw.desc.name)
+	} else if bw.desc.finalized {
+		err = verror.New(errBlobAlreadyFinalized, bw.ctx, bw.desc.name)
+	} else {
+		h := bw.hasher.Sum(nil)
+		_, err = fmt.Fprintf(bw.f.writer, "f %s\n", hashToString(h)) // finalize
+		_, err = bw.f.close(bw.ctx, err)
+		bw.f = nil
+		bw.fscabs.mu.Lock()
+		bw.desc.finalized = true
+		bw.desc.openWriter = false
+		bw.desc.cv.Broadcast() // Tell blobmap BlobReader that writing has ceased.
+		bw.fscabs.mu.Unlock()
+		err = bw.joinWriteBlobMap(err)
+		bw.fscabs.descUnref(bw.desc)
+	}
+	return err
+}
+
+// CloseWithoutFinalize() indicates that the client will perform no further
+// append operations on *bw, but does not finalize the blob.  Any internal open
+// file handles are closed.  Clients are expected to need this operation
+// infrequently.
+func (bw *BlobWriter) CloseWithoutFinalize() (err error) {
+	if bw.f == nil {
+		err = verror.New(errAlreadyClosed, bw.ctx, bw.desc.name)
+	} else {
+		bw.fscabs.mu.Lock()
+		bw.desc.openWriter = false
+		bw.desc.cv.Broadcast() // Tell blobmap BlobReader that writing has ceased.
+		bw.fscabs.mu.Unlock()
+		_, err = bw.f.close(bw.ctx, err)
+		bw.f = nil
+		err = bw.joinWriteBlobMap(err)
+		bw.fscabs.descUnref(bw.desc)
+	}
+	return err
+}
+
+// AppendFragment() appends a fragment to the blob being written by *bw, where
+// the fragment is composed of the byte vectors described by the elements of
+// item[].  The fragment is copied into the blob store.
+func (bw *BlobWriter) AppendFragment(item ...localblobstore.BlockOrFile) (err error) {
+	if bw.f == nil {
+		panic("fs_cablobstore.BlobWriter programming error: AppendFragment() after Close()")
+	}
+	var fragmentName string
+	var size int64
+	fragmentName, size, err = bw.fscabs.addFragment(bw.ctx, bw.hasher, bw.desc, item...)
+	if err == nil {
+		_, err = fmt.Fprintf(bw.f.writer, "d %d %d %s\n", size, 0 /*offset*/, fragmentName)
+	}
+	if err == nil {
+		err = bw.f.writer.Flush()
+	}
+	return err
+}
+
+// AppendBlob() adds a (substring of a) pre-existing blob to the blob being
+// written by *bw.  The fragments of the pre-existing blob are not physically
+// copied; they are referenced by both blobs.
+func (bw *BlobWriter) AppendBlob(blobName string, size int64, offset int64) (err error) {
+	if bw.f == nil {
+		panic("fs_cablobstore.BlobWriter programming error: AppendBlob() after Close()")
+	}
+	var desc *blobDesc
+	desc, err = bw.fscabs.getBlob(bw.ctx, blobName)
+	origSize := bw.desc.size
+	if err == nil {
+		if size == -1 {
+			size = desc.size - offset
+		}
+		if offset < 0 || desc.size < offset+size {
+			err = verror.New(errBadSizeOrOffset, bw.ctx, size, offset, blobName, desc.size)
+		}
+		for i := 0; i != len(desc.fragment) && err == nil && size > 0; i++ {
+			if desc.fragment[i].size <= offset {
+				offset -= desc.fragment[i].size
+			} else {
+				consume := desc.fragment[i].size - offset
+				if size < consume {
+					consume = size
+				}
+				_, err = fmt.Fprintf(bw.f.writer, "d %d %d %s\n",
+					consume, offset+desc.fragment[i].offset, desc.fragment[i].fileName)
+				if err == nil {
+					// Add fragment so garbage collector can see it.
+					// The garbage collector cannot be
+					// about to delete the fragment, because
+					// getBlob() already checked for that
+					// above, and kept a reference.
+					bw.fscabs.mu.Lock()
+					bw.desc.fragment = append(bw.desc.fragment, blobFragment{
+						pos:      bw.desc.size,
+						size:     consume,
+						offset:   offset + desc.fragment[i].offset,
+						fileName: desc.fragment[i].fileName})
+					bw.desc.size += consume
+					bw.desc.cv.Broadcast() // Tell blobmap BlobReader there's more to read.
+					bw.fscabs.mu.Unlock()
+				}
+				offset = 0
+				size -= consume
+			}
+		}
+		bw.fscabs.descUnref(desc)
+		// Add the new fragments to the running hash.
+		if !bw.fscabs.descRef(bw.desc) {
+			// Can't happen; descriptor's ref count was already
+			// non-zero.
+			panic(verror.New(errBlobDeleted, bw.ctx, blobName))
+		}
+		br := bw.fscabs.blobReaderFromDesc(bw.ctx, bw.desc, dontWaitForWriter)
+		if err == nil {
+			_, err = br.Seek(origSize, 0)
+		}
+		buf := make([]byte, 8192, 8192)
+		for err == nil {
+			var n int
+			n, err = br.Read(buf)
+			bw.hasher.Write(buf[0:n]) // Cannot fail; see Hash interface.
+		}
+		br.Close()
+		if err == io.EOF { // EOF is expected.
+			err = nil
+		}
+		if err == nil {
+			err = bw.f.writer.Flush()
+		}
+	}
+	return err
+}
+
+// IsFinalized() returns whether *bw has been finalized.
+func (bw *BlobWriter) IsFinalized() bool {
+	return bw.desc.finalized
+}
+
+// Size() returns *bw's size.
+func (bw *BlobWriter) Size() int64 {
+	return bw.desc.size
+}
+
+// Name() returns *bw's name.
+func (bw *BlobWriter) Name() string {
+	return bw.desc.name
+}
+
+// Hash() returns *bw's hash, reflecting the bytes written so far.
+func (bw *BlobWriter) Hash() []byte {
+	return bw.hasher.Sum(nil)
+}
+
+// -----------------------------------------------------------
+
+// A BlobReader allows a blob to be read using the standard ReadAt(), Read(),
+// and Seek() calls.  A BlobReader can be created with NewBlobReader(), and
+// should be closed with the Close() method to avoid leaking file handles.
+type BlobReader struct {
+	// The BlobReader exists within a particular FsCaBlobStore and context.T.
+	fscabs *FsCaBlobStore
+	ctx    *context.T
+
+	desc          *blobDesc // A description of the blob being read.
+	waitForWriter bool      // whether this reader should wait for a concurrent BlobWriter
+
+	pos int64 // The next position we will read from (used by Read/Seek, not ReadAt).
+
+	// The fields below represent a cached open fragment desc.fragment[fragmentIndex].
+	fragmentIndex int      // -1 or  0 <= fragmentIndex < len(desc.fragment).
+	fh            *os.File // non-nil iff fragmentIndex != -1.
+}
+
+// constants to make the calls to blobReaderFromDesc invocations more readable
+const (
+	dontWaitForWriter = false
+	waitForWriter     = true
+)
+
+// blobReaderFromDesc() returns a pointer to a newly allocated BlobReader given
+// a pre-existing blobDesc.  If waitForWriter is true, the reader will wait for
+// any BlobWriter to finish writing the part of the blob the reader is trying
+// to read.
+func (fscabs *FsCaBlobStore) blobReaderFromDesc(ctx *context.T, desc *blobDesc, waitForWriter bool) *BlobReader {
+	br := new(BlobReader)
+	br.fscabs = fscabs
+	br.ctx = ctx
+	br.fragmentIndex = -1
+	br.desc = desc
+	br.waitForWriter = waitForWriter
+	return br
+}
+
+// NewBlobReader() returns a pointer to a newly allocated BlobReader on the
+// specified blobName.  BlobReaders should not be used concurrently by multiple
+// threads.  Returned handles should be closed with Close().
+func (fscabs *FsCaBlobStore) NewBlobReader(ctx *context.T, blobName string) (br localblobstore.BlobReader, err error) {
+	var desc *blobDesc
+	desc, err = fscabs.getBlob(ctx, blobName)
+	if err == nil {
+		br = fscabs.blobReaderFromDesc(ctx, desc, dontWaitForWriter)
+	}
+	return br, err
+}
+
+// closeInternal() closes any open file handles within *br.
+func (br *BlobReader) closeInternal() {
+	if br.fh != nil {
+		br.fh.Close()
+		br.fh = nil
+	}
+	br.fragmentIndex = -1
+}
+
+// Close() indicates that the client will perform no further operations on *br.
+// It closes any open file handles within a BlobReader.
+func (br *BlobReader) Close() error {
+	br.closeInternal()
+	br.fscabs.descUnref(br.desc)
+	return nil
+}
+
+// findFragment() returns the index of the first element of fragment[] that may
+// contain "offset", based on the "pos" fields of each element.
+// Requires that fragment[] be sorted on the "pos" fields of the elements.
+func findFragment(fragment []blobFragment, offset int64) int {
+	lo := 0
+	hi := len(fragment)
+	for lo < hi {
+		mid := (lo + hi) >> 1
+		if offset < fragment[mid].pos {
+			hi = mid
+		} else {
+			lo = mid + 1
+		}
+	}
+	if lo > 0 {
+		lo--
+	}
+	return lo
+}
+
+// waitUntilAvailable() waits until position pos within *br is available for
+// reading, if this reader is waiting for writers.  This may be because:
+//  - *br is on an already written blob.
+//  - *br is on a blob being written that has been closed, or whose writes have
+//    passed position pos.
+// The value pos==math.MaxInt64 can be used to mean "until the writer is closed".
+// Requires br.fscabs.mu held.
+func (br *BlobReader) waitUntilAvailable(pos int64) {
+	for br.waitForWriter && br.desc.openWriter && br.desc.size < pos {
+		br.desc.cv.Wait()
+	}
+}
+
+// ReadAt() fills b[] with up to len(b) bytes of data starting at position "at"
+// within the blob that *br indicates, and returns the number of bytes read.
+func (br *BlobReader) ReadAt(b []byte, at int64) (n int, err error) {
+	br.fscabs.mu.Lock()
+	br.waitUntilAvailable(at + int64(len(b)))
+	i := findFragment(br.desc.fragment, at)
+	if i < len(br.desc.fragment) && at <= br.desc.size {
+		fragmenti := br.desc.fragment[i] // copy fragment data to allow releasing lock
+		br.fscabs.mu.Unlock()
+		if i != br.fragmentIndex {
+			br.closeInternal()
+		}
+		if br.fragmentIndex == -1 {
+			br.fh, err = os.Open(filepath.Join(br.fscabs.rootName, fragmenti.fileName))
+			if err == nil {
+				br.fragmentIndex = i
+			} else {
+				br.closeInternal()
+			}
+		}
+		var offset int64 = at - fragmenti.pos + fragmenti.offset
+		consume := fragmenti.size - (at - fragmenti.pos)
+		if int64(len(b)) < consume {
+			consume = int64(len(b))
+		}
+		if br.fh != nil {
+			n, err = br.fh.ReadAt(b[0:consume], offset)
+		} else if err == nil {
+			panic("failed to open blob fragment")
+		}
+		br.fscabs.mu.Lock()
+		// Return io.EOF if the Read reached the end of the last
+		// fragment, but not if it's merely the end of some interior
+		// fragment or the blob is still being extended.
+		if int64(n)+at >= br.desc.size && !(br.waitForWriter && br.desc.openWriter) {
+			if err == nil {
+				err = io.EOF
+			}
+		} else if err == io.EOF {
+			err = nil
+		}
+	} else if at == br.desc.size { // Reading at the end of the file, past the last fragment.
+		err = io.EOF
+	} else {
+		err = verror.New(errIllegalPositionForRead, br.ctx, br.pos, br.desc.size)
+	}
+	br.fscabs.mu.Unlock()
+	return n, err
+}
+
+// Read() fills b[] with up to len(b) bytes of data starting at the current
+// seek position of *br within the blob that *br indicates, and then both
+// returns the number of bytes read and advances *br's seek position by that
+// amount.
+func (br *BlobReader) Read(b []byte) (n int, err error) {
+	n, err = br.ReadAt(b, br.pos)
+	if err == nil {
+		br.pos += int64(n)
+	}
+	return n, err
+}
+
+// Seek() sets the seek position of *br to offset if whence==0,
+// offset+current_seek_position if whence==1, and offset+end_of_blob if
+// whence==2, and then returns the current seek position.
+func (br *BlobReader) Seek(offset int64, whence int) (result int64, err error) {
+	br.fscabs.mu.Lock()
+	if whence == 0 {
+		result = offset
+	} else if whence == 1 {
+		result = offset + br.pos
+	} else if whence == 2 {
+		br.waitUntilAvailable(math.MaxInt64)
+		result = offset + br.desc.size
+	} else {
+		err = verror.New(errBadSeekWhence, br.ctx, whence)
+		result = br.pos
+	}
+	if result < 0 {
+		err = verror.New(errNegativeSeekPosition, br.ctx, offset, whence)
+		result = br.pos
+	} else if result > br.desc.size {
+		err = verror.New(errIllegalPositionForRead, br.ctx, result, br.desc.size)
+		result = br.pos
+	} else if err == nil {
+		br.pos = result
+	}
+	br.fscabs.mu.Unlock()
+	return result, err
+}
+
+// IsFinalized() returns whether *br has been finalized.
+func (br *BlobReader) IsFinalized() bool {
+	br.fscabs.mu.Lock()
+	br.waitUntilAvailable(math.MaxInt64)
+	finalized := br.desc.finalized
+	br.fscabs.mu.Unlock()
+	return finalized
+}
+
+// Size() returns *br's size.
+func (br *BlobReader) Size() int64 {
+	br.fscabs.mu.Lock()
+	br.waitUntilAvailable(math.MaxInt64)
+	size := br.desc.size
+	br.fscabs.mu.Unlock()
+	return size
+}
+
+// Name() returns *br's name.
+func (br *BlobReader) Name() string {
+	return br.desc.name
+}
+
+// Hash() returns *br's hash.  It may be nil if the blob is not finalized.
+func (br *BlobReader) Hash() []byte {
+	br.fscabs.mu.Lock()
+	br.waitUntilAvailable(math.MaxInt64)
+	hash := br.desc.hash
+	br.fscabs.mu.Unlock()
+	return hash
+}
+
+// -----------------------------------------------------------
+
+// A dirListing is a list of names in a directory, plus a position, which
+// indexes the last item in nameList that has been processed.
+type dirListing struct {
+	pos      int      // Current position in nameList; may be -1 at the start of iteration.
+	nameList []string // List of directory entries.
+}
+
+// An FsCasIter represents an iterator that allows the client to enumerate all
+// the blobs or fragments in a FsCaBlobStore.
+type FsCasIter struct {
+	fscabs *FsCaBlobStore // The parent FsCaBlobStore.
+	err    error          // If non-nil, the error that terminated iteration.
+	stack  []dirListing   // The stack of dirListings leading to the current entry.
+	ctx    *context.T     // context passed to ListBlobIds() or ListCAIds()
+
+	mu        sync.Mutex // Protects cancelled.
+	cancelled bool       // Whether Cancel() has been called.
+}
+
+// ListBlobIds() returns an iterator that can be used to enumerate the blobs in
+// an FsCaBlobStore.  Expected use is:
+//    fscabsi := fscabs.ListBlobIds(ctx)
+//    for fscabsi.Advance() {
+//      // Process fscabsi.Value() here.
+//    }
+//    if fscabsi.Err() != nil {
+//      // The loop terminated early due to an error.
+//    }
+func (fscabs *FsCaBlobStore) ListBlobIds(ctx *context.T) localblobstore.Stream {
+	stack := make([]dirListing, 1)
+	stack[0] = dirListing{pos: -1, nameList: []string{blobDir}}
+	return &FsCasIter{fscabs: fscabs, stack: stack, ctx: ctx}
+}
+
+// ListCAIds() returns an iterator that can be used to enumerate the
+// content-addressable fragments in an FsCaBlobStore.
+// Expected use is:
+//    fscabsi := fscabs.ListCAIds(ctx)
+//    for fscabsi.Advance() {
+//      // Process fscabsi.Value() here.
+//    }
+//    if fscabsi.Err() != nil {
+//      // The loop terminated early due to an error.
+//    }
+func (fscabs *FsCaBlobStore) ListCAIds(ctx *context.T) localblobstore.Stream {
+	stack := make([]dirListing, 1)
+	stack[0] = dirListing{pos: -1, nameList: []string{casDir}}
+	return &FsCasIter{fscabs: fscabs, stack: stack, ctx: ctx}
+}
+
+// isCancelled() returns whether Cancel() has been called.
+func (fscabsi *FsCasIter) isCancelled() bool {
+	fscabsi.mu.Lock()
+	cancelled := fscabsi.cancelled
+	fscabsi.mu.Unlock()
+	return cancelled
+}
+
+// Advance() stages an item so that it may be retrieved via Value.  Returns
+// true iff there is an item to retrieve.  Advance must be called before Value
+// is called.
+func (fscabsi *FsCasIter) Advance() (advanced bool) {
+	stack := fscabsi.stack
+	err := fscabsi.err
+
+	for err == nil && !advanced && len(stack) != 0 && !fscabsi.isCancelled() {
+		last := len(stack) - 1
+		stack[last].pos++
+		if stack[last].pos == len(stack[last].nameList) {
+			stack = stack[0:last]
+			fscabsi.stack = stack
+		} else {
+			fullName := filepath.Join(fscabsi.fscabs.rootName, fscabsi.Value())
+			var fi os.FileInfo
+			fi, err = os.Lstat(fullName)
+			if err != nil {
+				// error: nothing to do
+			} else if fi.IsDir() {
+				var dirHandle *os.File
+				dirHandle, err = os.Open(fullName)
+				if err == nil {
+					var nameList []string
+					nameList, err = dirHandle.Readdirnames(0)
+					dirHandle.Close()
+					stack = append(stack, dirListing{pos: -1, nameList: nameList})
+					fscabsi.stack = stack
+					last = len(stack) - 1
+				}
+			} else {
+				advanced = true
+			}
+		}
+	}
+
+	if fscabsi.isCancelled() {
+		if err == nil {
+			fscabsi.err = verror.New(errStreamCancelled, fscabsi.ctx)
+		}
+		advanced = false
+	}
+
+	fscabsi.err = err
+	return advanced
+}
+
+// Value() returns the item that was staged by Advance.  May panic if Advance
+// returned false or was not called.  Never blocks.
+func (fscabsi *FsCasIter) Value() (name string) {
+	stack := fscabsi.stack
+	if fscabsi.err == nil && len(stack) != 0 && stack[0].pos >= 0 {
+		name = stack[0].nameList[stack[0].pos]
+		for i := 1; i != len(stack); i++ {
+			name = filepath.Join(name, stack[i].nameList[stack[i].pos])
+		}
+	}
+	return name
+}
+
+// Err() returns any error encountered by Advance.  Never blocks.
+func (fscabsi *FsCasIter) Err() error {
+	return fscabsi.err
+}
+
+// Cancel() indicates that the iteration stream should terminate early.
+// Never blocks.  May be called concurrently with other methods on fscabsi.
+func (fscabsi *FsCasIter) Cancel() {
+	fscabsi.mu.Lock()
+	fscabsi.cancelled = true
+	fscabsi.mu.Unlock()
+}
+
+// -----------------------------------------------------------
+
+// An errorChunkStream is a localblobstore.ChunkStream that yields an error.
+type errorChunkStream struct {
+	err error
+}
+
+func (*errorChunkStream) Advance() bool       { return false }
+func (*errorChunkStream) Value([]byte) []byte { return nil }
+func (ecs *errorChunkStream) Err() error      { return ecs.err }
+func (*errorChunkStream) Cancel()             {}
+
+// BlobChunkStream() returns a ChunkStream that can be used to read the ordered
+// list of content hashes of chunks in blob blobName.  It is expected that this
+// list will be presented to RecipeFromChunks() on another device, to create a
+// recipe for transmitting the blob efficiently to that other device.
+func (fscabs *FsCaBlobStore) BlobChunkStream(ctx *context.T, blobName string) (cs localblobstore.ChunkStream) {
+	blobID := fileNameToHash(blobDir, blobName)
+	if blobID == nil {
+		cs = &errorChunkStream{err: verror.New(errInvalidBlobName, ctx, blobName)}
+	} else {
+		cs = fscabs.bm.NewChunkStream(ctx, blobID)
+	}
+	return cs
+}
+
+// -----------------------------------------------------------
+
+// LookupChunk returns the location of a chunk with the specified chunk hash
+// within the store.
+func (fscabs *FsCaBlobStore) LookupChunk(ctx *context.T, chunkHash []byte) (loc localblobstore.Location, err error) {
+	var chunkMapLoc blobmap.Location
+	chunkMapLoc, err = fscabs.bm.LookupChunk(ctx, chunkHash)
+	if err == nil {
+		loc.BlobName = hashToFileName(blobDir, chunkMapLoc.BlobID)
+		loc.Size = chunkMapLoc.Size
+		loc.Offset = chunkMapLoc.Offset
+	}
+	return loc, err
+}
+
+// -----------------------------------------------------------
+
+// A RecipeStream implements localblobstore.RecipeStream.  It allows the client
+// to iterate over the recipe steps to recreate a blob identified by a stream
+// of chunk hashes (from chunkStream), but using parts of blobs in the current
+// blob store where possible.
+type RecipeStream struct {
+	fscabs *FsCaBlobStore
+	ctx    *context.T
+
+	chunkStream     localblobstore.ChunkStream // the stream of chunks in the blob
+	pendingChunkBuf [16]byte                   // a buffer for pendingChunk
+	pendingChunk    []byte                     // the last unprocessed chunk hash read chunkStream, or nil if none
+	step            localblobstore.RecipeStep  // the recipe step to be returned by Value()
+	mu              sync.Mutex                 // protects cancelled
+	cancelled       bool                       // whether Cancel() has been called
+}
+
+// RecipeStreamFromChunkStream() returns a pointer to a RecipeStream that allows
+// the client to iterate over each RecipeStep needed to create the blob formed
+// by the chunks in chunkStream.
+func (fscabs *FsCaBlobStore) RecipeStreamFromChunkStream(ctx *context.T, chunkStream localblobstore.ChunkStream) localblobstore.RecipeStream {
+	rs := new(RecipeStream)
+	rs.fscabs = fscabs
+	rs.ctx = ctx
+	rs.chunkStream = chunkStream
+	return rs
+}
+
+// isCancelled() returns whether rs.Cancel() has been called.
+func (rs *RecipeStream) isCancelled() bool {
+	rs.mu.Lock()
+	cancelled := rs.cancelled
+	rs.mu.Unlock()
+	return cancelled
+}
+
+// Advance() stages an item so that it may be retrieved via Value().
+// Returns true iff there is an item to retrieve.  Advance() must be
+// called before Value() is called.  The caller is expected to read
+// until Advance() returns false, or to call Cancel().
+func (rs *RecipeStream) Advance() (ok bool) {
+	if rs.pendingChunk == nil && rs.chunkStream.Advance() {
+		rs.pendingChunk = rs.chunkStream.Value(rs.pendingChunkBuf[:])
+	}
+	for !ok && rs.pendingChunk != nil && !rs.isCancelled() {
+		var err error
+		var loc0 blobmap.Location
+		loc0, err = rs.fscabs.bm.LookupChunk(rs.ctx, rs.pendingChunk)
+		if err == nil {
+			blobName := hashToFileName(blobDir, loc0.BlobID)
+			var blobDesc *blobDesc
+			if blobDesc, err = rs.fscabs.getBlob(rs.ctx, blobName); err != nil {
+				// The BlobMap contained a reference to a
+				// deleted blob.  Delete the reference in the
+				// BlobMap; the next loop iteration will
+				// consider the chunk again.
+				rs.fscabs.bm.DeleteBlob(rs.ctx, loc0.BlobID)
+			} else {
+				rs.fscabs.descUnref(blobDesc)
+				// The chunk is in a known blob.  Combine
+				// contiguous chunks into a single recipe
+				// entry.
+				rs.pendingChunk = nil // consumed
+				for rs.pendingChunk == nil && rs.chunkStream.Advance() {
+					rs.pendingChunk = rs.chunkStream.Value(rs.pendingChunkBuf[:])
+					var loc blobmap.Location
+					loc, err = rs.fscabs.bm.LookupChunk(rs.ctx, rs.pendingChunk)
+					if err == nil && bytes.Compare(loc0.BlobID, loc.BlobID) == 0 && loc.Offset == loc0.Offset+loc0.Size {
+						loc0.Size += loc.Size
+						rs.pendingChunk = nil // consumed
+					}
+				}
+				rs.step = localblobstore.RecipeStep{Blob: blobName, Offset: loc0.Offset, Size: loc0.Size}
+				ok = true
+			}
+		} else { // The chunk is not in the BlobMap; yield a single chunk hash.
+			rs.step = localblobstore.RecipeStep{Chunk: rs.pendingChunk}
+			rs.pendingChunk = nil // consumed
+			ok = true
+		}
+	}
+	return ok && !rs.isCancelled()
+}
+
+// Value() returns the item that was staged by Advance().  May panic if
+// Advance() returned false or was not called.  Never blocks.
+func (rs *RecipeStream) Value() localblobstore.RecipeStep {
+	return rs.step
+}
+
+// Err() returns any error encountered by Advance.  Never blocks.
+func (rs *RecipeStream) Err() error {
+	// There are no errors to return here.  The errors encountered in
+	// Advance() are expected and recoverable.
+	return nil
+}
+
+// Cancel() indicates that the client wishes to cease reading from the stream.
+// It causes the next call to Advance() to return false.  Never blocks.
+// It may be called concurrently with other calls on the stream.
+func (rs *RecipeStream) Cancel() {
+	rs.mu.Lock()
+	rs.cancelled = true
+	rs.mu.Unlock()
+	rs.chunkStream.Cancel()
+}
+
+// -----------------------------------------------------------
+
+// gcTemp() attempts to delete files in dirName older than threshold.
+// Errors are ignored.
+func gcTemp(dirName string, threshold time.Time) {
+	fh, err := os.Open(dirName)
+	if err == nil {
+		fi, _ := fh.Readdir(0)
+		fh.Close()
+		for i := 0; i < len(fi); i++ {
+			if fi[i].ModTime().Before(threshold) {
+				os.Remove(filepath.Join(dirName, fi[i].Name()))
+			}
+		}
+	}
+}
+
+// GC() removes old temp files and content-addressed blocks that are no longer
+// referenced by any blob.  It may be called concurrently with other calls to
+// GC(), and with uses of BlobReaders and BlobWriters.
+func (fscabs *FsCaBlobStore) GC(ctx *context.T) (err error) {
+	// Remove old temporary files.
+	gcTemp(filepath.Join(fscabs.rootName, tmpDir), time.Now().Add(-10*time.Hour))
+
+	// Add a key to caSet for each content-addressed fragment in *fscabs,
+	caSet := make(map[string]bool)
+	caIter := fscabs.ListCAIds(ctx)
+	for caIter.Advance() {
+		caSet[caIter.Value()] = true
+	}
+	err = caIter.Err()
+
+	// cmBlobs maps the names of blobs found in the BlobMap to their IDs.
+	// (The IDs can be derived from the names; the map is really being used
+	// to record which blobs exist, and the value merely avoids repeated
+	// conversions.)
+	cmBlobs := make(map[string][]byte)
+	if err == nil {
+		// Record all the blobs known to the BlobMap;
+		bs := fscabs.bm.NewBlobStream(ctx)
+		for bs.Advance() {
+			blobID := bs.Value(nil)
+			cmBlobs[hashToFileName(blobDir, blobID)] = blobID
+		}
+	}
+
+	if err == nil {
+		// Remove from cmBlobs all extant blobs, and remove from
+		// caSet all their fragments.
+		blobIter := fscabs.ListBlobIds(ctx)
+		for blobIter.Advance() {
+			var blobDesc *blobDesc
+			if blobDesc, err = fscabs.getBlob(ctx, blobIter.Value()); err == nil {
+				delete(cmBlobs, blobDesc.name)
+				for i := range blobDesc.fragment {
+					delete(caSet, blobDesc.fragment[i].fileName)
+				}
+				fscabs.descUnref(blobDesc)
+			}
+		}
+	}
+
+	if err == nil {
+		// Remove all blobs still mentioned in cmBlobs from the BlobMap;
+		// these are the ones that no longer exist in the blobs directory.
+		for _, blobID := range cmBlobs {
+			err = fscabs.bm.DeleteBlob(ctx, blobID)
+			if err != nil {
+				break
+			}
+		}
+	}
+
+	if err == nil {
+		// Remove from caSet all fragments referenced by open BlobReaders and
+		// BlobWriters.  Advertise to new readers and writers which blobs are
+		// about to be deleted.
+		fscabs.mu.Lock()
+		for _, desc := range fscabs.activeDesc {
+			for i := range desc.fragment {
+				delete(caSet, desc.fragment[i].fileName)
+			}
+		}
+		fscabs.toDelete = append(fscabs.toDelete, &caSet)
+		fscabs.mu.Unlock()
+
+		// Delete the things that still remain in caSet; they are no longer
+		// referenced.
+		for caName := range caSet {
+			os.Remove(filepath.Join(fscabs.rootName, caName))
+		}
+
+		// Stop advertising what's been deleted.
+		fscabs.mu.Lock()
+		n := len(fscabs.toDelete)
+		var i int
+		// We require that &caSet still be in the list.
+		for i = 0; fscabs.toDelete[i] != &caSet; i++ {
+		}
+		fscabs.toDelete[i] = fscabs.toDelete[n-1]
+		fscabs.toDelete = fscabs.toDelete[0 : n-1]
+		fscabs.mu.Unlock()
+	}
+	return err
+}
diff --git a/services/syncbase/localblobstore/fs_cablobstore/fs_cablobstore_test.go b/services/syncbase/localblobstore/fs_cablobstore/fs_cablobstore_test.go
new file mode 100644
index 0000000..0d964c3
--- /dev/null
+++ b/services/syncbase/localblobstore/fs_cablobstore/fs_cablobstore_test.go
@@ -0,0 +1,97 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// A test for fs_cablobstore
+package fs_cablobstore_test
+
+import "io/ioutil"
+import "os"
+import "testing"
+
+import "v.io/syncbase/x/ref/services/syncbase/localblobstore"
+import "v.io/syncbase/x/ref/services/syncbase/localblobstore/fs_cablobstore"
+import "v.io/syncbase/x/ref/services/syncbase/localblobstore/localblobstore_testlib"
+import "v.io/x/ref/test"
+import _ "v.io/x/ref/runtime/factories/generic"
+
+// This test case tests adding files, retrieving them and deleting them.  One
+// can't retrieve or delete something that hasn't been created, so it's all one
+// test case.
+func TestAddRetrieveAndDelete(t *testing.T) {
+	ctx, shutdown := test.V23Init()
+	defer shutdown()
+
+	// Make a temporary directory.
+	var err error
+	var testDirName string
+	testDirName, err = ioutil.TempDir("", "localblobstore_test")
+	if err != nil {
+		t.Fatalf("localblobstore_test: can't make tmp directory: %v\n", err)
+	}
+	defer os.RemoveAll(testDirName)
+
+	// Create an fs_cablobstore.
+	var bs localblobstore.BlobStore
+	bs, err = fs_cablobstore.Create(ctx, testDirName)
+	if err != nil {
+		t.Fatalf("fs_cablobstore.Create failed: %v", err)
+	}
+
+	// Test it.
+	localblobstore_testlib.AddRetrieveAndDelete(t, ctx, bs, testDirName)
+}
+
+// This test case tests the incremental transfer of blobs via chunks.
+func TestWritingViaChunks(t *testing.T) {
+	ctx, shutdown := test.V23Init()
+	defer shutdown()
+
+	var err error
+
+	// Make a pair of blobstores, each in its own temporary directory.
+	const nBlobStores = 2
+	var testDirName [nBlobStores]string
+	var bs [nBlobStores]localblobstore.BlobStore
+	for i := 0; i != nBlobStores; i++ {
+		testDirName[i], err = ioutil.TempDir("", "localblobstore_test")
+		if err != nil {
+			t.Fatalf("localblobstore_test: can't make tmp directory: %v\n", err)
+		}
+		defer os.RemoveAll(testDirName[i])
+
+		bs[i], err = fs_cablobstore.Create(ctx, testDirName[i])
+		if err != nil {
+			t.Fatalf("fs_cablobstore.Create failed: %v", err)
+		}
+	}
+
+	// Test it.
+	localblobstore_testlib.WriteViaChunks(t, ctx, bs)
+}
+
+// This test case checks that empty blobs can be created, then extended via
+// ResumeBlobWriter.
+func TestCreateAndResume(t *testing.T) {
+	ctx, shutdown := test.V23Init()
+	defer shutdown()
+
+	// Make a temporary directory.
+	var err error
+	var testDirName string
+	testDirName, err = ioutil.TempDir("", "localblobstore_test")
+	if err != nil {
+		t.Fatalf("localblobstore_test: can't make tmp directory: %v\n", err)
+	}
+	defer os.RemoveAll(testDirName)
+
+	// Create an fs_cablobstore.
+	var bs localblobstore.BlobStore
+	bs, err = fs_cablobstore.Create(ctx, testDirName)
+	if err != nil {
+		t.Fatalf("fs_cablobstore.Create failed: %v", err)
+	}
+
+	// Test it.
+	localblobstore_testlib.CreateAndResume(t, ctx, bs)
+}
diff --git a/services/syncbase/localblobstore/localblobstore_test.go b/services/syncbase/localblobstore/localblobstore_test.go
new file mode 100644
index 0000000..a258c0f
--- /dev/null
+++ b/services/syncbase/localblobstore/localblobstore_test.go
@@ -0,0 +1,97 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// A test for localblobstore
+package localblobstore_test
+
+import "io/ioutil"
+import "os"
+import "testing"
+
+import "v.io/syncbase/x/ref/services/syncbase/localblobstore"
+import "v.io/syncbase/x/ref/services/syncbase/localblobstore/fs_cablobstore"
+import "v.io/syncbase/x/ref/services/syncbase/localblobstore/localblobstore_testlib"
+import "v.io/x/ref/test"
+import _ "v.io/x/ref/runtime/factories/generic"
+
+// This test case tests adding files, retrieving them and deleting them.  One
+// can't retrieve or delete something that hasn't been created, so it's all one
+// test case.
+func TestAddRetrieveAndDelete(t *testing.T) {
+	ctx, shutdown := test.V23Init()
+	defer shutdown()
+
+	// Make a temporary directory.
+	var err error
+	var testDirName string
+	testDirName, err = ioutil.TempDir("", "localblobstore_test")
+	if err != nil {
+		t.Fatalf("localblobstore_test: can't make tmp directory: %v\n", err)
+	}
+	defer os.RemoveAll(testDirName)
+
+	// Create an fs_cablobstore.
+	var bs localblobstore.BlobStore
+	bs, err = fs_cablobstore.Create(ctx, testDirName)
+	if err != nil {
+		t.Fatalf("fs_cablobstore.Create failed: %v", err)
+	}
+
+	// Test it.
+	localblobstore_testlib.AddRetrieveAndDelete(t, ctx, bs, testDirName)
+}
+
+// This test case tests the incremental transfer of blobs via chunks.
+func TestWritingViaChunks(t *testing.T) {
+	ctx, shutdown := test.V23Init()
+	defer shutdown()
+
+	var err error
+
+	// Make a pair of blobstores, each in its own temporary directory.
+	const nBlobStores = 2
+	var testDirName [nBlobStores]string
+	var bs [nBlobStores]localblobstore.BlobStore
+	for i := 0; i != nBlobStores; i++ {
+		testDirName[i], err = ioutil.TempDir("", "localblobstore_test")
+		if err != nil {
+			t.Fatalf("localblobstore_test: can't make tmp directory: %v\n", err)
+		}
+		defer os.RemoveAll(testDirName[i])
+
+		bs[i], err = fs_cablobstore.Create(ctx, testDirName[i])
+		if err != nil {
+			t.Fatalf("fs_cablobstore.Create failed: %v", err)
+		}
+	}
+
+	// Test it.
+	localblobstore_testlib.WriteViaChunks(t, ctx, bs)
+}
+
+// This test case checks that empty blobs can be created, then extended via
+// ResumeBlobWriter.
+func TestCreateAndResume(t *testing.T) {
+	ctx, shutdown := test.V23Init()
+	defer shutdown()
+
+	// Make a temporary directory.
+	var err error
+	var testDirName string
+	testDirName, err = ioutil.TempDir("", "localblobstore_test")
+	if err != nil {
+		t.Fatalf("localblobstore_test: can't make tmp directory: %v\n", err)
+	}
+	defer os.RemoveAll(testDirName)
+
+	// Create an fs_cablobstore.
+	var bs localblobstore.BlobStore
+	bs, err = fs_cablobstore.Create(ctx, testDirName)
+	if err != nil {
+		t.Fatalf("fs_cablobstore.Create failed: %v", err)
+	}
+
+	// Test it.
+	localblobstore_testlib.CreateAndResume(t, ctx, bs)
+}
diff --git a/services/syncbase/localblobstore/localblobstore_testlib/localblobstore_testlib.go b/services/syncbase/localblobstore/localblobstore_testlib/localblobstore_testlib.go
new file mode 100644
index 0000000..d6c1d9d
--- /dev/null
+++ b/services/syncbase/localblobstore/localblobstore_testlib/localblobstore_testlib.go
@@ -0,0 +1,889 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// A test library for localblobstores.
+package localblobstore_testlib
+
+import "bytes"
+import "crypto/md5"
+import "fmt"
+import "io"
+import "io/ioutil"
+import "path/filepath"
+import "testing"
+
+import "v.io/syncbase/x/ref/services/syncbase/localblobstore"
+import "v.io/syncbase/x/ref/services/syncbase/localblobstore/chunker"
+import "v.io/v23/context"
+import "v.io/v23/verror"
+
+// A blobOrBlockOrFile represents some bytes that may be contained in a named
+// blob, a named file, or in an explicit slice of bytes.
+type blobOrBlockOrFile struct {
+	blob   string // If non-empty, the name of the blob containing the bytes.
+	file   string // If non-empty and blob is empty, the name of the file containing the bytes.
+	size   int64  // Size of part of file or blob, or -1 for "everything until EOF".
+	offset int64  // Offset within file or blob.
+	block  []byte // If both blob and file are empty, a slice containing the bytes.
+}
+
+// A testBlob records that some specified content has been stored with a given
+// blob name in the blob store.
+type testBlob struct {
+	content  []byte // content that has been stored.
+	blobName string // the name of the blob.
+}
+
+// removeBlobFromBlobVector() removes the entry named blobName from
+// blobVector[], returning the new vector.
+func removeBlobFromBlobVector(blobVector []testBlob, blobName string) []testBlob {
+	n := len(blobVector)
+	i := 0
+	for i = 0; i != n && blobName != blobVector[i].blobName; i++ {
+	}
+	if i != n {
+		blobVector[i] = blobVector[n-1]
+		blobVector = blobVector[0 : n-1]
+	}
+	return blobVector
+}
+
+// writeBlob() writes a new blob to bs, and returns its name.  The new
+// blob's content is described by the elements of data[].  Any error messages
+// generated include the index of the blob in blobVector and its content; the
+// latter is assumed to be printable.  The expected content of the the blob is
+// "content", so that this routine can check it.  If useResume is true, and data[]
+// has length more than 1, the function artificially uses ResumeBlobWriter(),
+// to test it.
+func writeBlob(t *testing.T, ctx *context.T, bs localblobstore.BlobStore, blobVector []testBlob,
+	content []byte, useResume bool, data ...blobOrBlockOrFile) []testBlob {
+	var bw localblobstore.BlobWriter
+	var err error
+	bw, err = bs.NewBlobWriter(ctx, "")
+	if err != nil {
+		t.Errorf("localblobstore.NewBlobWriter blob %d:%s failed: %v", len(blobVector), string(content), err)
+	}
+	blobName := bw.Name()
+
+	// Construct the blob from the pieces.
+	// There is a loop within the loop to exercise the possibility of
+	// passing multiple fragments to AppendFragment().
+	for i := 0; i != len(data) && err == nil; {
+		if len(data[i].blob) != 0 {
+			err = bw.AppendBlob(data[i].blob, data[i].size, data[i].offset)
+			if err != nil {
+				t.Errorf("localblobstore.AppendBlob %d:%s blob %s failed: %v", len(blobVector), string(content), data[i].blob, err)
+			}
+			i++
+		} else {
+			var pieces []localblobstore.BlockOrFile
+			for ; i != len(data) && len(data[i].blob) == 0; i++ {
+				if len(data[i].file) != 0 {
+					pieces = append(pieces, localblobstore.BlockOrFile{
+						FileName: data[i].file,
+						Size:     data[i].size,
+						Offset:   data[i].offset})
+				} else {
+					pieces = append(pieces, localblobstore.BlockOrFile{Block: data[i].block})
+				}
+			}
+			err = bw.AppendFragment(pieces...)
+			if err != nil {
+				t.Errorf("localblobstore.AppendFragment %d:%s failed on %v: %v", len(blobVector), string(content), pieces, err)
+			}
+		}
+		if useResume && i < len(data)-1 && err == nil {
+			err = bw.CloseWithoutFinalize()
+			if err == nil {
+				bw, err = bs.ResumeBlobWriter(ctx, blobName)
+			}
+		}
+	}
+
+	if bw != nil {
+		if bw.Size() != int64(len(content)) {
+			t.Errorf("localblobstore.Size before finalization %d:%s got %d, expected %d", len(blobVector), string(content), bw.Size(), len(content))
+		}
+		if bw.IsFinalized() {
+			t.Errorf("localblobstore.IsFinalized %d:%s got true, expected false", len(blobVector), string(content))
+		}
+		err = bw.Close()
+		if err != nil {
+			t.Errorf("localblobstore.Close %d:%s failed: %v", len(blobVector), string(content), err)
+		}
+		if !bw.IsFinalized() {
+			t.Errorf("localblobstore.IsFinalized %d:%s got true, expected false", len(blobVector), string(content))
+		}
+		if bw.Size() != int64(len(content)) {
+			t.Errorf("localblobstore.Size %d:%s after finalization got %d, expected %d", len(blobVector), string(content), bw.Size(), len(content))
+		}
+		if bw.Name() != blobName {
+			t.Errorf("localblobstore %d:%s name changed when finalized was %s now %s", len(blobVector), string(content), blobName, bw.Name())
+		}
+		hasher := md5.New()
+		hasher.Write(content)
+		if bytes.Compare(bw.Hash(), hasher.Sum(nil)) != 0 {
+			t.Errorf("localblobstore %d:%s BlobWriter.Hash got %v, expected %v", len(blobVector), string(content), bw.Hash(), hasher.Sum(nil))
+		}
+	}
+
+	return append(blobVector,
+		testBlob{
+			content:  content,
+			blobName: blobName,
+		})
+}
+
+// readBlob() returns a substring of the content of the blob named blobName in bs.
+// The return values are:
+// - the "size" bytes from the content, starting at the given "offset",
+//   measured from "whence" (as defined by io.Seeker.Seek).
+// - the position to which BlobBeader seeks to,
+// - the md5 hash of the bytes read, and
+// - the md5 hash of the bytes of the blob, as returned by BlobReader.Hash(),
+// - and error.
+func readBlob(ctx *context.T, bs localblobstore.BlobStore, blobName string,
+	size int64, offset int64, whence int) (content []byte, pos int64, hash []byte, fullHash []byte, err error) {
+
+	var br localblobstore.BlobReader
+	hasher := md5.New()
+	br, err = bs.NewBlobReader(ctx, blobName)
+	if err == nil {
+		buf := make([]byte, 8192, 8192)
+		fullHash = br.Hash()
+		pos, err = br.Seek(offset, whence)
+		if err == nil {
+			var n int
+			first := true // Read at least once, to test reading zero bytes.
+			for err == nil && (size == -1 || int64(len(content)) < size || first) {
+				// Read just what was asked for.
+				var toRead []byte = buf
+				if size >= 0 && int(size)-len(content) < len(buf) {
+					toRead = buf[0 : int(size)-len(content)]
+				}
+				n, err = br.Read(toRead)
+				hasher.Write(toRead[0:n])
+				if size >= 0 && int64(len(content)+n) > size {
+					n = int(size) - len(content)
+				}
+				content = append(content, toRead[0:n]...)
+				first = false
+			}
+		}
+		br.Close()
+	}
+	return content, pos, hasher.Sum(nil), fullHash, err
+}
+
+// checkWrittenBlobsAreReadable() checks that the blobs in blobVector[] can be
+// read, and that they contain the appropriate data.
+func checkWrittenBlobsAreReadable(t *testing.T, ctx *context.T, bs localblobstore.BlobStore, blobVector []testBlob) {
+	for i := range blobVector {
+		var size int64
+		data := blobVector[i].content
+		dataLen := int64(len(data))
+		blobName := blobVector[i].blobName
+		for size = -1; size != dataLen+1; size++ {
+			var offset int64
+			for offset = -dataLen - 1; offset != dataLen+1; offset++ {
+				for whence := -1; whence != 4; whence++ {
+					content, pos, hash, fullHash, err := readBlob(ctx, bs, blobName, size, offset, whence)
+
+					// Compute expected seek position.
+					expectedPos := offset
+					if whence == 2 {
+						expectedPos += dataLen
+					}
+
+					// Computed expected size.
+					expectedSize := size
+					if expectedSize == -1 || expectedPos+expectedSize > dataLen {
+						expectedSize = dataLen - expectedPos
+					}
+
+					// Check that reads behave as expected.
+					if (whence == -1 || whence == 3) &&
+						verror.ErrorID(err) == "v.io/syncbase/x/ref/services/syncbase/localblobstore/fs_cablobstore.errBadSeekWhence" {
+						// Expected error from bad "whence" value.
+					} else if expectedPos < 0 &&
+						verror.ErrorID(err) == "v.io/syncbase/x/ref/services/syncbase/localblobstore/fs_cablobstore.errNegativeSeekPosition" {
+						// Expected error from negative Seek position.
+					} else if expectedPos > dataLen &&
+						verror.ErrorID(err) == "v.io/syncbase/x/ref/services/syncbase/localblobstore/fs_cablobstore.errIllegalPositionForRead" {
+						// Expected error from too high a Seek position.
+					} else if 0 <= expectedPos && expectedPos+expectedSize <= int64(len(data)) &&
+						bytes.Compare(data[expectedPos:expectedPos+expectedSize], content) == 0 && err == io.EOF &&
+						pos == expectedPos && expectedPos+expectedSize == dataLen {
+						// Expected success with EOF.
+					} else if 0 <= expectedPos && expectedPos+expectedSize <= int64(len(data)) &&
+						bytes.Compare(data[expectedPos:expectedPos+expectedSize], content) == 0 && err == nil &&
+						pos == expectedPos && expectedPos+expectedSize != dataLen {
+						if pos == 0 && size == -1 && bytes.Compare(hash, fullHash) != 0 {
+							t.Errorf("localblobstore read test on %q size %d offset %d whence %d; got hash %v, expected %v  (blob is %q)",
+								string(data), size, offset, whence,
+								hash, fullHash, blobName)
+						} // Else expected success without EOF.
+					} else {
+						t.Errorf("localblobstore read test on %q size %d offset %d whence %d yields %q pos %d %v   (blob is %q)",
+							string(data), size, offset, whence,
+							content, pos, err, blobName)
+					}
+				}
+			}
+		}
+	}
+}
+
+// checkAllBlobs() checks all the blobs in bs to ensure they correspond to
+// those in blobVector[].
+func checkAllBlobs(t *testing.T, ctx *context.T, bs localblobstore.BlobStore, blobVector []testBlob, testDirName string) {
+	blobCount := 0
+	iterator := bs.ListBlobIds(ctx)
+	for iterator.Advance() {
+		fileName := iterator.Value()
+		i := 0
+		for ; i != len(blobVector) && fileName != blobVector[i].blobName; i++ {
+		}
+		if i == len(blobVector) {
+			t.Errorf("localblobstore.ListBlobIds found unexpected file %s", fileName)
+		} else {
+			content, pos, hash, fullHash, err := readBlob(ctx, bs, fileName, -1, 0, 0)
+			if err != nil && err != io.EOF {
+				t.Errorf("localblobstore.ListCAIds can't read %q: %v", filepath.Join(testDirName, fileName), err)
+			} else if bytes.Compare(blobVector[i].content, content) != 0 {
+				t.Errorf("localblobstore.ListCAIds found unexpected blob content: %q, contains %q, expected %q",
+					filepath.Join(testDirName, fileName), content, string(blobVector[i].content))
+			} else if pos != 0 {
+				t.Errorf("localblobstore.ListCAIds Seek on %q returned %d instead of 0",
+					filepath.Join(testDirName, fileName), pos)
+			}
+			if bytes.Compare(hash, fullHash) != 0 {
+				t.Errorf("localblobstore.ListCAIds read on %q; got hash %v, expected %v",
+					fileName, hash, fullHash)
+			}
+		}
+		blobCount++
+	}
+	if iterator.Err() != nil {
+		t.Errorf("localblobstore.ListBlobIds iteration failed: %v", iterator.Err())
+	}
+	if blobCount != len(blobVector) {
+		t.Errorf("localblobstore.ListBlobIds iteration expected 4 files, got %d", blobCount)
+	}
+}
+
+// checkFragments() checks all the fragments in bs to ensure they
+// correspond to those fragmentMap[], iff testDirName is non-empty.
+func checkFragments(t *testing.T, ctx *context.T, bs localblobstore.BlobStore, fragmentMap map[string]bool, testDirName string) {
+	if testDirName != "" {
+		caCount := 0
+		iterator := bs.ListCAIds(ctx)
+		for iterator.Advance() {
+			fileName := iterator.Value()
+			content, err := ioutil.ReadFile(filepath.Join(testDirName, fileName))
+			if err != nil && err != io.EOF {
+				t.Errorf("localblobstore.ListCAIds can't read %q: %v", filepath.Join(testDirName, fileName), err)
+			} else if !fragmentMap[string(content)] {
+				t.Errorf("localblobstore.ListCAIds found unexpected fragment entry: %q, contains %q", filepath.Join(testDirName, fileName), content)
+			} else {
+				hasher := md5.New()
+				hasher.Write(content)
+				hash := hasher.Sum(nil)
+				nameFromContent := filepath.Join("cas",
+					fmt.Sprintf("%02x", hash[0]),
+					fmt.Sprintf("%02x", hash[1]),
+					fmt.Sprintf("%02x", hash[2]),
+					fmt.Sprintf("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
+						hash[3],
+						hash[4], hash[5], hash[6], hash[7],
+						hash[8], hash[9], hash[10], hash[11],
+						hash[12], hash[13], hash[14], hash[15]))
+				if nameFromContent != fileName {
+					t.Errorf("localblobstore.ListCAIds hash of fragment: got %q, expected %q (content=%s)", nameFromContent, fileName, string(content))
+				}
+			}
+			caCount++
+		}
+		if iterator.Err() != nil {
+			t.Errorf("localblobstore.ListCAIds iteration failed: %v", iterator.Err())
+		}
+		if caCount != len(fragmentMap) {
+			t.Errorf("localblobstore.ListCAIds iteration expected %d files, got %d", len(fragmentMap), caCount)
+		}
+	}
+}
+
+// AddRetrieveAndDelete() tests adding, retrieving, and deleting blobs from a
+// blobstore bs.  One can't retrieve or delete something that hasn't been
+// created, so it's all done in one routine.    If testDirName is non-empty,
+// the blobstore is assumed to be accessible in the file system, and its
+// files are checked.
+func AddRetrieveAndDelete(t *testing.T, ctx *context.T, bs localblobstore.BlobStore, testDirName string) {
+	var err error
+
+	// Check that there are no files in the blobstore we were given.
+	iterator := bs.ListBlobIds(ctx)
+	for iterator.Advance() {
+		fileName := iterator.Value()
+		t.Errorf("unexpected file %q\n", fileName)
+	}
+	if iterator.Err() != nil {
+		t.Errorf("localblobstore.ListBlobIds iteration failed: %v", iterator.Err())
+	}
+
+	// Create the strings:  "wom", "bat", "wombat", "batwom", "atwo", "atwoatwoombatatwo".
+	womData := []byte("wom")
+	batData := []byte("bat")
+	wombatData := []byte("wombat")
+	batwomData := []byte("batwom")
+	atwoData := []byte("atwo")
+	atwoatwoombatatwoData := []byte("atwoatwoombatatwo")
+
+	// fragmentMap will have an entry per content-addressed fragment.
+	fragmentMap := make(map[string]bool)
+
+	// Create the blobs, by various means.
+
+	var blobVector []testBlob // Accumulate the blobs we create here.
+
+	blobVector = writeBlob(t, ctx, bs, blobVector,
+		womData, false,
+		blobOrBlockOrFile{block: womData})
+	womName := blobVector[len(blobVector)-1].blobName
+	fragmentMap[string(womData)] = true
+
+	blobVector = writeBlob(t, ctx, bs, blobVector,
+		batData, false,
+		blobOrBlockOrFile{block: batData})
+	batName := blobVector[len(blobVector)-1].blobName
+	fragmentMap[string(batData)] = true
+
+	blobVector = writeBlob(t, ctx, bs, blobVector,
+		wombatData, false,
+		blobOrBlockOrFile{block: wombatData})
+	firstWombatName := blobVector[len(blobVector)-1].blobName
+	fragmentMap[string(wombatData)] = true
+
+	blobVector = writeBlob(t, ctx, bs, blobVector,
+		wombatData, true,
+		blobOrBlockOrFile{block: womData},
+		blobOrBlockOrFile{block: batData})
+
+	blobVector = writeBlob(t, ctx, bs, blobVector,
+		wombatData, false,
+		blobOrBlockOrFile{
+			blob:   firstWombatName,
+			size:   -1,
+			offset: 0})
+
+	blobVector = writeBlob(t, ctx, bs, blobVector,
+		wombatData, false,
+		blobOrBlockOrFile{
+			blob:   firstWombatName,
+			size:   6,
+			offset: 0})
+
+	blobVector = writeBlob(t, ctx, bs, blobVector,
+		batwomData, false,
+		blobOrBlockOrFile{
+			blob:   firstWombatName,
+			size:   3,
+			offset: 3},
+		blobOrBlockOrFile{
+			blob:   firstWombatName,
+			size:   3,
+			offset: 0})
+	batwomName := blobVector[len(blobVector)-1].blobName
+
+	blobVector = writeBlob(t, ctx, bs, blobVector,
+		atwoData, false,
+		blobOrBlockOrFile{
+			blob:   batwomName,
+			size:   4,
+			offset: 1})
+	atwoName := blobVector[len(blobVector)-1].blobName
+
+	blobVector = writeBlob(t, ctx, bs, blobVector,
+		atwoatwoombatatwoData, true,
+		blobOrBlockOrFile{
+			blob:   atwoName,
+			size:   -1,
+			offset: 0},
+		blobOrBlockOrFile{
+			blob:   atwoName,
+			size:   4,
+			offset: 0},
+		blobOrBlockOrFile{
+			blob:   firstWombatName,
+			size:   -1,
+			offset: 1},
+		blobOrBlockOrFile{
+			blob:   batName,
+			size:   -1,
+			offset: 1},
+		blobOrBlockOrFile{
+			blob:   womName,
+			size:   2,
+			offset: 0})
+	atwoatwoombatatwoName := blobVector[len(blobVector)-1].blobName
+
+	// -------------------------------------------------
+	// Check that the state is as we expect.
+	checkWrittenBlobsAreReadable(t, ctx, bs, blobVector)
+	checkAllBlobs(t, ctx, bs, blobVector, testDirName)
+	checkFragments(t, ctx, bs, fragmentMap, testDirName)
+
+	// -------------------------------------------------
+	// Nothing should change if we garbage collect.
+	bs.GC(ctx)
+	checkWrittenBlobsAreReadable(t, ctx, bs, blobVector)
+	checkAllBlobs(t, ctx, bs, blobVector, testDirName)
+	checkFragments(t, ctx, bs, fragmentMap, testDirName)
+
+	// -------------------------------------------------
+	// Ensure that deleting non-existent blobs fails.
+	err = bs.DeleteBlob(ctx, "../../../../etc/passwd")
+	if verror.ErrorID(err) != "v.io/syncbase/x/ref/services/syncbase/localblobstore/fs_cablobstore.errInvalidBlobName" {
+		t.Errorf("DeleteBlob attempted to delete a bogus blob name")
+	}
+	err = bs.DeleteBlob(ctx, "foo/00/00/00/00000000000000000000000000")
+	if verror.ErrorID(err) != "v.io/syncbase/x/ref/services/syncbase/localblobstore/fs_cablobstore.errInvalidBlobName" {
+		t.Errorf("DeleteBlob attempted to delete a bogus blob name")
+	}
+
+	// -------------------------------------------------
+	// Delete a blob.
+	err = bs.DeleteBlob(ctx, batName)
+	if err != nil {
+		t.Errorf("DeleteBlob failed to delete blob %q: %v", batName, err)
+	}
+	blobVector = removeBlobFromBlobVector(blobVector, batName)
+
+	// -------------------------------------------------
+	// Check that the state is as we expect.
+	checkWrittenBlobsAreReadable(t, ctx, bs, blobVector)
+	checkAllBlobs(t, ctx, bs, blobVector, testDirName)
+	checkFragments(t, ctx, bs, fragmentMap, testDirName)
+
+	// -------------------------------------------------
+	// Nothing should change if we garbage collect.
+	bs.GC(ctx)
+	checkWrittenBlobsAreReadable(t, ctx, bs, blobVector)
+	checkAllBlobs(t, ctx, bs, blobVector, testDirName)
+	checkFragments(t, ctx, bs, fragmentMap, testDirName)
+
+	// -------------------------------------------------
+	// Open a BlobReader on a blob we're about to delete,
+	// so its fragments won't be garbage collected.
+
+	var br localblobstore.BlobReader
+	br, err = bs.NewBlobReader(ctx, atwoatwoombatatwoName)
+	if err != nil {
+		t.Errorf("NewBlobReader failed in blob %q: %v", atwoatwoombatatwoName, err)
+	}
+
+	// -------------------------------------------------
+	// Delete a blob.  This should be the last on-disc reference to the
+	// content-addressed fragment "bat", but the fragment won't be deleted
+	// until close the reader and garbage collect.
+	err = bs.DeleteBlob(ctx, atwoatwoombatatwoName)
+	if err != nil {
+		t.Errorf("DeleteBlob failed to delete blob %q: %v", atwoatwoombatatwoName, err)
+	}
+	blobVector = removeBlobFromBlobVector(blobVector, atwoatwoombatatwoName)
+
+	// -------------------------------------------------
+	// Check that the state is as we expect.
+	checkWrittenBlobsAreReadable(t, ctx, bs, blobVector)
+	checkAllBlobs(t, ctx, bs, blobVector, testDirName)
+	checkFragments(t, ctx, bs, fragmentMap, testDirName)
+
+	// -------------------------------------------------
+	// Garbage collection should change nothing; the fragment involved
+	// is still referenced from the open reader *br.
+	bs.GC(ctx)
+	checkWrittenBlobsAreReadable(t, ctx, bs, blobVector)
+	checkAllBlobs(t, ctx, bs, blobVector, testDirName)
+	checkFragments(t, ctx, bs, fragmentMap, testDirName)
+
+	// -------------------------------------------------
+
+	// Close the open BlobReader and garbage collect.
+	err = br.Close()
+	if err != nil {
+		t.Errorf("BlobReader.Close failed on blob %q: %v", atwoatwoombatatwoName, err)
+	}
+	delete(fragmentMap, string(batData))
+
+	bs.GC(ctx)
+	checkWrittenBlobsAreReadable(t, ctx, bs, blobVector)
+	checkAllBlobs(t, ctx, bs, blobVector, testDirName)
+	checkFragments(t, ctx, bs, fragmentMap, testDirName)
+
+	// -------------------------------------------------
+	// Delete all blobs.
+	for len(blobVector) != 0 {
+		err = bs.DeleteBlob(ctx, blobVector[0].blobName)
+		if err != nil {
+			t.Errorf("DeleteBlob failed to delete blob %q: %v", blobVector[0].blobName, err)
+		}
+		blobVector = removeBlobFromBlobVector(blobVector, blobVector[0].blobName)
+	}
+
+	// -------------------------------------------------
+	// Check that the state is as we expect.
+	checkWrittenBlobsAreReadable(t, ctx, bs, blobVector)
+	checkAllBlobs(t, ctx, bs, blobVector, testDirName)
+	checkFragments(t, ctx, bs, fragmentMap, testDirName)
+
+	// -------------------------------------------------
+	// The remaining fragments should be removed when we garbage collect.
+	for frag := range fragmentMap {
+		delete(fragmentMap, frag)
+	}
+	bs.GC(ctx)
+	checkWrittenBlobsAreReadable(t, ctx, bs, blobVector)
+	checkAllBlobs(t, ctx, bs, blobVector, testDirName)
+	checkFragments(t, ctx, bs, fragmentMap, testDirName)
+}
+
+// writeBlobFromReader() writes the contents of rd to blobstore bs, as blob
+// "name", or picks a name name if "name" is empty.  It returns the name of the
+// blob.  Errors cause the test to terminate.  Error messages contain the
+// "callSite" value to allow the test to tell which call site is which.
+func writeBlobFromReader(t *testing.T, ctx *context.T, bs localblobstore.BlobStore, name string, rd io.Reader, callSite int) string {
+	var err error
+	var bw localblobstore.BlobWriter
+	if bw, err = bs.NewBlobWriter(ctx, name); err != nil {
+		t.Fatalf("callSite %d: NewBlobWriter failed: %v", callSite, err)
+	}
+	blobName := bw.Name()
+	buf := make([]byte, 8192) // buffer for data read from rd.
+	for i := 0; err == nil; i++ {
+		var n int
+		if n, err = rd.Read(buf); err != nil && err != io.EOF {
+			t.Fatalf("callSite %d: unexpected error from reader: %v", callSite, err)
+		}
+		if n > 0 {
+			if err = bw.AppendFragment(localblobstore.BlockOrFile{Block: buf[:n]}); err != nil {
+				t.Fatalf("callSite %d: BlobWriter.AppendFragment failed: %v", callSite, err)
+			}
+			// Every so often, close without finalizing, and reopen.
+			if (i % 7) == 0 {
+				if err = bw.CloseWithoutFinalize(); err != nil {
+					t.Fatalf("callSite %d: BlobWriter.CloseWithoutFinalize failed: %v", callSite, err)
+				}
+				if bw, err = bs.ResumeBlobWriter(ctx, blobName); err != nil {
+					t.Fatalf("callSite %d: ResumeBlobWriter %q failed: %v", callSite, blobName, err)
+				}
+			}
+		}
+	}
+	if err = bw.Close(); err != nil {
+		t.Fatalf("callSite %d: BlobWriter.Close failed: %v", callSite, err)
+	}
+	return blobName
+}
+
+// checkBlobAgainstReader() verifies that the blob blobName has the same bytes as the reader rd.
+// Errors cause the test to terminate.  Error messages contain the
+// "callSite" value to allow the test to tell which call site is which.
+func checkBlobAgainstReader(t *testing.T, ctx *context.T, bs localblobstore.BlobStore, blobName string, rd io.Reader, callSite int) {
+	// Open a reader on the blob.
+	var blob_rd io.Reader
+	var blob_err error
+	if blob_rd, blob_err = bs.NewBlobReader(ctx, blobName); blob_err != nil {
+		t.Fatalf("callSite %d: NewBlobReader on %q failed: %v", callSite, blobName, blob_err)
+	}
+
+	// Variables for reading the two streams, indexed by "reader" and "blob".
+	type stream struct {
+		name string
+		rd   io.Reader // Reader for this stream
+		buf  []byte    // buffer for data
+		i    int       // bytes processed within current buffer
+		n    int       // valid bytes in current buffer
+		err  error     // error, or nil
+	}
+
+	s := [2]stream{
+		{name: "reader", rd: rd, buf: make([]byte, 8192)},
+		{name: blobName, rd: blob_rd, buf: make([]byte, 8192)},
+	}
+
+	// Descriptive names for the two elements of s, when we aren't treating them the same.
+	reader := &s[0]
+	blob := &s[1]
+
+	var pos int // position within file, for error reporting.
+
+	for x := 0; x != 2; x++ {
+		s[x].n, s[x].err = s[x].rd.Read(s[x].buf)
+		s[x].i = 0
+	}
+	for blob.n != 0 && reader.n != 0 {
+		for reader.i != reader.n && blob.i != blob.n && reader.buf[reader.i] == blob.buf[blob.i] {
+			pos++
+			blob.i++
+			reader.i++
+		}
+		if reader.i != reader.n && blob.i != blob.n {
+			t.Fatalf("callSite %d: BlobStore %q: BlobReader on blob %q and rd reader generated different bytes at position %d: 0x%x vs 0x%x",
+				callSite, bs.Root(), blobName, pos, reader.buf[reader.i], blob.buf[blob.i])
+		}
+		for x := 0; x != 2; x++ { // read more data from each reader, if needed
+			if s[x].i == s[x].n {
+				s[x].i = 0
+				s[x].n = 0
+				if s[x].err == nil {
+					s[x].n, s[x].err = s[x].rd.Read(s[x].buf)
+				}
+			}
+		}
+	}
+	for x := 0; x != 2; x++ {
+		if s[x].err != io.EOF {
+			t.Fatalf("callSite %d: %s got error %v", callSite, s[x].name, s[x].err)
+		}
+		if s[x].n != 0 {
+			t.Fatalf("callSite %d: %s is longer than %s", callSite, s[x].name, s[1-x].name)
+		}
+	}
+}
+
+// checkBlobAgainstReader() verifies that the blob blobName has the same chunks
+// (according to BlobChunkStream) as a chunker applied to the reader rd.
+// Errors cause the test to terminate.  Error messages contain the
+// "callSite" value to allow the test to tell which call site is which.
+func checkBlobChunksAgainstReader(t *testing.T, ctx *context.T, bs localblobstore.BlobStore, blobName string, rd io.Reader, callSite int) {
+	buf := make([]byte, 8192) // buffer used to hold data from the chunk stream from rd.
+	rawChunks := chunker.NewStream(ctx, &chunker.DefaultParam, rd)
+	cs := bs.BlobChunkStream(ctx, blobName)
+	pos := 0 // byte position within the blob, to be retported in error messages
+	i := 0   // chunk index, to be reported in error messages
+	rawMore, more := rawChunks.Advance(), cs.Advance()
+	for rawMore && more {
+		c := rawChunks.Value()
+		rawChunk := md5.Sum(rawChunks.Value())
+		chunk := cs.Value(buf)
+		if bytes.Compare(rawChunk[:], chunk) != 0 {
+			t.Errorf("raw random stream and chunk record for blob %q have different chunk %d:\n\t%v\nvs\n\t%v\n\tpos %d\n\tlen %d\n\tc %v",
+				blobName, i, rawChunk, chunk, pos, len(c), c)
+		}
+		pos += len(c)
+		i++
+		rawMore, more = rawChunks.Advance(), cs.Advance()
+	}
+	if rawMore {
+		t.Fatalf("callSite %d: blob %q has fewer chunks than raw stream", callSite, blobName)
+	}
+	if more {
+		t.Fatalf("callSite %d: blob %q has more chunks than raw stream", callSite, blobName)
+	}
+	if rawChunks.Err() != nil {
+		t.Fatalf("callSite %d: error reading raw chunk stream: %v", callSite, rawChunks.Err())
+	}
+	if cs.Err() != nil {
+		t.Fatalf("callSite %d: error reading chunk stream for blob %q; %v", callSite, blobName, cs.Err())
+	}
+}
+
+// WriteViaChunks() tests that a large blob in one blob store can be transmitted
+// to another incrementally, without transferring chunks already in the other blob store.
+func WriteViaChunks(t *testing.T, ctx *context.T, bs [2]localblobstore.BlobStore) {
+	// The original blob will be a megabyte.
+	totalLength := 1024 * 1024
+
+	// Write a random blob to bs[0], using seed 1, then check that the
+	// bytes and chunk we get from the blob just written are the same as
+	// those obtained from an identical byte stream.
+	blob0 := writeBlobFromReader(t, ctx, bs[0], "", NewRandReader(1, totalLength, 0, io.EOF), 0)
+	checkBlobAgainstReader(t, ctx, bs[0], blob0, NewRandReader(1, totalLength, 0, io.EOF), 1)
+	checkBlobChunksAgainstReader(t, ctx, bs[0], blob0, NewRandReader(1, totalLength, 0, io.EOF), 2)
+
+	// ---------------------------------------------------------------------
+	// Write into bs[1] a blob that is similar to blob0, but not identical, and check it as above.
+	insertionInterval := 20 * 1024
+	blob1 := writeBlobFromReader(t, ctx, bs[1], "", NewRandReader(1, totalLength, insertionInterval, io.EOF), 3)
+	checkBlobAgainstReader(t, ctx, bs[1], blob1, NewRandReader(1, totalLength, insertionInterval, io.EOF), 4)
+	checkBlobChunksAgainstReader(t, ctx, bs[1], blob1, NewRandReader(1, totalLength, insertionInterval, io.EOF), 5)
+
+	// ---------------------------------------------------------------------
+	// Count the number of chunks, and the number of steps in the recipe
+	// for copying blob0 from bs[0] to bs[1].  We expect the that the
+	// former to be significantly bigger than the latter, because the
+	// insertionInterval is significantly larger than the expected chunk
+	// size.
+	cs := bs[0].BlobChunkStream(ctx, blob0)          // Stream of chunks in blob0
+	rs := bs[1].RecipeStreamFromChunkStream(ctx, cs) // Recipe from bs[1]
+
+	recipeLen := 0
+	chunkCount := 0
+	for rs.Advance() {
+		step := rs.Value()
+		if step.Chunk != nil {
+			chunkCount++
+		}
+		recipeLen++
+	}
+	if rs.Err() != nil {
+		t.Fatalf("RecipeStream got error: %v", rs.Err())
+	}
+
+	cs = bs[0].BlobChunkStream(ctx, blob0) // Get the original chunk count.
+	origChunkCount := 0
+	for cs.Advance() {
+		origChunkCount++
+	}
+	if cs.Err() != nil {
+		t.Fatalf("ChunkStream got error: %v", cs.Err())
+	}
+	if origChunkCount < chunkCount*5 {
+		t.Errorf("expected fewer chunks in repipe: recipeLen %d  chunkCount %d  origChunkCount %d\n",
+			recipeLen, chunkCount, origChunkCount)
+	}
+
+	// Copy blob0 from bs[0] to bs[1], using chunks from blob1 (already in bs[1]) where possible.
+	cs = bs[0].BlobChunkStream(ctx, blob0) // Stream of chunks in blob0
+	// In a real application, at this point the stream cs would be sent to the device with bs[1].
+	rs = bs[1].RecipeStreamFromChunkStream(ctx, cs) // Recipe from bs[1]
+	// Write blob with known blob name.
+	var bw localblobstore.BlobWriter
+	var err error
+	if bw, err = bs[1].NewBlobWriter(ctx, blob0); err != nil {
+		t.Fatalf("bs[1].NewBlobWriter yields error: %v", err)
+	}
+	var br localblobstore.BlobReader
+	const maxFragment = 1024 * 1024
+	blocks := make([]localblobstore.BlockOrFile, maxFragment/chunker.DefaultParam.MinChunk)
+	for gotStep := rs.Advance(); gotStep; {
+		step := rs.Value()
+		if step.Chunk == nil {
+			// This part of the blob can be read from an existing blob locally (at bs[1]).
+			if err = bw.AppendBlob(step.Blob, step.Size, step.Offset); err != nil {
+				t.Fatalf("AppendBlob(%v) yields error: %v", step, err)
+			}
+			gotStep = rs.Advance()
+		} else {
+			var fragmentSize int64
+			// In a real application, the sequence of chunk hashes
+			// in recipe steps would be communicated back to bs[0],
+			// which then finds the associated chunks.
+			var b int
+			for b = 0; gotStep && step.Chunk != nil && fragmentSize+chunker.DefaultParam.MaxChunk < maxFragment; b++ {
+				var loc localblobstore.Location
+				if loc, err = bs[0].LookupChunk(ctx, step.Chunk); err != nil {
+					t.Fatalf("bs[0] unexpectedly does not have chunk %v", step.Chunk)
+				}
+				if br != nil && br.Name() != loc.BlobName { // Close blob if we need a different one.
+					if err = br.Close(); err != nil {
+						t.Fatalf("unexpected error in BlobReader.Close(): %v", err)
+					}
+					br = nil
+				}
+				if br == nil { // Open blob if needed.
+					if br, err = bs[0].NewBlobReader(ctx, loc.BlobName); err != nil {
+						t.Fatalf("unexpected failure to create BlobReader on %q: %v", loc.BlobName, err)
+					}
+				}
+				if loc.Size > chunker.DefaultParam.MaxChunk {
+					t.Fatalf("chunk exceeds max chunk size: %d vs %d", loc.Size, chunker.DefaultParam.MaxChunk)
+				}
+				fragmentSize += loc.Size
+				if blocks[b].Block == nil {
+					blocks[b].Block = make([]byte, chunker.DefaultParam.MaxChunk)
+				}
+				blocks[b].Block = blocks[b].Block[:loc.Size]
+				var i int
+				var n int64
+				for n = int64(0); n != loc.Size; n += int64(i) {
+					if i, err = br.ReadAt(blocks[b].Block[n:loc.Size], n+loc.Offset); err != nil && err != io.EOF {
+						t.Fatalf("ReadAt on %q failed: %v", br.Name(), err)
+					}
+				}
+				if gotStep = rs.Advance(); gotStep {
+					step = rs.Value()
+				}
+			}
+			if err = bw.AppendFragment(blocks[:b]...); err != nil {
+				t.Fatalf("AppendFragment on %q failed: %v", bw.Name(), err)
+			}
+		}
+	}
+	if err = bw.Close(); err != nil {
+		t.Fatalf("BlobWriter.Close on %q failed: %v", bw.Name(), err)
+	}
+
+	// Check that the transferred blob in bs[1] is the same as the original
+	// stream used to make the blob in bs[0].
+	checkBlobAgainstReader(t, ctx, bs[1], blob0, NewRandReader(1, totalLength, 0, io.EOF), 6)
+	checkBlobChunksAgainstReader(t, ctx, bs[1], blob0, NewRandReader(1, totalLength, 0, io.EOF), 7)
+}
+
+// checkBlobContent() checks that the named blob has the specified content.
+func checkBlobContent(t *testing.T, ctx *context.T, bs localblobstore.BlobStore, blobName string, content []byte) {
+	var err error
+	var br localblobstore.BlobReader
+	var data []byte
+	if br, err = bs.NewBlobReader(ctx, blobName); err != nil {
+		t.Fatalf("localblobstore.NewBlobReader failed: %v\n", err)
+	}
+	if data, err = ioutil.ReadAll(br); err != nil && err != io.EOF {
+		t.Fatalf("Read on br failed: %v\n", err)
+	}
+	if !bytes.Equal(data, content) {
+		t.Fatalf("Read on %q got %q, wanted %v\n", blobName, data, content)
+	}
+	if err = br.Close(); err != nil {
+		t.Fatalf("br.Close failed: %v\n", err)
+	}
+}
+
+// CreateAndResume() tests that it's possible to create a blob with
+// NewBlobWriter(), immediately close it, and then resume writing with
+// ResumeBlobWriter.  This test is called out because syncbase does this, and
+// it exposed a bug in the reader code, which could not cope with a request to
+// read starting at the very end of a file, thus returning no bytes.
+func CreateAndResume(t *testing.T, ctx *context.T, bs localblobstore.BlobStore) {
+	var err error
+
+	// Create an empty, unfinalized blob.
+	var bw localblobstore.BlobWriter
+	if bw, err = bs.NewBlobWriter(ctx, ""); err != nil {
+		t.Fatalf("localblobstore.NewBlobWriter failed: %v\n", err)
+	}
+	blobName := bw.Name()
+	if err = bw.CloseWithoutFinalize(); err != nil {
+		t.Fatalf("bw.CloseWithoutFinalize failed: %v\n", verror.DebugString(err))
+	}
+
+	checkBlobContent(t, ctx, bs, blobName, nil)
+
+	// Reopen the blob, but append no bytes (an empty byte vector).
+	if bw, err = bs.ResumeBlobWriter(ctx, blobName); err != nil {
+		t.Fatalf("localblobstore.ResumeBlobWriter failed: %v\n", err)
+	}
+	if err = bw.AppendFragment(localblobstore.BlockOrFile{Block: []byte("")}); err != nil {
+		t.Fatalf("bw.AppendFragment failed: %v", err)
+	}
+	if err = bw.CloseWithoutFinalize(); err != nil {
+		t.Fatalf("bw.Close failed: %v\n", err)
+	}
+
+	checkBlobContent(t, ctx, bs, blobName, nil)
+
+	// Reopen the blob, and append a non-empty sequence of bytes.
+	content := []byte("some content")
+	if bw, err = bs.ResumeBlobWriter(ctx, blobName); err != nil {
+		t.Fatalf("localblobstore.ResumeBlobWriter.Close failed: %v\n", err)
+	}
+	if err = bw.AppendFragment(localblobstore.BlockOrFile{Block: content}); err != nil {
+		t.Fatalf("bw.AppendFragment failed: %v", err)
+	}
+	if err = bw.Close(); err != nil {
+		t.Fatalf("bw.Close failed: %v\n", err)
+	}
+
+	checkBlobContent(t, ctx, bs, blobName, content)
+}
diff --git a/services/syncbase/localblobstore/localblobstore_testlib/randreader.go b/services/syncbase/localblobstore/localblobstore_testlib/randreader.go
new file mode 100644
index 0000000..85e32d3
--- /dev/null
+++ b/services/syncbase/localblobstore/localblobstore_testlib/randreader.go
@@ -0,0 +1,53 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package localblobstore_testlib
+
+import "math/rand"
+
+// A RandReader contains a pointer to a rand.Read, and a size limit.  Its
+// pointers implement the Read() method from io.Reader, which yields bytes
+// obtained from the random number generator.
+type RandReader struct {
+	rand           *rand.Rand // Source of random bytes.
+	pos            int        // Number of bytes read.
+	limit          int        // Max number of bytes that may be read.
+	insertInterval int        // If non-zero, number of bytes between insertions of zero bytes.
+	eofErr         error      // error to be returned at the end of the stream
+}
+
+// NewRandReader() returns a new RandReader with the specified seed and size limit.
+// It yields eofErr when the end of the stream is reached.
+// If insertInterval is non-zero, a zero byte is inserted into the stream every
+// insertInterval bytes, before resuming getting bytes from the random number
+// generator.
+func NewRandReader(seed int64, limit int, insertInterval int, eofErr error) *RandReader {
+	r := new(RandReader)
+	r.rand = rand.New(rand.NewSource(seed))
+	r.limit = limit
+	r.insertInterval = insertInterval
+	r.eofErr = eofErr
+	return r
+}
+
+// Read() implements the io.Reader Read() method for *RandReader.
+func (r *RandReader) Read(buf []byte) (n int, err error) {
+	// Generate bytes up to the end of the stream, or the end of the buffer.
+	max := r.limit - r.pos
+	if len(buf) < max {
+		max = len(buf)
+	}
+	for ; n != max; n++ {
+		if r.insertInterval == 0 || (r.pos%r.insertInterval) != 0 {
+			buf[n] = byte(r.rand.Int31n(256))
+		} else {
+			buf[n] = 0
+		}
+		r.pos++
+	}
+	if r.pos == r.limit {
+		err = r.eofErr
+	}
+	return n, err
+}
diff --git a/services/syncbase/localblobstore/localblobstore_transfer_test.go b/services/syncbase/localblobstore/localblobstore_transfer_test.go
new file mode 100644
index 0000000..b5378ec
--- /dev/null
+++ b/services/syncbase/localblobstore/localblobstore_transfer_test.go
@@ -0,0 +1,368 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Example code for transferring a blob from one device to another.
+// See the simulateResumption constant to choose whether to simulate a full
+// transfer or a resumed one.
+package localblobstore_test
+
+import "bytes"
+import "fmt"
+import "io"
+import "io/ioutil"
+import "math/rand"
+import "os"
+
+import "v.io/syncbase/x/ref/services/syncbase/localblobstore"
+import "v.io/syncbase/x/ref/services/syncbase/localblobstore/fs_cablobstore"
+import "v.io/v23/context"
+import "v.io/x/ref/test"
+import _ "v.io/x/ref/runtime/factories/generic"
+
+// simulateResumption tells the receiver whether to simulate having a partial
+// blob before blob transfer.
+const simulateResumption = true
+
+// createBlobStore() returns a new BlobStore, and the name of the directory
+// used to implement it.
+func createBlobStore(ctx *context.T) (bs localblobstore.BlobStore, dirName string) {
+	var err error
+	if dirName, err = ioutil.TempDir("", "localblobstore_transfer_test"); err != nil {
+		panic(err)
+	}
+	if bs, err = fs_cablobstore.Create(ctx, dirName); err != nil {
+		panic(err)
+	}
+	return bs, dirName
+}
+
+// createBlob writes a blob to bs of k 32kByte blocks drawn from a determinstic
+// but arbitrary random stream, starting at block offset within that stream.
+// Returns its name, which is "blob" if non-empty, and chosen arbitrarily otherwise.
+// The blob is finalized iff "complete" is true.
+func createBlob(ctx *context.T, bs localblobstore.BlobStore, blob string, complete bool, offset int, count int) string {
+	var bw localblobstore.BlobWriter
+	var err error
+	if bw, err = bs.NewBlobWriter(ctx, blob); err != nil {
+		panic(err)
+	}
+	blob = bw.Name()
+	var buffer [32 * 1024]byte
+	block := localblobstore.BlockOrFile{Block: buffer[:]}
+	r := rand.New(rand.NewSource(1)) // Always seed with 1 for repeatability.
+	for i := 0; i != offset+count; i++ {
+		for b := 0; b != len(buffer); b++ {
+			buffer[b] = byte(r.Int31n(256))
+		}
+		if i >= offset {
+			if err = bw.AppendFragment(block); err != nil {
+				panic(err)
+			}
+		}
+	}
+	if complete {
+		err = bw.Close()
+	} else {
+		err = bw.CloseWithoutFinalize()
+	}
+	if err != nil {
+		panic(err)
+	}
+	return blob
+}
+
+// A channelChunkStream turns a channel of chunk hashes into a ChunkStream.
+type channelChunkStream struct {
+	channel <-chan []byte
+	ok      bool
+	value   []byte
+}
+
+// newChannelChunkStream returns a ChunkStream, given a channel containing the
+// relevant chunk hashes.
+func newChannelChunkStream(ch <-chan []byte) localblobstore.ChunkStream {
+	return &channelChunkStream{channel: ch, ok: true}
+}
+
+// The following are the standard ChunkStream methods.
+func (cs *channelChunkStream) Advance() bool {
+	if cs.ok {
+		cs.value, cs.ok = <-cs.channel
+	}
+	return cs.ok
+}
+func (cs *channelChunkStream) Value(buf []byte) []byte { return cs.value }
+func (cs *channelChunkStream) Err() error              { return nil }
+func (cs *channelChunkStream) Cancel()                 {}
+
+// Example_blobTransfer() demonstrates how to transfer a blob incrementally
+// from one device's blob store to another.  In this code, the communication
+// between sender and receiver is modelled with Go channels.
+func Example_blobTransfer() {
+	ctx, shutdown := test.V23Init()
+	defer shutdown()
+
+	// ----------------------------------------------
+	// Channels used to send chunk hashes to receiver always end in
+	// ToSender or ToReceiver.
+	type blobData struct {
+		name     string
+		size     int64
+		checksum []byte
+	}
+	blobDataToReceiver := make(chan blobData)  // indicate basic data for blob
+	needChunksToSender := make(chan bool)      // indicate receiver does not have entire blob
+	chunkHashesToReceiver := make(chan []byte) // for initial trasfer of chunk hashes
+	chunkHashesToSender := make(chan []byte)   // to report which chunks receiver needs
+	chunksToReceiver := make(chan []byte)      // to report which chunks receiver needs
+
+	sDone := make(chan bool) // closed when sender done
+	rDone := make(chan bool) // closed when receiver done
+
+	// ----------------------------------------------
+	// The sender.
+	go func(ctx *context.T,
+		blobDataToReceiver chan<- blobData,
+		needChunksToSender <-chan bool,
+		chunkHashesToReceiver chan<- []byte,
+		chunkHashesToSender <-chan []byte,
+		chunksToReceiver chan<- []byte,
+		done chan<- bool) {
+
+		defer close(done)
+		var err error
+
+		bsS, bsSDir := createBlobStore(ctx)
+		defer os.RemoveAll(bsSDir)
+
+		blob := createBlob(ctx, bsS, "", true, 0, 32) // Create a 1M blob at the sender.
+
+		// 1. Send basic blob data to receiver.
+		var br localblobstore.BlobReader
+		if br, err = bsS.NewBlobReader(ctx, blob); err != nil {
+			panic(err)
+		}
+		blobDataToReceiver <- blobData{name: blob, size: br.Size(), checksum: br.Hash()}
+		br.Close()
+		close(blobDataToReceiver)
+
+		// 3. Get indication from receiver of whether it needs blob.
+		needChunks := <-needChunksToSender
+
+		if !needChunks { // Receiver has blob; done.
+			return
+		}
+
+		// 4. Send the chunk hashes to the receiver.  This proceeds concurrently
+		//    with the step below.
+		go func(ctx *context.T, blob string, chunkHashesToReceiver chan<- []byte) {
+			cs := bsS.BlobChunkStream(ctx, blob)
+			for cs.Advance() {
+				chunkHashesToReceiver <- cs.Value(nil)
+			}
+			if cs.Err() != nil {
+				panic(cs.Err())
+			}
+			close(chunkHashesToReceiver)
+		}(ctx, blob, chunkHashesToReceiver)
+
+		// 7. Get needed chunk hashes from receiver, find the relevant
+		//    data, and send it back to the receiver.
+		var cbr localblobstore.BlobReader // Cached read handle on most-recent-read blob, or nil
+		// Given chunk hash h from chunkHashesToSender, send chunk to chunksToReceiver.
+		for h := range chunkHashesToSender {
+			loc, err := bsS.LookupChunk(ctx, h)
+			for err == nil && (cbr == nil || cbr.Name() != loc.BlobName) {
+				if cbr != nil && cbr.Name() != loc.BlobName {
+					cbr.Close()
+					cbr = nil
+				}
+				if cbr == nil {
+					if cbr, err = bsS.NewBlobReader(ctx, loc.BlobName); err != nil {
+						bsS.GC(ctx) // A partially-deleted blob may be confusing things.
+						loc, err = bsS.LookupChunk(ctx, h)
+					}
+				}
+			}
+			var i int = 1
+			var n int64
+			buffer := make([]byte, loc.Size) // buffer for current chunk
+			for n = int64(0); n != loc.Size && i != 0 && err == nil; n += int64(i) {
+				if i, err = cbr.ReadAt(buffer[n:loc.Size], n+loc.Offset); err == io.EOF {
+					err = nil // EOF is expected
+				}
+			}
+			if n == loc.Size { // Got chunk.
+				chunksToReceiver <- buffer[:loc.Size]
+			}
+			if err != nil {
+				break
+			}
+		}
+		close(chunksToReceiver)
+		if cbr != nil {
+			cbr.Close()
+		}
+
+	}(ctx, blobDataToReceiver, needChunksToSender, chunkHashesToReceiver, chunkHashesToSender, chunksToReceiver, sDone)
+
+	// ----------------------------------------------
+	// The receiver.
+	go func(ctx *context.T,
+		blobDataToReceiver <-chan blobData,
+		needChunksToSender chan<- bool,
+		chunkHashesToReceiver <-chan []byte,
+		chunkHashesToSender chan<- []byte,
+		chunksToReceiver <-chan []byte,
+		done chan<- bool) {
+
+		defer close(done)
+		var err error
+
+		bsR, bsRDir := createBlobStore(ctx)
+		defer os.RemoveAll(bsRDir)
+
+		// 2. Receive basic blob data from sender.
+		blobInfo := <-blobDataToReceiver
+
+		if simulateResumption {
+			// Write a fraction of the (unfinalized) blob on the receiving side
+			// to check that the transfer process can resume a partial blob.
+			createBlob(ctx, bsR, blobInfo.name, false, 0, 10)
+		}
+
+		// 3. Tell sender whether the recevier already has the complete
+		//    blob.
+		needChunks := true
+		var br localblobstore.BlobReader
+		if br, err = bsR.NewBlobReader(ctx, blobInfo.name); err == nil {
+			if br.IsFinalized() {
+				if len(br.Hash()) == len(blobInfo.checksum) && bytes.Compare(br.Hash(), blobInfo.checksum) != 0 {
+					panic("receiver has a finalized blob with same name but different hash")
+				}
+				needChunks = false // The receiver already has the blob.
+			}
+			br.Close()
+		}
+		needChunksToSender <- needChunks
+		close(needChunksToSender)
+
+		if !needChunks { // Receiver has blob; done.
+			return
+		}
+
+		// 5. Receive the chunk hashes from the sender, and turn them
+		//    into a recipe.
+		cs := newChannelChunkStream(chunkHashesToReceiver)
+		rs := bsR.RecipeStreamFromChunkStream(ctx, cs)
+
+		// 6. The following thread sends the chunk hashes that the
+		//    receiver does not have to the sender.  It also makes
+		//    a duplicate of the stream on the channel rsCopy.  The
+		//    buffering in rsCopy allows the receiver to put several
+		//    chunks into a fragment.
+		rsCopy := make(chan localblobstore.RecipeStep, 100) // A buffered copy of the rs stream.
+		go func(ctx *context.T, rs localblobstore.RecipeStream, rsCopy chan<- localblobstore.RecipeStep, chunkHashesToSender chan<- []byte) {
+			for rs.Advance() {
+
+				step := rs.Value()
+				if step.Chunk != nil { // Data must be fetched from sender.
+					chunkHashesToSender <- step.Chunk
+				}
+				rsCopy <- step
+			}
+			close(chunkHashesToSender)
+			close(rsCopy)
+		}(ctx, rs, rsCopy, chunkHashesToSender)
+
+		// 8. The following thread splices the chunks from the sender
+		//    (on chunksToReceiver) into the recipe stream copy
+		//    (rsCopy) to generate a full recipe stream (rsFull) in
+		//    which chunks are actual data, rather than just hashes.
+		rsFull := make(chan localblobstore.RecipeStep) // A recipe stream containing chunk data, not just hashes.
+		go func(ctx *context.T, rsCopy <-chan localblobstore.RecipeStep, chunksToReceiver <-chan []byte, rsFull chan<- localblobstore.RecipeStep) {
+			var ok bool
+			for step := range rsCopy {
+				if step.Chunk != nil { // Data must be fetched from sender.
+					if step.Chunk, ok = <-chunksToReceiver; !ok {
+						break
+					}
+				}
+				rsFull <- step
+			}
+			close(rsFull)
+		}(ctx, rsCopy, chunksToReceiver, rsFull)
+
+		// 9. Write the blob using the recipe.
+		var chunksTransferred int
+		const fragmentThreshold = 1024 * 1024 // Try to write on-disc fragments fragments at least this big.
+		var ignoreBytes int64
+		var bw localblobstore.BlobWriter
+		if bw, err = bsR.ResumeBlobWriter(ctx, blobInfo.name); err != nil {
+			bw, err = bsR.NewBlobWriter(ctx, blobInfo.name)
+		} else {
+			ignoreBytes = bw.Size()
+		}
+		if err == nil {
+			var fragment []localblobstore.BlockOrFile
+			var fragmentSize int64
+			for step := range rsFull {
+				if step.Chunk == nil { // Data can be obtained from local blob.
+					if ignoreBytes >= step.Size { // Ignore chunks we already have.
+						ignoreBytes -= step.Size
+					} else {
+						err = bw.AppendBlob(step.Blob, step.Size-ignoreBytes, step.Offset+ignoreBytes)
+						ignoreBytes = 0
+					}
+				} else if ignoreBytes >= int64(len(step.Chunk)) { // Ignoer chunks we already have.
+					ignoreBytes -= int64(len(step.Chunk))
+				} else { // Data is from a chunk send by the sender.
+					chunksTransferred++
+					fragment = append(fragment, localblobstore.BlockOrFile{Block: step.Chunk[ignoreBytes:]})
+					fragmentSize += int64(len(step.Chunk)) - ignoreBytes
+					ignoreBytes = 0
+					if fragmentSize > fragmentThreshold {
+						err = bw.AppendFragment(fragment...)
+						fragment = fragment[:0]
+						fragmentSize = 0
+					}
+				}
+				if err != nil {
+					break
+				}
+			}
+			if err == nil && len(fragment) != 0 {
+				err = bw.AppendFragment(fragment...)
+			}
+			if err2 := bw.Close(); err == nil {
+				err = err2
+			}
+			if err != nil {
+				panic(err)
+			}
+		}
+
+		// 10. Verify that the blob was written correctly.
+		if br, err = bsR.NewBlobReader(ctx, blobInfo.name); err != nil {
+			panic(err)
+		}
+		if br.Size() != blobInfo.size {
+			panic("transferred blob has wrong size")
+		}
+		if len(br.Hash()) != len(blobInfo.checksum) || bytes.Compare(br.Hash(), blobInfo.checksum) != 0 {
+			panic("transferred blob has wrong checksum")
+		}
+		if err = br.Close(); err != nil {
+			panic(err)
+		}
+		fmt.Printf("%d chunks transferred\n", chunksTransferred)
+	}(ctx, blobDataToReceiver, needChunksToSender, chunkHashesToReceiver, chunkHashesToSender, chunksToReceiver, rDone)
+
+	// ----------------------------------------------
+	// Wait for sender and receiver to finish.
+	_ = <-sDone
+	_ = <-rDone
+
+	// Output: 635 chunks transferred
+}
diff --git a/services/syncbase/localblobstore/model.go b/services/syncbase/localblobstore/model.go
new file mode 100644
index 0000000..f51f455
--- /dev/null
+++ b/services/syncbase/localblobstore/model.go
@@ -0,0 +1,303 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package localblobstore is the interface to a local blob store.
+// Implementations include fs_cablobstore.
+//
+// Expected use
+// ============
+// These examples assume that bs, bsS (sender) and bsR (receiver) are blobstores.
+//
+// Writing blobs
+//      bw, err := bs.NewBlobWriter(ctx, "")  // For a new blob, implementation picks blob name.
+//      if err == nil {
+//		blobName := bw.Name()  // Get name the implementation picked.
+//   	  	... use bw.AppendFragment() to append data to the blob...
+//   	  	... and/or bw.AppendBlob() to append data that's in another existing blob...
+//        	err = bw.Close()
+//   	}
+//
+// Resume writing a blob that was partially written due to a crash (not yet finalized).
+//	bw, err := bs.ResumeBlobWriter(ctx, name)
+//	if err == nil {
+//		size := bw.Size() // The store has this many bytes from the blob.
+//		... write the remaining data using bwAppendFragment() and/or bw.AppendBlob()...
+//		err = bw.Close()
+//	}
+//
+// Reading blobs
+//	br, err := bs.NewBlobReader(ctx, name)
+//	if err == nil {
+//		... read bytes with br.ReadAt() or br.Read(), perhas with br.Seek()...
+//		err = br.Close()
+//	}
+//
+// Transferring blobs from one store to another:
+// See example in localblobstore_transfer_test.go
+// Summary:
+// - The sender sends the chunksum of the blob from BlobReader's Hash().
+// - The receiver checks whether it already has the blob, with the same
+//   checksum.
+// - If the receiver does not have the blob, the sender sends the list of chunk
+//   hashes in the blob using BlobChunkStream().
+// - The receiver uses RecipeStreamFromChunkStream() with the chunk hash stream
+//   from the sender, and tells the sender the chunk hashes of the chunks it
+//   needs.
+// - The sender uses LookupChunk() to find the data for each chunk the receiver
+//   needs, and sends it to the receiver.
+// - The receiver applies the recipe steps, with the actual chunkj data from
+//   the sender and its own local data.
+package localblobstore
+
+import "v.io/v23/context"
+
+// A BlobStore represents a simple, content-addressable store.
+type BlobStore interface {
+	// NewBlobReader() returns a pointer to a newly allocated BlobReader on
+	// the specified blobName.  BlobReaders should not be used concurrently
+	// by multiple threads.  Returned handles should be closed with
+	// Close().
+	NewBlobReader(ctx *context.T, blobName string) (br BlobReader, err error)
+
+	// NewBlobWriter() returns a pointer to a newly allocated BlobWriter on
+	// a newly created blob.  If "name" is non-empty, its is used to name
+	// the blob, and it must be in the format of a name returned by this
+	// interface (probably by another instance on another device).
+	// Otherwise, otherwise a new name is created, which can be found using
+	// the Name() method.  It is an error to attempt to overwrite a blob
+	// that already exists in this blob store.  BlobWriters should not be
+	// used concurrently by multiple threads.  The returned handle should
+	// be closed with either the Close() or CloseWithoutFinalize() method
+	// to avoid leaking file handles.
+	NewBlobWriter(ctx *context.T, name string) (bw BlobWriter, err error)
+
+	// ResumeBlobWriter() returns a pointer to a newly allocated BlobWriter on
+	// an old, but unfinalized blob name.
+	ResumeBlobWriter(ctx *context.T, blobName string) (bw BlobWriter, err error)
+
+	// DeleteBlob() deletes the named blob from the BlobStore.
+	DeleteBlob(ctx *context.T, blobName string) (err error)
+
+	// GC() removes old temp files and content-addressed blocks that are no
+	// longer referenced by any blob.  It may be called concurrently with
+	// other calls to GC(), and with uses of BlobReaders and BlobWriters.
+	GC(ctx *context.T) error
+
+	// BlobChunkStream() returns a ChunkStream that can be used to read the
+	// ordered list of content hashes of chunks in blob blobName.  It is
+	// expected that this list will be presented to
+	// RecipeStreamFromChunkStream() on another device, to create a recipe
+	// for transmitting the blob efficiently to that other device.
+	BlobChunkStream(ctx *context.T, blobName string) ChunkStream
+
+	// RecipeStreamFromChunkStream() returns a pointer to a RecipeStream
+	// that allows the client to iterate over each RecipeStep needed to
+	// create the blob formed by the chunks in chunkStream.  It is expected
+	// that this will be called on a receiving device, and be given a
+	// ChunkStream from a sending device, to yield a recipe for efficient
+	// chunk transfer.  RecipeStep values with non-nil Chunk fields need
+	// the chunk from the sender; once the data is returned it can be
+	// written with BlobWriter.AppendFragment().  Those with blob
+	// references can be written locally with BlobWriter.AppendBlob().
+	RecipeStreamFromChunkStream(ctx *context.T, chunkStream ChunkStream) RecipeStream
+
+	// LookupChunk() returns the location of a chunk with the specified chunk
+	// hash within the store.  It is expected that chunk hashes from
+	// RecipeStep entries from RecipeStreamFromChunkStream() will be mapped
+	// to blob Location values on the sender for transmission to the
+	// receiver.
+	LookupChunk(ctx *context.T, chunkHash []byte) (loc Location, err error)
+
+	// ListBlobIds() returns an iterator that can be used to enumerate the
+	// blobs in a BlobStore.  Expected use is:
+	//
+	//	iter := bs.ListBlobIds(ctx)
+	//	for iter.Advance() {
+	//	  // Process iter.Value() here.
+	//	}
+	//	if iter.Err() != nil {
+	//	  // The loop terminated early due to an error.
+	//	}
+	ListBlobIds(ctx *context.T) (iter Stream)
+
+	// ListCAIds() returns an iterator that can be used to enumerate the
+	// content-addressable fragments in a BlobStore.  Expected use is:
+	//
+	//	iter := bs.ListCAIds(ctx)
+	//	for iter.Advance() {
+	//	  // Process iter.Value() here.
+	//	}
+	//	if iter.Err() != nil {
+	//	  // The loop terminated early due to an error.
+	//	}
+	ListCAIds(ctx *context.T) (iter Stream)
+
+	// Root() returns the name of the root directory where the BlobStore is stored.
+	Root() string
+
+	// Close() closes the BlobStore.
+	Close() error
+}
+
+// A Location describes chunk's location within a blob.  It is returned by
+// BlobStore.LookupChunk().
+type Location struct {
+	BlobName string // name of blob
+	Offset   int64  // byte offset of chunk within blob
+	Size     int64  // size of chunk
+}
+
+// A BlobReader allows a blob to be read using the standard ReadAt(), Read(),
+// and Seek() calls.  A BlobReader can be created with NewBlobReader(), and
+// should be closed with the Close() method to avoid leaking file handles.
+type BlobReader interface {
+	// ReadAt() fills b[] with up to len(b) bytes of data starting at
+	// position "at" within the blob that the BlobReader indicates, and
+	// returns the number of bytes read.
+	ReadAt(b []byte, at int64) (n int, err error)
+
+	// Read() fills b[] with up to len(b) bytes of data starting at the
+	// current seek position of the BlobReader within the blob that the
+	// BlobReader indicates, and then both returns the number of bytes read
+	// and advances the BlobReader's seek position by that amount.
+	Read(b []byte) (n int, err error)
+
+	// Seek() sets the seek position of the BlobReader to offset if
+	// whence==0, offset+current_seek_position if whence==1, and
+	// offset+end_of_blob if whence==2, and then returns the current seek
+	// position.
+	Seek(offset int64, whence int) (result int64, err error)
+
+	// Close() indicates that the client will perform no further operations
+	// on the BlobReader.  It releases any resources held by the
+	// BlobReader.
+	Close() error
+
+	// Name() returns the BlobReader's name.
+	Name() string
+
+	// Size() returns the BlobReader's size.
+	Size() int64
+
+	// IsFinalized() returns whether the BlobReader has been finalized.
+	IsFinalized() bool
+
+	// Hash() returns the BlobReader's hash.  It may be nil if the blob is
+	// not finalized.
+	Hash() []byte
+}
+
+// A BlockOrFile represents a vector of bytes, and contains either a data
+// block (as a []byte), or a (file name, size, offset) triple.
+type BlockOrFile struct {
+	Block    []byte // If FileName is empty, the bytes represented.
+	FileName string // If non-empty, the name of the file containing the bytes.
+	Size     int64  // If FileName is non-empty, the number of bytes (or -1 for "all")
+	Offset   int64  // If FileName is non-empty, the offset of the relevant bytes within the file.
+}
+
+// A BlobWriter allows a blob to be written.  If a blob has not yet been
+// finalized, it also allows that blob to be extended.  A BlobWriter may be
+// created with NewBlobWriter(), and should be closed with Close() or
+// CloseWithoutFinalize().
+type BlobWriter interface {
+	// AppendBlob() adds a (substring of a) pre-existing blob to the blob
+	// being written by the BlobWriter.  The fragments of the pre-existing
+	// blob are not physically copied; they are referenced by both blobs.
+	AppendBlob(blobName string, size int64, offset int64) (err error)
+
+	// AppendFragment() appends a fragment to the blob being written by the
+	// BlobWriter, where the fragment is composed of the byte vectors
+	// described by the elements of item[].  The fragment is copied into
+	// the blob store.
+	AppendFragment(item ...BlockOrFile) (err error)
+
+	// Close() finalizes the BlobWriter, and indicates that the client will
+	// perform no further append operations on the BlobWriter.  Any
+	// internal open file handles are closed.
+	Close() (err error)
+
+	// CloseWithoutFinalize() indicates that the client will perform no
+	// further append operations on the BlobWriter, but does not finalize
+	// the blob.  Any internal open file handles are closed.  Clients are
+	// expected to need this operation infrequently.
+	CloseWithoutFinalize() (err error)
+
+	// Name() returns the BlobWriter's name.
+	Name() string
+
+	// Size() returns the BlobWriter's size.
+	Size() int64
+
+	// IsFinalized() returns whether the BlobWriter has been finalized.
+	IsFinalized() bool
+
+	// Hash() returns the BlobWriter's hash, reflecting the bytes written so far.
+	Hash() []byte
+}
+
+// A Stream represents an iterator that allows the client to enumerate
+// all the blobs or fragments in a BlobStore.
+//
+// The interfaces Stream, ChunkStream, RecipeStream all have four calls,
+// and differ only in the Value() call.
+type Stream interface {
+	// Advance() stages an item so that it may be retrieved via Value().
+	// Returns true iff there is an item to retrieve.  Advance() must be
+	// called before Value() is called.  The caller is expected to read
+	// until Advance() returns false, or to call Cancel().
+	Advance() bool
+
+	// Value() returns the item that was staged by Advance().  May panic if
+	// Advance() returned false or was not called.  Never blocks.
+	Value() (name string)
+
+	// Err() returns any error encountered by Advance.  Never blocks.
+	Err() error
+
+	// Cancel() indicates that the client wishes to cease reading from the stream.
+	// It causes the next call to Advance() to return false.  Never blocks.
+	// It may be called concurrently with other calls on the stream.
+	Cancel()
+}
+
+// A ChunkStream represents an iterator that allows the client to enumerate
+// the chunks in a blob.   See the comments for Stream for usage.
+type ChunkStream interface {
+	Advance() bool
+
+	// Value() returns the chunkHash that was staged by Advance().  May
+	// panic if Advance() returned false or was not called.  Never blocks.
+	// The result may share storage with buf[] if it is large enough;
+	// otherwise, a new buffer is allocated.  It is legal to call with
+	// buf==nil.
+	Value(buf []byte) (chunkHash []byte)
+
+	Err() error
+	Cancel()
+}
+
+// A RecipeStep describes one piece of a recipe for making a blob.
+// The step consists either of appending the chunk with content hash Chunk and size Size,
+// or (if Chunk==nil) the Size bytes from Blob, starting at Offset.
+type RecipeStep struct {
+	Chunk  []byte
+	Blob   string
+	Size   int64
+	Offset int64
+}
+
+// A RecipeStream represents an iterator that allows the client to obtain the
+// steps needed to construct a blob with a given ChunkStream, attempting to
+// reuse data in existing blobs.  See the comments for Stream for usage.
+type RecipeStream interface {
+	Advance() bool
+
+	// Value() returns the RecipeStep that was staged by Advance().  May panic if
+	// Advance() returned false or was not called.  Never blocks.
+	Value() RecipeStep
+
+	Err() error
+	Cancel()
+}
diff --git a/services/syncbase/server/app.go b/services/syncbase/server/app.go
new file mode 100644
index 0000000..c404f77
--- /dev/null
+++ b/services/syncbase/server/app.go
@@ -0,0 +1,292 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package server
+
+import (
+	"path"
+	"sync"
+
+	wire "v.io/syncbase/v23/services/syncbase"
+	nosqlwire "v.io/syncbase/v23/services/syncbase/nosql"
+	"v.io/syncbase/x/ref/services/syncbase/server/interfaces"
+	"v.io/syncbase/x/ref/services/syncbase/server/nosql"
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/context"
+	"v.io/v23/glob"
+	"v.io/v23/rpc"
+	"v.io/v23/security/access"
+	"v.io/v23/verror"
+	"v.io/x/lib/vlog"
+)
+
+// app is a per-app singleton (i.e. not per-request) that handles App RPCs.
+type app struct {
+	name string
+	s    *service
+	// The fields below are initialized iff this app exists.
+	exists bool
+	// Guards the fields below. Held during database Create, Delete, and
+	// SetPermissions.
+	mu  sync.Mutex
+	dbs map[string]interfaces.Database
+}
+
+var (
+	_ wire.AppServerMethods = (*app)(nil)
+	_ interfaces.App        = (*app)(nil)
+)
+
+////////////////////////////////////////
+// RPC methods
+
+// TODO(sadovsky): Require the app name to match the client's blessing name.
+// I.e. reserve names at the app level of the hierarchy.
+func (a *app) Create(ctx *context.T, call rpc.ServerCall, perms access.Permissions) error {
+	if a.exists {
+		return verror.New(verror.ErrExist, ctx, a.name)
+	}
+	// This app does not yet exist; a is just an ephemeral handle that holds
+	// {name string, s *service}. a.s.createApp will create a new app handle and
+	// store it in a.s.apps[a.name].
+	return a.s.createApp(ctx, call, a.name, perms)
+}
+
+func (a *app) Delete(ctx *context.T, call rpc.ServerCall) error {
+	return a.s.deleteApp(ctx, call, a.name)
+}
+
+func (a *app) Exists(ctx *context.T, call rpc.ServerCall) (bool, error) {
+	if !a.exists {
+		return false, nil
+	}
+	return util.ErrorToExists(util.GetWithAuth(ctx, call, a.s.st, a.stKey(), &appData{}))
+}
+
+func (a *app) SetPermissions(ctx *context.T, call rpc.ServerCall, perms access.Permissions, version string) error {
+	if !a.exists {
+		return verror.New(verror.ErrNoExist, ctx, a.name)
+	}
+	return a.s.setAppPerms(ctx, call, a.name, perms, version)
+}
+
+func (a *app) GetPermissions(ctx *context.T, call rpc.ServerCall) (perms access.Permissions, version string, err error) {
+	if !a.exists {
+		return nil, "", verror.New(verror.ErrNoExist, ctx, a.name)
+	}
+	data := &appData{}
+	if err := util.GetWithAuth(ctx, call, a.s.st, a.stKey(), data); err != nil {
+		return nil, "", err
+	}
+	return data.Perms, util.FormatVersion(data.Version), nil
+}
+
+func (a *app) GlobChildren__(ctx *context.T, call rpc.GlobChildrenServerCall, matcher *glob.Element) error {
+	if !a.exists {
+		return verror.New(verror.ErrNoExist, ctx, a.name)
+	}
+	// Check perms.
+	sn := a.s.st.NewSnapshot()
+	if err := util.GetWithAuth(ctx, call, sn, a.stKey(), &appData{}); err != nil {
+		sn.Abort()
+		return err
+	}
+	return util.Glob(ctx, call, matcher, sn, sn.Abort, util.JoinKeyParts(util.DbInfoPrefix, a.name))
+}
+
+////////////////////////////////////////
+// interfaces.App methods
+
+func (a *app) Service() interfaces.Service {
+	return a.s
+}
+
+func (a *app) NoSQLDatabase(ctx *context.T, call rpc.ServerCall, dbName string) (interfaces.Database, error) {
+	if !a.exists {
+		vlog.Fatalf("app %q does not exist", a.name)
+	}
+	a.mu.Lock()
+	defer a.mu.Unlock()
+	d, ok := a.dbs[dbName]
+	if !ok {
+		return nil, verror.New(verror.ErrNoExist, ctx, dbName)
+	}
+	return d, nil
+}
+
+func (a *app) NoSQLDatabaseNames(ctx *context.T, call rpc.ServerCall) ([]string, error) {
+	if !a.exists {
+		vlog.Fatalf("app %q does not exist", a.name)
+	}
+	// In the future this API will likely be replaced by one that streams the
+	// database names.
+	a.mu.Lock()
+	defer a.mu.Unlock()
+	dbNames := make([]string, 0, len(a.dbs))
+	for n := range a.dbs {
+		dbNames = append(dbNames, n)
+	}
+	return dbNames, nil
+}
+
+func (a *app) CreateNoSQLDatabase(ctx *context.T, call rpc.ServerCall, dbName string, perms access.Permissions, metadata *nosqlwire.SchemaMetadata) error {
+	if !a.exists {
+		vlog.Fatalf("app %q does not exist", a.name)
+	}
+	// TODO(sadovsky): Crash if any step fails, and use WAL to ensure that if we
+	// crash, upon restart we execute any remaining steps before we start handling
+	// client requests.
+	//
+	// Steps:
+	// 1. Check appData perms, create dbInfo record.
+	// 2. Initialize database.
+	// 3. Flip dbInfo.Initialized to true. <===== CHANGE BECOMES VISIBLE
+	a.mu.Lock()
+	defer a.mu.Unlock()
+	if _, ok := a.dbs[dbName]; ok {
+		// TODO(sadovsky): Should this be ErrExistOrNoAccess, for privacy?
+		return verror.New(verror.ErrExist, ctx, dbName)
+	}
+
+	// 1. Check appData perms, create dbInfo record.
+	rootDir, engine := a.rootDirForDb(dbName), a.s.opts.Engine
+	aData := &appData{}
+	if err := store.RunInTransaction(a.s.st, func(tx store.Transaction) error {
+		// Check appData perms.
+		if err := util.GetWithAuth(ctx, call, tx, a.stKey(), aData); err != nil {
+			return err
+		}
+		// Check for "database already exists".
+		if _, err := a.getDbInfo(ctx, tx, dbName); verror.ErrorID(err) != verror.ErrNoExist.ID {
+			if err != nil {
+				return err
+			}
+			// TODO(sadovsky): Should this be ErrExistOrNoAccess, for privacy?
+			return verror.New(verror.ErrExist, ctx, dbName)
+		}
+		// Write new dbInfo.
+		info := &dbInfo{
+			Name:    dbName,
+			RootDir: rootDir,
+			Engine:  engine,
+		}
+		return a.putDbInfo(ctx, tx, dbName, info)
+	}); err != nil {
+		return err
+	}
+
+	// 2. Initialize database.
+	if perms == nil {
+		perms = aData.Perms
+	}
+	d, err := nosql.NewDatabase(ctx, a, dbName, metadata, nosql.DatabaseOptions{
+		Perms:   perms,
+		RootDir: rootDir,
+		Engine:  engine,
+	})
+	if err != nil {
+		return err
+	}
+
+	// 3. Flip dbInfo.Initialized to true.
+	if err := store.RunInTransaction(a.s.st, func(tx store.Transaction) error {
+		return a.updateDbInfo(ctx, tx, dbName, func(info *dbInfo) error {
+			info.Initialized = true
+			return nil
+		})
+	}); err != nil {
+		return err
+	}
+
+	a.dbs[dbName] = d
+	return nil
+}
+
+func (a *app) DeleteNoSQLDatabase(ctx *context.T, call rpc.ServerCall, dbName string) error {
+	if !a.exists {
+		vlog.Fatalf("app %q does not exist", a.name)
+	}
+	// TODO(sadovsky): Crash if any step fails, and use WAL to ensure that if we
+	// crash, upon restart we execute any remaining steps before we start handling
+	// client requests.
+	//
+	// Steps:
+	// 1. Check databaseData perms.
+	// 2. Flip dbInfo.Deleted to true. <===== CHANGE BECOMES VISIBLE
+	// 3. Delete database.
+	// 4. Delete dbInfo record.
+	a.mu.Lock()
+	defer a.mu.Unlock()
+	d, ok := a.dbs[dbName]
+	if !ok {
+		return nil // delete is idempotent
+	}
+
+	// 1. Check databaseData perms.
+	if err := d.CheckPermsInternal(ctx, call, d.St()); err != nil {
+		if verror.ErrorID(err) == verror.ErrNoExist.ID {
+			return nil // delete is idempotent
+		}
+		return err
+	}
+
+	// 2. Flip dbInfo.Deleted to true.
+	if err := store.RunInTransaction(a.s.st, func(tx store.Transaction) error {
+		return a.updateDbInfo(ctx, tx, dbName, func(info *dbInfo) error {
+			info.Deleted = true
+			return nil
+		})
+	}); err != nil {
+		return err
+	}
+
+	// 3. Delete database.
+	if err := d.St().Close(); err != nil {
+		return err
+	}
+	if err := util.DestroyStore(a.s.opts.Engine, a.rootDirForDb(dbName)); err != nil {
+		return err
+	}
+
+	// 4. Delete dbInfo record.
+	if err := a.delDbInfo(ctx, a.s.st, dbName); err != nil {
+		return err
+	}
+
+	delete(a.dbs, dbName)
+	return nil
+}
+
+func (a *app) SetDatabasePerms(ctx *context.T, call rpc.ServerCall, dbName string, perms access.Permissions, version string) error {
+	if !a.exists {
+		vlog.Fatalf("app %q does not exist", a.name)
+	}
+	a.mu.Lock()
+	defer a.mu.Unlock()
+	d, ok := a.dbs[dbName]
+	if !ok {
+		return verror.New(verror.ErrNoExist, ctx, dbName)
+	}
+	return d.SetPermsInternal(ctx, call, perms, version)
+}
+
+func (a *app) Name() string {
+	return a.name
+}
+
+////////////////////////////////////////
+// Internal helpers
+
+func (a *app) stKey() string {
+	return util.JoinKeyParts(util.AppPrefix, a.stKeyPart())
+}
+
+func (a *app) stKeyPart() string {
+	return a.name
+}
+
+func (a *app) rootDirForDb(dbName string) string {
+	return path.Join(a.s.opts.RootDir, "apps", a.name, dbName)
+}
diff --git a/services/syncbase/server/db_info.go b/services/syncbase/server/db_info.go
new file mode 100644
index 0000000..f750d57
--- /dev/null
+++ b/services/syncbase/server/db_info.go
@@ -0,0 +1,55 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package server
+
+// This file defines internal app methods for manipulating dbInfo.
+// None of these methods perform authorization checks.
+//
+// The fundamental reason why these methods are needed is that information about
+// a database is spread across two storage engines. The source of truth for the
+// existence of the database, as well as things like the database type, is the
+// service-level storage engine, while database permissions are tracked in the
+// database's storage engine.
+
+import (
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/context"
+)
+
+func dbInfoStKey(a *app, dbName string) string {
+	return util.JoinKeyParts(util.DbInfoPrefix, a.stKeyPart(), dbName)
+}
+
+// getDbInfo reads data from the storage engine.
+func (a *app) getDbInfo(ctx *context.T, sntx store.SnapshotOrTransaction, dbName string) (*dbInfo, error) {
+	info := &dbInfo{}
+	if err := util.Get(ctx, sntx, dbInfoStKey(a, dbName), info); err != nil {
+		return nil, err
+	}
+	return info, nil
+}
+
+// putDbInfo writes data to the storage engine.
+func (a *app) putDbInfo(ctx *context.T, tx store.Transaction, dbName string, info *dbInfo) error {
+	return util.Put(ctx, tx, dbInfoStKey(a, dbName), info)
+}
+
+// delDbInfo deletes data from the storage engine.
+func (a *app) delDbInfo(ctx *context.T, stw store.StoreWriter, dbName string) error {
+	return util.Delete(ctx, stw, dbInfoStKey(a, dbName))
+}
+
+// updateDbInfo performs a read-modify-write. fn should "modify" v.
+func (a *app) updateDbInfo(ctx *context.T, tx store.Transaction, dbName string, fn func(info *dbInfo) error) error {
+	info, err := a.getDbInfo(ctx, tx, dbName)
+	if err != nil {
+		return err
+	}
+	if err := fn(info); err != nil {
+		return err
+	}
+	return a.putDbInfo(ctx, tx, dbName, info)
+}
diff --git a/services/syncbase/server/db_info_test.go b/services/syncbase/server/db_info_test.go
new file mode 100644
index 0000000..7bc0870
--- /dev/null
+++ b/services/syncbase/server/db_info_test.go
@@ -0,0 +1,25 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package server
+
+import (
+	"testing"
+)
+
+func TestStKey(t *testing.T) {
+	tests := []struct {
+		appName string
+		dbName  string
+		stKey   string
+	}{
+		{"app1", "db1", "$dbInfo:app1:db1"},
+	}
+	for _, test := range tests {
+		got, want := dbInfoStKey(&app{name: test.appName}, test.dbName), test.stKey
+		if got != want {
+			t.Errorf("wrong stKey: got %q, want %q", got, want)
+		}
+	}
+}
diff --git a/services/syncbase/server/dispatcher.go b/services/syncbase/server/dispatcher.go
new file mode 100644
index 0000000..4b51a00
--- /dev/null
+++ b/services/syncbase/server/dispatcher.go
@@ -0,0 +1,85 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package server
+
+import (
+	"strings"
+
+	wire "v.io/syncbase/v23/services/syncbase"
+	pubutil "v.io/syncbase/v23/syncbase/util"
+	"v.io/syncbase/x/ref/services/syncbase/server/interfaces"
+	"v.io/syncbase/x/ref/services/syncbase/server/nosql"
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/v23/context"
+	"v.io/v23/rpc"
+	"v.io/v23/security"
+	"v.io/v23/verror"
+)
+
+type dispatcher struct {
+	s *service
+}
+
+var _ rpc.Dispatcher = (*dispatcher)(nil)
+
+func NewDispatcher(s *service) *dispatcher {
+	return &dispatcher{s: s}
+}
+
+// We always return an AllowEveryone authorizer from Lookup(), and rely on our
+// RPC method implementations to perform proper authorization.
+var auth security.Authorizer = security.AllowEveryone()
+
+func (disp *dispatcher) Lookup(ctx *context.T, suffix string) (interface{}, security.Authorizer, error) {
+	suffix = strings.TrimPrefix(suffix, "/")
+	parts := strings.SplitN(suffix, "/", 2)
+
+	if len(suffix) == 0 {
+		return wire.ServiceServer(disp.s), auth, nil
+	}
+
+	if parts[0] == util.SyncbaseSuffix {
+		return interfaces.SyncServer(disp.s.sync), auth, nil
+	}
+
+	// Validate all key atoms up front, so that we can avoid doing so in all our
+	// method implementations.
+	appName := parts[0]
+	if !pubutil.ValidName(appName) {
+		return nil, nil, wire.NewErrInvalidName(nil, suffix)
+	}
+
+	aExists := false
+	var a *app
+	if aInt, err := disp.s.App(nil, nil, appName); err == nil {
+		a = aInt.(*app) // panics on failure, as desired
+		aExists = true
+	} else {
+		if verror.ErrorID(err) != verror.ErrNoExist.ID {
+			return nil, nil, err
+		} else {
+			a = &app{
+				name: appName,
+				s:    disp.s,
+			}
+		}
+	}
+
+	if len(parts) == 1 {
+		return wire.AppServer(a), auth, nil
+	}
+
+	// All database, table, and row methods require the app to exist. If it
+	// doesn't, abort early.
+	if !aExists {
+		return nil, nil, verror.New(verror.ErrNoExist, nil, a.name)
+	}
+
+	// Note, it's possible for the app to be deleted concurrently with downstream
+	// handling of this request. Depending on the order in which things execute,
+	// the client may not get an error, but in any case ultimately the store will
+	// end up in a consistent state.
+	return nosql.NewDispatcher(a).Lookup(ctx, parts[1])
+}
diff --git a/services/syncbase/server/interfaces/app.go b/services/syncbase/server/interfaces/app.go
new file mode 100644
index 0000000..e990b29
--- /dev/null
+++ b/services/syncbase/server/interfaces/app.go
@@ -0,0 +1,36 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package interfaces
+
+import (
+	wire "v.io/syncbase/v23/services/syncbase/nosql"
+	"v.io/v23/context"
+	"v.io/v23/rpc"
+	"v.io/v23/security/access"
+)
+
+// App is an internal interface to the app layer.
+type App interface {
+	// Service returns the service handle for this app.
+	Service() Service
+
+	// NoSQLDatabase returns the Database for the specified NoSQL database.
+	NoSQLDatabase(ctx *context.T, call rpc.ServerCall, dbName string) (Database, error)
+
+	// NoSQLDatabaseNames returns the names of the NoSQL databases within the App.
+	NoSQLDatabaseNames(ctx *context.T, call rpc.ServerCall) ([]string, error)
+
+	// CreateNoSQLDatabase creates the specified NoSQL database.
+	CreateNoSQLDatabase(ctx *context.T, call rpc.ServerCall, dbName string, perms access.Permissions, metadata *wire.SchemaMetadata) error
+
+	// DeleteNoSQLDatabase deletes the specified NoSQL database.
+	DeleteNoSQLDatabase(ctx *context.T, call rpc.ServerCall, dbName string) error
+
+	// SetDatabasePerms sets the perms for the specified database.
+	SetDatabasePerms(ctx *context.T, call rpc.ServerCall, dbName string, perms access.Permissions, version string) error
+
+	// Name returns the name of this app.
+	Name() string
+}
diff --git a/services/syncbase/server/interfaces/database.go b/services/syncbase/server/interfaces/database.go
new file mode 100644
index 0000000..8be30d8
--- /dev/null
+++ b/services/syncbase/server/interfaces/database.go
@@ -0,0 +1,33 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package interfaces
+
+import (
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/context"
+	"v.io/v23/rpc"
+	"v.io/v23/security/access"
+)
+
+// Database is an internal interface to the database layer.
+type Database interface {
+	// St returns the storage engine instance for this database.
+	St() store.Store
+
+	// App returns the app handle for this database.
+	App() App
+
+	// CheckPermsInternal checks whether the given RPC (ctx, call) is allowed per
+	// the database perms.
+	// Designed for use from within App.DeleteNoSQLDatabase.
+	CheckPermsInternal(ctx *context.T, call rpc.ServerCall, st store.StoreReader) error
+
+	// SetPermsInternal updates the database perms.
+	// Designed for use from within App.SetDatabasePerms.
+	SetPermsInternal(ctx *context.T, call rpc.ServerCall, perms access.Permissions, version string) error
+
+	// Name returns the name of this database.
+	Name() string
+}
diff --git a/services/syncbase/server/interfaces/doc.go b/services/syncbase/server/interfaces/doc.go
new file mode 100644
index 0000000..384f2f7
--- /dev/null
+++ b/services/syncbase/server/interfaces/doc.go
@@ -0,0 +1,10 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package interfaces defines internal interfaces for various objects in the
+// Syncbase server implementation. Defining these interfaces in a separate
+// package helps prevent import cycles: all other packages can import the
+// interfaces package, and individual modules can pass each other interfaces to
+// enable bidirectional cross-package communication.
+package interfaces
diff --git a/services/syncbase/server/interfaces/service.go b/services/syncbase/server/interfaces/service.go
new file mode 100644
index 0000000..ce665e2
--- /dev/null
+++ b/services/syncbase/server/interfaces/service.go
@@ -0,0 +1,26 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package interfaces
+
+import (
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/context"
+	"v.io/v23/rpc"
+)
+
+// Service is an internal interface to the service layer.
+type Service interface {
+	// St returns the storage engine instance for this service.
+	St() store.Store
+
+	// Sync returns the sync instance for this service.
+	Sync() SyncServerMethods
+
+	// App returns the App with the specified name.
+	App(ctx *context.T, call rpc.ServerCall, appName string) (App, error)
+
+	// AppNames returns the names of the Apps within the service.
+	AppNames(ctx *context.T, call rpc.ServerCall) ([]string, error)
+}
diff --git a/services/syncbase/server/interfaces/sync.vdl b/services/syncbase/server/interfaces/sync.vdl
new file mode 100644
index 0000000..b97e845
--- /dev/null
+++ b/services/syncbase/server/interfaces/sync.vdl
@@ -0,0 +1,59 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package interfaces
+
+import (
+	wire "v.io/syncbase/v23/services/syncbase/nosql"
+	"v.io/v23/security/access"
+)
+
+// Sync defines methods for data exchange between Syncbases.
+// TODO(hpucha): Flesh this out further.
+type Sync interface {
+	// GetDeltas returns the responder's current generation vector and all
+	// the missing log records when compared to the initiator's generation
+	// vector. This process happens one Database at a time encompassing all
+	// the SyncGroups common to the initiator and the responder. For each
+	// Database, the initiator sends a DeltaReq. In response, the
+	// responder sends a "Start" DeltaResp record, all the missing log
+	// records, the responder's genvector, and a "Finish" DeltaResp
+	// record. The initiator parses the stream between a Start and a Finish
+	// record as the response to its DeltaReq, and then moves on to the
+	// next Database in common with this responder.
+	GetDeltas(initiator string) stream<DeltaReq, DeltaResp> error {access.Read}
+
+	// SyncGroup-related methods.
+
+	// PublishSyncGroup is typically invoked on a "central" peer to publish
+	// the SyncGroup.
+	PublishSyncGroup(sg SyncGroup) error {access.Write}
+
+	// JoinSyncGroupAtAdmin is invoked by a prospective SyncGroup member's
+	// Syncbase on a SyncGroup admin. It checks whether the requestor is
+	// allowed to join the named SyncGroup, and if so, adds the requestor to
+	// the SyncGroup.
+	JoinSyncGroupAtAdmin(sgName, joinerName string, myInfo wire.SyncGroupMemberInfo) (SyncGroup | error) {access.Read}
+
+	// BlobSync methods.
+
+	// HaveBlob verifies that the peer has the requested blob, and if
+	// present, returns its size.
+	HaveBlob(br wire.BlobRef) (int64 | error)
+
+	// FetchBlob fetches the requested blob.
+	FetchBlob(br wire.BlobRef) stream<_, []byte> error
+
+	// Methods for incremental blob transfer. The transfer starts with the
+	// receiver making a FetchBlobRecipe call to the sender for a given
+	// BlobRef. The sender, in turn, sends the chunk hashes of all the
+	// chunks that make up the requested blob (blob recipe). The receiver
+	// looks up the chunk hashes in its local blob store, and identifies the
+	// missing ones. The receiver then fetches the missing chunks using a
+	// FetchChunks call from the sender. Finally, the receiver finishes the
+	// blob fetch by combining the chunks obtained over the network with the
+	// already available local chunks as per the blob recipe.
+	FetchBlobRecipe(br wire.BlobRef) stream<_, ChunkHash> error
+	FetchChunks() stream<ChunkHash, ChunkData> error
+}
diff --git a/services/syncbase/server/interfaces/sync.vdl.go b/services/syncbase/server/interfaces/sync.vdl.go
new file mode 100644
index 0000000..9cd1383
--- /dev/null
+++ b/services/syncbase/server/interfaces/sync.vdl.go
@@ -0,0 +1,946 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file was auto-generated by the vanadium vdl tool.
+// Source: sync.vdl
+
+package interfaces
+
+import (
+	// VDL system imports
+	"io"
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/rpc"
+	"v.io/v23/vdl"
+
+	// VDL user imports
+	"v.io/syncbase/v23/services/syncbase/nosql"
+	"v.io/v23/security/access"
+)
+
+// SyncClientMethods is the client interface
+// containing Sync methods.
+//
+// Sync defines methods for data exchange between Syncbases.
+// TODO(hpucha): Flesh this out further.
+type SyncClientMethods interface {
+	// GetDeltas returns the responder's current generation vector and all
+	// the missing log records when compared to the initiator's generation
+	// vector. This process happens one Database at a time encompassing all
+	// the SyncGroups common to the initiator and the responder. For each
+	// Database, the initiator sends a DeltaReq. In response, the
+	// responder sends a "Start" DeltaResp record, all the missing log
+	// records, the responder's genvector, and a "Finish" DeltaResp
+	// record. The initiator parses the stream between a Start and a Finish
+	// record as the response to its DeltaReq, and then moves on to the
+	// next Database in common with this responder.
+	GetDeltas(ctx *context.T, initiator string, opts ...rpc.CallOpt) (SyncGetDeltasClientCall, error)
+	// PublishSyncGroup is typically invoked on a "central" peer to publish
+	// the SyncGroup.
+	PublishSyncGroup(ctx *context.T, sg SyncGroup, opts ...rpc.CallOpt) error
+	// JoinSyncGroupAtAdmin is invoked by a prospective SyncGroup member's
+	// Syncbase on a SyncGroup admin. It checks whether the requestor is
+	// allowed to join the named SyncGroup, and if so, adds the requestor to
+	// the SyncGroup.
+	JoinSyncGroupAtAdmin(ctx *context.T, sgName string, joinerName string, myInfo nosql.SyncGroupMemberInfo, opts ...rpc.CallOpt) (SyncGroup, error)
+	// HaveBlob verifies that the peer has the requested blob, and if
+	// present, returns its size.
+	HaveBlob(ctx *context.T, br nosql.BlobRef, opts ...rpc.CallOpt) (int64, error)
+	// FetchBlob fetches the requested blob.
+	FetchBlob(ctx *context.T, br nosql.BlobRef, opts ...rpc.CallOpt) (SyncFetchBlobClientCall, error)
+	// Methods for incremental blob transfer. The transfer starts with the
+	// receiver making a FetchBlobRecipe call to the sender for a given
+	// BlobRef. The sender, in turn, sends the chunk hashes of all the
+	// chunks that make up the requested blob (blob recipe). The receiver
+	// looks up the chunk hashes in its local blob store, and identifies the
+	// missing ones. The receiver then fetches the missing chunks using a
+	// FetchChunks call from the sender. Finally, the receiver finishes the
+	// blob fetch by combining the chunks obtained over the network with the
+	// already available local chunks as per the blob recipe.
+	FetchBlobRecipe(ctx *context.T, br nosql.BlobRef, opts ...rpc.CallOpt) (SyncFetchBlobRecipeClientCall, error)
+	FetchChunks(*context.T, ...rpc.CallOpt) (SyncFetchChunksClientCall, error)
+}
+
+// SyncClientStub adds universal methods to SyncClientMethods.
+type SyncClientStub interface {
+	SyncClientMethods
+	rpc.UniversalServiceMethods
+}
+
+// SyncClient returns a client stub for Sync.
+func SyncClient(name string) SyncClientStub {
+	return implSyncClientStub{name}
+}
+
+type implSyncClientStub struct {
+	name string
+}
+
+func (c implSyncClientStub) GetDeltas(ctx *context.T, i0 string, opts ...rpc.CallOpt) (ocall SyncGetDeltasClientCall, err error) {
+	var call rpc.ClientCall
+	if call, err = v23.GetClient(ctx).StartCall(ctx, c.name, "GetDeltas", []interface{}{i0}, opts...); err != nil {
+		return
+	}
+	ocall = &implSyncGetDeltasClientCall{ClientCall: call}
+	return
+}
+
+func (c implSyncClientStub) PublishSyncGroup(ctx *context.T, i0 SyncGroup, opts ...rpc.CallOpt) (err error) {
+	err = v23.GetClient(ctx).Call(ctx, c.name, "PublishSyncGroup", []interface{}{i0}, nil, opts...)
+	return
+}
+
+func (c implSyncClientStub) JoinSyncGroupAtAdmin(ctx *context.T, i0 string, i1 string, i2 nosql.SyncGroupMemberInfo, opts ...rpc.CallOpt) (o0 SyncGroup, err error) {
+	err = v23.GetClient(ctx).Call(ctx, c.name, "JoinSyncGroupAtAdmin", []interface{}{i0, i1, i2}, []interface{}{&o0}, opts...)
+	return
+}
+
+func (c implSyncClientStub) HaveBlob(ctx *context.T, i0 nosql.BlobRef, opts ...rpc.CallOpt) (o0 int64, err error) {
+	err = v23.GetClient(ctx).Call(ctx, c.name, "HaveBlob", []interface{}{i0}, []interface{}{&o0}, opts...)
+	return
+}
+
+func (c implSyncClientStub) FetchBlob(ctx *context.T, i0 nosql.BlobRef, opts ...rpc.CallOpt) (ocall SyncFetchBlobClientCall, err error) {
+	var call rpc.ClientCall
+	if call, err = v23.GetClient(ctx).StartCall(ctx, c.name, "FetchBlob", []interface{}{i0}, opts...); err != nil {
+		return
+	}
+	ocall = &implSyncFetchBlobClientCall{ClientCall: call}
+	return
+}
+
+func (c implSyncClientStub) FetchBlobRecipe(ctx *context.T, i0 nosql.BlobRef, opts ...rpc.CallOpt) (ocall SyncFetchBlobRecipeClientCall, err error) {
+	var call rpc.ClientCall
+	if call, err = v23.GetClient(ctx).StartCall(ctx, c.name, "FetchBlobRecipe", []interface{}{i0}, opts...); err != nil {
+		return
+	}
+	ocall = &implSyncFetchBlobRecipeClientCall{ClientCall: call}
+	return
+}
+
+func (c implSyncClientStub) FetchChunks(ctx *context.T, opts ...rpc.CallOpt) (ocall SyncFetchChunksClientCall, err error) {
+	var call rpc.ClientCall
+	if call, err = v23.GetClient(ctx).StartCall(ctx, c.name, "FetchChunks", nil, opts...); err != nil {
+		return
+	}
+	ocall = &implSyncFetchChunksClientCall{ClientCall: call}
+	return
+}
+
+// SyncGetDeltasClientStream is the client stream for Sync.GetDeltas.
+type SyncGetDeltasClientStream interface {
+	// RecvStream returns the receiver side of the Sync.GetDeltas client stream.
+	RecvStream() interface {
+		// Advance stages an item so that it may be retrieved via Value.  Returns
+		// true iff there is an item to retrieve.  Advance must be called before
+		// Value is called.  May block if an item is not available.
+		Advance() bool
+		// Value returns the item that was staged by Advance.  May panic if Advance
+		// returned false or was not called.  Never blocks.
+		Value() DeltaResp
+		// Err returns any error encountered by Advance.  Never blocks.
+		Err() error
+	}
+	// SendStream returns the send side of the Sync.GetDeltas client stream.
+	SendStream() interface {
+		// Send places the item onto the output stream.  Returns errors
+		// encountered while sending, or if Send is called after Close or
+		// the stream has been canceled.  Blocks if there is no buffer
+		// space; will unblock when buffer space is available or after
+		// the stream has been canceled.
+		Send(item DeltaReq) error
+		// Close indicates to the server that no more items will be sent;
+		// server Recv calls will receive io.EOF after all sent items.
+		// This is an optional call - e.g. a client might call Close if it
+		// needs to continue receiving items from the server after it's
+		// done sending.  Returns errors encountered while closing, or if
+		// Close is called after the stream has been canceled.  Like Send,
+		// blocks if there is no buffer space available.
+		Close() error
+	}
+}
+
+// SyncGetDeltasClientCall represents the call returned from Sync.GetDeltas.
+type SyncGetDeltasClientCall interface {
+	SyncGetDeltasClientStream
+	// Finish performs the equivalent of SendStream().Close, then blocks until
+	// the server is done, and returns the positional return values for the call.
+	//
+	// Finish returns immediately if the call has been canceled; depending on the
+	// timing the output could either be an error signaling cancelation, or the
+	// valid positional return values from the server.
+	//
+	// Calling Finish is mandatory for releasing stream resources, unless the call
+	// has been canceled or any of the other methods return an error.  Finish should
+	// be called at most once.
+	Finish() error
+}
+
+type implSyncGetDeltasClientCall struct {
+	rpc.ClientCall
+	valRecv DeltaResp
+	errRecv error
+}
+
+func (c *implSyncGetDeltasClientCall) RecvStream() interface {
+	Advance() bool
+	Value() DeltaResp
+	Err() error
+} {
+	return implSyncGetDeltasClientCallRecv{c}
+}
+
+type implSyncGetDeltasClientCallRecv struct {
+	c *implSyncGetDeltasClientCall
+}
+
+func (c implSyncGetDeltasClientCallRecv) Advance() bool {
+	c.c.errRecv = c.c.Recv(&c.c.valRecv)
+	return c.c.errRecv == nil
+}
+func (c implSyncGetDeltasClientCallRecv) Value() DeltaResp {
+	return c.c.valRecv
+}
+func (c implSyncGetDeltasClientCallRecv) Err() error {
+	if c.c.errRecv == io.EOF {
+		return nil
+	}
+	return c.c.errRecv
+}
+func (c *implSyncGetDeltasClientCall) SendStream() interface {
+	Send(item DeltaReq) error
+	Close() error
+} {
+	return implSyncGetDeltasClientCallSend{c}
+}
+
+type implSyncGetDeltasClientCallSend struct {
+	c *implSyncGetDeltasClientCall
+}
+
+func (c implSyncGetDeltasClientCallSend) Send(item DeltaReq) error {
+	return c.c.Send(item)
+}
+func (c implSyncGetDeltasClientCallSend) Close() error {
+	return c.c.CloseSend()
+}
+func (c *implSyncGetDeltasClientCall) Finish() (err error) {
+	err = c.ClientCall.Finish()
+	return
+}
+
+// SyncFetchBlobClientStream is the client stream for Sync.FetchBlob.
+type SyncFetchBlobClientStream interface {
+	// RecvStream returns the receiver side of the Sync.FetchBlob client stream.
+	RecvStream() interface {
+		// Advance stages an item so that it may be retrieved via Value.  Returns
+		// true iff there is an item to retrieve.  Advance must be called before
+		// Value is called.  May block if an item is not available.
+		Advance() bool
+		// Value returns the item that was staged by Advance.  May panic if Advance
+		// returned false or was not called.  Never blocks.
+		Value() []byte
+		// Err returns any error encountered by Advance.  Never blocks.
+		Err() error
+	}
+}
+
+// SyncFetchBlobClientCall represents the call returned from Sync.FetchBlob.
+type SyncFetchBlobClientCall interface {
+	SyncFetchBlobClientStream
+	// Finish blocks until the server is done, and returns the positional return
+	// values for call.
+	//
+	// Finish returns immediately if the call has been canceled; depending on the
+	// timing the output could either be an error signaling cancelation, or the
+	// valid positional return values from the server.
+	//
+	// Calling Finish is mandatory for releasing stream resources, unless the call
+	// has been canceled or any of the other methods return an error.  Finish should
+	// be called at most once.
+	Finish() error
+}
+
+type implSyncFetchBlobClientCall struct {
+	rpc.ClientCall
+	valRecv []byte
+	errRecv error
+}
+
+func (c *implSyncFetchBlobClientCall) RecvStream() interface {
+	Advance() bool
+	Value() []byte
+	Err() error
+} {
+	return implSyncFetchBlobClientCallRecv{c}
+}
+
+type implSyncFetchBlobClientCallRecv struct {
+	c *implSyncFetchBlobClientCall
+}
+
+func (c implSyncFetchBlobClientCallRecv) Advance() bool {
+	c.c.errRecv = c.c.Recv(&c.c.valRecv)
+	return c.c.errRecv == nil
+}
+func (c implSyncFetchBlobClientCallRecv) Value() []byte {
+	return c.c.valRecv
+}
+func (c implSyncFetchBlobClientCallRecv) Err() error {
+	if c.c.errRecv == io.EOF {
+		return nil
+	}
+	return c.c.errRecv
+}
+func (c *implSyncFetchBlobClientCall) Finish() (err error) {
+	err = c.ClientCall.Finish()
+	return
+}
+
+// SyncFetchBlobRecipeClientStream is the client stream for Sync.FetchBlobRecipe.
+type SyncFetchBlobRecipeClientStream interface {
+	// RecvStream returns the receiver side of the Sync.FetchBlobRecipe client stream.
+	RecvStream() interface {
+		// Advance stages an item so that it may be retrieved via Value.  Returns
+		// true iff there is an item to retrieve.  Advance must be called before
+		// Value is called.  May block if an item is not available.
+		Advance() bool
+		// Value returns the item that was staged by Advance.  May panic if Advance
+		// returned false or was not called.  Never blocks.
+		Value() ChunkHash
+		// Err returns any error encountered by Advance.  Never blocks.
+		Err() error
+	}
+}
+
+// SyncFetchBlobRecipeClientCall represents the call returned from Sync.FetchBlobRecipe.
+type SyncFetchBlobRecipeClientCall interface {
+	SyncFetchBlobRecipeClientStream
+	// Finish blocks until the server is done, and returns the positional return
+	// values for call.
+	//
+	// Finish returns immediately if the call has been canceled; depending on the
+	// timing the output could either be an error signaling cancelation, or the
+	// valid positional return values from the server.
+	//
+	// Calling Finish is mandatory for releasing stream resources, unless the call
+	// has been canceled or any of the other methods return an error.  Finish should
+	// be called at most once.
+	Finish() error
+}
+
+type implSyncFetchBlobRecipeClientCall struct {
+	rpc.ClientCall
+	valRecv ChunkHash
+	errRecv error
+}
+
+func (c *implSyncFetchBlobRecipeClientCall) RecvStream() interface {
+	Advance() bool
+	Value() ChunkHash
+	Err() error
+} {
+	return implSyncFetchBlobRecipeClientCallRecv{c}
+}
+
+type implSyncFetchBlobRecipeClientCallRecv struct {
+	c *implSyncFetchBlobRecipeClientCall
+}
+
+func (c implSyncFetchBlobRecipeClientCallRecv) Advance() bool {
+	c.c.valRecv = ChunkHash{}
+	c.c.errRecv = c.c.Recv(&c.c.valRecv)
+	return c.c.errRecv == nil
+}
+func (c implSyncFetchBlobRecipeClientCallRecv) Value() ChunkHash {
+	return c.c.valRecv
+}
+func (c implSyncFetchBlobRecipeClientCallRecv) Err() error {
+	if c.c.errRecv == io.EOF {
+		return nil
+	}
+	return c.c.errRecv
+}
+func (c *implSyncFetchBlobRecipeClientCall) Finish() (err error) {
+	err = c.ClientCall.Finish()
+	return
+}
+
+// SyncFetchChunksClientStream is the client stream for Sync.FetchChunks.
+type SyncFetchChunksClientStream interface {
+	// RecvStream returns the receiver side of the Sync.FetchChunks client stream.
+	RecvStream() interface {
+		// Advance stages an item so that it may be retrieved via Value.  Returns
+		// true iff there is an item to retrieve.  Advance must be called before
+		// Value is called.  May block if an item is not available.
+		Advance() bool
+		// Value returns the item that was staged by Advance.  May panic if Advance
+		// returned false or was not called.  Never blocks.
+		Value() ChunkData
+		// Err returns any error encountered by Advance.  Never blocks.
+		Err() error
+	}
+	// SendStream returns the send side of the Sync.FetchChunks client stream.
+	SendStream() interface {
+		// Send places the item onto the output stream.  Returns errors
+		// encountered while sending, or if Send is called after Close or
+		// the stream has been canceled.  Blocks if there is no buffer
+		// space; will unblock when buffer space is available or after
+		// the stream has been canceled.
+		Send(item ChunkHash) error
+		// Close indicates to the server that no more items will be sent;
+		// server Recv calls will receive io.EOF after all sent items.
+		// This is an optional call - e.g. a client might call Close if it
+		// needs to continue receiving items from the server after it's
+		// done sending.  Returns errors encountered while closing, or if
+		// Close is called after the stream has been canceled.  Like Send,
+		// blocks if there is no buffer space available.
+		Close() error
+	}
+}
+
+// SyncFetchChunksClientCall represents the call returned from Sync.FetchChunks.
+type SyncFetchChunksClientCall interface {
+	SyncFetchChunksClientStream
+	// Finish performs the equivalent of SendStream().Close, then blocks until
+	// the server is done, and returns the positional return values for the call.
+	//
+	// Finish returns immediately if the call has been canceled; depending on the
+	// timing the output could either be an error signaling cancelation, or the
+	// valid positional return values from the server.
+	//
+	// Calling Finish is mandatory for releasing stream resources, unless the call
+	// has been canceled or any of the other methods return an error.  Finish should
+	// be called at most once.
+	Finish() error
+}
+
+type implSyncFetchChunksClientCall struct {
+	rpc.ClientCall
+	valRecv ChunkData
+	errRecv error
+}
+
+func (c *implSyncFetchChunksClientCall) RecvStream() interface {
+	Advance() bool
+	Value() ChunkData
+	Err() error
+} {
+	return implSyncFetchChunksClientCallRecv{c}
+}
+
+type implSyncFetchChunksClientCallRecv struct {
+	c *implSyncFetchChunksClientCall
+}
+
+func (c implSyncFetchChunksClientCallRecv) Advance() bool {
+	c.c.valRecv = ChunkData{}
+	c.c.errRecv = c.c.Recv(&c.c.valRecv)
+	return c.c.errRecv == nil
+}
+func (c implSyncFetchChunksClientCallRecv) Value() ChunkData {
+	return c.c.valRecv
+}
+func (c implSyncFetchChunksClientCallRecv) Err() error {
+	if c.c.errRecv == io.EOF {
+		return nil
+	}
+	return c.c.errRecv
+}
+func (c *implSyncFetchChunksClientCall) SendStream() interface {
+	Send(item ChunkHash) error
+	Close() error
+} {
+	return implSyncFetchChunksClientCallSend{c}
+}
+
+type implSyncFetchChunksClientCallSend struct {
+	c *implSyncFetchChunksClientCall
+}
+
+func (c implSyncFetchChunksClientCallSend) Send(item ChunkHash) error {
+	return c.c.Send(item)
+}
+func (c implSyncFetchChunksClientCallSend) Close() error {
+	return c.c.CloseSend()
+}
+func (c *implSyncFetchChunksClientCall) Finish() (err error) {
+	err = c.ClientCall.Finish()
+	return
+}
+
+// SyncServerMethods is the interface a server writer
+// implements for Sync.
+//
+// Sync defines methods for data exchange between Syncbases.
+// TODO(hpucha): Flesh this out further.
+type SyncServerMethods interface {
+	// GetDeltas returns the responder's current generation vector and all
+	// the missing log records when compared to the initiator's generation
+	// vector. This process happens one Database at a time encompassing all
+	// the SyncGroups common to the initiator and the responder. For each
+	// Database, the initiator sends a DeltaReq. In response, the
+	// responder sends a "Start" DeltaResp record, all the missing log
+	// records, the responder's genvector, and a "Finish" DeltaResp
+	// record. The initiator parses the stream between a Start and a Finish
+	// record as the response to its DeltaReq, and then moves on to the
+	// next Database in common with this responder.
+	GetDeltas(ctx *context.T, call SyncGetDeltasServerCall, initiator string) error
+	// PublishSyncGroup is typically invoked on a "central" peer to publish
+	// the SyncGroup.
+	PublishSyncGroup(ctx *context.T, call rpc.ServerCall, sg SyncGroup) error
+	// JoinSyncGroupAtAdmin is invoked by a prospective SyncGroup member's
+	// Syncbase on a SyncGroup admin. It checks whether the requestor is
+	// allowed to join the named SyncGroup, and if so, adds the requestor to
+	// the SyncGroup.
+	JoinSyncGroupAtAdmin(ctx *context.T, call rpc.ServerCall, sgName string, joinerName string, myInfo nosql.SyncGroupMemberInfo) (SyncGroup, error)
+	// HaveBlob verifies that the peer has the requested blob, and if
+	// present, returns its size.
+	HaveBlob(ctx *context.T, call rpc.ServerCall, br nosql.BlobRef) (int64, error)
+	// FetchBlob fetches the requested blob.
+	FetchBlob(ctx *context.T, call SyncFetchBlobServerCall, br nosql.BlobRef) error
+	// Methods for incremental blob transfer. The transfer starts with the
+	// receiver making a FetchBlobRecipe call to the sender for a given
+	// BlobRef. The sender, in turn, sends the chunk hashes of all the
+	// chunks that make up the requested blob (blob recipe). The receiver
+	// looks up the chunk hashes in its local blob store, and identifies the
+	// missing ones. The receiver then fetches the missing chunks using a
+	// FetchChunks call from the sender. Finally, the receiver finishes the
+	// blob fetch by combining the chunks obtained over the network with the
+	// already available local chunks as per the blob recipe.
+	FetchBlobRecipe(ctx *context.T, call SyncFetchBlobRecipeServerCall, br nosql.BlobRef) error
+	FetchChunks(*context.T, SyncFetchChunksServerCall) error
+}
+
+// SyncServerStubMethods is the server interface containing
+// Sync methods, as expected by rpc.Server.
+// The only difference between this interface and SyncServerMethods
+// is the streaming methods.
+type SyncServerStubMethods interface {
+	// GetDeltas returns the responder's current generation vector and all
+	// the missing log records when compared to the initiator's generation
+	// vector. This process happens one Database at a time encompassing all
+	// the SyncGroups common to the initiator and the responder. For each
+	// Database, the initiator sends a DeltaReq. In response, the
+	// responder sends a "Start" DeltaResp record, all the missing log
+	// records, the responder's genvector, and a "Finish" DeltaResp
+	// record. The initiator parses the stream between a Start and a Finish
+	// record as the response to its DeltaReq, and then moves on to the
+	// next Database in common with this responder.
+	GetDeltas(ctx *context.T, call *SyncGetDeltasServerCallStub, initiator string) error
+	// PublishSyncGroup is typically invoked on a "central" peer to publish
+	// the SyncGroup.
+	PublishSyncGroup(ctx *context.T, call rpc.ServerCall, sg SyncGroup) error
+	// JoinSyncGroupAtAdmin is invoked by a prospective SyncGroup member's
+	// Syncbase on a SyncGroup admin. It checks whether the requestor is
+	// allowed to join the named SyncGroup, and if so, adds the requestor to
+	// the SyncGroup.
+	JoinSyncGroupAtAdmin(ctx *context.T, call rpc.ServerCall, sgName string, joinerName string, myInfo nosql.SyncGroupMemberInfo) (SyncGroup, error)
+	// HaveBlob verifies that the peer has the requested blob, and if
+	// present, returns its size.
+	HaveBlob(ctx *context.T, call rpc.ServerCall, br nosql.BlobRef) (int64, error)
+	// FetchBlob fetches the requested blob.
+	FetchBlob(ctx *context.T, call *SyncFetchBlobServerCallStub, br nosql.BlobRef) error
+	// Methods for incremental blob transfer. The transfer starts with the
+	// receiver making a FetchBlobRecipe call to the sender for a given
+	// BlobRef. The sender, in turn, sends the chunk hashes of all the
+	// chunks that make up the requested blob (blob recipe). The receiver
+	// looks up the chunk hashes in its local blob store, and identifies the
+	// missing ones. The receiver then fetches the missing chunks using a
+	// FetchChunks call from the sender. Finally, the receiver finishes the
+	// blob fetch by combining the chunks obtained over the network with the
+	// already available local chunks as per the blob recipe.
+	FetchBlobRecipe(ctx *context.T, call *SyncFetchBlobRecipeServerCallStub, br nosql.BlobRef) error
+	FetchChunks(*context.T, *SyncFetchChunksServerCallStub) error
+}
+
+// SyncServerStub adds universal methods to SyncServerStubMethods.
+type SyncServerStub interface {
+	SyncServerStubMethods
+	// Describe the Sync interfaces.
+	Describe__() []rpc.InterfaceDesc
+}
+
+// SyncServer returns a server stub for Sync.
+// It converts an implementation of SyncServerMethods into
+// an object that may be used by rpc.Server.
+func SyncServer(impl SyncServerMethods) SyncServerStub {
+	stub := implSyncServerStub{
+		impl: impl,
+	}
+	// Initialize GlobState; always check the stub itself first, to handle the
+	// case where the user has the Glob method defined in their VDL source.
+	if gs := rpc.NewGlobState(stub); gs != nil {
+		stub.gs = gs
+	} else if gs := rpc.NewGlobState(impl); gs != nil {
+		stub.gs = gs
+	}
+	return stub
+}
+
+type implSyncServerStub struct {
+	impl SyncServerMethods
+	gs   *rpc.GlobState
+}
+
+func (s implSyncServerStub) GetDeltas(ctx *context.T, call *SyncGetDeltasServerCallStub, i0 string) error {
+	return s.impl.GetDeltas(ctx, call, i0)
+}
+
+func (s implSyncServerStub) PublishSyncGroup(ctx *context.T, call rpc.ServerCall, i0 SyncGroup) error {
+	return s.impl.PublishSyncGroup(ctx, call, i0)
+}
+
+func (s implSyncServerStub) JoinSyncGroupAtAdmin(ctx *context.T, call rpc.ServerCall, i0 string, i1 string, i2 nosql.SyncGroupMemberInfo) (SyncGroup, error) {
+	return s.impl.JoinSyncGroupAtAdmin(ctx, call, i0, i1, i2)
+}
+
+func (s implSyncServerStub) HaveBlob(ctx *context.T, call rpc.ServerCall, i0 nosql.BlobRef) (int64, error) {
+	return s.impl.HaveBlob(ctx, call, i0)
+}
+
+func (s implSyncServerStub) FetchBlob(ctx *context.T, call *SyncFetchBlobServerCallStub, i0 nosql.BlobRef) error {
+	return s.impl.FetchBlob(ctx, call, i0)
+}
+
+func (s implSyncServerStub) FetchBlobRecipe(ctx *context.T, call *SyncFetchBlobRecipeServerCallStub, i0 nosql.BlobRef) error {
+	return s.impl.FetchBlobRecipe(ctx, call, i0)
+}
+
+func (s implSyncServerStub) FetchChunks(ctx *context.T, call *SyncFetchChunksServerCallStub) error {
+	return s.impl.FetchChunks(ctx, call)
+}
+
+func (s implSyncServerStub) Globber() *rpc.GlobState {
+	return s.gs
+}
+
+func (s implSyncServerStub) Describe__() []rpc.InterfaceDesc {
+	return []rpc.InterfaceDesc{SyncDesc}
+}
+
+// SyncDesc describes the Sync interface.
+var SyncDesc rpc.InterfaceDesc = descSync
+
+// descSync hides the desc to keep godoc clean.
+var descSync = rpc.InterfaceDesc{
+	Name:    "Sync",
+	PkgPath: "v.io/syncbase/x/ref/services/syncbase/server/interfaces",
+	Doc:     "// Sync defines methods for data exchange between Syncbases.\n// TODO(hpucha): Flesh this out further.",
+	Methods: []rpc.MethodDesc{
+		{
+			Name: "GetDeltas",
+			Doc:  "// GetDeltas returns the responder's current generation vector and all\n// the missing log records when compared to the initiator's generation\n// vector. This process happens one Database at a time encompassing all\n// the SyncGroups common to the initiator and the responder. For each\n// Database, the initiator sends a DeltaReq. In response, the\n// responder sends a \"Start\" DeltaResp record, all the missing log\n// records, the responder's genvector, and a \"Finish\" DeltaResp\n// record. The initiator parses the stream between a Start and a Finish\n// record as the response to its DeltaReq, and then moves on to the\n// next Database in common with this responder.",
+			InArgs: []rpc.ArgDesc{
+				{"initiator", ``}, // string
+			},
+			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))},
+		},
+		{
+			Name: "PublishSyncGroup",
+			Doc:  "// PublishSyncGroup is typically invoked on a \"central\" peer to publish\n// the SyncGroup.",
+			InArgs: []rpc.ArgDesc{
+				{"sg", ``}, // SyncGroup
+			},
+			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Write"))},
+		},
+		{
+			Name: "JoinSyncGroupAtAdmin",
+			Doc:  "// JoinSyncGroupAtAdmin is invoked by a prospective SyncGroup member's\n// Syncbase on a SyncGroup admin. It checks whether the requestor is\n// allowed to join the named SyncGroup, and if so, adds the requestor to\n// the SyncGroup.",
+			InArgs: []rpc.ArgDesc{
+				{"sgName", ``},     // string
+				{"joinerName", ``}, // string
+				{"myInfo", ``},     // nosql.SyncGroupMemberInfo
+			},
+			OutArgs: []rpc.ArgDesc{
+				{"", ``}, // SyncGroup
+			},
+			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))},
+		},
+		{
+			Name: "HaveBlob",
+			Doc:  "// HaveBlob verifies that the peer has the requested blob, and if\n// present, returns its size.",
+			InArgs: []rpc.ArgDesc{
+				{"br", ``}, // nosql.BlobRef
+			},
+			OutArgs: []rpc.ArgDesc{
+				{"", ``}, // int64
+			},
+		},
+		{
+			Name: "FetchBlob",
+			Doc:  "// FetchBlob fetches the requested blob.",
+			InArgs: []rpc.ArgDesc{
+				{"br", ``}, // nosql.BlobRef
+			},
+		},
+		{
+			Name: "FetchBlobRecipe",
+			Doc:  "// Methods for incremental blob transfer. The transfer starts with the\n// receiver making a FetchBlobRecipe call to the sender for a given\n// BlobRef. The sender, in turn, sends the chunk hashes of all the\n// chunks that make up the requested blob (blob recipe). The receiver\n// looks up the chunk hashes in its local blob store, and identifies the\n// missing ones. The receiver then fetches the missing chunks using a\n// FetchChunks call from the sender. Finally, the receiver finishes the\n// blob fetch by combining the chunks obtained over the network with the\n// already available local chunks as per the blob recipe.",
+			InArgs: []rpc.ArgDesc{
+				{"br", ``}, // nosql.BlobRef
+			},
+		},
+		{
+			Name: "FetchChunks",
+		},
+	},
+}
+
+// SyncGetDeltasServerStream is the server stream for Sync.GetDeltas.
+type SyncGetDeltasServerStream interface {
+	// RecvStream returns the receiver side of the Sync.GetDeltas server stream.
+	RecvStream() interface {
+		// Advance stages an item so that it may be retrieved via Value.  Returns
+		// true iff there is an item to retrieve.  Advance must be called before
+		// Value is called.  May block if an item is not available.
+		Advance() bool
+		// Value returns the item that was staged by Advance.  May panic if Advance
+		// returned false or was not called.  Never blocks.
+		Value() DeltaReq
+		// Err returns any error encountered by Advance.  Never blocks.
+		Err() error
+	}
+	// SendStream returns the send side of the Sync.GetDeltas server stream.
+	SendStream() interface {
+		// Send places the item onto the output stream.  Returns errors encountered
+		// while sending.  Blocks if there is no buffer space; will unblock when
+		// buffer space is available.
+		Send(item DeltaResp) error
+	}
+}
+
+// SyncGetDeltasServerCall represents the context passed to Sync.GetDeltas.
+type SyncGetDeltasServerCall interface {
+	rpc.ServerCall
+	SyncGetDeltasServerStream
+}
+
+// SyncGetDeltasServerCallStub is a wrapper that converts rpc.StreamServerCall into
+// a typesafe stub that implements SyncGetDeltasServerCall.
+type SyncGetDeltasServerCallStub struct {
+	rpc.StreamServerCall
+	valRecv DeltaReq
+	errRecv error
+}
+
+// Init initializes SyncGetDeltasServerCallStub from rpc.StreamServerCall.
+func (s *SyncGetDeltasServerCallStub) Init(call rpc.StreamServerCall) {
+	s.StreamServerCall = call
+}
+
+// RecvStream returns the receiver side of the Sync.GetDeltas server stream.
+func (s *SyncGetDeltasServerCallStub) RecvStream() interface {
+	Advance() bool
+	Value() DeltaReq
+	Err() error
+} {
+	return implSyncGetDeltasServerCallRecv{s}
+}
+
+type implSyncGetDeltasServerCallRecv struct {
+	s *SyncGetDeltasServerCallStub
+}
+
+func (s implSyncGetDeltasServerCallRecv) Advance() bool {
+	s.s.valRecv = DeltaReq{}
+	s.s.errRecv = s.s.Recv(&s.s.valRecv)
+	return s.s.errRecv == nil
+}
+func (s implSyncGetDeltasServerCallRecv) Value() DeltaReq {
+	return s.s.valRecv
+}
+func (s implSyncGetDeltasServerCallRecv) Err() error {
+	if s.s.errRecv == io.EOF {
+		return nil
+	}
+	return s.s.errRecv
+}
+
+// SendStream returns the send side of the Sync.GetDeltas server stream.
+func (s *SyncGetDeltasServerCallStub) SendStream() interface {
+	Send(item DeltaResp) error
+} {
+	return implSyncGetDeltasServerCallSend{s}
+}
+
+type implSyncGetDeltasServerCallSend struct {
+	s *SyncGetDeltasServerCallStub
+}
+
+func (s implSyncGetDeltasServerCallSend) Send(item DeltaResp) error {
+	return s.s.Send(item)
+}
+
+// SyncFetchBlobServerStream is the server stream for Sync.FetchBlob.
+type SyncFetchBlobServerStream interface {
+	// SendStream returns the send side of the Sync.FetchBlob server stream.
+	SendStream() interface {
+		// Send places the item onto the output stream.  Returns errors encountered
+		// while sending.  Blocks if there is no buffer space; will unblock when
+		// buffer space is available.
+		Send(item []byte) error
+	}
+}
+
+// SyncFetchBlobServerCall represents the context passed to Sync.FetchBlob.
+type SyncFetchBlobServerCall interface {
+	rpc.ServerCall
+	SyncFetchBlobServerStream
+}
+
+// SyncFetchBlobServerCallStub is a wrapper that converts rpc.StreamServerCall into
+// a typesafe stub that implements SyncFetchBlobServerCall.
+type SyncFetchBlobServerCallStub struct {
+	rpc.StreamServerCall
+}
+
+// Init initializes SyncFetchBlobServerCallStub from rpc.StreamServerCall.
+func (s *SyncFetchBlobServerCallStub) Init(call rpc.StreamServerCall) {
+	s.StreamServerCall = call
+}
+
+// SendStream returns the send side of the Sync.FetchBlob server stream.
+func (s *SyncFetchBlobServerCallStub) SendStream() interface {
+	Send(item []byte) error
+} {
+	return implSyncFetchBlobServerCallSend{s}
+}
+
+type implSyncFetchBlobServerCallSend struct {
+	s *SyncFetchBlobServerCallStub
+}
+
+func (s implSyncFetchBlobServerCallSend) Send(item []byte) error {
+	return s.s.Send(item)
+}
+
+// SyncFetchBlobRecipeServerStream is the server stream for Sync.FetchBlobRecipe.
+type SyncFetchBlobRecipeServerStream interface {
+	// SendStream returns the send side of the Sync.FetchBlobRecipe server stream.
+	SendStream() interface {
+		// Send places the item onto the output stream.  Returns errors encountered
+		// while sending.  Blocks if there is no buffer space; will unblock when
+		// buffer space is available.
+		Send(item ChunkHash) error
+	}
+}
+
+// SyncFetchBlobRecipeServerCall represents the context passed to Sync.FetchBlobRecipe.
+type SyncFetchBlobRecipeServerCall interface {
+	rpc.ServerCall
+	SyncFetchBlobRecipeServerStream
+}
+
+// SyncFetchBlobRecipeServerCallStub is a wrapper that converts rpc.StreamServerCall into
+// a typesafe stub that implements SyncFetchBlobRecipeServerCall.
+type SyncFetchBlobRecipeServerCallStub struct {
+	rpc.StreamServerCall
+}
+
+// Init initializes SyncFetchBlobRecipeServerCallStub from rpc.StreamServerCall.
+func (s *SyncFetchBlobRecipeServerCallStub) Init(call rpc.StreamServerCall) {
+	s.StreamServerCall = call
+}
+
+// SendStream returns the send side of the Sync.FetchBlobRecipe server stream.
+func (s *SyncFetchBlobRecipeServerCallStub) SendStream() interface {
+	Send(item ChunkHash) error
+} {
+	return implSyncFetchBlobRecipeServerCallSend{s}
+}
+
+type implSyncFetchBlobRecipeServerCallSend struct {
+	s *SyncFetchBlobRecipeServerCallStub
+}
+
+func (s implSyncFetchBlobRecipeServerCallSend) Send(item ChunkHash) error {
+	return s.s.Send(item)
+}
+
+// SyncFetchChunksServerStream is the server stream for Sync.FetchChunks.
+type SyncFetchChunksServerStream interface {
+	// RecvStream returns the receiver side of the Sync.FetchChunks server stream.
+	RecvStream() interface {
+		// Advance stages an item so that it may be retrieved via Value.  Returns
+		// true iff there is an item to retrieve.  Advance must be called before
+		// Value is called.  May block if an item is not available.
+		Advance() bool
+		// Value returns the item that was staged by Advance.  May panic if Advance
+		// returned false or was not called.  Never blocks.
+		Value() ChunkHash
+		// Err returns any error encountered by Advance.  Never blocks.
+		Err() error
+	}
+	// SendStream returns the send side of the Sync.FetchChunks server stream.
+	SendStream() interface {
+		// Send places the item onto the output stream.  Returns errors encountered
+		// while sending.  Blocks if there is no buffer space; will unblock when
+		// buffer space is available.
+		Send(item ChunkData) error
+	}
+}
+
+// SyncFetchChunksServerCall represents the context passed to Sync.FetchChunks.
+type SyncFetchChunksServerCall interface {
+	rpc.ServerCall
+	SyncFetchChunksServerStream
+}
+
+// SyncFetchChunksServerCallStub is a wrapper that converts rpc.StreamServerCall into
+// a typesafe stub that implements SyncFetchChunksServerCall.
+type SyncFetchChunksServerCallStub struct {
+	rpc.StreamServerCall
+	valRecv ChunkHash
+	errRecv error
+}
+
+// Init initializes SyncFetchChunksServerCallStub from rpc.StreamServerCall.
+func (s *SyncFetchChunksServerCallStub) Init(call rpc.StreamServerCall) {
+	s.StreamServerCall = call
+}
+
+// RecvStream returns the receiver side of the Sync.FetchChunks server stream.
+func (s *SyncFetchChunksServerCallStub) RecvStream() interface {
+	Advance() bool
+	Value() ChunkHash
+	Err() error
+} {
+	return implSyncFetchChunksServerCallRecv{s}
+}
+
+type implSyncFetchChunksServerCallRecv struct {
+	s *SyncFetchChunksServerCallStub
+}
+
+func (s implSyncFetchChunksServerCallRecv) Advance() bool {
+	s.s.valRecv = ChunkHash{}
+	s.s.errRecv = s.s.Recv(&s.s.valRecv)
+	return s.s.errRecv == nil
+}
+func (s implSyncFetchChunksServerCallRecv) Value() ChunkHash {
+	return s.s.valRecv
+}
+func (s implSyncFetchChunksServerCallRecv) Err() error {
+	if s.s.errRecv == io.EOF {
+		return nil
+	}
+	return s.s.errRecv
+}
+
+// SendStream returns the send side of the Sync.FetchChunks server stream.
+func (s *SyncFetchChunksServerCallStub) SendStream() interface {
+	Send(item ChunkData) error
+} {
+	return implSyncFetchChunksServerCallSend{s}
+}
+
+type implSyncFetchChunksServerCallSend struct {
+	s *SyncFetchChunksServerCallStub
+}
+
+func (s implSyncFetchChunksServerCallSend) Send(item ChunkData) error {
+	return s.s.Send(item)
+}
diff --git a/services/syncbase/server/interfaces/sync_types.vdl b/services/syncbase/server/interfaces/sync_types.vdl
new file mode 100644
index 0000000..324928b
--- /dev/null
+++ b/services/syncbase/server/interfaces/sync_types.vdl
@@ -0,0 +1,129 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package interfaces
+
+import (
+	"time"
+
+	wire "v.io/syncbase/v23/services/syncbase/nosql"
+)
+
+const (
+	NoGroupId = GroupId(0)
+)
+
+// TODO(hpucha): These are not final yet. This is an intermediate step.
+
+const (
+        // NodeRec type log record adds a new node in the dag.
+        NodeRec = byte(0)
+
+        // LinkRec type log record adds a new link in the dag. Link records are
+        // added when a conflict is resolved by picking the local or the remote
+        // version as the resolution of a conflict, instead of creating a new
+        // version.
+        LinkRec = byte(1)
+)
+
+// PrefixGenVector is the generation vector for a data prefix, which maps each
+// device id to its last locally known generation in the scope of that prefix.
+type PrefixGenVector map[uint64]uint64
+
+// GenVector is the generation vector for a Database, and maps prefixes to their
+// generation vectors. Note that the prefixes in a GenVector are relative to the
+// the Application and Database name.
+type GenVector map[string]PrefixGenVector
+
+// LogRecMetadata represents the metadata of a single log record that is
+// exchanged between two peers. Each log record represents a change made to an
+// object in the store.
+//
+// TODO(hpucha): Add readset/scanset. Look into sending tx metadata only once
+// per transaction.
+type LogRecMetadata struct {
+        // Log related information.
+        Id        uint64 // device id that created the log record.
+	Gen       uint64 // generation number for the log record.
+	RecType   byte   // type of log record.
+
+        // Object related information.
+
+	// Id of the object that was updated. This id is relative to Application
+	// and Database names and is the store key for a particular row in a
+	// table.
+        ObjId       string
+        CurVers     string      // current version number of the object.
+        Parents     []string    // 0, 1 or 2 parent versions that the current version is derived from.
+	UpdTime     time.Time   // timestamp when the update is generated.
+	Delete      bool        // indicates whether the update resulted in object being deleted from the store.
+	BatchId     uint64      // unique id of the Batch this update belongs to.
+	BatchCount  uint64      // number of objects in the Batch.
+}
+
+// LogRec represents the on-wire representation of an entire log record: its
+// metadata and data. Value is the actual value of a store object.
+type LogRec struct {
+	Metadata LogRecMetadata
+	Value    []byte
+}
+
+// GroupId is a globally unique SyncGroup ID.
+type GroupId uint64
+
+// Possible states for a SyncGroup.
+type SyncGroupStatus enum {
+	// Indicates that a SyncGroup is operational, but publishing to the
+	// remote server is pending.
+	PublishPending
+
+	// Indicates that the SyncGroup is operational, but the publishing
+	// failed.
+	PublishRejected
+
+	// Indicates that the SyncGroup is operational and published.
+	Running
+}
+
+// SyncGroup contains the state of a SyncGroup object.
+type SyncGroup struct {
+	Id          GroupId                             // globally unique identifier generated by Syncbase
+	Name        string                              // globally unique Vanadium name chosen by app
+	SpecVersion string                              // version on SyncGroup spec for concurrency control
+	Spec        wire.SyncGroupSpec                  // app-given specification
+	Creator     string                              // Creator's Vanadium name
+	AppName     string                              // Globally unique App name
+	DbName      string                              // Database name within the App
+	Status      SyncGroupStatus                     // Status of the SyncGroup
+	Joiners     map[string]wire.SyncGroupMemberInfo // map of joiners to their metadata
+}
+
+// DeltaReq contains the initiator's genvector and the set of SyncGroups it is
+// interested in within a Database (specified by the AppName/DbName) when
+// requesting deltas for that Database.
+type DeltaReq struct {
+	AppName string
+	DbName string
+	SgIds   set[GroupId]
+	InitVec GenVector
+}
+
+// DeltaResp contains the responder's genvector or the missing log records
+// returned in response to an initiator's request for deltas for a Database.
+type DeltaResp union {
+	Start   bool
+	Finish  bool
+	Rec     LogRec
+	RespVec GenVector
+}
+
+// ChunkHash contains the hash of a chunk that is part of a blob's recipe.
+type ChunkHash struct {
+	Hash []byte
+}
+
+// ChunkData contains the data of a chunk.
+type ChunkData struct {
+	Data []byte
+}
diff --git a/services/syncbase/server/interfaces/sync_types.vdl.go b/services/syncbase/server/interfaces/sync_types.vdl.go
new file mode 100644
index 0000000..8ef80a8
--- /dev/null
+++ b/services/syncbase/server/interfaces/sync_types.vdl.go
@@ -0,0 +1,274 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file was auto-generated by the vanadium vdl tool.
+// Source: sync_types.vdl
+
+package interfaces
+
+import (
+	// VDL system imports
+	"fmt"
+	"v.io/v23/vdl"
+
+	// VDL user imports
+	"time"
+	"v.io/syncbase/v23/services/syncbase/nosql"
+	_ "v.io/v23/vdlroot/time"
+)
+
+// PrefixGenVector is the generation vector for a data prefix, which maps each
+// device id to its last locally known generation in the scope of that prefix.
+type PrefixGenVector map[uint64]uint64
+
+func (PrefixGenVector) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/server/interfaces.PrefixGenVector"`
+}) {
+}
+
+// GenVector is the generation vector for a Database, and maps prefixes to their
+// generation vectors. Note that the prefixes in a GenVector are relative to the
+// the Application and Database name.
+type GenVector map[string]PrefixGenVector
+
+func (GenVector) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/server/interfaces.GenVector"`
+}) {
+}
+
+// LogRecMetadata represents the metadata of a single log record that is
+// exchanged between two peers. Each log record represents a change made to an
+// object in the store.
+//
+// TODO(hpucha): Add readset/scanset. Look into sending tx metadata only once
+// per transaction.
+type LogRecMetadata struct {
+	// Log related information.
+	Id      uint64 // device id that created the log record.
+	Gen     uint64 // generation number for the log record.
+	RecType byte   // type of log record.
+	// Id of the object that was updated. This id is relative to Application
+	// and Database names and is the store key for a particular row in a
+	// table.
+	ObjId      string
+	CurVers    string    // current version number of the object.
+	Parents    []string  // 0, 1 or 2 parent versions that the current version is derived from.
+	UpdTime    time.Time // timestamp when the update is generated.
+	Delete     bool      // indicates whether the update resulted in object being deleted from the store.
+	BatchId    uint64    // unique id of the Batch this update belongs to.
+	BatchCount uint64    // number of objects in the Batch.
+}
+
+func (LogRecMetadata) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/server/interfaces.LogRecMetadata"`
+}) {
+}
+
+// LogRec represents the on-wire representation of an entire log record: its
+// metadata and data. Value is the actual value of a store object.
+type LogRec struct {
+	Metadata LogRecMetadata
+	Value    []byte
+}
+
+func (LogRec) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/server/interfaces.LogRec"`
+}) {
+}
+
+// GroupId is a globally unique SyncGroup ID.
+type GroupId uint64
+
+func (GroupId) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/server/interfaces.GroupId"`
+}) {
+}
+
+// Possible states for a SyncGroup.
+type SyncGroupStatus int
+
+const (
+	SyncGroupStatusPublishPending SyncGroupStatus = iota
+	SyncGroupStatusPublishRejected
+	SyncGroupStatusRunning
+)
+
+// SyncGroupStatusAll holds all labels for SyncGroupStatus.
+var SyncGroupStatusAll = [...]SyncGroupStatus{SyncGroupStatusPublishPending, SyncGroupStatusPublishRejected, SyncGroupStatusRunning}
+
+// SyncGroupStatusFromString creates a SyncGroupStatus from a string label.
+func SyncGroupStatusFromString(label string) (x SyncGroupStatus, err error) {
+	err = x.Set(label)
+	return
+}
+
+// Set assigns label to x.
+func (x *SyncGroupStatus) Set(label string) error {
+	switch label {
+	case "PublishPending", "publishpending":
+		*x = SyncGroupStatusPublishPending
+		return nil
+	case "PublishRejected", "publishrejected":
+		*x = SyncGroupStatusPublishRejected
+		return nil
+	case "Running", "running":
+		*x = SyncGroupStatusRunning
+		return nil
+	}
+	*x = -1
+	return fmt.Errorf("unknown label %q in interfaces.SyncGroupStatus", label)
+}
+
+// String returns the string label of x.
+func (x SyncGroupStatus) String() string {
+	switch x {
+	case SyncGroupStatusPublishPending:
+		return "PublishPending"
+	case SyncGroupStatusPublishRejected:
+		return "PublishRejected"
+	case SyncGroupStatusRunning:
+		return "Running"
+	}
+	return ""
+}
+
+func (SyncGroupStatus) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/server/interfaces.SyncGroupStatus"`
+	Enum struct{ PublishPending, PublishRejected, Running string }
+}) {
+}
+
+// SyncGroup contains the state of a SyncGroup object.
+type SyncGroup struct {
+	Id          GroupId                              // globally unique identifier generated by Syncbase
+	Name        string                               // globally unique Vanadium name chosen by app
+	SpecVersion string                               // version on SyncGroup spec for concurrency control
+	Spec        nosql.SyncGroupSpec                  // app-given specification
+	Creator     string                               // Creator's Vanadium name
+	AppName     string                               // Globally unique App name
+	DbName      string                               // Database name within the App
+	Status      SyncGroupStatus                      // Status of the SyncGroup
+	Joiners     map[string]nosql.SyncGroupMemberInfo // map of joiners to their metadata
+}
+
+func (SyncGroup) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/server/interfaces.SyncGroup"`
+}) {
+}
+
+// DeltaReq contains the initiator's genvector and the set of SyncGroups it is
+// interested in within a Database (specified by the AppName/DbName) when
+// requesting deltas for that Database.
+type DeltaReq struct {
+	AppName string
+	DbName  string
+	SgIds   map[GroupId]struct{}
+	InitVec GenVector
+}
+
+func (DeltaReq) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/server/interfaces.DeltaReq"`
+}) {
+}
+
+type (
+	// DeltaResp represents any single field of the DeltaResp union type.
+	//
+	// DeltaResp contains the responder's genvector or the missing log records
+	// returned in response to an initiator's request for deltas for a Database.
+	DeltaResp interface {
+		// Index returns the field index.
+		Index() int
+		// Interface returns the field value as an interface.
+		Interface() interface{}
+		// Name returns the field name.
+		Name() string
+		// __VDLReflect describes the DeltaResp union type.
+		__VDLReflect(__DeltaRespReflect)
+	}
+	// DeltaRespStart represents field Start of the DeltaResp union type.
+	DeltaRespStart struct{ Value bool }
+	// DeltaRespFinish represents field Finish of the DeltaResp union type.
+	DeltaRespFinish struct{ Value bool }
+	// DeltaRespRec represents field Rec of the DeltaResp union type.
+	DeltaRespRec struct{ Value LogRec }
+	// DeltaRespRespVec represents field RespVec of the DeltaResp union type.
+	DeltaRespRespVec struct{ Value GenVector }
+	// __DeltaRespReflect describes the DeltaResp union type.
+	__DeltaRespReflect struct {
+		Name  string `vdl:"v.io/syncbase/x/ref/services/syncbase/server/interfaces.DeltaResp"`
+		Type  DeltaResp
+		Union struct {
+			Start   DeltaRespStart
+			Finish  DeltaRespFinish
+			Rec     DeltaRespRec
+			RespVec DeltaRespRespVec
+		}
+	}
+)
+
+func (x DeltaRespStart) Index() int                      { return 0 }
+func (x DeltaRespStart) Interface() interface{}          { return x.Value }
+func (x DeltaRespStart) Name() string                    { return "Start" }
+func (x DeltaRespStart) __VDLReflect(__DeltaRespReflect) {}
+
+func (x DeltaRespFinish) Index() int                      { return 1 }
+func (x DeltaRespFinish) Interface() interface{}          { return x.Value }
+func (x DeltaRespFinish) Name() string                    { return "Finish" }
+func (x DeltaRespFinish) __VDLReflect(__DeltaRespReflect) {}
+
+func (x DeltaRespRec) Index() int                      { return 2 }
+func (x DeltaRespRec) Interface() interface{}          { return x.Value }
+func (x DeltaRespRec) Name() string                    { return "Rec" }
+func (x DeltaRespRec) __VDLReflect(__DeltaRespReflect) {}
+
+func (x DeltaRespRespVec) Index() int                      { return 3 }
+func (x DeltaRespRespVec) Interface() interface{}          { return x.Value }
+func (x DeltaRespRespVec) Name() string                    { return "RespVec" }
+func (x DeltaRespRespVec) __VDLReflect(__DeltaRespReflect) {}
+
+// ChunkHash contains the hash of a chunk that is part of a blob's recipe.
+type ChunkHash struct {
+	Hash []byte
+}
+
+func (ChunkHash) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/server/interfaces.ChunkHash"`
+}) {
+}
+
+// ChunkData contains the data of a chunk.
+type ChunkData struct {
+	Data []byte
+}
+
+func (ChunkData) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/server/interfaces.ChunkData"`
+}) {
+}
+
+func init() {
+	vdl.Register((*PrefixGenVector)(nil))
+	vdl.Register((*GenVector)(nil))
+	vdl.Register((*LogRecMetadata)(nil))
+	vdl.Register((*LogRec)(nil))
+	vdl.Register((*GroupId)(nil))
+	vdl.Register((*SyncGroupStatus)(nil))
+	vdl.Register((*SyncGroup)(nil))
+	vdl.Register((*DeltaReq)(nil))
+	vdl.Register((*DeltaResp)(nil))
+	vdl.Register((*ChunkHash)(nil))
+	vdl.Register((*ChunkData)(nil))
+}
+
+const NoGroupId = GroupId(0)
+
+// NodeRec type log record adds a new node in the dag.
+const NodeRec = byte(0)
+
+// LinkRec type log record adds a new link in the dag. Link records are
+// added when a conflict is resolved by picking the local or the remote
+// version as the resolution of a conflict, instead of creating a new
+// version.
+const LinkRec = byte(1)
diff --git a/services/syncbase/server/mojo_call.go b/services/syncbase/server/mojo_call.go
new file mode 100644
index 0000000..d0b9dac
--- /dev/null
+++ b/services/syncbase/server/mojo_call.go
@@ -0,0 +1,68 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build mojo
+
+package server
+
+import (
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/naming"
+	"v.io/v23/rpc"
+	"v.io/v23/security"
+)
+
+type mojoServerCall struct {
+	sec    security.Call
+	srv    rpc.Server
+	suffix string
+}
+
+// TODO(sadovsky): Synthesize endpoints and discharges as needed.
+func newMojoServerCall(ctx *context.T, srv rpc.Server, suffix string, method rpc.MethodDesc) rpc.ServerCall {
+	p := v23.GetPrincipal(ctx)
+	// HACK: For now, we set the remote (client, i.e. Mojo app) blessing to be the
+	// same as the local (server, i.e. Syncbase Mojo service) blessing.
+	// TODO(sadovsky): Eliminate this hack.
+	blessings := p.BlessingStore().Default()
+	return &mojoServerCall{
+		sec: security.NewCall(&security.CallParams{
+			Method:          method.Name,
+			MethodTags:      method.Tags,
+			Suffix:          suffix,
+			LocalPrincipal:  p,
+			LocalBlessings:  blessings,
+			RemoteBlessings: blessings,
+		}),
+		srv:    srv,
+		suffix: suffix,
+	}
+}
+
+var _ rpc.ServerCall = (*mojoServerCall)(nil)
+
+func (call *mojoServerCall) Security() security.Call {
+	return call.sec
+}
+
+func (call *mojoServerCall) Suffix() string {
+	return call.suffix
+}
+
+func (call *mojoServerCall) LocalEndpoint() naming.Endpoint {
+	return call.sec.LocalEndpoint()
+}
+
+func (call *mojoServerCall) RemoteEndpoint() naming.Endpoint {
+	return call.sec.RemoteEndpoint()
+}
+
+func (call *mojoServerCall) GrantedBlessings() security.Blessings {
+	return security.Blessings{}
+}
+
+func (call *mojoServerCall) Server() rpc.Server {
+	return call.srv
+}
diff --git a/services/syncbase/server/mojo_impl.go b/services/syncbase/server/mojo_impl.go
new file mode 100644
index 0000000..973bf7d
--- /dev/null
+++ b/services/syncbase/server/mojo_impl.go
@@ -0,0 +1,535 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build mojo
+
+// Implementation of Syncbase Mojo stubs. Our strategy is to translate Mojo stub
+// requests into Vanadium stub requests, and Vanadium stub responses into Mojo
+// stub responses. As part of this procedure, we synthesize "fake" ctx and call
+// objects to pass to the Vanadium stubs.
+
+package server
+
+import (
+	"bytes"
+	"fmt"
+	"strings"
+
+	"mojo/public/go/bindings"
+
+	mojom "mojom/syncbase"
+	wire "v.io/syncbase/v23/services/syncbase"
+	nosqlwire "v.io/syncbase/v23/services/syncbase/nosql"
+	"v.io/v23/context"
+	"v.io/v23/rpc"
+	"v.io/v23/security/access"
+	"v.io/v23/services/permissions"
+	"v.io/v23/verror"
+	"v.io/v23/vtrace"
+)
+
+const NoSchema int32 = -1
+
+type mojoImpl struct {
+	ctx  *context.T
+	srv  rpc.Server
+	disp rpc.Dispatcher
+}
+
+func NewMojoImpl(ctx *context.T, srv rpc.Server, disp rpc.Dispatcher) *mojoImpl {
+	return &mojoImpl{ctx: ctx, srv: srv, disp: disp}
+}
+
+func methodDesc(desc rpc.InterfaceDesc, name string) rpc.MethodDesc {
+	for _, method := range desc.Methods {
+		if method.Name == name {
+			return method
+		}
+	}
+	panic(fmt.Sprintf("unknown method: %s.%s", desc.Name, name))
+}
+
+func (m *mojoImpl) newCtxCall(suffix string, method rpc.MethodDesc) (*context.T, rpc.ServerCall) {
+	ctx, _ := vtrace.WithNewTrace(m.ctx)
+	return ctx, newMojoServerCall(ctx, m.srv, suffix, method)
+}
+
+////////////////////////////////////////
+// Struct converters
+
+func toMojoError(err error) mojom.Error {
+	if err == nil {
+		return mojom.Error{}
+	}
+	return mojom.Error{
+		Id:         string(verror.ErrorID(err)),
+		ActionCode: uint32(verror.Action(err)),
+		Msg:        err.Error(),
+	}
+}
+
+func toV23Perms(mPerms mojom.Perms) (access.Permissions, error) {
+	return access.ReadPermissions(strings.NewReader(mPerms.Json))
+}
+
+func toMojoPerms(vPerms access.Permissions) (mojom.Perms, error) {
+	b := new(bytes.Buffer)
+	if err := access.WritePermissions(b, vPerms); err != nil {
+		return mojom.Perms{}, err
+	}
+	return mojom.Perms{Json: b.String()}, nil
+}
+
+////////////////////////////////////////
+// Stub getters
+
+func (m *mojoImpl) lookupAndAuthorize(ctx *context.T, call rpc.ServerCall, suffix string) (interface{}, error) {
+	resInt, auth, err := m.disp.Lookup(ctx, suffix)
+	if err != nil {
+		return nil, err
+	}
+	if err := auth.Authorize(ctx, call.Security()); err != nil {
+		return nil, verror.New(verror.ErrNoAccess, ctx, err)
+	}
+	return resInt, nil
+}
+
+func (m *mojoImpl) getService(ctx *context.T, call rpc.ServerCall) (wire.ServiceServerStubMethods, error) {
+	resInt, err := m.lookupAndAuthorize(ctx, call, "")
+	if err != nil {
+		return nil, err
+	}
+	if res, ok := resInt.(wire.ServiceServerStubMethods); !ok {
+		return nil, verror.NewErrInternal(ctx)
+	} else {
+		return res, nil
+	}
+}
+
+func (m *mojoImpl) getApp(ctx *context.T, call rpc.ServerCall, name string) (wire.AppServerStubMethods, error) {
+	resInt, err := m.lookupAndAuthorize(ctx, call, name)
+	if err != nil {
+		return nil, err
+	}
+	if res, ok := resInt.(wire.AppServerStubMethods); !ok {
+		return nil, verror.NewErrInternal(ctx)
+	} else {
+		return res, nil
+	}
+}
+
+func (m *mojoImpl) getDb(ctx *context.T, call rpc.ServerCall, name string) (nosqlwire.DatabaseServerStubMethods, error) {
+	resInt, err := m.lookupAndAuthorize(ctx, call, name)
+	if err != nil {
+		return nil, err
+	}
+	if res, ok := resInt.(nosqlwire.DatabaseServerStubMethods); !ok {
+		return nil, verror.NewErrInternal(ctx)
+	} else {
+		return res, nil
+	}
+}
+
+func (m *mojoImpl) getTable(ctx *context.T, call rpc.ServerCall, name string) (nosqlwire.TableServerStubMethods, error) {
+	resInt, err := m.lookupAndAuthorize(ctx, call, name)
+	if err != nil {
+		return nil, err
+	}
+	if res, ok := resInt.(nosqlwire.TableServerStubMethods); !ok {
+		return nil, verror.NewErrInternal(ctx)
+	} else {
+		return res, nil
+	}
+}
+
+func (m *mojoImpl) getRow(ctx *context.T, call rpc.ServerCall, name string) (nosqlwire.RowServerStubMethods, error) {
+	resInt, err := m.lookupAndAuthorize(ctx, call, name)
+	if err != nil {
+		return nil, err
+	}
+	if res, ok := resInt.(nosqlwire.RowServerStubMethods); !ok {
+		return nil, verror.NewErrInternal(ctx)
+	} else {
+		return res, nil
+	}
+}
+
+////////////////////////////////////////
+// Service
+
+// TODO(sadovsky): All stub implementations return a nil error (the last return
+// value), since that error doesn't make it back to the IPC client. Chat with
+// rogulenko@ about whether we should change the Go Mojo stub generator to drop
+// these errors.
+func (m *mojoImpl) ServiceGetPermissions() (mojom.Error, mojom.Perms, string, error) {
+	ctx, call := m.newCtxCall("", methodDesc(permissions.ObjectDesc, "GetPermissions"))
+	stub, err := m.getService(ctx, call)
+	if err != nil {
+		return toMojoError(err), mojom.Perms{}, "", nil
+	}
+	vPerms, version, err := stub.GetPermissions(ctx, call)
+	if err != nil {
+		return toMojoError(err), mojom.Perms{}, "", nil
+	}
+	mPerms, err := toMojoPerms(vPerms)
+	if err != nil {
+		return toMojoError(err), mojom.Perms{}, "", nil
+	}
+	return toMojoError(err), mPerms, version, nil
+}
+
+func (m *mojoImpl) ServiceSetPermissions(mPerms mojom.Perms, version string) (mojom.Error, error) {
+	ctx, call := m.newCtxCall("", methodDesc(permissions.ObjectDesc, "SetPermissions"))
+	stub, err := m.getService(ctx, call)
+	if err != nil {
+		return toMojoError(err), nil
+	}
+	vPerms, err := toV23Perms(mPerms)
+	if err != nil {
+		return toMojoError(err), nil
+	}
+	err = stub.SetPermissions(ctx, call, vPerms, version)
+	return toMojoError(err), nil
+}
+
+////////////////////////////////////////
+// App
+
+func (m *mojoImpl) AppCreate(name string, mPerms mojom.Perms) (mojom.Error, error) {
+	ctx, call := m.newCtxCall(name, methodDesc(wire.AppDesc, "Create"))
+	stub, err := m.getApp(ctx, call, name)
+	if err != nil {
+		return toMojoError(err), nil
+	}
+	vPerms, err := toV23Perms(mPerms)
+	if err != nil {
+		return toMojoError(err), nil
+	}
+	err = stub.Create(ctx, call, vPerms)
+	return toMojoError(err), nil
+}
+
+func (m *mojoImpl) AppDelete(name string) (mojom.Error, error) {
+	ctx, call := m.newCtxCall(name, methodDesc(wire.AppDesc, "Delete"))
+	stub, err := m.getApp(ctx, call, name)
+	if err != nil {
+		return toMojoError(err), nil
+	}
+	err = stub.Delete(ctx, call)
+	return toMojoError(err), nil
+}
+
+func (m *mojoImpl) AppExists(name string) (mojom.Error, bool, error) {
+	ctx, call := m.newCtxCall(name, methodDesc(wire.AppDesc, "Exists"))
+	stub, err := m.getApp(ctx, call, name)
+	if err != nil {
+		return toMojoError(err), false, nil
+	}
+	exists, err := stub.Exists(ctx, call)
+	return toMojoError(err), exists, nil
+}
+
+func (m *mojoImpl) AppGetPermissions(name string) (mojom.Error, mojom.Perms, string, error) {
+	ctx, call := m.newCtxCall(name, methodDesc(permissions.ObjectDesc, "GetPermissions"))
+	stub, err := m.getApp(ctx, call, name)
+	if err != nil {
+		return toMojoError(err), mojom.Perms{}, "", nil
+	}
+	vPerms, version, err := stub.GetPermissions(ctx, call)
+	if err != nil {
+		return toMojoError(err), mojom.Perms{}, "", nil
+	}
+	mPerms, err := toMojoPerms(vPerms)
+	if err != nil {
+		return toMojoError(err), mojom.Perms{}, "", nil
+	}
+	return toMojoError(err), mPerms, version, nil
+}
+
+func (m *mojoImpl) AppSetPermissions(name string, mPerms mojom.Perms, version string) (mojom.Error, error) {
+	ctx, call := m.newCtxCall(name, methodDesc(permissions.ObjectDesc, "SetPermissions"))
+	stub, err := m.getApp(ctx, call, name)
+	if err != nil {
+		return toMojoError(err), nil
+	}
+	vPerms, err := toV23Perms(mPerms)
+	if err != nil {
+		return toMojoError(err), nil
+	}
+	err = stub.SetPermissions(ctx, call, vPerms, version)
+	return toMojoError(err), nil
+}
+
+////////////////////////////////////////
+// nosql.Database
+
+func (m *mojoImpl) DbCreate(name string, mPerms mojom.Perms) (mojom.Error, error) {
+	ctx, call := m.newCtxCall(name, methodDesc(nosqlwire.DatabaseDesc, "Create"))
+	stub, err := m.getDb(ctx, call, name)
+	if err != nil {
+		return toMojoError(err), nil
+	}
+	vPerms, err := toV23Perms(mPerms)
+	if err != nil {
+		return toMojoError(err), nil
+	}
+	err = stub.Create(ctx, call, nil, vPerms)
+	return toMojoError(err), nil
+}
+
+func (m *mojoImpl) DbDelete(name string) (mojom.Error, error) {
+	ctx, call := m.newCtxCall(name, methodDesc(nosqlwire.DatabaseDesc, "Delete"))
+	stub, err := m.getDb(ctx, call, name)
+	if err != nil {
+		return toMojoError(err), nil
+	}
+	err = stub.Delete(ctx, call, NoSchema)
+	return toMojoError(err), nil
+}
+
+func (m *mojoImpl) DbExists(name string) (mojom.Error, bool, error) {
+	ctx, call := m.newCtxCall(name, methodDesc(nosqlwire.DatabaseDesc, "Exists"))
+	stub, err := m.getDb(ctx, call, name)
+	if err != nil {
+		return toMojoError(err), false, nil
+	}
+	exists, err := stub.Exists(ctx, call, NoSchema)
+	return toMojoError(err), exists, nil
+}
+
+func (m *mojoImpl) DbExec(name string, query string, stream mojom.ExecStream_Pointer) (mojom.Error, error) {
+	return mojom.Error{}, nil
+}
+
+func (m *mojoImpl) DbBeginBatch(name string, bo *mojom.BatchOptions) (mojom.Error, string, error) {
+	return mojom.Error{}, "", nil
+}
+
+func (m *mojoImpl) DbCommit(name string) (mojom.Error, error) {
+	return mojom.Error{}, nil
+}
+
+func (m *mojoImpl) DbAbort(name string) (mojom.Error, error) {
+	return mojom.Error{}, nil
+}
+
+func (m *mojoImpl) DbGetPermissions(name string) (mojom.Error, mojom.Perms, string, error) {
+	ctx, call := m.newCtxCall(name, methodDesc(permissions.ObjectDesc, "GetPermissions"))
+	stub, err := m.getDb(ctx, call, name)
+	if err != nil {
+		return toMojoError(err), mojom.Perms{}, "", nil
+	}
+	vPerms, version, err := stub.GetPermissions(ctx, call)
+	if err != nil {
+		return toMojoError(err), mojom.Perms{}, "", nil
+	}
+	mPerms, err := toMojoPerms(vPerms)
+	if err != nil {
+		return toMojoError(err), mojom.Perms{}, "", nil
+	}
+	return toMojoError(err), mPerms, version, nil
+}
+
+func (m *mojoImpl) DbSetPermissions(name string, mPerms mojom.Perms, version string) (mojom.Error, error) {
+	ctx, call := m.newCtxCall(name, methodDesc(permissions.ObjectDesc, "SetPermissions"))
+	stub, err := m.getDb(ctx, call, name)
+	if err != nil {
+		return toMojoError(err), nil
+	}
+	vPerms, err := toV23Perms(mPerms)
+	if err != nil {
+		return toMojoError(err), nil
+	}
+	err = stub.SetPermissions(ctx, call, vPerms, version)
+	return toMojoError(err), nil
+}
+
+////////////////////////////////////////
+// nosql.Database:SyncGroupManager
+
+func (m *mojoImpl) DbGetSyncGroupNames(name string) (mojom.Error, []string, error) {
+	return mojom.Error{}, nil, nil
+}
+
+func (m *mojoImpl) DbCreateSyncGroup(name, sgName string, spec mojom.SyncGroupSpec, myInfo mojom.SyncGroupMemberInfo) (mojom.Error, error) {
+	return mojom.Error{}, nil
+}
+
+func (m *mojoImpl) DbJoinSyncGroup(name, sgName string, myInfo mojom.SyncGroupMemberInfo) (mojom.Error, error) {
+	return mojom.Error{}, nil
+}
+
+func (m *mojoImpl) DbLeaveSyncGroup(name, sgName string) (mojom.Error, error) {
+	return mojom.Error{}, nil
+}
+
+func (m *mojoImpl) DbDestroySyncGroup(name, sgName string) (mojom.Error, error) {
+	return mojom.Error{}, nil
+}
+
+func (m *mojoImpl) DbEjectFromSyncGroup(name, sgName string, member string) (mojom.Error, error) {
+	return mojom.Error{}, nil
+}
+
+func (m *mojoImpl) DbGetSyncGroupSpec(name, sgName string) (mojom.Error, mojom.SyncGroupSpec, string, error) {
+	return mojom.Error{}, mojom.SyncGroupSpec{}, "", nil
+}
+
+func (m *mojoImpl) DbSetSyncGroupSpec(name, sgName string, spec mojom.SyncGroupSpec, version string) (mojom.Error, error) {
+	return mojom.Error{}, nil
+}
+
+func (m *mojoImpl) DbGetSyncGroupMembers(name, sgName string) (mojom.Error, map[string]mojom.SyncGroupMemberInfo, error) {
+	return mojom.Error{}, nil, nil
+}
+
+////////////////////////////////////////
+// nosql.Table
+
+func (m *mojoImpl) TableCreate(name string, mPerms mojom.Perms) (mojom.Error, error) {
+	ctx, call := m.newCtxCall(name, methodDesc(nosqlwire.TableDesc, "Create"))
+	stub, err := m.getTable(ctx, call, name)
+	if err != nil {
+		return toMojoError(err), nil
+	}
+	vPerms, err := toV23Perms(mPerms)
+	if err != nil {
+		return toMojoError(err), nil
+	}
+	err = stub.Create(ctx, call, NoSchema, vPerms)
+	return toMojoError(err), nil
+}
+
+func (m *mojoImpl) TableDelete(name string) (mojom.Error, error) {
+	ctx, call := m.newCtxCall(name, methodDesc(nosqlwire.TableDesc, "Delete"))
+	stub, err := m.getTable(ctx, call, name)
+	if err != nil {
+		return toMojoError(err), nil
+	}
+	err = stub.Delete(ctx, call, NoSchema)
+	return toMojoError(err), nil
+}
+
+func (m *mojoImpl) TableExists(name string) (mojom.Error, bool, error) {
+	ctx, call := m.newCtxCall(name, methodDesc(nosqlwire.TableDesc, "Exists"))
+	stub, err := m.getTable(ctx, call, name)
+	if err != nil {
+		return toMojoError(err), false, nil
+	}
+	exists, err := stub.Exists(ctx, call, NoSchema)
+	return toMojoError(err), exists, nil
+}
+
+func (m *mojoImpl) TableDeleteRowRange(name string, start, limit []byte) (mojom.Error, error) {
+	return mojom.Error{}, nil
+}
+
+type scanStreamImpl struct {
+	ctx   *context.T
+	proxy *mojom.ScanStream_Proxy
+}
+
+func (s *scanStreamImpl) Send(item interface{}) error {
+	kv, ok := item.(nosqlwire.KeyValue)
+	if !ok {
+		return verror.NewErrInternal(s.ctx)
+	}
+
+	return s.proxy.OnKeyValue(mojom.KeyValue{
+		Key:   kv.Key,
+		Value: kv.Value,
+	})
+}
+
+func (s *scanStreamImpl) Recv(_ interface{}) error {
+	// This should never be called.
+	return verror.NewErrInternal(s.ctx)
+}
+
+var _ rpc.Stream = (*scanStreamImpl)(nil)
+
+// TODO(nlacasse): Provide some way for the client to cancel the stream.
+func (m *mojoImpl) TableScan(name string, start, limit []byte, ptr mojom.ScanStream_Pointer) (mojom.Error, error) {
+	ctx, call := m.newCtxCall(name, methodDesc(nosqlwire.TableDesc, "Scan"))
+	stub, err := m.getTable(ctx, call, name)
+	if err != nil {
+		return toMojoError(err), nil
+	}
+
+	proxy := mojom.NewScanStreamProxy(ptr, bindings.GetAsyncWaiter())
+
+	tableScanServerCallStub := &nosqlwire.TableScanServerCallStub{struct {
+		rpc.Stream
+		rpc.ServerCall
+	}{
+		&scanStreamImpl{
+			ctx:   ctx,
+			proxy: proxy,
+		},
+		call,
+	}}
+
+	err = stub.Scan(ctx, tableScanServerCallStub, NoSchema, start, limit)
+
+	// NOTE(nlacasse): Since we are already streaming, we send any error back
+	// to the client on the stream.  The TableScan function itself should not
+	// return an error at this point.
+	proxy.OnDone(toMojoError(err))
+	return mojom.Error{}, nil
+}
+
+func (m *mojoImpl) TableGetPermissions(name, key string) (mojom.Error, []mojom.PrefixPerms, error) {
+	return mojom.Error{}, nil, nil
+}
+
+func (m *mojoImpl) TableSetPermissions(name, prefix string, mPerms mojom.Perms) (mojom.Error, error) {
+	return mojom.Error{}, nil
+}
+
+func (m *mojoImpl) TableDeletePermissions(name, prefix string) (mojom.Error, error) {
+	return mojom.Error{}, nil
+}
+
+////////////////////////////////////////
+// nosql.Row
+
+func (m *mojoImpl) RowExists(name string) (mojom.Error, bool, error) {
+	ctx, call := m.newCtxCall(name, methodDesc(nosqlwire.RowDesc, "Exists"))
+	stub, err := m.getRow(ctx, call, name)
+	if err != nil {
+		return toMojoError(err), false, nil
+	}
+	exists, err := stub.Exists(ctx, call, NoSchema)
+	return toMojoError(err), exists, nil
+}
+
+func (m *mojoImpl) RowGet(name string) (mojom.Error, []byte, error) {
+	ctx, call := m.newCtxCall(name, methodDesc(nosqlwire.RowDesc, "Get"))
+	stub, err := m.getRow(ctx, call, name)
+	if err != nil {
+		return toMojoError(err), nil, nil
+	}
+	value, err := stub.Get(ctx, call, NoSchema)
+	return toMojoError(err), value, nil
+}
+
+func (m *mojoImpl) RowPut(name string, value []byte) (mojom.Error, error) {
+	ctx, call := m.newCtxCall(name, methodDesc(nosqlwire.RowDesc, "Put"))
+	stub, err := m.getRow(ctx, call, name)
+	if err != nil {
+		return toMojoError(err), nil
+	}
+	err = stub.Put(ctx, call, NoSchema, value)
+	return toMojoError(err), nil
+}
+
+func (m *mojoImpl) RowDelete(name string) (mojom.Error, error) {
+	ctx, call := m.newCtxCall(name, methodDesc(nosqlwire.RowDesc, "Delete"))
+	stub, err := m.getRow(ctx, call, name)
+	if err != nil {
+		return toMojoError(err), nil
+	}
+	err = stub.Delete(ctx, call, NoSchema)
+	return toMojoError(err), nil
+}
diff --git a/services/syncbase/server/nosql/database.go b/services/syncbase/server/nosql/database.go
new file mode 100644
index 0000000..a9f503f
--- /dev/null
+++ b/services/syncbase/server/nosql/database.go
@@ -0,0 +1,577 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package nosql
+
+import (
+	"math/rand"
+	"path"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	wire "v.io/syncbase/v23/services/syncbase/nosql"
+	"v.io/syncbase/v23/syncbase/nosql/query_db"
+	"v.io/syncbase/v23/syncbase/nosql/query_exec"
+	"v.io/syncbase/x/ref/services/syncbase/clock"
+	"v.io/syncbase/x/ref/services/syncbase/server/interfaces"
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/syncbase/x/ref/services/syncbase/server/watchable"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/context"
+	"v.io/v23/glob"
+	"v.io/v23/rpc"
+	"v.io/v23/security/access"
+	"v.io/v23/vdl"
+	"v.io/v23/verror"
+	"v.io/v23/vom"
+	"v.io/x/lib/vlog"
+)
+
+// database is a per-database singleton (i.e. not per-request). It does not
+// directly handle RPCs.
+// Note: If a database does not exist at the time of a database RPC, the
+// dispatcher creates a short-lived database object to service that particular
+// request.
+type database struct {
+	name string
+	a    interfaces.App
+	// The fields below are initialized iff this database exists.
+	exists bool
+	// TODO(sadovsky): Make st point to a store.Store wrapper that handles paging,
+	// and do not actually open the store in NewDatabase.
+	st store.Store // stores all data for a single database
+
+	// Active snapshots and transactions corresponding to client batches.
+	// TODO(sadovsky): Add timeouts and GC.
+	mu  sync.Mutex // protects the fields below
+	sns map[uint64]store.Snapshot
+	txs map[uint64]store.Transaction
+
+	// Active ConflictResolver connection from the app to this database.
+	// NOTE: For now, we assume there's only one open conflict resolution stream
+	// per database (typically, from the app that owns the database).
+	resolver wire.ConflictManagerStartConflictResolverServerCall
+}
+
+// databaseReq is a per-request object that handles Database RPCs.
+// It embeds database and tracks request-specific batch state.
+type databaseReq struct {
+	*database
+	// If non-nil, sn or tx will be non-nil.
+	batchId *uint64
+	sn      store.Snapshot
+	tx      store.Transaction
+}
+
+var (
+	_ wire.DatabaseServerMethods = (*databaseReq)(nil)
+	_ interfaces.Database        = (*database)(nil)
+)
+
+// DatabaseOptions configures a database.
+type DatabaseOptions struct {
+	// Database-level permissions.
+	Perms access.Permissions
+	// Root dir for data storage.
+	RootDir string
+	// Storage engine to use.
+	Engine string
+}
+
+// OpenDatabase opens a database and returns a *database for it. Designed for
+// use from within NewDatabase and server.NewService.
+func OpenDatabase(ctx *context.T, a interfaces.App, name string, opts DatabaseOptions, openOpts util.OpenOptions) (*database, error) {
+	st, err := util.OpenStore(opts.Engine, path.Join(opts.RootDir, opts.Engine), openOpts)
+	if err != nil {
+		return nil, err
+	}
+	vclock := clock.NewVClock(a.Service().St())
+	st, err = watchable.Wrap(st, vclock, &watchable.Options{
+		ManagedPrefixes: []string{util.RowPrefix, util.PermsPrefix},
+	})
+	if err != nil {
+		return nil, err
+	}
+	return &database{
+		name:   name,
+		a:      a,
+		exists: true,
+		st:     st,
+		sns:    make(map[uint64]store.Snapshot),
+		txs:    make(map[uint64]store.Transaction),
+	}, nil
+}
+
+// NewDatabase creates a new database instance and returns it.
+// Designed for use from within App.CreateNoSQLDatabase.
+func NewDatabase(ctx *context.T, a interfaces.App, name string, metadata *wire.SchemaMetadata, opts DatabaseOptions) (*database, error) {
+	if opts.Perms == nil {
+		return nil, verror.New(verror.ErrInternal, ctx, "perms must be specified")
+	}
+	d, err := OpenDatabase(ctx, a, name, opts, util.OpenOptions{CreateIfMissing: true, ErrorIfExists: true})
+	if err != nil {
+		return nil, err
+	}
+	data := &databaseData{
+		Name:           d.name,
+		Perms:          opts.Perms,
+		SchemaMetadata: metadata,
+	}
+	if err := util.Put(ctx, d.st, d.stKey(), data); err != nil {
+		return nil, err
+	}
+	return d, nil
+}
+
+////////////////////////////////////////
+// RPC methods
+
+func (d *databaseReq) Create(ctx *context.T, call rpc.ServerCall, metadata *wire.SchemaMetadata, perms access.Permissions) error {
+	if d.exists {
+		return verror.New(verror.ErrExist, ctx, d.name)
+	}
+	if d.batchId != nil {
+		return wire.NewErrBoundToBatch(ctx)
+	}
+	// This database does not yet exist; d is just an ephemeral handle that holds
+	// {name string, a *app}. d.a.CreateNoSQLDatabase will create a new database
+	// handle and store it in d.a.dbs[d.name].
+	return d.a.CreateNoSQLDatabase(ctx, call, d.name, perms, metadata)
+}
+
+func (d *databaseReq) Delete(ctx *context.T, call rpc.ServerCall, schemaVersion int32) error {
+	if d.batchId != nil {
+		return wire.NewErrBoundToBatch(ctx)
+	}
+	if err := d.checkSchemaVersion(ctx, schemaVersion); err != nil {
+		return err
+	}
+	return d.a.DeleteNoSQLDatabase(ctx, call, d.name)
+}
+
+func (d *databaseReq) Exists(ctx *context.T, call rpc.ServerCall, schemaVersion int32) (bool, error) {
+	if !d.exists {
+		return false, nil
+	}
+	if err := d.checkSchemaVersion(ctx, schemaVersion); err != nil {
+		return false, err
+	}
+	return util.ErrorToExists(util.GetWithAuth(ctx, call, d.st, d.stKey(), &databaseData{}))
+}
+
+var rng *rand.Rand = rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
+
+func (d *databaseReq) BeginBatch(ctx *context.T, call rpc.ServerCall, schemaVersion int32, bo wire.BatchOptions) (string, error) {
+	if !d.exists {
+		return "", verror.New(verror.ErrNoExist, ctx, d.name)
+	}
+	if d.batchId != nil {
+		return "", wire.NewErrBoundToBatch(ctx)
+	}
+	if err := d.checkSchemaVersion(ctx, schemaVersion); err != nil {
+		return "", err
+	}
+
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	var id uint64
+	var batchType string
+	for {
+		id = uint64(rng.Int63())
+		if bo.ReadOnly {
+			if _, ok := d.sns[id]; !ok {
+				d.sns[id] = d.st.NewSnapshot()
+				batchType = "sn"
+				break
+			}
+		} else {
+			if _, ok := d.txs[id]; !ok {
+				d.txs[id] = d.st.NewTransaction()
+				batchType = "tx"
+				break
+			}
+		}
+	}
+	return strings.Join([]string{d.name, batchType, strconv.FormatUint(id, 10)}, util.BatchSep), nil
+}
+
+func (d *databaseReq) Commit(ctx *context.T, call rpc.ServerCall, schemaVersion int32) error {
+	if !d.exists {
+		return verror.New(verror.ErrNoExist, ctx, d.name)
+	}
+	if d.batchId == nil {
+		return wire.NewErrNotBoundToBatch(ctx)
+	}
+	if d.tx == nil {
+		return wire.NewErrReadOnlyBatch(ctx)
+	}
+	if err := d.checkSchemaVersion(ctx, schemaVersion); err != nil {
+		return err
+	}
+	var err error
+	if err = d.tx.Commit(); err == nil {
+		d.mu.Lock()
+		delete(d.txs, *d.batchId)
+		d.mu.Unlock()
+	}
+	if verror.ErrorID(err) == store.ErrConcurrentTransaction.ID {
+		return verror.New(wire.ErrConcurrentBatch, ctx, err)
+	}
+	return err
+}
+
+func (d *databaseReq) Abort(ctx *context.T, call rpc.ServerCall, schemaVersion int32) error {
+	if !d.exists {
+		return verror.New(verror.ErrNoExist, ctx, d.name)
+	}
+	if d.batchId == nil {
+		return wire.NewErrNotBoundToBatch(ctx)
+	}
+	if err := d.checkSchemaVersion(ctx, schemaVersion); err != nil {
+		return err
+	}
+	var err error
+	if d.tx != nil {
+		if err = d.tx.Abort(); err == nil {
+			d.mu.Lock()
+			delete(d.txs, *d.batchId)
+			d.mu.Unlock()
+		}
+	} else {
+		if err = d.sn.Abort(); err == nil {
+			d.mu.Lock()
+			delete(d.sns, *d.batchId)
+			d.mu.Unlock()
+		}
+	}
+	return err
+}
+
+func (d *databaseReq) Exec(ctx *context.T, call wire.DatabaseExecServerCall, schemaVersion int32, q string) error {
+	if err := d.checkSchemaVersion(ctx, schemaVersion); err != nil {
+		return err
+	}
+	impl := func(headers []string, rs ResultStream, err error) error {
+		if err != nil {
+			return err
+		}
+		sender := call.SendStream()
+		// Push the headers first -- the client will retrieve them and return
+		// them separately from the results.
+		var resultHeaders []*vdl.Value
+		for _, header := range headers {
+			resultHeaders = append(resultHeaders, vdl.ValueOf(header))
+		}
+		sender.Send(resultHeaders)
+		for rs.Advance() {
+			result := rs.Result()
+			if err := sender.Send(result); err != nil {
+				rs.Cancel()
+				return err
+			}
+		}
+		return rs.Err()
+	}
+	var sntx store.SnapshotOrTransaction
+	if d.batchId != nil {
+		sntx = d.batchReader()
+	} else {
+		sntx = d.st.NewSnapshot()
+		defer sntx.Abort()
+	}
+	// queryDb implements query_db.Database
+	// which is needed by the query package's
+	// Exec function.
+	db := &queryDb{
+		ctx:  ctx,
+		call: call,
+		req:  d,
+		sntx: sntx,
+	}
+
+	return impl(query_exec.Exec(db, q))
+}
+
+func (d *databaseReq) SetPermissions(ctx *context.T, call rpc.ServerCall, perms access.Permissions, version string) error {
+	if !d.exists {
+		return verror.New(verror.ErrNoExist, ctx, d.name)
+	}
+	if d.batchId != nil {
+		return wire.NewErrBoundToBatch(ctx)
+	}
+	return d.a.SetDatabasePerms(ctx, call, d.name, perms, version)
+}
+
+func (d *databaseReq) GetPermissions(ctx *context.T, call rpc.ServerCall) (perms access.Permissions, version string, err error) {
+	if !d.exists {
+		return nil, "", verror.New(verror.ErrNoExist, ctx, d.name)
+	}
+	if d.batchId != nil {
+		return nil, "", wire.NewErrBoundToBatch(ctx)
+	}
+	data := &databaseData{}
+	if err := util.GetWithAuth(ctx, call, d.st, d.stKey(), data); err != nil {
+		return nil, "", err
+	}
+	return data.Perms, util.FormatVersion(data.Version), nil
+}
+
+func (d *databaseReq) GlobChildren__(ctx *context.T, call rpc.GlobChildrenServerCall, matcher *glob.Element) error {
+	if !d.exists {
+		return verror.New(verror.ErrNoExist, ctx, d.name)
+	}
+	if d.batchId != nil {
+		return wire.NewErrBoundToBatch(ctx)
+	}
+	// Check perms.
+	sn := d.st.NewSnapshot()
+	if err := util.GetWithAuth(ctx, call, sn, d.stKey(), &databaseData{}); err != nil {
+		sn.Abort()
+		return err
+	}
+	return util.Glob(ctx, call, matcher, sn, sn.Abort, util.TablePrefix)
+}
+
+////////////////////////////////////////
+// ResultStream interface
+
+// ResultStream is an interface for iterating through results (a.k.a, rows) returned from a
+// query.  Each resulting rows are arrays of vdl objects.
+type ResultStream interface {
+	// Advance stages an element so the client can retrieve it with Result.
+	// Advance returns true iff there is a result to retrieve. The client must
+	// call Advance before calling Result. The client must call Cancel if it
+	// does not iterate through all elements (i.e. until Advance returns false).
+	// Advance may block if an element is not immediately available.
+	Advance() bool
+
+	// Result returns the row (i.e., array of vdl Values) that was staged by Advance.
+	// Result may panic if Advance returned false or was not called at all.
+	// Result does not block.
+	Result() []*vdl.Value
+
+	// Err returns a non-nil error iff the stream encountered any errors. Err does
+	// not block.
+	Err() error
+
+	// Cancel notifies the ResultStream provider that it can stop producing results.
+	// The client must call Cancel if it does not iterate through all results
+	// (i.e. until Advance returns false). Cancel is idempotent and can be called
+	// concurrently with a goroutine that is iterating via Advance/Result.
+	// Cancel causes Advance to subsequently return false. Cancel does not block.
+	Cancel()
+}
+
+////////////////////////////////////////
+// interfaces.Database methods
+
+func (d *database) St() store.Store {
+	if !d.exists {
+		vlog.Fatalf("database %q does not exist", d.name)
+	}
+	return d.st
+}
+
+func (d *database) App() interfaces.App {
+	return d.a
+}
+
+func (d *database) CheckPermsInternal(ctx *context.T, call rpc.ServerCall, st store.StoreReader) error {
+	if !d.exists {
+		vlog.Fatalf("database %q does not exist", d.name)
+	}
+	return util.GetWithAuth(ctx, call, st, d.stKey(), &databaseData{})
+}
+
+func (d *database) SetPermsInternal(ctx *context.T, call rpc.ServerCall, perms access.Permissions, version string) error {
+	if !d.exists {
+		vlog.Fatalf("database %q does not exist", d.name)
+	}
+	return store.RunInTransaction(d.st, func(tx store.Transaction) error {
+		data := &databaseData{}
+		return util.UpdateWithAuth(ctx, call, tx, d.stKey(), data, func() error {
+			if err := util.CheckVersion(ctx, version, data.Version); err != nil {
+				return err
+			}
+			data.Perms = perms
+			data.Version++
+			return nil
+		})
+	})
+}
+
+func (d *database) Name() string {
+	return d.name
+}
+
+////////////////////////////////////////
+// query_db implementation
+
+// Implement query_db's Database, Table and KeyValueStream interfaces.
+type queryDb struct {
+	ctx  *context.T
+	call wire.DatabaseExecServerCall
+	req  *databaseReq
+	sntx store.SnapshotOrTransaction
+}
+
+func (db *queryDb) GetContext() *context.T {
+	return db.ctx
+}
+
+func (db *queryDb) GetTable(name string) (query_db.Table, error) {
+	tDb := &tableDb{
+		qdb: db,
+		req: &tableReq{
+			name: name,
+			d:    db.req,
+		},
+	}
+	// Now that we have a table, we need to check permissions.
+	if err := util.GetWithAuth(db.ctx, db.call, db.sntx, tDb.req.stKey(), &tableData{}); err != nil {
+		return nil, err
+	}
+	return tDb, nil
+}
+
+type tableDb struct {
+	qdb *queryDb
+	req *tableReq
+}
+
+func (t *tableDb) Scan(keyRanges query_db.KeyRanges) (query_db.KeyValueStream, error) {
+	streams := []store.Stream{}
+	for _, keyRange := range keyRanges {
+		// TODO(jkline): For now, acquire all of the streams at once to minimize the race condition.
+		//               Need a way to Scan multiple ranges at the same state of uncommitted changes.
+		streams = append(streams, t.qdb.sntx.Scan(util.ScanRangeArgs(util.JoinKeyParts(util.RowPrefix, t.req.name), keyRange.Start, keyRange.Limit)))
+	}
+	return &kvs{
+		t:        t,
+		curr:     0,
+		validRow: false,
+		it:       streams,
+		err:      nil,
+	}, nil
+}
+
+type kvs struct {
+	t         *tableDb
+	curr      int
+	validRow  bool
+	currKey   string
+	currValue *vdl.Value
+	it        []store.Stream // array of store.Streams
+	err       error
+}
+
+func (s *kvs) Advance() bool {
+	if s.err != nil {
+		return false
+	}
+	for s.curr < len(s.it) {
+		if s.it[s.curr].Advance() {
+			// key
+			keyBytes := s.it[s.curr].Key(nil)
+			parts := util.SplitKeyParts(string(keyBytes))
+			// TODO(rogulenko): Check access for the key.
+			s.currKey = parts[len(parts)-1]
+			// value
+			valueBytes := s.it[s.curr].Value(nil)
+			var currValue *vdl.Value
+			if err := vom.Decode(valueBytes, &currValue); err != nil {
+				s.validRow = false
+				s.err = err
+				return false
+			}
+			s.currValue = currValue
+			s.validRow = true
+			return true
+		}
+		// Advance returned false.  It could be an err, or it could
+		// be we've reached the end.
+		if err := s.it[s.curr].Err(); err != nil {
+			s.validRow = false
+			s.err = err
+			return false
+		}
+		// We've reached the end of the iterator for this keyRange.
+		// Jump to the next one.
+		s.it[s.curr] = nil
+		s.curr++
+		s.validRow = false
+	}
+	// There are no more prefixes to scan.
+	return false
+}
+
+func (s *kvs) KeyValue() (string, *vdl.Value) {
+	if !s.validRow {
+		return "", nil
+	}
+	return s.currKey, s.currValue
+}
+
+func (s *kvs) Err() error {
+	return s.err
+}
+
+func (s *kvs) Cancel() {
+	if s.it != nil {
+		for i := s.curr; i < len(s.it); i++ {
+			s.it[i].Cancel()
+		}
+		s.it = nil
+	}
+	// set curr to end of keyRanges so Advance will return false
+	s.curr = len(s.it)
+}
+
+////////////////////////////////////////
+// Internal helpers
+
+func (d *database) stKey() string {
+	return util.DatabasePrefix
+}
+
+func (d *databaseReq) batchReader() store.SnapshotOrTransaction {
+	if d.batchId == nil {
+		return nil
+	} else if d.sn != nil {
+		return d.sn
+	} else {
+		return d.tx
+	}
+}
+
+func (d *databaseReq) batchTransaction() (store.Transaction, error) {
+	if d.batchId == nil {
+		return nil, nil
+	} else if d.tx != nil {
+		return d.tx, nil
+	} else {
+		return nil, wire.NewErrReadOnlyBatch(nil)
+	}
+}
+
+// TODO(jlodhia): Schema check should happen within a transaction for each
+// operation in database, table and row. Do schema check along with permissions
+// check when fully-specified permission model is implemented.
+func (d *databaseReq) checkSchemaVersion(ctx *context.T, schemaVersion int32) error {
+	if !d.exists {
+		// database does not exist yet and hence there is no schema to check.
+		// This can happen if delete is called twice on the same database.
+		return nil
+	}
+	schemaMetadata, err := d.getSchemaMetadataWithoutAuth(ctx)
+	if err != nil {
+		return err
+	}
+	if (schemaMetadata == nil) || (schemaMetadata.Version == schemaVersion) {
+		return nil
+	}
+	return wire.NewErrSchemaVersionMismatch(ctx)
+}
diff --git a/services/syncbase/server/nosql/database_bm.go b/services/syncbase/server/nosql/database_bm.go
new file mode 100644
index 0000000..e646c42
--- /dev/null
+++ b/services/syncbase/server/nosql/database_bm.go
@@ -0,0 +1,95 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package nosql
+
+import (
+	wire "v.io/syncbase/v23/services/syncbase/nosql"
+	"v.io/syncbase/x/ref/services/syncbase/vsync"
+	"v.io/v23/context"
+	"v.io/v23/rpc"
+)
+
+////////////////////////////////////////////////////////////////////////////////
+// RPCs for managing blobs between Syncbase and its clients.
+
+func (d *databaseReq) CreateBlob(ctx *context.T, call rpc.ServerCall) (wire.BlobRef, error) {
+	if d.batchId != nil {
+		return wire.NullBlobRef, wire.NewErrBoundToBatch(ctx)
+	}
+	sd := vsync.NewSyncDatabase(d)
+	return sd.CreateBlob(ctx, call)
+}
+
+func (d *databaseReq) PutBlob(ctx *context.T, call wire.BlobManagerPutBlobServerCall, br wire.BlobRef) error {
+	if d.batchId != nil {
+		return wire.NewErrBoundToBatch(ctx)
+	}
+	sd := vsync.NewSyncDatabase(d)
+	return sd.PutBlob(ctx, call, br)
+}
+
+func (d *databaseReq) CommitBlob(ctx *context.T, call rpc.ServerCall, br wire.BlobRef) error {
+	if d.batchId != nil {
+		return wire.NewErrBoundToBatch(ctx)
+	}
+	sd := vsync.NewSyncDatabase(d)
+	return sd.CommitBlob(ctx, call, br)
+}
+
+func (d *databaseReq) GetBlobSize(ctx *context.T, call rpc.ServerCall, br wire.BlobRef) (int64, error) {
+	if d.batchId != nil {
+		return 0, wire.NewErrBoundToBatch(ctx)
+	}
+	sd := vsync.NewSyncDatabase(d)
+	return sd.GetBlobSize(ctx, call, br)
+}
+
+func (d *databaseReq) DeleteBlob(ctx *context.T, call rpc.ServerCall, br wire.BlobRef) error {
+	if d.batchId != nil {
+		return wire.NewErrBoundToBatch(ctx)
+	}
+	sd := vsync.NewSyncDatabase(d)
+	return sd.DeleteBlob(ctx, call, br)
+}
+
+func (d *databaseReq) GetBlob(ctx *context.T, call wire.BlobManagerGetBlobServerCall, br wire.BlobRef, offset int64) error {
+	if d.batchId != nil {
+		return wire.NewErrBoundToBatch(ctx)
+	}
+	sd := vsync.NewSyncDatabase(d)
+	return sd.GetBlob(ctx, call, br, offset)
+}
+
+func (d *databaseReq) FetchBlob(ctx *context.T, call wire.BlobManagerFetchBlobServerCall, br wire.BlobRef, priority uint64) error {
+	if d.batchId != nil {
+		return wire.NewErrBoundToBatch(ctx)
+	}
+	sd := vsync.NewSyncDatabase(d)
+	return sd.FetchBlob(ctx, call, br, priority)
+}
+
+func (d *databaseReq) PinBlob(ctx *context.T, call rpc.ServerCall, br wire.BlobRef) error {
+	if d.batchId != nil {
+		return wire.NewErrBoundToBatch(ctx)
+	}
+	sd := vsync.NewSyncDatabase(d)
+	return sd.PinBlob(ctx, call, br)
+}
+
+func (d *databaseReq) UnpinBlob(ctx *context.T, call rpc.ServerCall, br wire.BlobRef) error {
+	if d.batchId != nil {
+		return wire.NewErrBoundToBatch(ctx)
+	}
+	sd := vsync.NewSyncDatabase(d)
+	return sd.UnpinBlob(ctx, call, br)
+}
+
+func (d *databaseReq) KeepBlob(ctx *context.T, call rpc.ServerCall, br wire.BlobRef, rank uint64) error {
+	if d.batchId != nil {
+		return wire.NewErrBoundToBatch(ctx)
+	}
+	sd := vsync.NewSyncDatabase(d)
+	return sd.KeepBlob(ctx, call, br, rank)
+}
diff --git a/services/syncbase/server/nosql/database_crm.go b/services/syncbase/server/nosql/database_crm.go
new file mode 100644
index 0000000..e1135a9
--- /dev/null
+++ b/services/syncbase/server/nosql/database_crm.go
@@ -0,0 +1,20 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package nosql
+
+import (
+	wire "v.io/syncbase/v23/services/syncbase/nosql"
+	"v.io/v23/context"
+)
+
+////////////////////////////////////////
+// ConflictManager RPC methods
+
+func (d *databaseReq) StartConflictResolver(ctx *context.T, call wire.ConflictManagerStartConflictResolverServerCall) error {
+	// Store the conflict resolver connection in the per-app, per-database
+	// singleton so that sync can access it.
+	d.database.resolver = call
+	return nil
+}
diff --git a/services/syncbase/server/nosql/database_sgm.go b/services/syncbase/server/nosql/database_sgm.go
new file mode 100644
index 0000000..cc1a73d
--- /dev/null
+++ b/services/syncbase/server/nosql/database_sgm.go
@@ -0,0 +1,85 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package nosql
+
+import (
+	wire "v.io/syncbase/v23/services/syncbase/nosql"
+	"v.io/syncbase/x/ref/services/syncbase/vsync"
+	"v.io/v23/context"
+	"v.io/v23/rpc"
+	"v.io/v23/verror"
+)
+
+////////////////////////////////////////
+// SyncGroup RPC methods
+
+func (d *databaseReq) GetSyncGroupNames(ctx *context.T, call rpc.ServerCall) ([]string, error) {
+	if d.batchId != nil {
+		return nil, wire.NewErrBoundToBatch(ctx)
+	}
+	sd := vsync.NewSyncDatabase(d)
+	return sd.GetSyncGroupNames(ctx, call)
+}
+
+func (d *databaseReq) CreateSyncGroup(ctx *context.T, call rpc.ServerCall, sgName string, spec wire.SyncGroupSpec, myInfo wire.SyncGroupMemberInfo) error {
+	if d.batchId != nil {
+		return wire.NewErrBoundToBatch(ctx)
+	}
+	sd := vsync.NewSyncDatabase(d)
+	return sd.CreateSyncGroup(ctx, call, sgName, spec, myInfo)
+}
+
+func (d *databaseReq) JoinSyncGroup(ctx *context.T, call rpc.ServerCall, sgName string, myInfo wire.SyncGroupMemberInfo) (wire.SyncGroupSpec, error) {
+	if d.batchId != nil {
+		return wire.SyncGroupSpec{}, wire.NewErrBoundToBatch(ctx)
+	}
+	sd := vsync.NewSyncDatabase(d)
+	return sd.JoinSyncGroup(ctx, call, sgName, myInfo)
+}
+
+func (d *databaseReq) LeaveSyncGroup(ctx *context.T, call rpc.ServerCall, sgName string) error {
+	if d.batchId != nil {
+		return wire.NewErrBoundToBatch(ctx)
+	}
+	return verror.NewErrNotImplemented(ctx)
+}
+
+func (d *databaseReq) DestroySyncGroup(ctx *context.T, call rpc.ServerCall, sgName string) error {
+	if d.batchId != nil {
+		return wire.NewErrBoundToBatch(ctx)
+	}
+	return verror.NewErrNotImplemented(ctx)
+}
+
+func (d *databaseReq) EjectFromSyncGroup(ctx *context.T, call rpc.ServerCall, sgName, member string) error {
+	if d.batchId != nil {
+		return wire.NewErrBoundToBatch(ctx)
+	}
+	return verror.NewErrNotImplemented(ctx)
+}
+
+func (d *databaseReq) GetSyncGroupSpec(ctx *context.T, call rpc.ServerCall, sgName string) (wire.SyncGroupSpec, string, error) {
+	if d.batchId != nil {
+		return wire.SyncGroupSpec{}, "", wire.NewErrBoundToBatch(ctx)
+	}
+	sd := vsync.NewSyncDatabase(d)
+	return sd.GetSyncGroupSpec(ctx, call, sgName)
+}
+
+func (d *databaseReq) SetSyncGroupSpec(ctx *context.T, call rpc.ServerCall, sgName string, spec wire.SyncGroupSpec, version string) error {
+	if d.batchId != nil {
+		return wire.NewErrBoundToBatch(ctx)
+	}
+	sd := vsync.NewSyncDatabase(d)
+	return sd.SetSyncGroupSpec(ctx, call, sgName, spec, version)
+}
+
+func (d *databaseReq) GetSyncGroupMembers(ctx *context.T, call rpc.ServerCall, sgName string) (map[string]wire.SyncGroupMemberInfo, error) {
+	if d.batchId != nil {
+		return nil, wire.NewErrBoundToBatch(ctx)
+	}
+	sd := vsync.NewSyncDatabase(d)
+	return sd.GetSyncGroupMembers(ctx, call, sgName)
+}
diff --git a/services/syncbase/server/nosql/database_sm.go b/services/syncbase/server/nosql/database_sm.go
new file mode 100644
index 0000000..3c87a6b
--- /dev/null
+++ b/services/syncbase/server/nosql/database_sm.go
@@ -0,0 +1,64 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package nosql
+
+import (
+	wire "v.io/syncbase/v23/services/syncbase/nosql"
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/context"
+	"v.io/v23/rpc"
+	"v.io/v23/verror"
+)
+
+////////////////////////////////////////
+// SchemaManager RPC methods
+
+func (d *databaseReq) GetSchemaMetadata(ctx *context.T, call rpc.ServerCall) (wire.SchemaMetadata, error) {
+	metadata := wire.SchemaMetadata{}
+
+	if !d.exists {
+		return metadata, verror.New(verror.ErrNoExist, ctx, d.Name())
+	}
+
+	// Check permissions on Database and retreve schema metadata.
+	dbData := databaseData{}
+	if err := util.GetWithAuth(ctx, call, d.st, d.stKey(), &dbData); err != nil {
+		return metadata, err
+	}
+	if dbData.SchemaMetadata == nil {
+		return metadata, verror.New(verror.ErrNoExist, ctx, "Schema does not exist for the db")
+	}
+	return *dbData.SchemaMetadata, nil
+}
+
+func (d *databaseReq) SetSchemaMetadata(ctx *context.T, call rpc.ServerCall, metadata wire.SchemaMetadata) error {
+	// Check if database exists
+	if !d.exists {
+		return verror.New(verror.ErrNoExist, ctx, d.Name())
+	}
+
+	// Check permissions on Database and store schema metadata.
+	return store.RunInTransaction(d.st, func(tx store.Transaction) error {
+		dbData := databaseData{}
+		return util.UpdateWithAuth(ctx, call, tx, d.stKey(), &dbData, func() error {
+			// NOTE: For now we expect the client to not issue multiple
+			// concurrent SetSchemaMetadata calls.
+			dbData.SchemaMetadata = &metadata
+			return nil
+		})
+	})
+}
+
+func (d *databaseReq) getSchemaMetadataWithoutAuth(ctx *context.T) (*wire.SchemaMetadata, error) {
+	if !d.exists {
+		return nil, verror.New(verror.ErrInternal, ctx, "field store in database cannot be nil")
+	}
+	dbData := databaseData{}
+	if err := util.Get(ctx, d.st, d.stKey(), &dbData); err != nil {
+		return nil, err
+	}
+	return dbData.SchemaMetadata, nil
+}
diff --git a/services/syncbase/server/nosql/database_watch.go b/services/syncbase/server/nosql/database_watch.go
new file mode 100644
index 0000000..35228e6
--- /dev/null
+++ b/services/syncbase/server/nosql/database_watch.go
@@ -0,0 +1,217 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package nosql
+
+import (
+	"bytes"
+	"strings"
+
+	wire "v.io/syncbase/v23/services/syncbase/nosql"
+	pubutil "v.io/syncbase/v23/syncbase/util"
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/syncbase/x/ref/services/syncbase/server/watchable"
+	"v.io/v23/context"
+	"v.io/v23/naming"
+	"v.io/v23/rpc"
+	"v.io/v23/services/watch"
+	"v.io/v23/vdl"
+	"v.io/v23/verror"
+)
+
+// GetResumeMarker implements the wire.DatabaseWatcher interface.
+func (d *databaseReq) GetResumeMarker(ctx *context.T, call rpc.ServerCall) (watch.ResumeMarker, error) {
+	if !d.exists {
+		return nil, verror.New(verror.ErrNoExist, ctx, d.name)
+	}
+	if d.batchId != nil {
+		return watchable.GetResumeMarker(d.batchReader())
+	} else {
+		return watchable.GetResumeMarker(d.st)
+	}
+}
+
+// WatchGlob implements the wire.DatabaseWatcher interface.
+func (d *databaseReq) WatchGlob(ctx *context.T, call watch.GlobWatcherWatchGlobServerCall, req watch.GlobRequest) error {
+	// TODO(rogulenko): Check permissions here and in other methods.
+	if !d.exists {
+		return verror.New(verror.ErrNoExist, ctx, d.name)
+	}
+	if d.batchId != nil {
+		return wire.NewErrBoundToBatch(ctx)
+	}
+	// Parse the pattern.
+	if !strings.HasSuffix(req.Pattern, "*") {
+		return verror.New(verror.ErrBadArg, ctx, req.Pattern)
+	}
+	table, prefix, err := pubutil.ParseTableRowPair(ctx, strings.TrimSuffix(req.Pattern, "*"))
+	if err != nil {
+		return err
+	}
+	// Get the resume marker and fetch the initial state if necessary.
+	resumeMarker := req.ResumeMarker
+	if bytes.Equal(resumeMarker, []byte("now")) || len(resumeMarker) == 0 {
+		var err error
+		if resumeMarker, err = watchable.GetResumeMarker(d.st); err != nil {
+			return err
+		}
+		if len(req.ResumeMarker) == 0 {
+			// TODO(rogulenko): Fetch the initial state.
+			return verror.NewErrNotImplemented(ctx)
+		}
+	}
+	t := tableReq{
+		name: table,
+		d:    d,
+	}
+	return t.watchUpdates(ctx, call, prefix, resumeMarker)
+}
+
+// watchUpdates waits for database updates and sends them to the client.
+// This function does two steps in a for loop:
+// - scan through the watch log until the end, sending all updates to the client
+// - wait for one of two signals: new updates available or the call is canceled.
+// The 'new updates' signal is sent by a worker goroutine that translates a
+// condition variable signal to a Go channel. The worker goroutine waits on the
+// condition variable for changes. Whenever the state changes, the worker sends
+// a signal through the Go channel.
+func (t *tableReq) watchUpdates(ctx *context.T, call watch.GlobWatcherWatchGlobServerCall, prefix string, resumeMarker watch.ResumeMarker) error {
+	// The Go channel to send notifications from the worker to the main
+	// goroutine.
+	hasUpdates := make(chan struct{})
+	// The Go channel to signal the worker to stop. The worker might block
+	// on the condition variable, but we don't want the main goroutine
+	// to wait for the worker to stop, so we create a buffered channel.
+	cancelWorker := make(chan struct{}, 1)
+	defer close(cancelWorker)
+	go func() {
+		waitForChange := watchable.WatchUpdates(t.d.st)
+		var state, newState uint64 = 1, 1
+		for {
+			// Wait until the state changes or the main function returns.
+			for newState == state {
+				select {
+				case <-cancelWorker:
+					return
+				default:
+				}
+				newState = waitForChange(state)
+			}
+			// Update the current state to the new value and sends a signal to
+			// the main goroutine.
+			state = newState
+			if state == 0 {
+				close(hasUpdates)
+				return
+			}
+			// cancelWorker is closed as soons as the main function returns.
+			select {
+			case hasUpdates <- struct{}{}:
+			case <-cancelWorker:
+				return
+			}
+		}
+	}()
+
+	sender := call.SendStream()
+	for {
+		// Drain the log queue.
+		for {
+			logs, nextResumeMarker, err := watchable.ReadBatchFromLog(t.d.st, resumeMarker)
+			if err != nil {
+				return err
+			}
+			if logs == nil {
+				// No new log records available now.
+				break
+			}
+			resumeMarker = nextResumeMarker
+			changes, err := t.processLogBatch(ctx, call, prefix, logs)
+			if err != nil {
+				return err
+			}
+			if changes == nil {
+				// All batch changes are filtered out.
+				continue
+			}
+			changes[len(changes)-1].ResumeMarker = resumeMarker
+			for _, change := range changes {
+				if err := sender.Send(change); err != nil {
+					return err
+				}
+			}
+		}
+		// Wait for new updates or cancel.
+		select {
+		case _, ok := <-hasUpdates:
+			if !ok {
+				return verror.NewErrAborted(ctx)
+			}
+		case <-ctx.Done():
+			return ctx.Err()
+		}
+	}
+}
+
+// processLogBatch converts []*watchable.LogEntry to []watch.Change, filtering
+// out unnecessary or inaccessible log records.
+func (t *tableReq) processLogBatch(ctx *context.T, call rpc.ServerCall, prefix string, logs []*watchable.LogEntry) ([]watch.Change, error) {
+	sn := t.d.st.NewSnapshot()
+	defer sn.Abort()
+	var changes []watch.Change
+	for _, logEntry := range logs {
+		var opKey string
+		switch op := logEntry.Op.(type) {
+		case watchable.OpPut:
+			opKey = string(op.Value.Key)
+		case watchable.OpDelete:
+			opKey = string(op.Value.Key)
+		default:
+			continue
+		}
+		parts := util.SplitKeyParts(opKey)
+		// TODO(rogulenko): Currently we process only rows, i.e. keys of the form
+		// $row:xxx:yyy. Consider processing other keys.
+		if len(parts) != 3 || parts[0] != util.RowPrefix {
+			continue
+		}
+		table, row := parts[1], parts[2]
+		// Filter out unnecessary rows and rows that we can't access.
+		if table != t.name || !strings.HasPrefix(row, prefix) {
+			continue
+		}
+		if err := t.checkAccess(ctx, call, sn, row); err != nil {
+			if verror.ErrorID(err) != verror.ErrNoAccess.ID {
+				return nil, err
+			}
+			continue
+		}
+		change := watch.Change{
+			Name:      naming.Join(table, row),
+			Continued: true,
+		}
+		switch op := logEntry.Op.(type) {
+		case watchable.OpPut:
+			rowValue, err := watchable.GetAtVersion(ctx, sn, op.Value.Key, nil, op.Value.Version)
+			if err != nil {
+				return nil, err
+			}
+			change.State = watch.Exists
+			change.Value = vdl.ValueOf(wire.StoreChange{
+				Value:    rowValue,
+				FromSync: logEntry.FromSync,
+			})
+		case watchable.OpDelete:
+			change.State = watch.DoesNotExist
+			change.Value = vdl.ValueOf(wire.StoreChange{
+				FromSync: logEntry.FromSync,
+			})
+		}
+		changes = append(changes, change)
+	}
+	if len(changes) > 0 {
+		changes[len(changes)-1].Continued = false
+	}
+	return changes, nil
+}
diff --git a/services/syncbase/server/nosql/dispatcher.go b/services/syncbase/server/nosql/dispatcher.go
new file mode 100644
index 0000000..284f939
--- /dev/null
+++ b/services/syncbase/server/nosql/dispatcher.go
@@ -0,0 +1,141 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package nosql
+
+import (
+	"strconv"
+	"strings"
+
+	wire "v.io/syncbase/v23/services/syncbase"
+	nosqlWire "v.io/syncbase/v23/services/syncbase/nosql"
+	pubutil "v.io/syncbase/v23/syncbase/util"
+	"v.io/syncbase/x/ref/services/syncbase/server/interfaces"
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/v23/context"
+	"v.io/v23/rpc"
+	"v.io/v23/security"
+	"v.io/v23/verror"
+	"v.io/x/lib/vlog"
+)
+
+type dispatcher struct {
+	a interfaces.App
+}
+
+var _ rpc.Dispatcher = (*dispatcher)(nil)
+
+func NewDispatcher(a interfaces.App) *dispatcher {
+	return &dispatcher{a: a}
+}
+
+// We always return an AllowEveryone authorizer from Lookup(), and rely on our
+// RPC method implementations to perform proper authorization.
+var auth security.Authorizer = security.AllowEveryone()
+
+func (disp *dispatcher) Lookup(_ *context.T, suffix string) (interface{}, security.Authorizer, error) {
+	suffix = strings.TrimPrefix(suffix, "/")
+	parts := strings.Split(suffix, "/")
+
+	if len(parts) == 0 {
+		vlog.Fatal("invalid nosql.dispatcher Lookup")
+	}
+
+	dParts := strings.Split(parts[0], util.BatchSep)
+	dName := dParts[0]
+
+	// Validate all key atoms up front, so that we can avoid doing so in all our
+	// method implementations.
+	if !pubutil.ValidName(dName) {
+		return nil, nil, wire.NewErrInvalidName(nil, suffix)
+	}
+	for _, s := range parts[1:] {
+		if !pubutil.ValidName(s) {
+			return nil, nil, wire.NewErrInvalidName(nil, suffix)
+		}
+	}
+
+	dExists := false
+	var d *database
+	if dInt, err := disp.a.NoSQLDatabase(nil, nil, dName); err == nil {
+		d = dInt.(*database) // panics on failure, as desired
+		dExists = true
+	} else {
+		if verror.ErrorID(err) != verror.ErrNoExist.ID {
+			return nil, nil, err
+		} else {
+			// Database does not exist. Create a short-lived database object to
+			// service this request.
+			d = &database{
+				name: dName,
+				a:    disp.a,
+			}
+		}
+	}
+
+	dReq := &databaseReq{database: d}
+	if !setBatchFields(dReq, dParts) {
+		return nil, nil, wire.NewErrInvalidName(nil, suffix)
+	}
+	if len(parts) == 1 {
+		return nosqlWire.DatabaseServer(dReq), auth, nil
+	}
+
+	// All table and row methods require the database to exist. If it doesn't,
+	// abort early.
+	if !dExists {
+		return nil, nil, verror.New(verror.ErrNoExist, nil, d.name)
+	}
+
+	// Note, it's possible for the database to be deleted concurrently with
+	// downstream handling of this request. Depending on the order in which things
+	// execute, the client may not get an error, but in any case ultimately the
+	// store will end up in a consistent state.
+	tReq := &tableReq{
+		name: parts[1],
+		d:    dReq,
+	}
+	if len(parts) == 2 {
+		return nosqlWire.TableServer(tReq), auth, nil
+	}
+
+	rReq := &rowReq{
+		key: parts[2],
+		t:   tReq,
+	}
+	if len(parts) == 3 {
+		return nosqlWire.RowServer(rReq), auth, nil
+	}
+
+	return nil, nil, verror.NewErrNoExist(nil)
+}
+
+// setBatchFields sets the batch-related fields in databaseReq based on the
+// value of dParts, the parts of the database name component. It returns false
+// if dParts is malformed.
+func setBatchFields(d *databaseReq, dParts []string) bool {
+	if len(dParts) == 1 {
+		return true
+	}
+	if len(dParts) != 3 {
+		return false
+	}
+	batchId, err := strconv.ParseUint(dParts[2], 0, 64)
+	if err != nil {
+		return false
+	}
+	d.batchId = &batchId
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	var ok bool
+	switch dParts[1] {
+	case "sn":
+		d.sn, ok = d.sns[batchId]
+	case "tx":
+		d.tx, ok = d.txs[batchId]
+	default:
+		return false
+	}
+	return ok
+}
diff --git a/services/syncbase/server/nosql/row.go b/services/syncbase/server/nosql/row.go
new file mode 100644
index 0000000..a6fbf9d
--- /dev/null
+++ b/services/syncbase/server/nosql/row.go
@@ -0,0 +1,141 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package nosql
+
+import (
+	wire "v.io/syncbase/v23/services/syncbase/nosql"
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/context"
+	"v.io/v23/rpc"
+	"v.io/v23/verror"
+)
+
+// rowReq is a per-request object that handles Row RPCs.
+type rowReq struct {
+	key string
+	t   *tableReq
+}
+
+var (
+	_ wire.RowServerMethods = (*rowReq)(nil)
+)
+
+////////////////////////////////////////
+// RPC methods
+
+func (r *rowReq) Exists(ctx *context.T, call rpc.ServerCall, schemaVersion int32) (bool, error) {
+	_, err := r.Get(ctx, call, schemaVersion)
+	return util.ErrorToExists(err)
+}
+
+func (r *rowReq) Get(ctx *context.T, call rpc.ServerCall, schemaVersion int32) ([]byte, error) {
+	impl := func(sntx store.SnapshotOrTransaction) ([]byte, error) {
+		if err := r.t.d.checkSchemaVersion(ctx, schemaVersion); err != nil {
+			return []byte{}, err
+		}
+		return r.get(ctx, call, sntx)
+	}
+	if r.t.d.batchId != nil {
+		return impl(r.t.d.batchReader())
+	} else {
+		sn := r.t.d.st.NewSnapshot()
+		defer sn.Abort()
+		return impl(sn)
+	}
+}
+
+func (r *rowReq) Put(ctx *context.T, call rpc.ServerCall, schemaVersion int32, value []byte) error {
+	impl := func(tx store.Transaction) error {
+		if err := r.t.d.checkSchemaVersion(ctx, schemaVersion); err != nil {
+			return err
+		}
+		return r.put(ctx, call, tx, value)
+	}
+	if r.t.d.batchId != nil {
+		if tx, err := r.t.d.batchTransaction(); err != nil {
+			return err
+		} else {
+			return impl(tx)
+		}
+	} else {
+		return store.RunInTransaction(r.t.d.st, impl)
+	}
+}
+
+func (r *rowReq) Delete(ctx *context.T, call rpc.ServerCall, schemaVersion int32) error {
+	impl := func(tx store.Transaction) error {
+		if err := r.t.d.checkSchemaVersion(ctx, schemaVersion); err != nil {
+			return err
+		}
+		return r.delete(ctx, call, tx)
+	}
+	if r.t.d.batchId != nil {
+		if tx, err := r.t.d.batchTransaction(); err != nil {
+			return err
+		} else {
+			return impl(tx)
+		}
+	} else {
+		return store.RunInTransaction(r.t.d.st, impl)
+	}
+}
+
+////////////////////////////////////////
+// Internal helpers
+
+func (r *rowReq) stKey() string {
+	return util.JoinKeyParts(util.RowPrefix, r.stKeyPart())
+}
+
+func (r *rowReq) stKeyPart() string {
+	return util.JoinKeyParts(r.t.stKeyPart(), r.key)
+}
+
+// checkAccess checks that this row's table exists in the database, and performs
+// an authorization check.
+func (r *rowReq) checkAccess(ctx *context.T, call rpc.ServerCall, sntx store.SnapshotOrTransaction) error {
+	return r.t.checkAccess(ctx, call, sntx, r.key)
+}
+
+// get reads data from the storage engine.
+// Performs authorization check.
+func (r *rowReq) get(ctx *context.T, call rpc.ServerCall, sntx store.SnapshotOrTransaction) ([]byte, error) {
+	if err := r.checkAccess(ctx, call, sntx); err != nil {
+		return nil, err
+	}
+	value, err := sntx.Get([]byte(r.stKey()), nil)
+	if err != nil {
+		if verror.ErrorID(err) == store.ErrUnknownKey.ID {
+			return nil, verror.New(verror.ErrNoExist, ctx, r.stKey())
+		}
+		return nil, verror.New(verror.ErrInternal, ctx, err)
+	}
+	return value, nil
+}
+
+// put writes data to the storage engine.
+// Performs authorization check.
+func (r *rowReq) put(ctx *context.T, call rpc.ServerCall, tx store.Transaction, value []byte) error {
+	if err := r.checkAccess(ctx, call, tx); err != nil {
+		return err
+	}
+	if err := tx.Put([]byte(r.stKey()), value); err != nil {
+		return verror.New(verror.ErrInternal, ctx, err)
+	}
+	return nil
+}
+
+// delete deletes data from the storage engine.
+// Performs authorization check.
+func (r *rowReq) delete(ctx *context.T, call rpc.ServerCall, tx store.Transaction) error {
+	if err := r.checkAccess(ctx, call, tx); err != nil {
+		return err
+	}
+	if err := tx.Delete([]byte(r.stKey())); err != nil {
+		return verror.New(verror.ErrInternal, ctx, err)
+	}
+	return nil
+}
diff --git a/services/syncbase/server/nosql/table.go b/services/syncbase/server/nosql/table.go
new file mode 100644
index 0000000..8cf744e
--- /dev/null
+++ b/services/syncbase/server/nosql/table.go
@@ -0,0 +1,486 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package nosql
+
+import (
+	"strings"
+
+	wire "v.io/syncbase/v23/services/syncbase/nosql"
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/context"
+	"v.io/v23/glob"
+	"v.io/v23/rpc"
+	"v.io/v23/security/access"
+	"v.io/v23/verror"
+	"v.io/v23/vom"
+)
+
+// tableReq is a per-request object that handles Table RPCs.
+type tableReq struct {
+	name string
+	d    *databaseReq
+}
+
+var (
+	_ wire.TableServerMethods = (*tableReq)(nil)
+)
+
+////////////////////////////////////////
+// RPC methods
+
+func (t *tableReq) Create(ctx *context.T, call rpc.ServerCall, schemaVersion int32, perms access.Permissions) error {
+	if t.d.batchId != nil {
+		return wire.NewErrBoundToBatch(ctx)
+	}
+	if err := t.d.checkSchemaVersion(ctx, schemaVersion); err != nil {
+		return err
+	}
+	return store.RunInTransaction(t.d.st, func(tx store.Transaction) error {
+		// Check databaseData perms.
+		dData := &databaseData{}
+		if err := util.GetWithAuth(ctx, call, tx, t.d.stKey(), dData); err != nil {
+			return err
+		}
+		// Check for "table already exists".
+		if err := util.Get(ctx, tx, t.stKey(), &tableData{}); verror.ErrorID(err) != verror.ErrNoExist.ID {
+			if err != nil {
+				return err
+			}
+			// TODO(sadovsky): Should this be ErrExistOrNoAccess, for privacy?
+			return verror.New(verror.ErrExist, ctx, t.name)
+		}
+		// Write new tableData.
+		if perms == nil {
+			perms = dData.Perms
+		}
+		data := &tableData{
+			Name:  t.name,
+			Perms: perms,
+		}
+		return util.Put(ctx, tx, t.stKey(), data)
+	})
+}
+
+func (t *tableReq) Delete(ctx *context.T, call rpc.ServerCall, schemaVersion int32) error {
+	if t.d.batchId != nil {
+		return wire.NewErrBoundToBatch(ctx)
+	}
+	if err := t.d.checkSchemaVersion(ctx, schemaVersion); err != nil {
+		return err
+	}
+	return store.RunInTransaction(t.d.st, func(tx store.Transaction) error {
+		// Read-check-delete tableData.
+		if err := util.GetWithAuth(ctx, call, tx, t.stKey(), &tableData{}); err != nil {
+			if verror.ErrorID(err) == verror.ErrNoExist.ID {
+				return nil // delete is idempotent
+			}
+			return err
+		}
+		// TODO(sadovsky): Delete all rows in this table.
+		return util.Delete(ctx, tx, t.stKey())
+	})
+}
+
+func (t *tableReq) Exists(ctx *context.T, call rpc.ServerCall, schemaVersion int32) (bool, error) {
+	if err := t.d.checkSchemaVersion(ctx, schemaVersion); err != nil {
+		return false, err
+	}
+	return util.ErrorToExists(util.GetWithAuth(ctx, call, t.d.st, t.stKey(), &tableData{}))
+}
+
+func (t *tableReq) DeleteRowRange(ctx *context.T, call rpc.ServerCall, schemaVersion int32, start, limit []byte) error {
+	impl := func(tx store.Transaction) error {
+		// Check for table-level access before doing a scan.
+		if err := t.checkAccess(ctx, call, tx, ""); err != nil {
+			return err
+		}
+		// Check if the db schema version and the version provided by client
+		// matches.
+		if err := t.d.checkSchemaVersion(ctx, schemaVersion); err != nil {
+			return err
+		}
+		it := tx.Scan(util.ScanRangeArgs(util.JoinKeyParts(util.RowPrefix, t.name), string(start), string(limit)))
+		key := []byte{}
+		for it.Advance() {
+			key = it.Key(key)
+			// Check perms.
+			parts := util.SplitKeyParts(string(key))
+			externalKey := parts[len(parts)-1]
+			if err := t.checkAccess(ctx, call, tx, externalKey); err != nil {
+				// TODO(rogulenko): Revisit this behavior. Probably we should
+				// delete all rows that we have access to.
+				it.Cancel()
+				return err
+			}
+			// Delete the key-value pair.
+			if err := tx.Delete(key); err != nil {
+				return verror.New(verror.ErrInternal, ctx, err)
+			}
+		}
+		if err := it.Err(); err != nil {
+			return verror.New(verror.ErrInternal, ctx, err)
+		}
+		return nil
+	}
+	if t.d.batchId != nil {
+		if tx, err := t.d.batchTransaction(); err != nil {
+			return err
+		} else {
+			return impl(tx)
+		}
+	} else {
+		return store.RunInTransaction(t.d.st, impl)
+	}
+}
+
+func (t *tableReq) Scan(ctx *context.T, call wire.TableScanServerCall, schemaVersion int32, start, limit []byte) error {
+	impl := func(sntx store.SnapshotOrTransaction) error {
+		// Check for table-level access before doing a scan.
+		if err := t.checkAccess(ctx, call, sntx, ""); err != nil {
+			return err
+		}
+		if err := t.d.checkSchemaVersion(ctx, schemaVersion); err != nil {
+			return err
+		}
+		it := sntx.Scan(util.ScanRangeArgs(util.JoinKeyParts(util.RowPrefix, t.name), string(start), string(limit)))
+		sender := call.SendStream()
+		key, value := []byte{}, []byte{}
+		for it.Advance() {
+			key, value = it.Key(key), it.Value(value)
+			// Check perms.
+			parts := util.SplitKeyParts(string(key))
+			externalKey := parts[len(parts)-1]
+			if err := t.checkAccess(ctx, call, sntx, externalKey); err != nil {
+				it.Cancel()
+				return err
+			}
+			if err := sender.Send(wire.KeyValue{Key: externalKey, Value: value}); err != nil {
+				it.Cancel()
+				return err
+			}
+		}
+		if err := it.Err(); err != nil {
+			return verror.New(verror.ErrInternal, ctx, err)
+		}
+		return nil
+	}
+	if t.d.batchId != nil {
+		return impl(t.d.batchReader())
+	} else {
+		sntx := t.d.st.NewSnapshot()
+		defer sntx.Abort()
+		return impl(sntx)
+	}
+}
+
+func (t *tableReq) GetPermissions(ctx *context.T, call rpc.ServerCall, schemaVersion int32, key string) ([]wire.PrefixPermissions, error) {
+	impl := func(sntx store.SnapshotOrTransaction) ([]wire.PrefixPermissions, error) {
+		// Check permissions only at table level.
+		if err := t.checkAccess(ctx, call, sntx, ""); err != nil {
+			return nil, err
+		}
+		if err := t.d.checkSchemaVersion(ctx, schemaVersion); err != nil {
+			return nil, err
+		}
+		// Get the most specific permissions object.
+		prefix, prefixPerms, err := t.permsForKey(ctx, sntx, key)
+		if err != nil {
+			return nil, err
+		}
+		result := []wire.PrefixPermissions{{Prefix: prefix, Perms: prefixPerms.Perms}}
+		// Collect all parent permissions objects all the way up to the table level.
+		for prefix != "" {
+			prefix = prefixPerms.Parent
+			if prefixPerms, err = t.permsForPrefix(ctx, sntx, prefixPerms.Parent); err != nil {
+				return nil, err
+			}
+			result = append(result, wire.PrefixPermissions{Prefix: prefix, Perms: prefixPerms.Perms})
+		}
+		return result, nil
+	}
+	if t.d.batchId != nil {
+		return impl(t.d.batchReader())
+	} else {
+		sntx := t.d.st.NewSnapshot()
+		defer sntx.Abort()
+		return impl(sntx)
+	}
+}
+
+func (t *tableReq) SetPermissions(ctx *context.T, call rpc.ServerCall, schemaVersion int32, prefix string, perms access.Permissions) error {
+	impl := func(tx store.Transaction) error {
+		if err := t.checkAccess(ctx, call, tx, prefix); err != nil {
+			return err
+		}
+		if err := t.d.checkSchemaVersion(ctx, schemaVersion); err != nil {
+			return err
+		}
+		// Concurrent transactions that touch this table should fail with
+		// ErrConcurrentTransaction when this transaction commits.
+		if err := t.lock(ctx, tx); err != nil {
+			return err
+		}
+		if prefix == "" {
+			data := &tableData{}
+			return util.UpdateWithAuth(ctx, call, tx, t.stKey(), data, func() error {
+				data.Perms = perms
+				return nil
+			})
+		}
+		// Get the most specific permissions object.
+		parent, prefixPerms, err := t.permsForKey(ctx, tx, prefix)
+		if err != nil {
+			return err
+		}
+		// In case there is no permissions object for the given prefix, we need
+		// to add a new node to the prefix permissions tree. We do it by updating
+		// parents for all children of the prefix to the node corresponding to
+		// the prefix.
+		if parent != prefix {
+			if err := t.updateParentRefs(ctx, tx, prefix, prefix); err != nil {
+				return err
+			}
+		} else {
+			parent = prefixPerms.Parent
+		}
+		stPrefix := t.prefixPermsKey(prefix)
+		stPrefixLimit := stPrefix + util.PrefixRangeLimitSuffix
+		prefixPerms = stPrefixPerms{Parent: parent, Perms: perms}
+		// Put the (prefix, perms) pair to the database.
+		if err := util.Put(ctx, tx, stPrefix, prefixPerms); err != nil {
+			return err
+		}
+		return util.Put(ctx, tx, stPrefixLimit, prefixPerms)
+	}
+	if t.d.batchId != nil {
+		if tx, err := t.d.batchTransaction(); err != nil {
+			return err
+		} else {
+			return impl(tx)
+		}
+	} else {
+		return store.RunInTransaction(t.d.st, impl)
+	}
+}
+
+func (t *tableReq) DeletePermissions(ctx *context.T, call rpc.ServerCall, schemaVersion int32, prefix string) error {
+	if prefix == "" {
+		return verror.New(verror.ErrBadArg, ctx, prefix)
+	}
+	impl := func(tx store.Transaction) error {
+		if err := t.checkAccess(ctx, call, tx, prefix); err != nil {
+			return err
+		}
+		if err := t.d.checkSchemaVersion(ctx, schemaVersion); err != nil {
+			return err
+		}
+		// Concurrent transactions that touch this table should fail with
+		// ErrConcurrentTransaction when this transaction commits.
+		if err := t.lock(ctx, tx); err != nil {
+			return err
+		}
+		// Get the most specific permissions object.
+		parent, prefixPerms, err := t.permsForKey(ctx, tx, prefix)
+		if err != nil {
+			return err
+		}
+		if parent != prefix {
+			// This can happen only if there is no permissions object for the
+			// given prefix. Since DeletePermissions is idempotent, return nil.
+			return nil
+		}
+		// We need to delete the node corresponding to the prefix from the prefix
+		// permissions tree. We do it by updating parents for all children of the
+		// prefix to the parent of the node corresponding to the prefix.
+		if err := t.updateParentRefs(ctx, tx, prefix, prefixPerms.Parent); err != nil {
+			return err
+		}
+		stPrefix := []byte(t.prefixPermsKey(prefix))
+		stPrefixLimit := append(stPrefix, util.PrefixRangeLimitSuffix...)
+		if err := tx.Delete(stPrefix); err != nil {
+			return err
+		}
+		return tx.Delete(stPrefixLimit)
+	}
+	if t.d.batchId != nil {
+		if tx, err := t.d.batchTransaction(); err != nil {
+			return err
+		} else {
+			return impl(tx)
+		}
+	} else {
+		return store.RunInTransaction(t.d.st, impl)
+	}
+}
+
+func (t *tableReq) GlobChildren__(ctx *context.T, call rpc.GlobChildrenServerCall, matcher *glob.Element) error {
+	impl := func(sntx store.SnapshotOrTransaction, closeSntx func() error) error {
+		// Check perms.
+		if err := t.checkAccess(ctx, call, sntx, ""); err != nil {
+			closeSntx()
+			return err
+		}
+		// TODO(rogulenko): Check prefix permissions for children.
+		return util.Glob(ctx, call, matcher, sntx, closeSntx, util.JoinKeyParts(util.RowPrefix, t.name))
+	}
+	if t.d.batchId != nil {
+		return impl(t.d.batchReader(), func() error {
+			return nil
+		})
+	} else {
+		sn := t.d.st.NewSnapshot()
+		return impl(sn, sn.Abort)
+	}
+}
+
+////////////////////////////////////////
+// Internal helpers
+
+func (t *tableReq) stKey() string {
+	return util.JoinKeyParts(util.TablePrefix, t.stKeyPart())
+}
+
+func (t *tableReq) stKeyPart() string {
+	return t.name
+}
+
+// updateParentRefs updates the parent for all children of the given
+// prefix to newParent.
+func (t *tableReq) updateParentRefs(ctx *context.T, tx store.Transaction, prefix, newParent string) error {
+	stPrefix := []byte(t.prefixPermsKey(prefix))
+	stPrefixStart := append(stPrefix, 0)
+	stPrefixLimit := append(stPrefix, util.PrefixRangeLimitSuffix...)
+	it := tx.Scan(stPrefixStart, stPrefixLimit)
+	var key, value []byte
+	for it.Advance() {
+		key, value = it.Key(key), it.Value(value)
+		var prefixPerms stPrefixPerms
+		if err := vom.Decode(value, &prefixPerms); err != nil {
+			it.Cancel()
+			return verror.New(verror.ErrInternal, ctx, err)
+		}
+		prefixPerms.Parent = newParent
+		if err := util.Put(ctx, tx, string(key), prefixPerms); err != nil {
+			it.Cancel()
+			return err
+		}
+	}
+	if err := it.Err(); err != nil {
+		return verror.New(verror.ErrInternal, ctx, err)
+	}
+	return nil
+}
+
+// lock invalidates all in-flight transactions that have touched this table,
+// such that any subsequent tx.Commit() will return ErrConcurrentTransaction.
+//
+// It is necessary to call lock() every time prefix permissions are updated so
+// that snapshots inside all transactions reflect up-to-date permissions. Since
+// every public function that touches this table has to read the table-level
+// permissions object, it suffices to add the key of this object to the write
+// set of the current transaction.
+//
+// TODO(rogulenko): Revisit this behavior to provide more granularity.
+// One option is to add a prefix and its parent to the write set of the current
+// transaction when the permissions object for that prefix is updated.
+func (t *tableReq) lock(ctx *context.T, tx store.Transaction) error {
+	var data tableData
+	if err := util.Get(ctx, tx, t.stKey(), &data); err != nil {
+		return err
+	}
+	return util.Put(ctx, tx, t.stKey(), data)
+}
+
+// checkAccess checks that this table exists in the database, and performs
+// an authorization check. The access is checked at table level and at the
+// level of the most specific prefix for the given key.
+// TODO(rogulenko): Revisit this behavior. Eventually we'll want the table-level
+// access check to be a check for "Resolve", i.e. also check access to
+// service, app and database.
+func (t *tableReq) checkAccess(ctx *context.T, call rpc.ServerCall, sntx store.SnapshotOrTransaction, key string) error {
+	prefix, prefixPerms, err := t.permsForKey(ctx, sntx, key)
+	if err != nil {
+		return err
+	}
+	if prefix != "" {
+		if err := util.GetWithAuth(ctx, call, sntx, t.stKey(), &tableData{}); err != nil {
+			return err
+		}
+	}
+	auth, _ := access.PermissionsAuthorizer(prefixPerms.Perms, access.TypicalTagType())
+	if err := auth.Authorize(ctx, call.Security()); err != nil {
+		return verror.New(verror.ErrNoAccess, ctx, prefix)
+	}
+	return nil
+}
+
+// permsForKey returns the longest prefix of the given key that has
+// associated permissions, along with its permissions object.
+// permsForKey doesn't perform an authorization check.
+//
+// Effectively, we represent all prefixes as a forest T, where each vertex maps
+// to a prefix. A parent for a string is the maximum proper prefix of it that
+// belongs to T. Each prefix P from T is represented as a pair of entries with
+// keys P and P~ with values of type stPrefixPerms (parent + perms). High level
+// explanation of how this function works:
+// 1	iter = db.Scan(K, "")
+// 		Here last character of iter.Key() is removed automatically if it is '~'
+// 2	if hasPrefix(K, iter.Key()) return iter.Value()
+// 3	return parent(iter.Key())
+// Short proof:
+// iter returned on line 1 points to one of the following:
+// - a string t that is equal to K;
+// - a string t~: if t is not a prefix of K, then K < t < t~ which
+//   contradicts with property of returned iterator on line 1 => t is prefix of
+//   K; also t is the largest prefix of K, as all larger prefixes of K are
+//   less than t~; in this case line 2 returns correct result;
+// - a string t that doesn't end with '~': it can't be a prefix of K, as all
+//   proper prefixes of K are less than K; parent(t) is a prefix of K, otherwise
+//   K < parent(t) < t; parent(t) is the largest prefix of K, otherwise t is a
+//   prefix of K; in this case line 3 returns correct result.
+func (t *tableReq) permsForKey(ctx *context.T, sntx store.SnapshotOrTransaction, key string) (string, stPrefixPerms, error) {
+	it := sntx.Scan(util.ScanRangeArgs(util.JoinKeyParts(util.PermsPrefix, t.name), key, ""))
+	if !it.Advance() {
+		prefixPerms, err := t.permsForPrefix(ctx, sntx, "")
+		return "", prefixPerms, err
+	}
+	defer it.Cancel()
+	parts := util.SplitKeyParts(string(it.Key(nil)))
+	prefix := strings.TrimSuffix(parts[len(parts)-1], util.PrefixRangeLimitSuffix)
+	value := it.Value(nil)
+	var prefixPerms stPrefixPerms
+	if err := vom.Decode(value, &prefixPerms); err != nil {
+		return "", stPrefixPerms{}, verror.New(verror.ErrInternal, ctx, err)
+	}
+	if strings.HasPrefix(key, prefix) {
+		return prefix, prefixPerms, nil
+	}
+	prefixPerms, err := t.permsForPrefix(ctx, sntx, prefixPerms.Parent)
+	return prefixPerms.Parent, prefixPerms, err
+}
+
+// permsForPrefix returns the permissions object associated with the
+// provided prefix.
+func (t *tableReq) permsForPrefix(ctx *context.T, sntx store.SnapshotOrTransaction, prefix string) (stPrefixPerms, error) {
+	if prefix == "" {
+		var data tableData
+		if err := util.Get(ctx, sntx, t.stKey(), &data); err != nil {
+			return stPrefixPerms{}, err
+		}
+		return stPrefixPerms{Perms: data.Perms}, nil
+	}
+	var prefixPerms stPrefixPerms
+	if err := util.Get(ctx, sntx, t.prefixPermsKey(prefix), &prefixPerms); err != nil {
+		return stPrefixPerms{}, verror.New(verror.ErrInternal, ctx, err)
+	}
+	return prefixPerms, nil
+}
+
+// prefixPermsKey returns the key used for storing permissions for the given
+// prefix in the table.
+func (t *tableReq) prefixPermsKey(prefix string) string {
+	return util.JoinKeyParts(util.PermsPrefix, t.name, prefix)
+}
diff --git a/services/syncbase/server/nosql/types.go b/services/syncbase/server/nosql/types.go
new file mode 100644
index 0000000..7a87916
--- /dev/null
+++ b/services/syncbase/server/nosql/types.go
@@ -0,0 +1,23 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package nosql
+
+import (
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/v23/security/access"
+)
+
+var (
+	_ util.Permser = (*databaseData)(nil)
+	_ util.Permser = (*tableData)(nil)
+)
+
+func (data *databaseData) GetPerms() access.Permissions {
+	return data.Perms
+}
+
+func (data *tableData) GetPerms() access.Permissions {
+	return data.Perms
+}
diff --git a/services/syncbase/server/nosql/types.vdl b/services/syncbase/server/nosql/types.vdl
new file mode 100644
index 0000000..8ede239
--- /dev/null
+++ b/services/syncbase/server/nosql/types.vdl
@@ -0,0 +1,39 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package nosql
+
+import (
+	"v.io/v23/security/access"
+	"v.io/syncbase/v23/services/syncbase/nosql"
+)
+
+// databaseData represents the persistent state of a Database.
+type databaseData struct {
+	Name           string
+	Version        uint64 // covers the Perms field below
+	Perms          access.Permissions
+	SchemaMetadata ?nosql.SchemaMetadata
+}
+
+// tableData represents the persistent state of a Table.
+// TODO(sadovsky): Decide whether to track "empty-prefix" perms here.
+type tableData struct {
+	Name  string
+	Perms access.Permissions
+}
+
+// stPrefixPerms describes internal representation of prefix permissions
+// in the store.
+//
+// Each pair of (key, perms) is stored as two key-value pairs:
+// "$perms:%table:key"  - stPrefixPerms{parent, perms}
+// "$perms:%table:key~" - stPrefixPerms{parent, perms}
+// where "~" represents a reserved char that's lexicographically greater than
+// all chars allowed by clients, %table is the name of the table and parent is
+// the longest proper prefix of the key that has associated permissions object.
+type stPrefixPerms struct {
+	Parent string
+	Perms  access.Permissions
+}
diff --git a/services/syncbase/server/nosql/types.vdl.go b/services/syncbase/server/nosql/types.vdl.go
new file mode 100644
index 0000000..bf5f346
--- /dev/null
+++ b/services/syncbase/server/nosql/types.vdl.go
@@ -0,0 +1,67 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file was auto-generated by the vanadium vdl tool.
+// Source: types.vdl
+
+package nosql
+
+import (
+	// VDL system imports
+	"v.io/v23/vdl"
+
+	// VDL user imports
+	"v.io/syncbase/v23/services/syncbase/nosql"
+	"v.io/v23/security/access"
+)
+
+// databaseData represents the persistent state of a Database.
+type databaseData struct {
+	Name           string
+	Version        uint64 // covers the Perms field below
+	Perms          access.Permissions
+	SchemaMetadata *nosql.SchemaMetadata
+}
+
+func (databaseData) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/server/nosql.databaseData"`
+}) {
+}
+
+// tableData represents the persistent state of a Table.
+// TODO(sadovsky): Decide whether to track "empty-prefix" perms here.
+type tableData struct {
+	Name  string
+	Perms access.Permissions
+}
+
+func (tableData) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/server/nosql.tableData"`
+}) {
+}
+
+// stPrefixPerms describes internal representation of prefix permissions
+// in the store.
+//
+// Each pair of (key, perms) is stored as two key-value pairs:
+// "$perms:%table:key"  - stPrefixPerms{parent, perms}
+// "$perms:%table:key~" - stPrefixPerms{parent, perms}
+// where "~" represents a reserved char that's lexicographically greater than
+// all chars allowed by clients, %table is the name of the table and parent is
+// the longest proper prefix of the key that has associated permissions object.
+type stPrefixPerms struct {
+	Parent string
+	Perms  access.Permissions
+}
+
+func (stPrefixPerms) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/server/nosql.stPrefixPerms"`
+}) {
+}
+
+func init() {
+	vdl.Register((*databaseData)(nil))
+	vdl.Register((*tableData)(nil))
+	vdl.Register((*stPrefixPerms)(nil))
+}
diff --git a/services/syncbase/server/server_test.go b/services/syncbase/server/server_test.go
new file mode 100644
index 0000000..d6b51ad
--- /dev/null
+++ b/services/syncbase/server/server_test.go
@@ -0,0 +1,25 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package server_test
+
+// Note: Most of our unit tests are client-side and cover end-to-end behavior.
+// Tests of the "server" package (and below) specifically target aspects of the
+// implementation that are difficult to test from the client side.
+
+import (
+	"testing"
+
+	tu "v.io/syncbase/v23/syncbase/testutil"
+	_ "v.io/x/ref/runtime/factories/generic"
+)
+
+////////////////////////////////////////
+// Test cases
+
+// TODO(sadovsky): Write some tests.
+func TestSomething(t *testing.T) {
+	_, _, cleanup := tu.SetupOrDie(nil)
+	defer cleanup()
+}
diff --git a/services/syncbase/server/service.go b/services/syncbase/server/service.go
new file mode 100644
index 0000000..180692f
--- /dev/null
+++ b/services/syncbase/server/service.go
@@ -0,0 +1,307 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package server
+
+// TODO(sadovsky): Check Resolve access on parent where applicable. Relatedly,
+// convert ErrNoExist and ErrNoAccess to ErrNoExistOrNoAccess where needed to
+// preserve privacy.
+
+import (
+	"path"
+	"sync"
+
+	wire "v.io/syncbase/v23/services/syncbase"
+	"v.io/syncbase/x/ref/services/syncbase/server/interfaces"
+	"v.io/syncbase/x/ref/services/syncbase/server/nosql"
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/syncbase/x/ref/services/syncbase/vsync"
+	"v.io/v23/context"
+	"v.io/v23/glob"
+	"v.io/v23/rpc"
+	"v.io/v23/security/access"
+	"v.io/v23/verror"
+	"v.io/v23/vom"
+)
+
+// service is a singleton (i.e. not per-request) that handles Service RPCs.
+type service struct {
+	st   store.Store // keeps track of which apps and databases exist, etc.
+	sync interfaces.SyncServerMethods
+	opts ServiceOptions
+	// Guards the fields below. Held during app Create, Delete, and
+	// SetPermissions.
+	mu   sync.Mutex
+	apps map[string]*app
+}
+
+var (
+	_ wire.ServiceServerMethods = (*service)(nil)
+	_ interfaces.Service        = (*service)(nil)
+)
+
+// ServiceOptions configures a service.
+type ServiceOptions struct {
+	// Service-level permissions.
+	Perms access.Permissions
+	// Root dir for data storage.
+	RootDir string
+	// Storage engine to use (for service and per-database engines).
+	Engine string
+	// RPC server for this service. Needed to advertise this service in
+	// mount tables attached to SyncGroups.
+	Server rpc.Server
+}
+
+// NewService creates a new service instance and returns it.
+// TODO(sadovsky): If possible, close all stores when the server is stopped.
+func NewService(ctx *context.T, call rpc.ServerCall, opts ServiceOptions) (*service, error) {
+	if opts.Perms == nil {
+		return nil, verror.New(verror.ErrInternal, ctx, "perms must be specified")
+	}
+	st, err := util.OpenStore(opts.Engine, path.Join(opts.RootDir, opts.Engine), util.OpenOptions{CreateIfMissing: true, ErrorIfExists: false})
+	if err != nil {
+		return nil, err
+	}
+	s := &service{
+		st:   st,
+		opts: opts,
+		apps: map[string]*app{},
+	}
+	data := &serviceData{
+		Perms: opts.Perms,
+	}
+	if err := util.Get(ctx, st, s.stKey(), &serviceData{}); verror.ErrorID(err) != verror.ErrNoExist.ID {
+		if err != nil {
+			return nil, err
+		}
+		// Service exists. Initialize in-memory data structures.
+		// Read all apps, populate apps map.
+		aIt := st.Scan(util.ScanPrefixArgs(util.AppPrefix, ""))
+		aBytes := []byte{}
+		for aIt.Advance() {
+			aBytes = aIt.Value(aBytes)
+			aData := &appData{}
+			if err := vom.Decode(aBytes, aData); err != nil {
+				return nil, verror.New(verror.ErrInternal, ctx, err)
+			}
+			a := &app{
+				name:   aData.Name,
+				s:      s,
+				exists: true,
+				dbs:    make(map[string]interfaces.Database),
+			}
+			s.apps[a.name] = a
+			// Read all dbs for this app, populate dbs map.
+			dIt := st.Scan(util.ScanPrefixArgs(util.JoinKeyParts(util.DbInfoPrefix, aData.Name), ""))
+			dBytes := []byte{}
+			for dIt.Advance() {
+				dBytes = dIt.Value(dBytes)
+				info := &dbInfo{}
+				if err := vom.Decode(dBytes, info); err != nil {
+					return nil, verror.New(verror.ErrInternal, ctx, err)
+				}
+				d, err := nosql.OpenDatabase(ctx, a, info.Name, nosql.DatabaseOptions{
+					RootDir: info.RootDir,
+					Engine:  info.Engine,
+				}, util.OpenOptions{
+					CreateIfMissing: false,
+					ErrorIfExists:   false,
+				})
+				if err != nil {
+					return nil, verror.New(verror.ErrInternal, ctx, err)
+				}
+				a.dbs[info.Name] = d
+			}
+			if err := dIt.Err(); err != nil {
+				return nil, verror.New(verror.ErrInternal, ctx, err)
+			}
+		}
+		if err := aIt.Err(); err != nil {
+			return nil, verror.New(verror.ErrInternal, ctx, err)
+		}
+	} else {
+		// Service does not exist.
+		if err := util.Put(ctx, st, s.stKey(), data); err != nil {
+			return nil, err
+		}
+	}
+	// Note, vsync.New internally handles both first-time and subsequent
+	// invocations.
+	if s.sync, err = vsync.New(ctx, call, s, opts.Server, opts.RootDir); err != nil {
+		return nil, err
+	}
+	return s, nil
+}
+
+////////////////////////////////////////
+// RPC methods
+
+func (s *service) SetPermissions(ctx *context.T, call rpc.ServerCall, perms access.Permissions, version string) error {
+	return store.RunInTransaction(s.st, func(tx store.Transaction) error {
+		data := &serviceData{}
+		return util.UpdateWithAuth(ctx, call, tx, s.stKey(), data, func() error {
+			if err := util.CheckVersion(ctx, version, data.Version); err != nil {
+				return err
+			}
+			data.Perms = perms
+			data.Version++
+			return nil
+		})
+	})
+}
+
+func (s *service) GetPermissions(ctx *context.T, call rpc.ServerCall) (perms access.Permissions, version string, err error) {
+	data := &serviceData{}
+	if err := util.GetWithAuth(ctx, call, s.st, s.stKey(), data); err != nil {
+		return nil, "", err
+	}
+	return data.Perms, util.FormatVersion(data.Version), nil
+}
+
+func (s *service) GlobChildren__(ctx *context.T, call rpc.GlobChildrenServerCall, matcher *glob.Element) error {
+	// Check perms.
+	sn := s.st.NewSnapshot()
+	if err := util.GetWithAuth(ctx, call, sn, s.stKey(), &serviceData{}); err != nil {
+		sn.Abort()
+		return err
+	}
+	return util.Glob(ctx, call, matcher, sn, sn.Abort, util.AppPrefix)
+}
+
+////////////////////////////////////////
+// interfaces.Service methods
+
+func (s *service) St() store.Store {
+	return s.st
+}
+
+func (s *service) Sync() interfaces.SyncServerMethods {
+	return s.sync
+}
+
+func (s *service) App(ctx *context.T, call rpc.ServerCall, appName string) (interfaces.App, error) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	// Note, currently the service's apps map as well as per-app dbs maps are
+	// populated at startup.
+	a, ok := s.apps[appName]
+	if !ok {
+		return nil, verror.New(verror.ErrNoExist, ctx, appName)
+	}
+	return a, nil
+}
+
+func (s *service) AppNames(ctx *context.T, call rpc.ServerCall) ([]string, error) {
+	// In the future this API will likely be replaced by one that streams the app
+	// names.
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	appNames := make([]string, 0, len(s.apps))
+	for n := range s.apps {
+		appNames = append(appNames, n)
+	}
+	return appNames, nil
+}
+
+////////////////////////////////////////
+// App management methods
+
+func (s *service) createApp(ctx *context.T, call rpc.ServerCall, appName string, perms access.Permissions) error {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	if _, ok := s.apps[appName]; ok {
+		return verror.New(verror.ErrExist, ctx, appName)
+	}
+
+	a := &app{
+		name:   appName,
+		s:      s,
+		exists: true,
+		dbs:    make(map[string]interfaces.Database),
+	}
+
+	if err := store.RunInTransaction(s.st, func(tx store.Transaction) error {
+		// Check serviceData perms.
+		sData := &serviceData{}
+		if err := util.GetWithAuth(ctx, call, tx, s.stKey(), sData); err != nil {
+			return err
+		}
+		// Check for "app already exists".
+		if err := util.Get(ctx, tx, a.stKey(), &appData{}); verror.ErrorID(err) != verror.ErrNoExist.ID {
+			if err != nil {
+				return err
+			}
+			return verror.New(verror.ErrExist, ctx, appName)
+		}
+		// Write new appData.
+		if perms == nil {
+			perms = sData.Perms
+		}
+		data := &appData{
+			Name:  appName,
+			Perms: perms,
+		}
+		return util.Put(ctx, tx, a.stKey(), data)
+	}); err != nil {
+		return err
+	}
+
+	s.apps[appName] = a
+	return nil
+}
+
+func (s *service) deleteApp(ctx *context.T, call rpc.ServerCall, appName string) error {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	a, ok := s.apps[appName]
+	if !ok {
+		return nil // delete is idempotent
+	}
+
+	if err := store.RunInTransaction(s.st, func(tx store.Transaction) error {
+		// Read-check-delete appData.
+		if err := util.GetWithAuth(ctx, call, tx, a.stKey(), &appData{}); err != nil {
+			if verror.ErrorID(err) == verror.ErrNoExist.ID {
+				return nil // delete is idempotent
+			}
+			return err
+		}
+		// TODO(sadovsky): Delete all databases in this app.
+		return util.Delete(ctx, tx, a.stKey())
+	}); err != nil {
+		return err
+	}
+
+	delete(s.apps, appName)
+	return nil
+}
+
+func (s *service) setAppPerms(ctx *context.T, call rpc.ServerCall, appName string, perms access.Permissions, version string) error {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	a, ok := s.apps[appName]
+	if !ok {
+		return verror.New(verror.ErrNoExist, ctx, appName)
+	}
+	return store.RunInTransaction(s.st, func(tx store.Transaction) error {
+		data := &appData{}
+		return util.UpdateWithAuth(ctx, call, tx, a.stKey(), data, func() error {
+			if err := util.CheckVersion(ctx, version, data.Version); err != nil {
+				return err
+			}
+			data.Perms = perms
+			data.Version++
+			return nil
+		})
+	})
+}
+
+////////////////////////////////////////
+// Other internal helpers
+
+func (s *service) stKey() string {
+	return util.ServicePrefix
+}
diff --git a/services/syncbase/server/types.go b/services/syncbase/server/types.go
new file mode 100644
index 0000000..2879d56
--- /dev/null
+++ b/services/syncbase/server/types.go
@@ -0,0 +1,23 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package server
+
+import (
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/v23/security/access"
+)
+
+var (
+	_ util.Permser = (*serviceData)(nil)
+	_ util.Permser = (*appData)(nil)
+)
+
+func (data *serviceData) GetPerms() access.Permissions {
+	return data.Perms
+}
+
+func (data *appData) GetPerms() access.Permissions {
+	return data.Perms
+}
diff --git a/services/syncbase/server/types.vdl b/services/syncbase/server/types.vdl
new file mode 100644
index 0000000..4999f77
--- /dev/null
+++ b/services/syncbase/server/types.vdl
@@ -0,0 +1,34 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package server
+
+import (
+	"v.io/v23/security/access"
+)
+
+// serviceData represents the persistent state of a Service.
+type serviceData struct {
+	Version uint64 // covers the fields below
+	Perms   access.Permissions
+}
+
+// appData represents the persistent state of an App.
+type appData struct {
+	Name    string
+	Version uint64 // covers the fields below
+	Perms   access.Permissions
+}
+
+// dbInfo contains information about one database for an App.
+// TODO(sadovsky): Track NoSQL vs. SQL.
+type dbInfo struct {
+	Name        string
+	Initialized bool
+	Deleted     bool
+	// Select fields from nosql.DatabaseOptions, needed in order to open storage
+	// engine on restart.
+	RootDir string // interpreted by storage engine
+	Engine string // name of storage engine, e.g. "leveldb"
+}
diff --git a/services/syncbase/server/types.vdl.go b/services/syncbase/server/types.vdl.go
new file mode 100644
index 0000000..aec38cd
--- /dev/null
+++ b/services/syncbase/server/types.vdl.go
@@ -0,0 +1,62 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file was auto-generated by the vanadium vdl tool.
+// Source: types.vdl
+
+package server
+
+import (
+	// VDL system imports
+	"v.io/v23/vdl"
+
+	// VDL user imports
+	"v.io/v23/security/access"
+)
+
+// serviceData represents the persistent state of a Service.
+type serviceData struct {
+	Version uint64 // covers the fields below
+	Perms   access.Permissions
+}
+
+func (serviceData) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/server.serviceData"`
+}) {
+}
+
+// appData represents the persistent state of an App.
+type appData struct {
+	Name    string
+	Version uint64 // covers the fields below
+	Perms   access.Permissions
+}
+
+func (appData) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/server.appData"`
+}) {
+}
+
+// dbInfo contains information about one database for an App.
+// TODO(sadovsky): Track NoSQL vs. SQL.
+type dbInfo struct {
+	Name        string
+	Initialized bool
+	Deleted     bool
+	// Select fields from nosql.DatabaseOptions, needed in order to open storage
+	// engine on restart.
+	RootDir string // interpreted by storage engine
+	Engine  string // name of storage engine, e.g. "leveldb"
+}
+
+func (dbInfo) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/server.dbInfo"`
+}) {
+}
+
+func init() {
+	vdl.Register((*serviceData)(nil))
+	vdl.Register((*appData)(nil))
+	vdl.Register((*dbInfo)(nil))
+}
diff --git a/services/syncbase/server/util/constants.go b/services/syncbase/server/util/constants.go
new file mode 100644
index 0000000..ab2e401
--- /dev/null
+++ b/services/syncbase/server/util/constants.go
@@ -0,0 +1,51 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package util
+
+import (
+	"time"
+)
+
+// TODO(sadovsky): Consider using shorter strings.
+
+// Constants related to storage engine keys.
+const (
+	AppPrefix      = "$app"
+	ClockPrefix    = "$clock"
+	DatabasePrefix = "$database"
+	DbInfoPrefix   = "$dbInfo"
+	LogPrefix      = "$log"
+	PermsPrefix    = "$perms"
+	RowPrefix      = "$row"
+	ServicePrefix  = "$service"
+	SyncPrefix     = "$sync"
+	TablePrefix    = "$table"
+	VersionPrefix  = "$version"
+)
+
+// Constants related to object names.
+const (
+	// Service object name suffix for Syncbase-to-Syncbase RPCs.
+	SyncbaseSuffix = "$sync"
+	// Separator for batch info in database names.
+	BatchSep = ":"
+	// Separator for parts of storage engine keys.
+	KeyPartSep = ":"
+	// PrefixRangeLimitSuffix is the suffix of a key which indicates the end of
+	// a prefix range. Should be more than any regular key in the store.
+	// TODO(rogulenko): Change this constant to something out of the UTF8 space.
+	PrefixRangeLimitSuffix = "~"
+)
+
+// Constants related to syncbase clock.
+const (
+	// The pool.ntp.org project is a big virtual cluster of timeservers
+	// providing reliable easy to use NTP service for millions of clients.
+	// See more at http://www.pool.ntp.org/en/
+	NtpServerPool            = "pool.ntp.org"
+	NtpSampleCount           = 15
+	LocalClockDriftThreshold = float64(time.Second)
+	NtpDiffThreshold         = float64(2 * time.Second)
+)
diff --git a/services/syncbase/server/util/glob.go b/services/syncbase/server/util/glob.go
new file mode 100644
index 0000000..4a73870
--- /dev/null
+++ b/services/syncbase/server/util/glob.go
@@ -0,0 +1,40 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package util
+
+import (
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/context"
+	"v.io/v23/glob"
+	"v.io/v23/naming"
+	"v.io/v23/rpc"
+	"v.io/x/lib/vlog"
+)
+
+// NOTE(nlacasse): Syncbase handles Glob requests by implementing
+// GlobChildren__ at each level (service, app, database, table).
+
+// Glob performs a glob. It calls closeSntx to close sntx.
+func Glob(ctx *context.T, call rpc.GlobChildrenServerCall, matcher *glob.Element, sntx store.SnapshotOrTransaction, closeSntx func() error, stKeyPrefix string) error {
+	prefix, _ := matcher.FixedPrefix()
+	it := sntx.Scan(ScanPrefixArgs(stKeyPrefix, prefix))
+	defer closeSntx()
+	key := []byte{}
+	for it.Advance() {
+		key = it.Key(key)
+		parts := SplitKeyParts(string(key))
+		name := parts[len(parts)-1]
+		if matcher.Match(name) {
+			if err := call.SendStream().Send(naming.GlobChildrenReplyName{Value: name}); err != nil {
+				return err
+			}
+		}
+	}
+	if err := it.Err(); err != nil {
+		vlog.VI(1).Infof("Glob() failed: %v", err)
+		call.SendStream().Send(naming.GlobChildrenReplyError{Value: naming.GlobError{Error: err}})
+	}
+	return nil
+}
diff --git a/services/syncbase/server/util/key_util.go b/services/syncbase/server/util/key_util.go
new file mode 100644
index 0000000..80a8a6d
--- /dev/null
+++ b/services/syncbase/server/util/key_util.go
@@ -0,0 +1,37 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package util
+
+import (
+	"strings"
+
+	"v.io/syncbase/v23/syncbase/util"
+)
+
+// JoinKeyParts builds keys for accessing data in the storage engine.
+func JoinKeyParts(parts ...string) string {
+	// TODO(sadovsky): Figure out which delimiter makes the most sense.
+	return strings.Join(parts, KeyPartSep)
+}
+
+// SplitKeyParts is the inverse of JoinKeyParts.
+func SplitKeyParts(key string) []string {
+	return strings.Split(key, KeyPartSep)
+}
+
+// ScanPrefixArgs returns args for sn.Scan() for the specified prefix.
+func ScanPrefixArgs(stKeyPrefix, prefix string) ([]byte, []byte) {
+	return ScanRangeArgs(stKeyPrefix, util.PrefixRangeStart(prefix), util.PrefixRangeLimit(prefix))
+}
+
+// ScanRangeArgs returns args for sn.Scan() for the specified range.
+// If limit is "", all rows with keys >= start are included.
+func ScanRangeArgs(stKeyPrefix, start, limit string) ([]byte, []byte) {
+	fullStart, fullLimit := JoinKeyParts(stKeyPrefix, start), JoinKeyParts(stKeyPrefix, limit)
+	if limit == "" {
+		fullLimit = util.PrefixRangeLimit(fullLimit)
+	}
+	return []byte(fullStart), []byte(fullLimit)
+}
diff --git a/services/syncbase/server/util/key_util_test.go b/services/syncbase/server/util/key_util_test.go
new file mode 100644
index 0000000..2240531
--- /dev/null
+++ b/services/syncbase/server/util/key_util_test.go
@@ -0,0 +1,83 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package util_test
+
+import (
+	"reflect"
+	"testing"
+
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+)
+
+type kpt struct {
+	parts []string
+	key   string
+}
+
+var keyPartTests []kpt = []kpt{
+	{[]string{"a", "b"}, "a:b"},
+	{[]string{"aa", "bb"}, "aa:bb"},
+	{[]string{"a", "b", "c"}, "a:b:c"},
+}
+
+func TestJoinKeyParts(t *testing.T) {
+	for _, test := range keyPartTests {
+		got, want := util.JoinKeyParts(test.parts...), test.key
+		if !reflect.DeepEqual(got, want) {
+			t.Errorf("%v: got %q, want %q", test.parts, got, want)
+		}
+	}
+}
+
+func TestSplitKeyParts(t *testing.T) {
+	for _, test := range keyPartTests {
+		got, want := util.SplitKeyParts(test.key), test.parts
+		if !reflect.DeepEqual(got, want) {
+			t.Errorf("%q: got %v, want %v", test.key, got, want)
+		}
+	}
+}
+
+func TestScanPrefixArgs(t *testing.T) {
+	tests := []struct {
+		stKeyPrefix, prefix, wantStart, wantLimit string
+	}{
+		{"x", "", "x:", "x;"},
+		{"x", "a", "x:a", "x:b"},
+		{"x", "a\xff", "x:a\xff", "x:b"},
+	}
+	for _, test := range tests {
+		start, limit := util.ScanPrefixArgs(test.stKeyPrefix, test.prefix)
+		gotStart, gotLimit := string(start), string(limit)
+		if gotStart != test.wantStart {
+			t.Errorf("{%q, %q} start: got %q, want %q", test.stKeyPrefix, test.prefix, gotStart, test.wantStart)
+		}
+		if gotLimit != test.wantLimit {
+			t.Errorf("{%q, %q} limit: got %q, want %q", test.stKeyPrefix, test.prefix, gotLimit, test.wantLimit)
+		}
+	}
+}
+
+func TestScanRangeArgs(t *testing.T) {
+	tests := []struct {
+		stKeyPrefix, start, limit, wantStart, wantLimit string
+	}{
+		{"x", "", "", "x:", "x;"},   // limit "" means "no limit"
+		{"x", "a", "", "x:a", "x;"}, // limit "" means "no limit"
+		{"x", "a", "b", "x:a", "x:b"},
+		{"x", "a", "a", "x:a", "x:a"}, // empty range
+		{"x", "b", "a", "x:b", "x:a"}, // empty range
+	}
+	for _, test := range tests {
+		start, limit := util.ScanRangeArgs(test.stKeyPrefix, test.start, test.limit)
+		gotStart, gotLimit := string(start), string(limit)
+		if gotStart != test.wantStart {
+			t.Errorf("{%q, %q, %q} start: got %q, want %q", test.stKeyPrefix, test.start, test.limit, gotStart, test.wantStart)
+		}
+		if gotLimit != test.wantLimit {
+			t.Errorf("{%q, %q, %q} limit: got %q, want %q", test.stKeyPrefix, test.start, test.limit, gotLimit, test.wantLimit)
+		}
+	}
+}
diff --git a/services/syncbase/server/util/store_util.go b/services/syncbase/server/util/store_util.go
new file mode 100644
index 0000000..b8f1905
--- /dev/null
+++ b/services/syncbase/server/util/store_util.go
@@ -0,0 +1,164 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package util
+
+import (
+	"os"
+	"strconv"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/syncbase/x/ref/services/syncbase/store/leveldb"
+	"v.io/syncbase/x/ref/services/syncbase/store/memstore"
+	"v.io/v23/context"
+	"v.io/v23/rpc"
+	"v.io/v23/security/access"
+	"v.io/v23/verror"
+	"v.io/v23/vom"
+)
+
+func FormatVersion(version uint64) string {
+	return strconv.FormatUint(version, 10)
+}
+
+func CheckVersion(ctx *context.T, presented string, actual uint64) error {
+	if presented != "" && presented != FormatVersion(actual) {
+		return verror.NewErrBadVersion(ctx)
+	}
+	return nil
+}
+
+// TODO(sadovsky): Perhaps these functions should strip key prefixes such as
+// "$table:" from the error messages they return.
+
+type Permser interface {
+	// GetPerms returns the Permissions for this Layer.
+	GetPerms() access.Permissions
+}
+
+// Get does st.Get(k, v) and wraps the returned error.
+func Get(ctx *context.T, st store.StoreReader, k string, v interface{}) error {
+	bytes, err := st.Get([]byte(k), nil)
+	if err != nil {
+		if verror.ErrorID(err) == store.ErrUnknownKey.ID {
+			return verror.New(verror.ErrNoExist, ctx, k)
+		}
+		return verror.New(verror.ErrInternal, ctx, err)
+	}
+	if err = vom.Decode(bytes, v); err != nil {
+		return verror.New(verror.ErrInternal, ctx, err)
+	}
+	return nil
+}
+
+// GetWithAuth does Get followed by an auth check.
+func GetWithAuth(ctx *context.T, call rpc.ServerCall, st store.StoreReader, k string, v Permser) error {
+	if err := Get(ctx, st, k, v); err != nil {
+		return err
+	}
+	auth, _ := access.PermissionsAuthorizer(v.GetPerms(), access.TypicalTagType())
+	if err := auth.Authorize(ctx, call.Security()); err != nil {
+		return verror.New(verror.ErrNoAccess, ctx, err)
+	}
+	return nil
+}
+
+// Put does stw.Put(k, v) and wraps the returned error.
+func Put(ctx *context.T, stw store.StoreWriter, k string, v interface{}) error {
+	bytes, err := vom.Encode(v)
+	if err != nil {
+		return verror.New(verror.ErrInternal, ctx, err)
+	}
+	if err = stw.Put([]byte(k), bytes); err != nil {
+		return verror.New(verror.ErrInternal, ctx, err)
+	}
+	return nil
+}
+
+// Delete does stw.Delete(k, v) and wraps the returned error.
+func Delete(ctx *context.T, stw store.StoreWriter, k string) error {
+	if err := stw.Delete([]byte(k)); err != nil {
+		return verror.New(verror.ErrInternal, ctx, err)
+	}
+	return nil
+}
+
+// UpdateWithAuth performs a read-modify-write.
+// Input v is populated by the "read" step. fn should "modify" v.
+// Performs an auth check as part of the "read" step.
+func UpdateWithAuth(ctx *context.T, call rpc.ServerCall, tx store.Transaction, k string, v Permser, fn func() error) error {
+	if err := GetWithAuth(ctx, call, tx, k, v); err != nil {
+		return err
+	}
+	if err := fn(); err != nil {
+		return err
+	}
+	return Put(ctx, tx, k, v)
+}
+
+// Wraps a call to Get and returns true if Get found the object, false
+// otherwise, suppressing ErrNoExist. Access errors are suppressed as well
+// because they imply existence in some Get implementations.
+// TODO(ivanpi): Revisit once ACL specification is finalized.
+func ErrorToExists(err error) (bool, error) {
+	if err == nil {
+		return true, nil
+	}
+	switch verror.ErrorID(err) {
+	case verror.ErrNoExist.ID:
+		return false, nil
+	case verror.ErrNoAccess.ID, verror.ErrNoExistOrNoAccess.ID:
+		return false, nil
+	default:
+		return false, err
+	}
+}
+
+type OpenOptions struct {
+	CreateIfMissing bool
+	ErrorIfExists   bool
+}
+
+// OpenStore opens the given store.Store. OpenOptions are respected to the
+// degree possible for the specified engine.
+func OpenStore(engine, path string, opts OpenOptions) (store.Store, error) {
+	switch engine {
+	case "memstore":
+		if !opts.CreateIfMissing {
+			return nil, verror.New(verror.ErrInternal, nil, "cannot open memstore")
+		}
+		// By definition, the memstore does not already exist.
+		return memstore.New(), nil
+	case "leveldb":
+		leveldbOpts := leveldb.OpenOptions{
+			CreateIfMissing: opts.CreateIfMissing,
+			ErrorIfExists:   opts.ErrorIfExists,
+		}
+		if opts.CreateIfMissing {
+			// Note, os.MkdirAll is a noop if the path already exists. We rely on
+			// leveldb to enforce ErrorIfExists.
+			if err := os.MkdirAll(path, 0700); err != nil {
+				return nil, verror.New(verror.ErrInternal, nil, err)
+			}
+		}
+		return leveldb.Open(path, leveldbOpts)
+	default:
+		return nil, verror.New(verror.ErrBadArg, nil, engine)
+	}
+}
+
+func DestroyStore(engine, path string) error {
+	switch engine {
+	case "memstore":
+		// memstore doesn't persist any data on the disc, do nothing.
+		return nil
+	case "leveldb":
+		if err := os.RemoveAll(path); err != nil {
+			return verror.New(verror.ErrInternal, nil, err)
+		}
+		return nil
+	default:
+		return verror.New(verror.ErrBadArg, nil, engine)
+	}
+}
diff --git a/services/syncbase/server/watchable/snapshot.go b/services/syncbase/server/watchable/snapshot.go
new file mode 100644
index 0000000..37af4e1
--- /dev/null
+++ b/services/syncbase/server/watchable/snapshot.go
@@ -0,0 +1,45 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package watchable
+
+import (
+	"v.io/syncbase/x/ref/services/syncbase/store"
+)
+
+type snapshot struct {
+	store.SnapshotSpecImpl
+	isn store.Snapshot
+	st  *wstore
+}
+
+var _ store.Snapshot = (*snapshot)(nil)
+
+func newSnapshot(st *wstore) *snapshot {
+	return &snapshot{
+		isn: st.ist.NewSnapshot(),
+		st:  st,
+	}
+}
+
+// Abort implements the store.Snapshot interface.
+func (s *snapshot) Abort() error {
+	return s.isn.Abort()
+}
+
+// Get implements the store.StoreReader interface.
+func (s *snapshot) Get(key, valbuf []byte) ([]byte, error) {
+	if !s.st.managesKey(key) {
+		return s.isn.Get(key, valbuf)
+	}
+	return getVersioned(s.isn, key, valbuf)
+}
+
+// Scan implements the store.StoreReader interface.
+func (s *snapshot) Scan(start, limit []byte) store.Stream {
+	if !s.st.managesRange(start, limit) {
+		return s.isn.Scan(start, limit)
+	}
+	return newStreamVersioned(s.isn, start, limit)
+}
diff --git a/services/syncbase/server/watchable/store.go b/services/syncbase/server/watchable/store.go
new file mode 100644
index 0000000..0b19678
--- /dev/null
+++ b/services/syncbase/server/watchable/store.go
@@ -0,0 +1,159 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package watchable provides a Syncbase-specific store.Store wrapper that
+// provides versioned storage for specified prefixes and maintains a watchable
+// log of operations performed on versioned records. This log forms the basis
+// for the implementation of client-facing watch as well as the sync module's
+// internal watching of store updates.
+//
+// LogEntry records are stored chronologically, using keys of the form
+// "$log:<seq>". Sequence numbers are zero-padded to ensure that the
+// lexicographic order matches the numeric order.
+//
+// Version number records are stored using keys of the form "$version:<key>",
+// where <key> is the client-specified key.
+package watchable
+
+import (
+	"fmt"
+	"strings"
+	"sync"
+
+	pubutil "v.io/syncbase/v23/syncbase/util"
+	"v.io/syncbase/x/ref/services/syncbase/clock"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+)
+
+// Store is a store.Store that provides versioned storage and a watchable oplog.
+// TODO(sadovsky): Extend interface.
+type Store interface {
+	store.Store
+}
+
+// Options configures a watchable.Store.
+type Options struct {
+	// Key prefixes to version and log. If nil, all keys are managed.
+	ManagedPrefixes []string
+}
+
+// Wrap returns a watchable.Store that wraps the given store.Store.
+func Wrap(st store.Store, vclock *clock.VClock, opts *Options) (Store, error) {
+	seq, err := getNextLogSeq(st)
+	if err != nil {
+		return nil, err
+	}
+	return &wstore{
+		ist:     st,
+		watcher: newWatcher(),
+		opts:    opts,
+		seq:     seq,
+		clock:   vclock,
+	}, nil
+}
+
+type wstore struct {
+	ist     store.Store
+	watcher *watcher
+	opts    *Options
+	mu      sync.Mutex    // held during transaction commits; protects seq
+	seq     uint64        // the next sequence number to be used for a new commit
+	clock   *clock.VClock // used to provide write timestamps
+}
+
+var _ Store = (*wstore)(nil)
+
+// Close implements the store.Store interface.
+func (st *wstore) Close() error {
+	st.watcher.close()
+	return st.ist.Close()
+}
+
+// Get implements the store.StoreReader interface.
+func (st *wstore) Get(key, valbuf []byte) ([]byte, error) {
+	if !st.managesKey(key) {
+		return st.ist.Get(key, valbuf)
+	}
+	sn := newSnapshot(st)
+	defer sn.Abort()
+	return sn.Get(key, valbuf)
+}
+
+// Scan implements the store.StoreReader interface.
+func (st *wstore) Scan(start, limit []byte) store.Stream {
+	if !st.managesRange(start, limit) {
+		return st.ist.Scan(start, limit)
+	}
+	// TODO(sadovsky): Close snapshot once stream is finished or canceled.
+	return newSnapshot(st).Scan(start, limit)
+}
+
+// Put implements the store.StoreWriter interface.
+func (st *wstore) Put(key, value []byte) error {
+	// Use watchable.Store transaction so this op gets logged.
+	return store.RunInTransaction(st, func(tx store.Transaction) error {
+		return tx.Put(key, value)
+	})
+}
+
+// Delete implements the store.StoreWriter interface.
+func (st *wstore) Delete(key []byte) error {
+	// Use watchable.Store transaction so this op gets logged.
+	return store.RunInTransaction(st, func(tx store.Transaction) error {
+		return tx.Delete(key)
+	})
+}
+
+// NewTransaction implements the store.Store interface.
+func (st *wstore) NewTransaction() store.Transaction {
+	return newTransaction(st)
+}
+
+// NewSnapshot implements the store.Store interface.
+func (st *wstore) NewSnapshot() store.Snapshot {
+	return newSnapshot(st)
+}
+
+// GetOptions returns the options configured on a watchable.Store.
+// TODO(rdaoud): expose watchable store through an interface and change this
+// function to be a method on the store.
+func GetOptions(st store.Store) (*Options, error) {
+	wst := st.(*wstore)
+	return wst.opts, nil
+}
+
+////////////////////////////////////////
+// Internal helpers
+
+func (st *wstore) managesKey(key []byte) bool {
+	if st.opts.ManagedPrefixes == nil {
+		return true
+	}
+	ikey := string(key)
+	// TODO(sadovsky): Optimize, e.g. use binary search (here and below).
+	for _, p := range st.opts.ManagedPrefixes {
+		if strings.HasPrefix(ikey, p) {
+			return true
+		}
+	}
+	return false
+}
+
+func (st *wstore) managesRange(start, limit []byte) bool {
+	if st.opts.ManagedPrefixes == nil {
+		return true
+	}
+	istart, ilimit := string(start), string(limit)
+	for _, p := range st.opts.ManagedPrefixes {
+		pstart, plimit := pubutil.PrefixRangeStart(p), pubutil.PrefixRangeLimit(p)
+		if pstart <= istart && ilimit <= plimit {
+			return true
+		}
+		if !(plimit <= istart || ilimit <= pstart) {
+			// If this happens, there's a bug in the Syncbase server implementation.
+			panic(fmt.Sprintf("partial overlap: %q %q %q", p, start, limit))
+		}
+	}
+	return false
+}
diff --git a/services/syncbase/server/watchable/store_test.go b/services/syncbase/server/watchable/store_test.go
new file mode 100644
index 0000000..8c1c370
--- /dev/null
+++ b/services/syncbase/server/watchable/store_test.go
@@ -0,0 +1,74 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package watchable
+
+import (
+	"runtime"
+	"testing"
+
+	"v.io/syncbase/x/ref/services/syncbase/clock"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/syncbase/x/ref/services/syncbase/store/test"
+)
+
+func init() {
+	runtime.GOMAXPROCS(10)
+}
+
+func TestStream(t *testing.T) {
+	runTest(t, []string{}, test.RunStreamTest)
+	runTest(t, nil, test.RunStreamTest)
+}
+
+func TestSnapshot(t *testing.T) {
+	runTest(t, []string{}, test.RunSnapshotTest)
+	runTest(t, nil, test.RunSnapshotTest)
+}
+
+func TestStoreState(t *testing.T) {
+	runTest(t, []string{}, test.RunStoreStateTest)
+	runTest(t, nil, test.RunStoreStateTest)
+}
+
+func TestClose(t *testing.T) {
+	runTest(t, []string{}, test.RunCloseTest)
+	runTest(t, nil, test.RunCloseTest)
+}
+
+func TestReadWriteBasic(t *testing.T) {
+	runTest(t, []string{}, test.RunReadWriteBasicTest)
+	runTest(t, nil, test.RunReadWriteBasicTest)
+}
+
+func TestReadWriteRandom(t *testing.T) {
+	runTest(t, []string{}, test.RunReadWriteRandomTest)
+	runTest(t, nil, test.RunReadWriteRandomTest)
+}
+
+func TestConcurrentTransactions(t *testing.T) {
+	runTest(t, []string{}, test.RunConcurrentTransactionsTest)
+	runTest(t, nil, test.RunConcurrentTransactionsTest)
+}
+
+func TestTransactionState(t *testing.T) {
+	runTest(t, []string{}, test.RunTransactionStateTest)
+	runTest(t, nil, test.RunTransactionStateTest)
+}
+
+func TestTransactionsWithGet(t *testing.T) {
+	runTest(t, []string{}, test.RunTransactionsWithGetTest)
+	runTest(t, nil, test.RunTransactionsWithGetTest)
+}
+
+func runTest(t *testing.T, mp []string, f func(t *testing.T, st store.Store)) {
+	st, destroy := createStore()
+	defer destroy()
+	vClock := clock.NewVClockWithMockServices(clock.MockStorageAdapter(), nil, nil)
+	st, err := Wrap(st, vClock, &Options{ManagedPrefixes: mp})
+	if err != nil {
+		t.Fatal(err)
+	}
+	f(t, st)
+}
diff --git a/services/syncbase/server/watchable/stream.go b/services/syncbase/server/watchable/stream.go
new file mode 100644
index 0000000..26502e1
--- /dev/null
+++ b/services/syncbase/server/watchable/stream.go
@@ -0,0 +1,94 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package watchable
+
+import (
+	"sync"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+)
+
+// stream streams keys and values for versioned records.
+type stream struct {
+	iit      store.Stream
+	sntx     store.SnapshotOrTransaction
+	mu       sync.Mutex
+	err      error
+	hasValue bool
+	key      []byte
+	value    []byte
+}
+
+var _ store.Stream = (*stream)(nil)
+
+// newStreamVersioned creates a new stream. It assumes all records in range
+// [start, limit) are managed, i.e. versioned.
+func newStreamVersioned(sntx store.SnapshotOrTransaction, start, limit []byte) *stream {
+	return &stream{
+		iit:  sntx.Scan(makeVersionKey(start), makeVersionKey(limit)),
+		sntx: sntx,
+	}
+}
+
+// Advance implements the store.Stream interface.
+func (s *stream) Advance() bool {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.hasValue = false
+	if s.err != nil {
+		return false
+	}
+	if advanced := s.iit.Advance(); !advanced {
+		return false
+	}
+	versionKey, version := s.iit.Key(nil), s.iit.Value(nil)
+	s.key = []byte(join(split(string(versionKey))[1:]...)) // drop "$version" prefix
+	s.value, s.err = s.sntx.Get(makeAtVersionKey(s.key, version), nil)
+	if s.err != nil {
+		return false
+	}
+	s.hasValue = true
+	return true
+}
+
+// Key implements the store.Stream interface.
+func (s *stream) Key(keybuf []byte) []byte {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	if !s.hasValue {
+		panic("nothing staged")
+	}
+	return store.CopyBytes(keybuf, s.key)
+}
+
+// Value implements the store.Stream interface.
+func (s *stream) Value(valbuf []byte) []byte {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	if !s.hasValue {
+		panic("nothing staged")
+	}
+	return store.CopyBytes(valbuf, s.value)
+}
+
+// Err implements the store.Stream interface.
+func (s *stream) Err() error {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	if s.err != nil {
+		return convertError(s.err)
+	}
+	return s.iit.Err()
+}
+
+// Cancel implements the store.Stream interface.
+func (s *stream) Cancel() {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	if s.err != nil {
+		return
+	}
+	s.iit.Cancel()
+}
diff --git a/services/syncbase/server/watchable/test_util.go b/services/syncbase/server/watchable/test_util.go
new file mode 100644
index 0000000..e14854a
--- /dev/null
+++ b/services/syncbase/server/watchable/test_util.go
@@ -0,0 +1,144 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package watchable
+
+import (
+	"fmt"
+	"io/ioutil"
+	"math"
+	"time"
+
+	"v.io/syncbase/x/ref/services/syncbase/clock"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/syncbase/x/ref/services/syncbase/store/leveldb"
+	"v.io/syncbase/x/ref/services/syncbase/store/memstore"
+	"v.io/v23/vom"
+)
+
+// This file provides utility methods for tests related to watchable store.
+
+////////////////////////////////////////////////////////////
+// Functions for store creation/cleanup
+
+// createStore returns a store along with a function to destroy the store
+// once it is no longer needed.
+func createStore() (store.Store, func()) {
+	var st store.Store
+	// With Memstore, TestReadWriteRandom is slow with ManagedPrefixes=nil since
+	// every watchable.Store.Get() takes a snapshot, and memstore snapshots are
+	// relatively expensive since the entire data map is copied. LevelDB snapshots
+	// are cheap, so with LevelDB ManagedPrefixes=nil is still reasonably fast.
+	if false {
+		st = memstore.New()
+		return st, func() {
+			st.Close()
+		}
+	} else {
+		path := getPath()
+		st = createLevelDB(path)
+		return st, func() {
+			destroyLevelDB(st, path)
+		}
+	}
+}
+
+func getPath() string {
+	path, err := ioutil.TempDir("", "syncbase_leveldb")
+	if err != nil {
+		panic(fmt.Sprintf("can't create temp dir: %v", err))
+	}
+	return path
+}
+
+func createLevelDB(path string) store.Store {
+	st, err := leveldb.Open(path, leveldb.OpenOptions{CreateIfMissing: true, ErrorIfExists: true})
+	if err != nil {
+		panic(fmt.Sprintf("can't open db at %v: %v", path, err))
+	}
+	return st
+}
+
+func destroyLevelDB(st store.Store, path string) {
+	st.Close()
+	if err := leveldb.Destroy(path); err != nil {
+		panic(fmt.Sprintf("can't destroy db at %v: %v", path, err))
+	}
+}
+
+////////////////////////////////////////////////////////////
+// Functions related to watchable store
+
+func getSeq(st Store) uint64 {
+	wst := st.(*wstore)
+	return wst.seq
+}
+
+// logEntryReader provides a stream-like interface to scan over the log entries
+// of a single batch, starting for a given sequence number.  It opens a stream
+// that scans the log from the sequence number given.  It stops after reading
+// the last entry in that batch (indicated by a false Continued flag).
+type logEntryReader struct {
+	stream store.Stream // scan stream on the store Database
+	done   bool         // true after reading the last batch entry
+	key    string       // key of most recent log entry read
+	entry  LogEntry     // most recent log entry read
+}
+
+func newLogEntryReader(st store.Store, seq uint64) *logEntryReader {
+	stream := st.Scan([]byte(logEntryKey(seq)), []byte(logEntryKey(math.MaxUint64)))
+	return &logEntryReader{stream: stream}
+}
+
+func (ler *logEntryReader) Advance() bool {
+	if ler.done {
+		return false
+	}
+
+	if ler.stream.Advance() {
+		ler.key = string(ler.stream.Key(nil))
+		if err := vom.Decode(ler.stream.Value(nil), &ler.entry); err != nil {
+			panic(fmt.Errorf("Failed to decode LogEntry for key: %q", ler.key))
+		}
+		if ler.entry.Continued == false {
+			ler.done = true
+		}
+		return true
+	}
+
+	ler.key = ""
+	ler.entry = LogEntry{}
+	return false
+}
+
+func (ler *logEntryReader) GetEntry() (string, LogEntry) {
+	return ler.key, ler.entry
+}
+
+////////////////////////////////////////////////////////////
+// Clock related utility code
+
+type mockSystemClock struct {
+	time      time.Time     // current time returned by call to Now()
+	increment time.Duration // how much to increment the clock by for subsequent calls to Now()
+}
+
+func newMockSystemClock(firstTimestamp time.Time, increment time.Duration) *mockSystemClock {
+	return &mockSystemClock{
+		time:      firstTimestamp,
+		increment: increment,
+	}
+}
+
+func (sc *mockSystemClock) Now() time.Time {
+	now := sc.time
+	sc.time = sc.time.Add(sc.increment)
+	return now
+}
+
+func (sc *mockSystemClock) ElapsedTime() (time.Duration, error) {
+	return sc.increment, nil
+}
+
+var _ clock.SystemClock = (*mockSystemClock)(nil)
diff --git a/services/syncbase/server/watchable/transaction.go b/services/syncbase/server/watchable/transaction.go
new file mode 100644
index 0000000..8a67f8f
--- /dev/null
+++ b/services/syncbase/server/watchable/transaction.go
@@ -0,0 +1,304 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package watchable
+
+import (
+	"fmt"
+	"math"
+	"sync"
+
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/context"
+	"v.io/v23/verror"
+)
+
+type transaction struct {
+	itx store.Transaction
+	st  *wstore
+	mu  sync.Mutex // protects the fields below
+	err error
+	ops []Op
+	// fromSync is true when a transaction is created by sync.  This causes
+	// the log entries written at commit time to have their "FromSync" field
+	// set to true.  That in turn causes the sync watcher to filter out such
+	// updates since sync already knows about them (echo suppression).
+	fromSync bool
+}
+
+var _ store.Transaction = (*transaction)(nil)
+
+func cp(src []byte) []byte {
+	dst := make([]byte, len(src))
+	for i := 0; i < len(src); i++ {
+		dst[i] = src[i]
+	}
+	return dst
+}
+
+func cpStrings(src []string) []string {
+	dst := make([]string, len(src))
+	for i := 0; i < len(src); i++ {
+		dst[i] = src[i]
+	}
+	return dst
+}
+
+func newTransaction(st *wstore) *transaction {
+	return &transaction{
+		itx: st.ist.NewTransaction(),
+		st:  st,
+	}
+}
+
+// Get implements the store.StoreReader interface.
+func (tx *transaction) Get(key, valbuf []byte) ([]byte, error) {
+	tx.mu.Lock()
+	defer tx.mu.Unlock()
+	if tx.err != nil {
+		return valbuf, convertError(tx.err)
+	}
+	var err error
+	if !tx.st.managesKey(key) {
+		valbuf, err = tx.itx.Get(key, valbuf)
+	} else {
+		valbuf, err = getVersioned(tx.itx, key, valbuf)
+		tx.ops = append(tx.ops, &OpGet{GetOp{Key: cp(key)}})
+	}
+	return valbuf, err
+}
+
+// Scan implements the store.StoreReader interface.
+func (tx *transaction) Scan(start, limit []byte) store.Stream {
+	tx.mu.Lock()
+	defer tx.mu.Unlock()
+	if tx.err != nil {
+		return &store.InvalidStream{Error: tx.err}
+	}
+	var it store.Stream
+	if !tx.st.managesRange(start, limit) {
+		it = tx.itx.Scan(start, limit)
+	} else {
+		it = newStreamVersioned(tx.itx, start, limit)
+		tx.ops = append(tx.ops, &OpScan{ScanOp{Start: cp(start), Limit: cp(limit)}})
+	}
+	return it
+}
+
+// Put implements the store.StoreWriter interface.
+func (tx *transaction) Put(key, value []byte) error {
+	tx.mu.Lock()
+	defer tx.mu.Unlock()
+	if tx.err != nil {
+		return convertError(tx.err)
+	}
+	if !tx.st.managesKey(key) {
+		return tx.itx.Put(key, value)
+	}
+	version, err := putVersioned(tx.itx, key, value)
+	if err != nil {
+		return err
+	}
+	tx.ops = append(tx.ops, &OpPut{PutOp{Key: cp(key), Version: version}})
+	return nil
+}
+
+// Delete implements the store.StoreWriter interface.
+func (tx *transaction) Delete(key []byte) error {
+	tx.mu.Lock()
+	defer tx.mu.Unlock()
+	if tx.err != nil {
+		return convertError(tx.err)
+	}
+	var err error
+	if !tx.st.managesKey(key) {
+		return tx.itx.Delete(key)
+	}
+	err = deleteVersioned(tx.itx, key)
+	if err != nil {
+		return err
+	}
+	tx.ops = append(tx.ops, &OpDelete{DeleteOp{Key: cp(key)}})
+	return nil
+}
+
+// Commit implements the store.Transaction interface.
+func (tx *transaction) Commit() error {
+	tx.mu.Lock()
+	defer tx.mu.Unlock()
+	if tx.err != nil {
+		return convertError(tx.err)
+	}
+	tx.err = verror.New(verror.ErrBadState, nil, store.ErrMsgCommittedTxn)
+	tx.st.mu.Lock()
+	defer tx.st.mu.Unlock()
+	// Check if there is enough space left in the sequence number.
+	if (math.MaxUint64 - tx.st.seq) < uint64(len(tx.ops)) {
+		return verror.New(verror.ErrInternal, nil, "seq maxed out")
+	}
+	// Write LogEntry records.
+	timestamp := tx.st.clock.Now(nil).UnixNano()
+	seq := tx.st.seq
+	for i, op := range tx.ops {
+		key := logEntryKey(seq)
+		value := &LogEntry{
+			Op:              op,
+			CommitTimestamp: timestamp,
+			FromSync:        tx.fromSync,
+			Continued:       i < len(tx.ops)-1,
+		}
+		if err := util.Put(nil, tx.itx, key, value); err != nil {
+			return err
+		}
+		seq++
+	}
+	if err := tx.itx.Commit(); err != nil {
+		return err
+	}
+	tx.st.seq = seq
+	tx.st.watcher.broadcastUpdates()
+	return nil
+}
+
+// Abort implements the store.Transaction interface.
+func (tx *transaction) Abort() error {
+	tx.mu.Lock()
+	defer tx.mu.Unlock()
+	if tx.err != nil {
+		return convertError(tx.err)
+	}
+	tx.err = verror.New(verror.ErrCanceled, nil, store.ErrMsgAbortedTxn)
+	return tx.itx.Abort()
+}
+
+// AddSyncGroupOp injects a SyncGroup operation notification in the log entries
+// that the transaction writes when it is committed.  It allows the SyncGroup
+// operations (create, join, leave, destroy) to notify the sync watcher of the
+// change at its proper position in the timeline (the transaction commit).
+// Note: this is an internal function used by sync, not part of the interface.
+func AddSyncGroupOp(ctx *context.T, tx store.Transaction, prefixes []string, remove bool) error {
+	wtx := tx.(*transaction)
+	wtx.mu.Lock()
+	defer wtx.mu.Unlock()
+	if wtx.err != nil {
+		return convertError(wtx.err)
+	}
+	// Make a defensive copy of prefixes slice.
+	wtx.ops = append(wtx.ops, &OpSyncGroup{SyncGroupOp{Prefixes: cpStrings(prefixes), Remove: remove}})
+	return nil
+}
+
+// AddSyncSnapshotOp injects a sync snapshot operation notification in the log
+// entries that the transaction writes when it is committed.  It allows the
+// SyncGroup create or join operations to notify the sync watcher of the
+// current keys and their versions to use when initializing the sync metadata
+// at the point in the timeline when these keys become syncable (at commit).
+// Note: this is an internal function used by sync, not part of the interface.
+func AddSyncSnapshotOp(ctx *context.T, tx store.Transaction, key, version []byte) error {
+	wtx := tx.(*transaction)
+	wtx.mu.Lock()
+	defer wtx.mu.Unlock()
+	if wtx.err != nil {
+		return convertError(wtx.err)
+	}
+	if !wtx.st.managesKey(key) {
+		return verror.New(verror.ErrInternal, ctx, fmt.Sprintf("cannot create SyncSnapshotOp on unmanaged key: %s", string(key)))
+	}
+	wtx.ops = append(wtx.ops, &OpSyncSnapshot{SyncSnapshotOp{Key: cp(key), Version: cp(version)}})
+	return nil
+}
+
+// SetTransactionFromSync marks this transaction as created by sync as opposed
+// to one created by an application.  The net effect is that, at commit time,
+// the log entries written are marked as made by sync.  This allows the sync
+// Watcher to ignore them (echo suppression) because it made these updates.
+// Note: this is an internal function used by sync, not part of the interface.
+// TODO(rdaoud): support a generic echo-suppression mechanism for apps as well
+// maybe by having a creator ID in the transaction and log entries.
+// TODO(rdaoud): fold this flag (or creator ID) into Tx options when available.
+func SetTransactionFromSync(tx store.Transaction) {
+	wtx := tx.(*transaction)
+	wtx.mu.Lock()
+	defer wtx.mu.Unlock()
+	wtx.fromSync = true
+}
+
+// GetVersion returns the current version of a managed key. This method is used
+// by the Sync module when the initiator is attempting to add new versions of
+// objects. Reading the version key is used for optimistic concurrency
+// control. At minimum, an object implementing the Transaction interface is
+// required since this is a Get operation.
+func GetVersion(ctx *context.T, tx store.Transaction, key []byte) ([]byte, error) {
+	switch w := tx.(type) {
+	case *transaction:
+		w.mu.Lock()
+		defer w.mu.Unlock()
+		if w.err != nil {
+			return nil, convertError(w.err)
+		}
+		return getVersion(w.itx, key)
+	}
+	return nil, verror.New(verror.ErrInternal, ctx, "unsupported store type")
+}
+
+// GetAtVersion returns the value of a managed key at the requested
+// version. This method is used by the Sync module when the responder needs to
+// send objects over the wire. At minimum, an object implementing the
+// StoreReader interface is required since this is a Get operation.
+func GetAtVersion(ctx *context.T, st store.StoreReader, key, valbuf, version []byte) ([]byte, error) {
+	switch w := st.(type) {
+	case *snapshot:
+		return getAtVersion(w.isn, key, valbuf, version)
+	case *transaction:
+		w.mu.Lock()
+		defer w.mu.Unlock()
+		if w.err != nil {
+			return valbuf, convertError(w.err)
+		}
+		return getAtVersion(w.itx, key, valbuf, version)
+	case *wstore:
+		return getAtVersion(w.ist, key, valbuf, version)
+	}
+	return nil, verror.New(verror.ErrInternal, ctx, "unsupported store type")
+}
+
+// PutAtVersion puts a value for the managed key at the requested version. This
+// method is used by the Sync module exclusively when the initiator adds objects
+// with versions created on other Syncbases. At minimum, an object implementing
+// the Transaction interface is required since this is a Put operation.
+func PutAtVersion(ctx *context.T, tx store.Transaction, key, valbuf, version []byte) error {
+	wtx := tx.(*transaction)
+
+	wtx.mu.Lock()
+	defer wtx.mu.Unlock()
+	if wtx.err != nil {
+		return convertError(wtx.err)
+	}
+
+	// Note that we do not enqueue a PutOp in the log since this Put is not
+	// updating the current version of a key.
+	return wtx.itx.Put(makeAtVersionKey(key, version), valbuf)
+}
+
+// PutVersion updates the version of a managed key to the requested
+// version. This method is used by the Sync module exclusively when the
+// initiator selects which of the already stored versions (via PutAtVersion
+// calls) becomes the current version. At minimum, an object implementing
+// the Transaction interface is required since this is a Put operation.
+func PutVersion(ctx *context.T, tx store.Transaction, key, version []byte) error {
+	wtx := tx.(*transaction)
+
+	wtx.mu.Lock()
+	defer wtx.mu.Unlock()
+	if wtx.err != nil {
+		return convertError(wtx.err)
+	}
+
+	if err := wtx.itx.Put(makeVersionKey(key), version); err != nil {
+		return err
+	}
+	wtx.ops = append(wtx.ops, &OpPut{PutOp{Key: cp(key), Version: cp(version)}})
+	return nil
+}
diff --git a/services/syncbase/server/watchable/transaction_test.go b/services/syncbase/server/watchable/transaction_test.go
new file mode 100644
index 0000000..5fcdf94
--- /dev/null
+++ b/services/syncbase/server/watchable/transaction_test.go
@@ -0,0 +1,225 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package watchable
+
+import (
+	"bytes"
+	"fmt"
+	"reflect"
+	"runtime/debug"
+	"testing"
+	"time"
+
+	"v.io/syncbase/x/ref/services/syncbase/clock"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+)
+
+type testData struct {
+	key       string
+	createVal string
+	updateVal string
+}
+
+var data1 testData = testData{
+	key:       "key-a",
+	createVal: "val-a1",
+	updateVal: "val-a2",
+}
+
+var data2 testData = testData{
+	key:       "key-b",
+	createVal: "val-b1",
+	updateVal: "val-b2",
+}
+
+func checkAndUpdate(tx store.Transaction, data testData) error {
+	// check and update data1
+	keyBytes := []byte(data.key)
+	val, err := tx.Get(keyBytes, nil)
+	if err != nil {
+		return fmt.Errorf("can't get key %q: %v", data.key, err)
+	}
+	if !bytes.Equal(val, []byte(data.createVal)) {
+		return fmt.Errorf("Unexpected value for key %q: %q", data.key, string(val))
+	}
+	if err := tx.Put(keyBytes, []byte(data.updateVal)); err != nil {
+		return fmt.Errorf("can't put {%q: %v}: %v", data.key, data.updateVal, err)
+	}
+	return nil
+}
+
+func verifyCommitLog(t *testing.T, st store.Store, seq uint64, wantNumEntries int, wantTimestamp time.Time) {
+	ler := newLogEntryReader(st, seq)
+	numEntries := 0
+	for ler.Advance() {
+		_, entry := ler.GetEntry()
+		numEntries++
+		if entry.CommitTimestamp != wantTimestamp.UnixNano() {
+			t.Errorf("Unexpected timestamp found for entry: got %v, want %v", entry.CommitTimestamp, wantTimestamp.UnixNano())
+		}
+	}
+	if numEntries != wantNumEntries {
+		t.Errorf("Unexpected number of log entries: got %v, want %v", numEntries, wantNumEntries)
+	}
+}
+
+func TestLogEntryTimestamps(t *testing.T) {
+	ist, destroy := createStore()
+	defer destroy()
+	t1 := time.Now()
+	inc := time.Duration(1) * time.Second
+	mockClock := newMockSystemClock(t1, inc)
+	var mockAdapter clock.StorageAdapter = clock.MockStorageAdapter()
+
+	vclock := clock.NewVClockWithMockServices(mockAdapter, mockClock, nil)
+	wst1, err := Wrap(ist, vclock, &Options{ManagedPrefixes: nil})
+	if err != nil {
+		t.Fatalf("Wrap failed: %v", err)
+	}
+	seqForCreate := getSeq(wst1)
+
+	// Create data in store
+	if err := store.RunInTransaction(wst1, func(tx store.Transaction) error {
+		// add data1
+		if err := tx.Put([]byte(data1.key), []byte(data1.createVal)); err != nil {
+			return fmt.Errorf("can't put {%q: %v}: %v", data1.key, data1.createVal, err)
+		}
+		// add data2
+		if err := tx.Put([]byte(data2.key), []byte(data2.createVal)); err != nil {
+			return fmt.Errorf("can't put {%q: %v}: %v", data2.key, data2.createVal, err)
+		}
+		return nil
+	}); err != nil {
+		panic(fmt.Errorf("can't commit transaction: %v", err))
+	}
+
+	// read and verify LogEntries written as part of above transaction
+	// We expect 2 entries in the log for the two puts.
+	// Timestamp from mockclock for the commit should be t1
+	verifyCommitLog(t, ist, seqForCreate, 2, t1)
+
+	// Update data already present in store with a new watchable store
+	wst2, err := Wrap(ist, vclock, &Options{ManagedPrefixes: nil})
+	if err != nil {
+		t.Fatalf("Wrap failed: %v", err)
+	}
+	seqForUpdate := getSeq(wst2)
+	// We expect the sequence number to have moved by +2 for the two puts.
+	if seqForUpdate != (seqForCreate + 2) {
+		t.Errorf("unexpected sequence number for update. seq for create: %d, seq for update: %d", seqForCreate, seqForUpdate)
+	}
+
+	if err := store.RunInTransaction(wst2, func(tx store.Transaction) error {
+		if err := checkAndUpdate(tx, data1); err != nil {
+			return err
+		}
+		if err := checkAndUpdate(tx, data2); err != nil {
+			return err
+		}
+		return nil
+	}); err != nil {
+		panic(fmt.Errorf("can't commit transaction: %v", err))
+	}
+
+	// read and verify LogEntries written as part of above transaction
+	// We expect 4 entries in the log for the two gets and two puts.
+	// Timestamp from mockclock for the commit should be t1 + 1 sec
+	t2 := t1.Add(inc)
+	verifyCommitLog(t, ist, seqForUpdate, 4, t2)
+}
+
+func eq(t *testing.T, got, want interface{}) {
+	if !reflect.DeepEqual(got, want) {
+		debug.PrintStack()
+		t.Fatalf("got %v, want %v", got, want)
+	}
+}
+
+func TestOpLogConsistency(t *testing.T) {
+	ist, destroy := createStore()
+	defer destroy()
+	t1 := time.Now()
+	inc := time.Duration(1) * time.Second
+	mockClock := newMockSystemClock(t1, inc)
+	var mockAdapter clock.StorageAdapter = clock.MockStorageAdapter()
+
+	vclock := clock.NewVClockWithMockServices(mockAdapter, mockClock, nil)
+	wst, err := Wrap(ist, vclock, &Options{ManagedPrefixes: nil})
+	if err != nil {
+		t.Fatalf("Wrap failed: %v", err)
+	}
+
+	if err := store.RunInTransaction(wst, func(tx store.Transaction) error {
+		putKey, putVal := []byte("foo"), []byte("bar")
+		if err := tx.Put(putKey, putVal); err != nil {
+			return err
+		}
+		getKey := []byte("foo")
+		if getVal, err := tx.Get(getKey, nil); err != nil {
+			return err
+		} else {
+			eq(t, getVal, putVal)
+		}
+		start, limit := []byte("aaa"), []byte("bbb")
+		tx.Scan(start, limit)
+		delKey := []byte("foo")
+		if err := tx.Delete(delKey); err != nil {
+			return err
+		}
+		sgPrefixes := []string{"sga", "sgb"}
+		if err := AddSyncGroupOp(nil, tx, sgPrefixes, false); err != nil {
+			return err
+		}
+		snKey, snVersion := []byte("aa"), []byte("123")
+		if err := AddSyncSnapshotOp(nil, tx, snKey, snVersion); err != nil {
+			return err
+		}
+		pvKey, pvVersion := []byte("pv"), []byte("456")
+		if err := PutVersion(nil, tx, pvKey, pvVersion); err != nil {
+			return err
+		}
+		for _, buf := range [][]byte{putKey, putVal, getKey, start, limit, delKey, snKey, snVersion, pvKey, pvVersion} {
+			buf[0] = '#'
+		}
+		sgPrefixes[0] = "zebra"
+		return nil
+	}); err != nil {
+		t.Fatalf("failed to commit txn: %v", err)
+	}
+
+	// Read first (and only) batch.
+	ler := newLogEntryReader(ist, 0)
+	numEntries, wantNumEntries := 0, 7
+	sawPut := false
+	for ler.Advance() {
+		_, entry := ler.GetEntry()
+		numEntries++
+		switch op := entry.Op.(type) {
+		case OpGet:
+			eq(t, string(op.Value.Key), "foo")
+		case OpScan:
+			eq(t, string(op.Value.Start), "aaa")
+			eq(t, string(op.Value.Limit), "bbb")
+		case OpPut:
+			if !sawPut {
+				eq(t, string(op.Value.Key), "foo")
+				sawPut = true
+			} else {
+				eq(t, string(op.Value.Key), "pv")
+				eq(t, string(op.Value.Version), "456")
+			}
+		case OpDelete:
+			eq(t, string(op.Value.Key), "foo")
+		case OpSyncGroup:
+			eq(t, op.Value.Prefixes, []string{"sga", "sgb"})
+		case OpSyncSnapshot:
+			eq(t, string(op.Value.Key), "aa")
+			eq(t, string(op.Value.Version), "123")
+		default:
+			t.Fatalf("Unexpected op type in entry: %v", entry)
+		}
+	}
+	eq(t, numEntries, wantNumEntries)
+}
diff --git a/services/syncbase/server/watchable/types.vdl b/services/syncbase/server/watchable/types.vdl
new file mode 100644
index 0000000..3f5181b
--- /dev/null
+++ b/services/syncbase/server/watchable/types.vdl
@@ -0,0 +1,77 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package watchable
+
+// GetOp represents a store get operation.
+type GetOp struct {
+	Key []byte
+}
+
+// ScanOp represents a store scan operation.
+type ScanOp struct {
+	Start []byte
+	Limit []byte
+}
+
+// PutOp represents a store put operation.  The new version is written instead
+// of the value to avoid duplicating the user data in the store.  The version
+// is used to access the user data of that specific mutation.
+type PutOp struct {
+	Key     []byte
+	Version []byte
+}
+
+// DeleteOp represents a store delete operation.
+type DeleteOp struct {
+	Key []byte
+}
+
+// SyncGroupOp represents a change in SyncGroup tracking, adding or removing
+// key prefixes to sync.  SyncGroup prefixes cannot be changed, this is used
+// to track changes due to SyncGroup create/join/leave/destroy.
+type SyncGroupOp struct {
+	Prefixes []string
+	Remove   bool
+}
+
+// SyncSnapshotOp represents a snapshot operation when creating and joining a
+// SyncGroup.  The sync watcher needs to get a snapshot of the Database at the
+// point of creating/joining a SyncGroup.  A SyncSnapshotOp entry is written to
+// the log for each Database key that falls within the SyncGroup prefixes.  This
+// allows sync to initialize its metadata at the correct versions of the objects
+// when they become syncable.  These log entries should be filtered by the
+// client-facing Watch interface because the user data did not actually change.
+type SyncSnapshotOp struct {
+	Key     []byte
+	Version []byte
+}
+
+// Op represents a store operation.
+type Op union {
+	Get          GetOp
+	Scan         ScanOp
+	Put          PutOp
+	Delete       DeleteOp
+	SyncGroup    SyncGroupOp
+	SyncSnapshot SyncSnapshotOp
+}
+
+// LogEntry represents a single store operation. This operation may have been
+// part of a transaction, as signified by the Continued boolean. Read-only
+// operations (and read-only transactions) are not logged.
+type LogEntry struct {
+	// The store operation that was performed.
+	Op Op
+
+	// Time when the operation was committed.
+	CommitTimestamp int64
+
+	// Operation came from sync (used for echo suppression).
+	FromSync bool
+
+	// If true, this entry is followed by more entries that belong to the same
+	// commit as this entry.
+	Continued bool
+}
diff --git a/services/syncbase/server/watchable/types.vdl.go b/services/syncbase/server/watchable/types.vdl.go
new file mode 100644
index 0000000..5fd2e04
--- /dev/null
+++ b/services/syncbase/server/watchable/types.vdl.go
@@ -0,0 +1,189 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file was auto-generated by the vanadium vdl tool.
+// Source: types.vdl
+
+package watchable
+
+import (
+	// VDL system imports
+	"v.io/v23/vdl"
+)
+
+// GetOp represents a store get operation.
+type GetOp struct {
+	Key []byte
+}
+
+func (GetOp) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/server/watchable.GetOp"`
+}) {
+}
+
+// ScanOp represents a store scan operation.
+type ScanOp struct {
+	Start []byte
+	Limit []byte
+}
+
+func (ScanOp) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/server/watchable.ScanOp"`
+}) {
+}
+
+// PutOp represents a store put operation.  The new version is written instead
+// of the value to avoid duplicating the user data in the store.  The version
+// is used to access the user data of that specific mutation.
+type PutOp struct {
+	Key     []byte
+	Version []byte
+}
+
+func (PutOp) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/server/watchable.PutOp"`
+}) {
+}
+
+// DeleteOp represents a store delete operation.
+type DeleteOp struct {
+	Key []byte
+}
+
+func (DeleteOp) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/server/watchable.DeleteOp"`
+}) {
+}
+
+// SyncGroupOp represents a change in SyncGroup tracking, adding or removing
+// key prefixes to sync.  SyncGroup prefixes cannot be changed, this is used
+// to track changes due to SyncGroup create/join/leave/destroy.
+type SyncGroupOp struct {
+	Prefixes []string
+	Remove   bool
+}
+
+func (SyncGroupOp) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/server/watchable.SyncGroupOp"`
+}) {
+}
+
+// SyncSnapshotOp represents a snapshot operation when creating and joining a
+// SyncGroup.  The sync watcher needs to get a snapshot of the Database at the
+// point of creating/joining a SyncGroup.  A SyncSnapshotOp entry is written to
+// the log for each Database key that falls within the SyncGroup prefixes.  This
+// allows sync to initialize its metadata at the correct versions of the objects
+// when they become syncable.  These log entries should be filtered by the
+// client-facing Watch interface because the user data did not actually change.
+type SyncSnapshotOp struct {
+	Key     []byte
+	Version []byte
+}
+
+func (SyncSnapshotOp) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/server/watchable.SyncSnapshotOp"`
+}) {
+}
+
+type (
+	// Op represents any single field of the Op union type.
+	//
+	// Op represents a store operation.
+	Op interface {
+		// Index returns the field index.
+		Index() int
+		// Interface returns the field value as an interface.
+		Interface() interface{}
+		// Name returns the field name.
+		Name() string
+		// __VDLReflect describes the Op union type.
+		__VDLReflect(__OpReflect)
+	}
+	// OpGet represents field Get of the Op union type.
+	OpGet struct{ Value GetOp }
+	// OpScan represents field Scan of the Op union type.
+	OpScan struct{ Value ScanOp }
+	// OpPut represents field Put of the Op union type.
+	OpPut struct{ Value PutOp }
+	// OpDelete represents field Delete of the Op union type.
+	OpDelete struct{ Value DeleteOp }
+	// OpSyncGroup represents field SyncGroup of the Op union type.
+	OpSyncGroup struct{ Value SyncGroupOp }
+	// OpSyncSnapshot represents field SyncSnapshot of the Op union type.
+	OpSyncSnapshot struct{ Value SyncSnapshotOp }
+	// __OpReflect describes the Op union type.
+	__OpReflect struct {
+		Name  string `vdl:"v.io/syncbase/x/ref/services/syncbase/server/watchable.Op"`
+		Type  Op
+		Union struct {
+			Get          OpGet
+			Scan         OpScan
+			Put          OpPut
+			Delete       OpDelete
+			SyncGroup    OpSyncGroup
+			SyncSnapshot OpSyncSnapshot
+		}
+	}
+)
+
+func (x OpGet) Index() int               { return 0 }
+func (x OpGet) Interface() interface{}   { return x.Value }
+func (x OpGet) Name() string             { return "Get" }
+func (x OpGet) __VDLReflect(__OpReflect) {}
+
+func (x OpScan) Index() int               { return 1 }
+func (x OpScan) Interface() interface{}   { return x.Value }
+func (x OpScan) Name() string             { return "Scan" }
+func (x OpScan) __VDLReflect(__OpReflect) {}
+
+func (x OpPut) Index() int               { return 2 }
+func (x OpPut) Interface() interface{}   { return x.Value }
+func (x OpPut) Name() string             { return "Put" }
+func (x OpPut) __VDLReflect(__OpReflect) {}
+
+func (x OpDelete) Index() int               { return 3 }
+func (x OpDelete) Interface() interface{}   { return x.Value }
+func (x OpDelete) Name() string             { return "Delete" }
+func (x OpDelete) __VDLReflect(__OpReflect) {}
+
+func (x OpSyncGroup) Index() int               { return 4 }
+func (x OpSyncGroup) Interface() interface{}   { return x.Value }
+func (x OpSyncGroup) Name() string             { return "SyncGroup" }
+func (x OpSyncGroup) __VDLReflect(__OpReflect) {}
+
+func (x OpSyncSnapshot) Index() int               { return 5 }
+func (x OpSyncSnapshot) Interface() interface{}   { return x.Value }
+func (x OpSyncSnapshot) Name() string             { return "SyncSnapshot" }
+func (x OpSyncSnapshot) __VDLReflect(__OpReflect) {}
+
+// LogEntry represents a single store operation. This operation may have been
+// part of a transaction, as signified by the Continued boolean. Read-only
+// operations (and read-only transactions) are not logged.
+type LogEntry struct {
+	// The store operation that was performed.
+	Op Op
+	// Time when the operation was committed.
+	CommitTimestamp int64
+	// Operation came from sync (used for echo suppression).
+	FromSync bool
+	// If true, this entry is followed by more entries that belong to the same
+	// commit as this entry.
+	Continued bool
+}
+
+func (LogEntry) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/server/watchable.LogEntry"`
+}) {
+}
+
+func init() {
+	vdl.Register((*GetOp)(nil))
+	vdl.Register((*ScanOp)(nil))
+	vdl.Register((*PutOp)(nil))
+	vdl.Register((*DeleteOp)(nil))
+	vdl.Register((*SyncGroupOp)(nil))
+	vdl.Register((*SyncSnapshotOp)(nil))
+	vdl.Register((*Op)(nil))
+	vdl.Register((*LogEntry)(nil))
+}
diff --git a/services/syncbase/server/watchable/util.go b/services/syncbase/server/watchable/util.go
new file mode 100644
index 0000000..8eb606e
--- /dev/null
+++ b/services/syncbase/server/watchable/util.go
@@ -0,0 +1,93 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package watchable
+
+// TODO(sadovsky): Avoid copying back and forth between []byte's and strings.
+// We should probably convert incoming strings to []byte's as early as possible,
+// and deal exclusively in []byte's internally.
+// TODO(rdaoud): I propose we standardize on key and version being strings and
+// the value being []byte within Syncbase.  We define invalid characters in the
+// key space (and reserve "$" and ":").  The lower storage engine layers are
+// free to map that to what they need internally ([]byte or string).
+
+import (
+	"fmt"
+	"math/rand"
+	"sync"
+	"time"
+
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/verror"
+)
+
+var (
+	rng     *rand.Rand = rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
+	rngLock sync.Mutex
+)
+
+// NewVersion returns a new version for a store entry mutation.
+func NewVersion() []byte {
+	// TODO(rdaoud): revisit the number of bits: should we use 128 bits?
+	// Note: the version has to be unique per object key, not on its own.
+	// TODO(rdaoud): move sync's rand64() to a general Syncbase spot and
+	// reuse it here.
+	rngLock.Lock()
+	num := rng.Int63()
+	rngLock.Unlock()
+
+	return []byte(fmt.Sprintf("%x", num))
+}
+
+func makeVersionKey(key []byte) []byte {
+	return []byte(join(util.VersionPrefix, string(key)))
+}
+
+func makeAtVersionKey(key, version []byte) []byte {
+	return []byte(join(string(key), string(version)))
+}
+
+func getVersion(sntx store.SnapshotOrTransaction, key []byte) ([]byte, error) {
+	return sntx.Get(makeVersionKey(key), nil)
+}
+
+func getAtVersion(st store.StoreReader, key, valbuf, version []byte) ([]byte, error) {
+	return st.Get(makeAtVersionKey(key, version), valbuf)
+}
+
+func getVersioned(sntx store.SnapshotOrTransaction, key, valbuf []byte) ([]byte, error) {
+	version, err := getVersion(sntx, key)
+	if err != nil {
+		return valbuf, err
+	}
+	return getAtVersion(sntx, key, valbuf, version)
+}
+
+func putVersioned(tx store.Transaction, key, value []byte) ([]byte, error) {
+	version := NewVersion()
+	if err := tx.Put(makeVersionKey(key), version); err != nil {
+		return nil, err
+	}
+	if err := tx.Put(makeAtVersionKey(key, version), value); err != nil {
+		return nil, err
+	}
+	return version, nil
+}
+
+func deleteVersioned(tx store.Transaction, key []byte) error {
+	return tx.Delete(makeVersionKey(key))
+}
+
+func join(parts ...string) string {
+	return util.JoinKeyParts(parts...)
+}
+
+func split(key string) []string {
+	return util.SplitKeyParts(key)
+}
+
+func convertError(err error) error {
+	return verror.Convert(verror.IDAction{}, nil, err)
+}
diff --git a/services/syncbase/server/watchable/util_test.go b/services/syncbase/server/watchable/util_test.go
new file mode 100644
index 0000000..193c06d
--- /dev/null
+++ b/services/syncbase/server/watchable/util_test.go
@@ -0,0 +1,33 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package watchable
+
+import (
+	"testing"
+
+	"v.io/syncbase/x/ref/services/syncbase/clock"
+)
+
+// TestGetNextLogSeq tests that the getNextLogSeq helper works on range 0..10.
+func TestGetNextLogSeq(t *testing.T) {
+	st, destroy := createStore()
+	defer destroy()
+	var mockAdapter clock.StorageAdapter = clock.MockStorageAdapter()
+	vclock := clock.NewVClockWithMockServices(mockAdapter, nil, nil)
+	st, err := Wrap(st, vclock, &Options{})
+	if err != nil {
+		t.Fatal(err)
+	}
+	for i := uint64(0); i <= uint64(10); i++ {
+		seq, err := getNextLogSeq(st)
+		if err != nil {
+			t.Fatalf("failed to get log seq: %v", err)
+		}
+		if got, want := seq, i; got != want {
+			t.Fatalf("unexpected log seq: got %v, want %v", got, want)
+		}
+		st.Put([]byte(logEntryKey(i)), nil)
+	}
+}
diff --git a/services/syncbase/server/watchable/watcher.go b/services/syncbase/server/watchable/watcher.go
new file mode 100644
index 0000000..fc0481a
--- /dev/null
+++ b/services/syncbase/server/watchable/watcher.go
@@ -0,0 +1,212 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package watchable
+
+import (
+	"fmt"
+	"strconv"
+	"sync"
+
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/services/watch"
+	"v.io/v23/verror"
+	"v.io/v23/vom"
+	"v.io/x/lib/vlog"
+)
+
+// watcher maintains a state and a condition variable. The watcher sends
+// a broadcast signal every time the state changes. The state is increased
+// by 1 every time the store has new data. Initially the state equals to 1.
+// If the state becomes 0, then the watcher is closed and the state will not
+// be changed later.
+// TODO(rogulenko): Broadcast a signal from time to time to unblock waiting
+// clients.
+type watcher struct {
+	mu    *sync.RWMutex
+	cond  *sync.Cond
+	state uint64
+}
+
+func newWatcher() *watcher {
+	mu := &sync.RWMutex{}
+	return &watcher{
+		mu:    mu,
+		cond:  sync.NewCond(mu.RLocker()),
+		state: 1,
+	}
+}
+
+// close closes the watcher.
+func (w *watcher) close() {
+	w.mu.Lock()
+	w.state = 0
+	w.cond.Broadcast()
+	w.mu.Unlock()
+}
+
+// broadcastUpdates broadcast the update notification to watch clients.
+func (w *watcher) broadcastUpdates() {
+	w.mu.Lock()
+	if w.state != 0 {
+		w.state++
+		w.cond.Broadcast()
+	} else {
+		vlog.Error("broadcastUpdates() called on a closed watcher")
+	}
+	w.mu.Unlock()
+}
+
+// WatchUpdates returns a function that can be used to watch for changes of
+// the database. The store maintains a state (initially 1) that is increased
+// by 1 every time the store has new data. The waitForChange function takes
+// the last returned state and blocks until the state changes, returning the new
+// state. State equal to 0 means the store is closed and no updates will come
+// later. If waitForChange function takes a state different from the current
+// state of the store or the store is closed, the waitForChange function returns
+// immediately. It might happen that the waitForChange function returns
+// a non-zero state equal to the state passed as the argument. This behavior
+// helps to unblock clients if the store doesn't have updates for a long period
+// of time.
+func WatchUpdates(st store.Store) (waitForChange func(state uint64) uint64) {
+	// TODO(rogulenko): Remove dynamic type assertion here and in other places.
+	watcher := st.(*wstore).watcher
+	return func(state uint64) uint64 {
+		watcher.cond.L.Lock()
+		defer watcher.cond.L.Unlock()
+		if watcher.state != 0 && watcher.state == state {
+			watcher.cond.Wait()
+		}
+		return watcher.state
+	}
+}
+
+// GetResumeMarker returns the ResumeMarker that points to the current end
+// of the event log.
+func GetResumeMarker(st store.StoreReader) (watch.ResumeMarker, error) {
+	seq, err := getNextLogSeq(st)
+	return watch.ResumeMarker(logEntryKey(seq)), err
+}
+
+// MakeResumeMarker converts a sequence number to the resume marker.
+func MakeResumeMarker(seq uint64) watch.ResumeMarker {
+	return watch.ResumeMarker(logEntryKey(seq))
+}
+
+func logEntryKey(seq uint64) string {
+	// Note: MaxUint64 is 0xffffffffffffffff.
+	// TODO(sadovsky): Use a more space-efficient lexicographic number encoding.
+	return join(util.LogPrefix, fmt.Sprintf("%016x", seq))
+}
+
+// ReadBatchFromLog returns a batch of watch log records (a transaction) from
+// the given database and the new resume marker at the end of the batch.
+func ReadBatchFromLog(st store.Store, resumeMarker watch.ResumeMarker) ([]*LogEntry, watch.ResumeMarker, error) {
+	seq, err := parseResumeMarker(string(resumeMarker))
+	if err != nil {
+		return nil, resumeMarker, err
+	}
+	_, scanLimit := util.ScanPrefixArgs(util.LogPrefix, "")
+	scanStart := resumeMarker
+	endOfBatch := false
+
+	// Use the store directly to scan these read-only log entries, no need
+	// to create a snapshot since they are never overwritten.  Read and
+	// buffer a batch before processing it.
+	var logs []*LogEntry
+	stream := st.Scan(scanStart, scanLimit)
+	for stream.Advance() {
+		seq++
+		var logEnt LogEntry
+		if err := vom.Decode(stream.Value(nil), &logEnt); err != nil {
+			return nil, resumeMarker, err
+		}
+
+		logs = append(logs, &logEnt)
+
+		// Stop if this is the end of the batch.
+		if logEnt.Continued == false {
+			endOfBatch = true
+			break
+		}
+	}
+
+	if err = stream.Err(); err != nil {
+		return nil, resumeMarker, err
+	}
+	if !endOfBatch {
+		if len(logs) > 0 {
+			vlog.Fatalf("end of batch not found after %d entries", len(logs))
+		}
+		return nil, resumeMarker, nil
+	}
+	return logs, watch.ResumeMarker(logEntryKey(seq)), nil
+}
+
+func parseResumeMarker(resumeMarker string) (uint64, error) {
+	parts := split(resumeMarker)
+	if len(parts) != 2 {
+		return 0, verror.New(watch.ErrUnknownResumeMarker, nil, resumeMarker)
+	}
+	seq, err := strconv.ParseUint(parts[1], 16, 64)
+	if err != nil {
+		return 0, verror.New(watch.ErrUnknownResumeMarker, nil, resumeMarker)
+	}
+	return seq, nil
+}
+
+// logEntryExists returns true iff the log contains an entry with the given
+// sequence number.
+func logEntryExists(st store.StoreReader, seq uint64) (bool, error) {
+	_, err := st.Get([]byte(logEntryKey(seq)), nil)
+	if err != nil && verror.ErrorID(err) != store.ErrUnknownKey.ID {
+		return false, err
+	}
+	return err == nil, nil
+}
+
+// getNextLogSeq returns the next sequence number to be used for a new commit.
+// NOTE: this function assumes that all sequence numbers in the log represent
+// some range [start, limit] without gaps.
+func getNextLogSeq(st store.StoreReader) (uint64, error) {
+	// Determine initial value for seq.
+	// TODO(sadovsky): Consider using a bigger seq.
+
+	// Find the beginning of the log.
+	it := st.Scan(util.ScanPrefixArgs(util.LogPrefix, ""))
+	if !it.Advance() {
+		return 0, nil
+	}
+	if it.Err() != nil {
+		return 0, it.Err()
+	}
+	seq, err := parseResumeMarker(string(it.Key(nil)))
+	if err != nil {
+		return 0, err
+	}
+	var step uint64 = 1
+	// Suppose the actual value we are looking for is S. First, we estimate the
+	// range for S. We find seq, step: seq < S <= seq + step.
+	for {
+		if ok, err := logEntryExists(st, seq+step); err != nil {
+			return 0, err
+		} else if !ok {
+			break
+		}
+		seq += step
+		step *= 2
+	}
+	// Next we keep the seq < S <= seq + step invariant, reducing step to 1.
+	for step > 1 {
+		step /= 2
+		if ok, err := logEntryExists(st, seq+step); err != nil {
+			return 0, err
+		} else if ok {
+			seq += step
+		}
+	}
+	// Now seq < S <= seq + 1, thus S = seq + 1.
+	return seq + 1, nil
+}
diff --git a/services/syncbase/server/watchable/watcher_test.go b/services/syncbase/server/watchable/watcher_test.go
new file mode 100644
index 0000000..c978123
--- /dev/null
+++ b/services/syncbase/server/watchable/watcher_test.go
@@ -0,0 +1,93 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package watchable
+
+import (
+	"bytes"
+	"fmt"
+	"testing"
+
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+)
+
+// TestWatchLogBatch tests fetching a batch of log records.
+func TestWatchLogBatch(t *testing.T) {
+	runTest(t, []string{util.RowPrefix, util.PermsPrefix}, runWatchLogBatchTest)
+}
+
+// runWatchLogBatchTest tests fetching a batch of log records.
+func runWatchLogBatchTest(t *testing.T, st store.Store) {
+	// Create a set of batches to fill the log queue.
+	numTx, numPut := 3, 4
+
+	makeKeyVal := func(batchNum, recNum int) ([]byte, []byte) {
+		key := util.JoinKeyParts(util.RowPrefix, fmt.Sprintf("foo-%d-%d", batchNum, recNum))
+		val := fmt.Sprintf("val-%d-%d", batchNum, recNum)
+		return []byte(key), []byte(val)
+	}
+
+	for i := 0; i < numTx; i++ {
+		tx := st.NewTransaction()
+		for j := 0; j < numPut; j++ {
+			key, val := makeKeyVal(i, j)
+			if err := tx.Put(key, val); err != nil {
+				t.Errorf("cannot put %s (%s): %v", key, val, err)
+			}
+		}
+		tx.Commit()
+	}
+
+	// Fetch the batches and a few more empty fetches and verify them.
+	resmark := MakeResumeMarker(0)
+	var seq uint64
+
+	for i := 0; i < (numTx + 3); i++ {
+		logs, newResmark, err := ReadBatchFromLog(st, resmark)
+		if err != nil {
+			t.Fatalf("can't get watch log batch: %v", err)
+		}
+		if i < numTx {
+			if len(logs) != numPut {
+				t.Errorf("log fetch (i=%d) wrong log seq: %d instead of %d",
+					i, len(logs), numPut)
+			}
+
+			seq += uint64(len(logs))
+			expResmark := MakeResumeMarker(seq)
+			if !bytes.Equal(newResmark, expResmark) {
+				t.Errorf("log fetch (i=%d) wrong resmark: %s instead of %s",
+					i, newResmark, expResmark)
+			}
+
+			for j, log := range logs {
+				op := log.Op.(OpPut)
+				expKey, expVal := makeKeyVal(i, j)
+				key := op.Value.Key
+				if !bytes.Equal(key, expKey) {
+					t.Errorf("log fetch (i=%d, j=%d) bad key: %s instead of %s",
+						i, j, key, expKey)
+				}
+				tx := st.NewTransaction()
+				var val []byte
+				val, err := GetAtVersion(nil, tx, key, val, op.Value.Version)
+				if err != nil {
+					t.Errorf("log fetch (i=%d, j=%d) cannot GetAtVersion(): %v", i, j, err)
+				}
+				if !bytes.Equal(val, expVal) {
+					t.Errorf("log fetch (i=%d, j=%d) bad value: %s instead of %s",
+						i, j, val, expVal)
+				}
+				tx.Abort()
+			}
+		} else {
+			if logs != nil || !bytes.Equal(newResmark, resmark) {
+				t.Errorf("NOP log fetch (i=%d) had changes: %d logs, resmask %s",
+					i, len(logs), newResmark)
+			}
+		}
+		resmark = newResmark
+	}
+}
diff --git a/services/syncbase/signing/hashcache/hashcache.go b/services/syncbase/signing/hashcache/hashcache.go
new file mode 100644
index 0000000..26e8c94
--- /dev/null
+++ b/services/syncbase/signing/hashcache/hashcache.go
@@ -0,0 +1,77 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package hashcache implements a simple cache intended to be indexed by hash
+// values.  The keys are of type []byte.  Values are arbitrary interface{}
+// values.  Entries may expire if not used for a duration specified by the
+// client.
+package hashcache
+
+import "sync"
+import "time"
+
+// An internalValue is the client's data plus the data's expiry time.
+type internalValue struct {
+	data   interface{}
+	expiry time.Time
+}
+
+// A Cache allows the user to store arbitrary values, keyed by the contents of
+// byte vectors.  Entries may be added, deleted, and looked up.  They may
+// expire if not used.
+type Cache struct {
+	expiry            time.Duration
+	mu                sync.Mutex // protects fields below.
+	entries           map[string]*internalValue
+	insertionsSinceGC int // number of insertions since last GC
+}
+
+// New() returns a pointer to a new, empty Cache.
+// Entries may expire if not used for "expiry".
+func New(expiry time.Duration) *Cache {
+	return &Cache{expiry: expiry, entries: make(map[string]*internalValue)}
+}
+
+// Lookup() returns the data associated with key[] in *c, and whether there is
+// such a value.  The client may not modify the returned data; it is shared
+// with *c.
+func (c *Cache) Lookup(key []byte) (data interface{}, isPresent bool) {
+	var value *internalValue
+	c.mu.Lock()
+	value, isPresent = c.entries[string(key)]
+	if isPresent {
+		value.expiry = time.Now().Add(c.expiry)
+		data = value.data
+	}
+	c.mu.Unlock()
+	return data, isPresent
+}
+
+// Add() associates data with key[] in *c.  Any data previously associated with
+// key[] are forgotten.  The implementation may discard the association at some
+// future time (set by NewCache()) to limit the size of the cache.  data may
+// not be modified after this call; it is shared with *c.
+func (c *Cache) Add(key []byte, data interface{}) {
+	c.mu.Lock()
+	now := time.Now()
+	c.entries[string(key)] = &internalValue{data: data, expiry: now.Add(c.expiry)}
+	c.insertionsSinceGC++
+	// Scan to expire entries if 20% were added since last scan.
+	if c.insertionsSinceGC*5 > len(c.entries) {
+		for ik, iv := range c.entries {
+			if iv.expiry.Before(now) {
+				delete(c.entries, ik)
+			}
+		}
+		c.insertionsSinceGC = 0
+	}
+	c.mu.Unlock()
+}
+
+// Delete() removes any association of data with key[] in *c.
+func (c *Cache) Delete(key []byte) {
+	c.mu.Lock()
+	delete(c.entries, string(key))
+	c.mu.Unlock()
+}
diff --git a/services/syncbase/signing/hashcache/hashcache_test.go b/services/syncbase/signing/hashcache/hashcache_test.go
new file mode 100644
index 0000000..96ba865
--- /dev/null
+++ b/services/syncbase/signing/hashcache/hashcache_test.go
@@ -0,0 +1,83 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package hashcache_test tests the hashcache package.
+package hashcache_test
+
+import "runtime"
+import "testing"
+import "time"
+
+import "v.io/syncbase/x/ref/services/syncbase/signing/hashcache"
+
+// checkHashesWithNoData() checks that hash[start:] have no data in the cache.
+// (The start index is passed, rather than expecting the caller to sub-slice,
+// so that error messages refer to the index.)
+func checkHashesWithNoData(t *testing.T, cache *hashcache.Cache, start int, hash [][]byte) {
+	_, _, callerLine, _ := runtime.Caller(1)
+	for i := start; i != len(hash); i++ {
+		value, found := cache.Lookup(hash[i])
+		if value != nil || found {
+			t.Errorf("line %d: unset cache entry hash[%d]=%v has value %v, but is expected not to be set", callerLine, i, hash[i], value)
+		}
+	}
+}
+
+func TestCache(t *testing.T) {
+	hash := [][]byte{
+		[]byte{0x00, 0x01, 0x02, 0x3},
+		[]byte{0x04, 0x05, 0x06, 0x7},
+		[]byte{0x08, 0x09, 0x0a, 0xb}}
+	var value interface{}
+	var found bool
+	var want string
+
+	cache := hashcache.New(5 * time.Second)
+
+	// The cache should initially have none of the keys.
+	checkHashesWithNoData(t, cache, 0, hash)
+
+	// Add the first key, and check that it's there.
+	want = "hash0"
+	cache.Add(hash[0], want)
+	value, found = cache.Lookup(hash[0])
+	if s, ok := value.(string); !found || !ok || s != want {
+		t.Errorf("cache entry hash[%d]=%v got %v, want %v", 0, hash[0], s, want)
+	}
+	checkHashesWithNoData(t, cache, 1, hash)
+
+	// Add the second key, and check that both it and the first key are there.
+	want = "hash1"
+	cache.Add(hash[1], want)
+	value, found = cache.Lookup(hash[1])
+	if s, ok := value.(string); !ok || s != want {
+		t.Errorf("cache entry hash[%d]=%v got %v, want %v", 1, hash[1], s, want)
+	}
+	want = "hash0"
+	value, found = cache.Lookup(hash[0])
+	if s, ok := value.(string); !found || !ok || s != want {
+		t.Errorf("cache entry hash[%d]=%v got %v, want %v", 0, hash[0], s, want)
+	}
+	checkHashesWithNoData(t, cache, 2, hash)
+
+	// Wait for all entries to time out.
+	time.Sleep(6 * time.Second) // sleep past expiry time
+
+	// Add the first key again, and so many that it will trigger garbage
+	// collection.
+	for i := 0; i != 10; i++ {
+		want = "hash0 again"
+		cache.Add(hash[0], want)
+		value, found = cache.Lookup(hash[0])
+		if s, ok := value.(string); !found || !ok || s != want {
+			t.Errorf("cache entry hash[%d]=%v got %v, want %v", 0, hash[0], s, want)
+		}
+	}
+	// The entry for hash1 should have expired, since the expiry time has
+	// passed, and many things have been inserted into the cache.
+	checkHashesWithNoData(t, cache, 1, hash)
+
+	cache.Delete(hash[0])
+	checkHashesWithNoData(t, cache, 0, hash)
+}
diff --git a/services/syncbase/signing/krl/krl.go b/services/syncbase/signing/krl/krl.go
new file mode 100644
index 0000000..422f53e
--- /dev/null
+++ b/services/syncbase/signing/krl/krl.go
@@ -0,0 +1,39 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package krl implements a trivial, in-memory key revocation list.
+// It is a placeholder for a real key revocation mechanism.
+package krl
+
+import "crypto/sha256"
+import "time"
+
+// A KRL is a key revocation list.  It maps the hashes of keys that have been revoked
+// to revocation times.
+type KRL struct {
+	table map[[sha256.Size]byte]time.Time
+}
+
+var notYetRevoked = time.Now().Add(100 * 365 * 24 * time.Hour) // far future
+
+// New() returns a pointer to a new, empty key recovation list.
+func New() *KRL {
+	return &KRL{table: make(map[[sha256.Size]byte]time.Time)}
+}
+
+// Revoke() inserts an entry into *krl recording that key[] was revoked at time
+// "when".
+func (krl *KRL) Revoke(key []byte, when time.Time) {
+	krl.table[sha256.Sum256(key)] = when
+}
+
+// RevocationTime() returns the revocation time for key[].
+// If key[] is not in the list, a time in the far future is returned.
+func (krl *KRL) RevocationTime(key []byte) (whenRevoked time.Time) {
+	var found bool
+	if whenRevoked, found = krl.table[sha256.Sum256(key)]; !found {
+		whenRevoked = notYetRevoked
+	}
+	return whenRevoked
+}
diff --git a/services/syncbase/signing/krl/krl_test.go b/services/syncbase/signing/krl/krl_test.go
new file mode 100644
index 0000000..73f48ad
--- /dev/null
+++ b/services/syncbase/signing/krl/krl_test.go
@@ -0,0 +1,54 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package krl tests the key revocation list package.
+package krl_test
+
+import "runtime"
+import "testing"
+import "time"
+
+import "v.io/syncbase/x/ref/services/syncbase/signing/krl"
+
+// checkKeysNotRevoked() checks that key[start:] have not been revoked.  (The
+// start index is passed, rather than expecting the called to sub-slice, so
+// that error messages refer to the expected index.)
+func checkKeysNotRevoked(t *testing.T, krl *krl.KRL, start int, key [][]byte, now time.Time) {
+	_, _, callerLine, _ := runtime.Caller(1)
+	year := 365 * 24 * time.Hour
+	for i := start; i != len(key); i++ {
+		revoked := krl.RevocationTime(key[i])
+		if revoked.Before(now.Add(year)) {
+			t.Errorf("line %d: unrevoked key[%d]=%v has revocation time %v, which is not far enough in the future", callerLine, i, key[i], revoked)
+		}
+	}
+}
+
+func TestKRL(t *testing.T) {
+	now := time.Now()
+	key := [][]byte{
+		[]byte{0x00, 0x01, 0x02, 0x3},
+		[]byte{0x04, 0x05, 0x06, 0x7},
+		[]byte{0x08, 0x09, 0x0a, 0xb}}
+	var revoked time.Time
+
+	krl := krl.New()
+
+	checkKeysNotRevoked(t, krl, 0, key, now)
+
+	krl.Revoke(key[0], now)
+	if revoked = krl.RevocationTime(key[0]); !revoked.Equal(now) {
+		t.Errorf("unrevoked key %v has revocation time %v, but expected %v", key[0], revoked, now)
+	}
+	checkKeysNotRevoked(t, krl, 1, key, now)
+
+	krl.Revoke(key[1], now)
+	if revoked = krl.RevocationTime(key[0]); !revoked.Equal(now) {
+		t.Errorf("unrevoked key %v has revocation time %v, but expected %v", key[0], revoked, now)
+	}
+	if revoked = krl.RevocationTime(key[1]); !revoked.Equal(now) {
+		t.Errorf("unrevoked key %v has revocation time %v, but expected %v", key[1], revoked, now)
+	}
+	checkKeysNotRevoked(t, krl, 2, key, now)
+}
diff --git a/services/syncbase/signing/signeddata.vdl b/services/syncbase/signing/signeddata.vdl
new file mode 100644
index 0000000..4b4ceb8
--- /dev/null
+++ b/services/syncbase/signing/signeddata.vdl
@@ -0,0 +1,72 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package signing
+
+import "v.io/v23/security"
+
+// A DataWithSignature represents a signed, and possibily validated, collection
+// of Item structs.
+//
+// If IsValidated==false and the AuthorSigned signature is valid, it means:
+//    The signer whose Blessings have hash BlessingsHash asserts Data.
+//
+// If IsValidated==true and both AuthorSigned and ValidatorSigned signatures are is valid,
+// it means both:
+// 1) The signer whose Blessings b have hash BlessingsHash asserts Data.
+// 2) If vd is the ValidatorData with hash ValidatorDataHash, the owner of
+//    vd.PublicKey asserts that it checked that at least the names vd.Names[] were
+//    valid in b.
+//
+// The sender obtains:
+// - BlessingsHash (and the wire form of the blessings) with ValidationCache.AddBlessings().  
+// - ValidatorDataHash (and the wire form of the ValidataData)  with ValidationCache.AddValidatorData().
+//
+// The receiver looks up:
+// - BlessingsHash with ValidationCache.LookupBlessingsData()
+// - ValidatorDataHash with ValidationCache.LookupValidatorData()
+//
+// If not yet there, the receiver inserts the valus into its ValidationCache with:
+// - ValidationCache.AddWireBlessings()
+// - ValidationCache.AddValidatorData()
+type DataWithSignature struct {
+	Data          []Item
+        // BlessingsHash is a key for the validation cache; the corresponding
+        // cached value is a security.Blessings.
+	BlessingsHash []byte
+	// AuthorSigned is the signature of Data and BlessingsHash using the
+	// private key associated with the blessings hashed in BlessingsHash.
+	AuthorSigned  security.Signature
+
+	IsValidated bool // Whether fields below are meaningful.
+
+        // ValidatorDataHash is a key for the validation cache returned by
+        // ValidatorData.Hash(); the corresponding cached value is the
+        // ValidatorData.
+	ValidatorDataHash []byte
+	ValidatorSigned   security.Signature
+}
+
+// An Item represents either a marshalled data item or its SHA-256 hash.
+// The Data field is a []byte, rather than an "any" to make signatures
+// determistic.  VOM encoding is not deterministic for two reasons:
+// - map elements may be marshalled in any order
+// - different versions of VOM may marshal in different ways.
+// Thus, the initial producer of a data item marshals the data once, and it is
+// this marshalled form that is transmitted from device to device.  If the
+// data were unmarshalled and then remarsahalled, the signatures might not
+// match.  The Hash field is used instead of the Data field when the recipient
+// of the DataWithSignature is not permitted to see certain Items' Data
+// fields.
+type Item union {
+	Data []byte   // Marshalled form of data.
+	Hash []byte   // Hash of what would have been in Data, as returned by SumByteVectorWithLength(Data).
+}
+
+// WireValidatorData is the wire form of ValidatorData.
+// It excludes the unmarshalled form of the public key.
+type WireValidatorData struct {
+	Names               []string  // Names of valid signing blessings in the Blessings referred to by BlessingsHash.
+	MarshalledPublicKey []byte    // PublicKey, marshalled with MarshalBinary().
+}
diff --git a/services/syncbase/signing/signeddata.vdl.go b/services/syncbase/signing/signeddata.vdl.go
new file mode 100644
index 0000000..f96fe50
--- /dev/null
+++ b/services/syncbase/signing/signeddata.vdl.go
@@ -0,0 +1,128 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file was auto-generated by the vanadium vdl tool.
+// Source: signeddata.vdl
+
+package signing
+
+import (
+	// VDL system imports
+	"v.io/v23/vdl"
+
+	// VDL user imports
+	"v.io/v23/security"
+)
+
+// A DataWithSignature represents a signed, and possibily validated, collection
+// of Item structs.
+//
+// If IsValidated==false and the AuthorSigned signature is valid, it means:
+//    The signer whose Blessings have hash BlessingsHash asserts Data.
+//
+// If IsValidated==true and both AuthorSigned and ValidatorSigned signatures are is valid,
+// it means both:
+// 1) The signer whose Blessings b have hash BlessingsHash asserts Data.
+// 2) If vd is the ValidatorData with hash ValidatorDataHash, the owner of
+//    vd.PublicKey asserts that it checked that at least the names vd.Names[] were
+//    valid in b.
+//
+// The sender obtains:
+// - BlessingsHash (and the wire form of the blessings) with ValidationCache.AddBlessings().
+// - ValidatorDataHash (and the wire form of the ValidataData)  with ValidationCache.AddValidatorData().
+//
+// The receiver looks up:
+// - BlessingsHash with ValidationCache.LookupBlessingsData()
+// - ValidatorDataHash with ValidationCache.LookupValidatorData()
+//
+// If not yet there, the receiver inserts the valus into its ValidationCache with:
+// - ValidationCache.AddWireBlessings()
+// - ValidationCache.AddValidatorData()
+type DataWithSignature struct {
+	Data []Item
+	// BlessingsHash is a key for the validation cache; the corresponding
+	// cached value is a security.Blessings.
+	BlessingsHash []byte
+	// AuthorSigned is the signature of Data and BlessingsHash using the
+	// private key associated with the blessings hashed in BlessingsHash.
+	AuthorSigned security.Signature
+	IsValidated  bool // Whether fields below are meaningful.
+	// ValidatorDataHash is a key for the validation cache returned by
+	// ValidatorData.Hash(); the corresponding cached value is the
+	// ValidatorData.
+	ValidatorDataHash []byte
+	ValidatorSigned   security.Signature
+}
+
+func (DataWithSignature) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/signing.DataWithSignature"`
+}) {
+}
+
+type (
+	// Item represents any single field of the Item union type.
+	//
+	// An Item represents either a marshalled data item or its SHA-256 hash.
+	// The Data field is a []byte, rather than an "any" to make signatures
+	// determistic.  VOM encoding is not deterministic for two reasons:
+	// - map elements may be marshalled in any order
+	// - different versions of VOM may marshal in different ways.
+	// Thus, the initial producer of a data item marshals the data once, and it is
+	// this marshalled form that is transmitted from device to device.  If the
+	// data were unmarshalled and then remarsahalled, the signatures might not
+	// match.  The Hash field is used instead of the Data field when the recipient
+	// of the DataWithSignature is not permitted to see certain Items' Data
+	// fields.
+	Item interface {
+		// Index returns the field index.
+		Index() int
+		// Interface returns the field value as an interface.
+		Interface() interface{}
+		// Name returns the field name.
+		Name() string
+		// __VDLReflect describes the Item union type.
+		__VDLReflect(__ItemReflect)
+	}
+	// ItemData represents field Data of the Item union type.
+	ItemData struct{ Value []byte } // Marshalled form of data.
+	// ItemHash represents field Hash of the Item union type.
+	ItemHash struct{ Value []byte } // Hash of what would have been in Data, as returned by SumByteVectorWithLength(Data).
+	// __ItemReflect describes the Item union type.
+	__ItemReflect struct {
+		Name  string `vdl:"v.io/syncbase/x/ref/services/syncbase/signing.Item"`
+		Type  Item
+		Union struct {
+			Data ItemData
+			Hash ItemHash
+		}
+	}
+)
+
+func (x ItemData) Index() int                 { return 0 }
+func (x ItemData) Interface() interface{}     { return x.Value }
+func (x ItemData) Name() string               { return "Data" }
+func (x ItemData) __VDLReflect(__ItemReflect) {}
+
+func (x ItemHash) Index() int                 { return 1 }
+func (x ItemHash) Interface() interface{}     { return x.Value }
+func (x ItemHash) Name() string               { return "Hash" }
+func (x ItemHash) __VDLReflect(__ItemReflect) {}
+
+// WireValidatorData is the wire form of ValidatorData.
+// It excludes the unmarshalled form of the public key.
+type WireValidatorData struct {
+	Names               []string // Names of valid signing blessings in the Blessings referred to by BlessingsHash.
+	MarshalledPublicKey []byte   // PublicKey, marshalled with MarshalBinary().
+}
+
+func (WireValidatorData) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/signing.WireValidatorData"`
+}) {
+}
+
+func init() {
+	vdl.Register((*DataWithSignature)(nil))
+	vdl.Register((*Item)(nil))
+	vdl.Register((*WireValidatorData)(nil))
+}
diff --git a/services/syncbase/signing/signing.go b/services/syncbase/signing/signing.go
new file mode 100644
index 0000000..0115f37
--- /dev/null
+++ b/services/syncbase/signing/signing.go
@@ -0,0 +1,358 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package signing signs syncbase updates using public key signatures, and
+// allows these signatures to be checked on other nodes.
+//
+// The functionality is geared specifically towards syncbase synchronization
+// because it is designed to allow a signature to remain valid during its
+// propagation across the syncgroup once it has been accepted by at least one
+// member of a syncgroup, even if the original key or its blessings are
+// invalidated in the meantime.
+//
+// There are three types of participant:
+// - an "author", which creates an update, and signs it with Sign().
+// - one or more "validators", each of which receives a change directly from
+//   the author, and applies Check() to validate it.
+// - zero or more "checkers', each of whom receives a change from a validator
+//   or another checker, and applied Check() to check it.
+//
+// A validator checks the signature and blessings provided by the author, and
+// then appends its own signature, vouching for the fact that the author's
+// signature was good at the time the validator saw it.
+//
+// A checker checks the signatures of both the author and validator but uses
+// weaker checks for signature validity than a validator.  In particular, it
+// uses a significant grace period for key expiry so that a change admitted to
+// the syncgroup by a validator has an opportunity to propagate to all the
+// nodes in the syncgroup if the keys or blessings are revoked after the change
+// is admitted, but before it is fully propagated.  The intent is that the
+// grace period be chosen to be greater than the diameter of the syncgroup
+// (measured in time).  One way to ensure that is to insist that members sync
+// with a central server at least every T time units, and make the grace period
+// be 2T.  The central server may sign the data anew to allow new members to pick
+// it up.
+//
+// The model is further complicated by performance concerns.  An update written
+// to syncbase might be quite small (perhaps tens of bytes) but:
+// a) a public key signature or verification can take on the order of a
+//    millisecond.  (Currently, ECDSA signing might a little under 1ms and
+//    verification just over 2ms on a workstation.  A checker performs two such
+//    verifications.)
+// b) unmarshalling even a simple Blessings object can take milliseconds. (!)
+// c) marshalling a public key can take 10us.
+// d) a Blessings object is of the order of a kilobyte of more, which may
+//    represent substantial space overhead if duplicated.
+//
+// Because of (a), we wish to batch syncbase updates, so that a single
+// signature check applies to several updates.  Thus the Data in a
+// DataWithSignature is a vector of Item, rather than a single Item.
+//
+// However, we will not always wish to put all updates in the same batch.  For
+// example, an author and a validator might share two different syncgroups with
+// different memberships.  In such a case, the author might keep the batches
+// for one syncgoup separate from batches for the other syncgroup, even though
+// the author blessings and validator identities are the same for all the
+// batches.  Thus, because of (b,c,d), it's worth decoupling the author's
+// Blessings data and the validator's key data separately from the signed
+// batches itself, so that the blessings and validator data can be processed
+// once, even though several batches of updates are being sent.  A
+// ValidationCache is used to hold this data separately, and allow it to be
+// sent just once, rather than once per signature.
+//
+// Lastly, imagine that the author sends a batch of 10 updates to a validator,
+// and the validator then syncs with a checker that is permitted to see only
+// half of the updates; perhaps ACLs prevent if from seeing the others.  This
+// requires that the signature on the batch remain valid even if some of the
+// updates in the batch are removed.  This is accomplished via the Item type,
+// which is a VDL union type that contains either the bytes of the marshalled
+// form of the update, or (if the update must not be sent) the SHA-256 hash of
+// the data (which can be computed with SumByteVectorWithLength()).
+package signing
+
+import "bytes"
+import "crypto/sha256"
+import "encoding/binary"
+import "hash"
+import "time"
+
+import "v.io/syncbase/x/ref/services/syncbase/signing/krl"
+import "v.io/v23/context"
+import "v.io/v23/security"
+import "v.io/v23/verror"
+
+const pkgPath = "v.io/syncbase/x/ref/services/syncbase/signing"
+
+// These are among the errors may be returned by Check(), and indicate that the
+// operation should be retried when new data has been added to the
+// ValidationCache.  The errors are public to make it easier for the client to
+// test for them.
+var (
+	ErrNeedAuthorBlessingsAndValidatorDataForHash = verror.Register(
+		pkgPath+".ErrNeedAuthorBlessingsAndValidatorDataForHash",
+		verror.RetryRefetch,
+		"{1:}{2:} The ValidationCache contains neither the author blessings nor the validator data{:_}")
+	ErrNeedAuthorBlessingsForHash = verror.Register(
+		pkgPath+".ErrNeedAuthorBlessingsForHash",
+		verror.RetryRefetch,
+		"{1:}{2:} The ValidationCache does not contain the author blessings{:_}")
+	ErrNeedValidatorDataForHash = verror.Register(
+		pkgPath+".ErrNeedValidatorDataForHash",
+		verror.RetryRefetch,
+		"{1:}{2:} The ValidationCache does not contain the validator data{:_}")
+)
+
+// These errors are less likely to be tested for, and so are not exported.
+var (
+	errAuthorKeyIsRevoked = verror.Register(
+		pkgPath+".errAuthorKeyIsRevoked",
+		verror.NoRetry,
+		"{1:}{2:} The author key has been revoked{:_}")
+	errBadAuthorSignature = verror.Register(
+		pkgPath+".errBadAuthorSignature",
+		verror.NoRetry,
+		"{1:}{2:} Author signature verification failed{:_}")
+	errBadValidatorSignature = verror.Register(
+		pkgPath+".errBadValidatorSignature",
+		verror.NoRetry,
+		"{1:}{2:} Validator signature verification failed{:_}")
+	errAuthorBlessingsHaveNoValidNames = verror.Register(
+		pkgPath+".errAuthorBlessingsHaveNoValidNames",
+		verror.NoRetry,
+		"{1:}{2:} Author Blessings have no valid names{:_}")
+	errMayNotValidateOwnSignature = verror.Register(
+		pkgPath+".errMayNotValidateOwnSignature",
+		verror.NoRetry,
+		"{1:}{2:} Author may not validate its own signature{:_}")
+	errSenderIsNotAuthor = verror.Register(
+		pkgPath+".errSenderIsNotAuthor",
+		verror.NoRetry,
+		"{1:}{2:} Author is not sender of RPC; will not validate{:_}")
+	errValidatesWrongNames = verror.Register(
+		pkgPath+".errValidatesWrongNames",
+		verror.NoRetry,
+		"{1:}{2:} The validated names are not a subset of the names sent by the checker{:_}")
+	errValidatorIsSigner = verror.Register(
+		pkgPath+".errValidatorIsSigner",
+		verror.NoRetry,
+		"{1:}{2:} The signature was validated by its author; treating as invalid{:_}")
+	errValidatorKeyIsRevoked = verror.Register(
+		pkgPath+".errValidatorKeyIsRevoked",
+		verror.NoRetry,
+		"{1:}{2:} The validator key is revoked{:_}")
+)
+
+// --------------------------------------------
+
+// SignData() uses authorPrincipal to sign data using blessings (which must be
+// associated with the authorPrincipal).  A pointer to a newly constructed
+// DataWithSignature with IsValidated==false is returned.  Ensures that the
+// blessings are stored in *cache.  Typically, "authorPrincipal" is obtained from
+// v23.GetPrincipal(ctx).
+//
+// If a recipient of the result *d complains that it does not understand the
+// hash d.BlessingHash, the signer should present it with
+// blessingsData.MarshalledBlessings, which will allow the recipient to
+// construct the Blessings.  The Blessings are transmitted out of line because
+// they are large, and may be reused for multiple signatures.
+func SignData(ctx *context.T, cache *ValidationCache, authorPrincipal security.Principal,
+	blessings security.Blessings, data []Item) (d *DataWithSignature, blessingsData *BlessingsData, err error) {
+
+	d = new(DataWithSignature)
+	d.Data = data
+	d.BlessingsHash, blessingsData, err = cache.AddBlessings(ctx, blessings)
+	if err == nil {
+		d.AuthorSigned, err = authorPrincipal.Sign(d.authorSignatureHash())
+	}
+	return d, blessingsData, err
+}
+
+// hashByteVectorWithLength() calls hasher.Write() on a representation of
+// len(b), followed by the contents of b.
+func hashByteVectorWithLength(hasher hash.Hash, b []byte) {
+	var length [8]byte
+	binary.LittleEndian.PutUint64(length[:], uint64(len(b)))
+	hasher.Write(length[:])
+	hasher.Write(b)
+}
+
+// SumByteVectorWithLength() returns a SHA-256 hash of
+// len(b), followed by the contents of b.
+func SumByteVectorWithLength(b []byte) []byte {
+	hasher := sha256.New()
+	var length [8]byte
+	binary.LittleEndian.PutUint64(length[:], uint64(len(b)))
+	hasher.Write(length[:])
+	hasher.Write(b)
+	return hasher.Sum(nil)[:]
+}
+
+// authorSignatureHash() returns the hash that the author should sign.
+func (d *DataWithSignature) authorSignatureHash() []byte {
+	hasher := sha256.New()
+	var length [8]byte
+	binary.LittleEndian.PutUint64(length[:], uint64(len(d.Data)))
+	hasher.Write(length[:])
+	for i := range d.Data {
+		if data, gotData := d.Data[i].(ItemData); gotData {
+			hasher.Write(SumByteVectorWithLength(data.Value))
+		} else if hash, gotHash := d.Data[i].(ItemHash); gotHash {
+			hasher.Write(hash.Value)
+		} else {
+			// d.Data is neither a Data nor a Hash.  This shouldn't
+			// happen unless the mashalled data is somehow
+			// corrupted.  The signature will not match unless the
+			// original author of the data was seeing the same.
+			hasher.Write([]byte("no data"))
+		}
+	}
+	hashByteVectorWithLength(hasher, d.BlessingsHash)
+	return hasher.Sum(nil)[:]
+}
+
+// validatorSignatureHash() returns the hash that the validator should sign,
+// given the hash that the author signed.
+func (d *DataWithSignature) validatorSignatureHash(authorSignatureHash []byte) []byte {
+	var buffer [32]byte
+	var buf []byte = buffer[:]
+	if len(d.AuthorSigned.Hash) > len(buf) {
+		buf = make([]byte, len(d.AuthorSigned.Hash))
+	}
+	hasher := sha256.New()
+	hashByteVectorWithLength(hasher, authorSignatureHash)
+	hashByteVectorWithLength(hasher, d.AuthorSigned.Purpose)
+	hashByteVectorWithLength(hasher, buf[:copy(buf, d.AuthorSigned.Hash)])
+	hashByteVectorWithLength(hasher, d.AuthorSigned.R)
+	hashByteVectorWithLength(hasher, d.AuthorSigned.S)
+	hashByteVectorWithLength(hasher, d.ValidatorDataHash)
+	return hasher.Sum(nil)[:]
+}
+
+// Check() verifies the signature(s) on *d:
+//
+// If d.IsValidated==false, checks that:
+//   1. the author's signature is available in *cache.
+//   2. the author's signature over its blessings and the data is
+//      cyprotgraphically valid.
+//   3. security.SigningBlessingNames() yields a non-empty list of names when
+//      applied to the author's blessings.
+//   4. the author's public key is not known to be revoked.
+//   5. the local's public key (call.LocalPrincipal().PublicKey()) is not known
+//      to be revoked.
+//   6. the author's public key is the public key of the RPC caller.
+//   7. the author's public key and the local public key differ.
+// If checks pass and there are no other errors:
+//   - records the list of names found in check (3) in the ValidationData
+//   - adds a validation signature using the local public key (which is now the
+//     validator)
+//   - sets d.IsValidated
+//   - returns the list of names found in check (3), and a nil error.
+// Otherwise returns a nil list of names and a non-nil error.
+//
+// If d.Validated==true, checks that:
+//   1. the author's signature and the validator data are available in *cache.
+//   2. the author's signature over its blessings and the data is
+//      cyprotgraphically valid.
+//   8. the list of names stored in the ValidatorData by the validator is
+//      non-empty.
+//   9. the author's public key and the validator's public key differ.
+//  10. the list of names stored in the ValidatorData by the validator is a
+//      subset of the list of names that the author's blessings could have
+//      represented.
+//  11. the author's public key is not known to be revoked more than
+//      gracePeriod ago.
+//  12. the validator's public key is not known to be revoked more than
+//      gracePeriod ago.
+//  13. the validator's signature is cryptographically valid.
+// If checks pass and there are no other errors:
+//   - returns the list of names in the validator's data, and a nil error.
+// Otherwise returns a nil list of names and a non-nil error.
+func (d *DataWithSignature) Check(ctx *context.T, cache *ValidationCache, call security.Call,
+	krl *krl.KRL, gracePeriod time.Duration) (names []string, err error) {
+
+	// Verify that we have the Blessings and ValidatorData.
+	var authorBlessingsData *BlessingsData = cache.LookupBlessingsData(ctx, d.BlessingsHash)
+	var validatorData *ValidatorData
+	if d.IsValidated {
+		validatorData = cache.LookupValidatorData(ctx, d.ValidatorDataHash)
+	}
+	if authorBlessingsData == nil || (validatorData == nil && d.IsValidated) { // Check (1).
+		if authorBlessingsData == nil && (validatorData == nil && d.IsValidated) {
+			err = verror.New(ErrNeedAuthorBlessingsAndValidatorDataForHash, ctx)
+		} else if authorBlessingsData == nil {
+			err = verror.New(ErrNeedAuthorBlessingsForHash, ctx)
+		} else {
+			err = verror.New(ErrNeedValidatorDataForHash, ctx)
+		}
+	}
+
+	// Check the author signature.
+	var authorSignatureHash []byte
+	if err == nil {
+		authorSignatureHash = d.authorSignatureHash()
+		if !d.AuthorSigned.Verify(authorBlessingsData.UnmarshalledBlessings.PublicKey(), authorSignatureHash) { // Check (2).
+			err = verror.New(errBadAuthorSignature, ctx)
+		}
+	}
+
+	// Check or create the validator signature.
+	now := time.Now()
+	if err != nil {
+		// err already set
+	} else if !d.IsValidated {
+		// Not yet validated, so this run will attempt to validate.
+		var validatedNames []string
+		var localKeyMarshalled []byte
+		var senderKeyMarshalled []byte
+		validatedNames, _ = security.SigningBlessingNames(ctx, call.LocalPrincipal(),
+			authorBlessingsData.UnmarshalledBlessings)
+		if len(validatedNames) == 0 { // Check (3).
+			err = verror.New(errAuthorBlessingsHaveNoValidNames, ctx)
+		} else if localKeyMarshalled, err = call.LocalPrincipal().PublicKey().MarshalBinary(); err != nil {
+			// err already set
+		} else if krl.RevocationTime(authorBlessingsData.MarshalledPublicKey).Before(now) { // Check (4).
+			err = verror.New(errAuthorKeyIsRevoked, ctx)
+		} else if krl.RevocationTime(localKeyMarshalled).Before(now) { // Check (5).
+			err = verror.New(errValidatorKeyIsRevoked, ctx)
+		} else if senderKeyMarshalled, err = call.RemoteBlessings().PublicKey().MarshalBinary(); err != nil {
+			// err already set
+		} else if !bytes.Equal(senderKeyMarshalled, authorBlessingsData.MarshalledPublicKey) { // Check (6).
+			err = verror.New(errSenderIsNotAuthor, ctx)
+		} else if bytes.Equal(localKeyMarshalled, authorBlessingsData.MarshalledPublicKey) { // Check (7).
+			err = verror.New(errMayNotValidateOwnSignature, ctx)
+		} else {
+			// Local principal is different from author, so can validate.
+			validatorData = &ValidatorData{
+				Names:               validatedNames,
+				PublicKey:           call.LocalPrincipal().PublicKey(),
+				MarshalledPublicKey: localKeyMarshalled,
+			}
+			d.ValidatorDataHash = cache.AddValidatorData(ctx, validatorData)
+			d.ValidatorSigned, err = call.LocalPrincipal().Sign(d.validatorSignatureHash(authorSignatureHash))
+			d.IsValidated = (err == nil)
+		}
+	} else { // Data already validated; check the validator siganture.
+		if len(validatorData.Names) == 0 { // Check (8).
+			err = verror.New(errAuthorBlessingsHaveNoValidNames, ctx)
+		} else if bytes.Equal(validatorData.MarshalledPublicKey, authorBlessingsData.MarshalledPublicKey) { // Check (9).
+			err = verror.New(errValidatorIsSigner, ctx)
+		} else if !authorBlessingsData.UnmarshalledBlessings.CouldHaveNames(validatorData.Names) { // Check (10).
+			err = verror.New(errValidatesWrongNames, ctx)
+		} else if krl.RevocationTime(authorBlessingsData.MarshalledPublicKey).Before(now.Add(-gracePeriod)) { // Check (11).
+			err = verror.New(errAuthorKeyIsRevoked, ctx)
+		} else if krl.RevocationTime(validatorData.MarshalledPublicKey).Before(now.Add(-gracePeriod)) { // Check (12).
+			err = verror.New(errValidatorKeyIsRevoked, ctx)
+		} else if !d.ValidatorSigned.Verify(validatorData.PublicKey, d.validatorSignatureHash(authorSignatureHash)) { // Check (13).
+			err = verror.New(errBadValidatorSignature, ctx)
+		} // else success.
+	}
+
+	// If there were no errors, return the list of names from the validator.
+	if err == nil {
+		names = make([]string, len(validatorData.Names))
+		copy(names, validatorData.Names)
+	}
+
+	return names, err
+}
diff --git a/services/syncbase/signing/signing_test.go b/services/syncbase/signing/signing_test.go
new file mode 100644
index 0000000..96881f3
--- /dev/null
+++ b/services/syncbase/signing/signing_test.go
@@ -0,0 +1,421 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package signing_test implements a test for the package
+// v.io/syncbase/x/ref/services/syncbase/signing
+package signing_test
+
+import "crypto/sha256"
+import "testing"
+import "time"
+
+import "v.io/syncbase/x/ref/services/syncbase/signing"
+import "v.io/syncbase/x/ref/services/syncbase/signing/krl"
+import "v.io/v23/naming"
+import "v.io/v23/security"
+import "v.io/v23/vdl"
+import "v.io/v23/vom"
+import "v.io/v23/verror"
+import "v.io/x/ref/test"
+import lib_security "v.io/x/ref/lib/security"
+
+import _ "v.io/x/ref/runtime/factories/generic"
+
+// --------------------------------------
+// The following implements a fake security.Call.
+type fakeCall struct {
+	localPrincipal  security.Principal
+	localBlessings  security.Blessings
+	remoteBlessings security.Blessings
+}
+
+func (fc *fakeCall) Timestamp() time.Time                            { return time.Now() }
+func (fc *fakeCall) Method() string                                  { return "the_method_name" }
+func (fc *fakeCall) MethodTags() []*vdl.Value                        { return nil }
+func (fc *fakeCall) Suffix() string                                  { return "the_suffix" }
+func (fc *fakeCall) LocalDischarges() map[string]security.Discharge  { return nil }
+func (fc *fakeCall) RemoteDischarges() map[string]security.Discharge { return nil }
+func (fc *fakeCall) LocalPrincipal() security.Principal              { return fc.localPrincipal }
+func (fc *fakeCall) LocalBlessings() security.Blessings              { return fc.localBlessings }
+func (fc *fakeCall) RemoteBlessings() security.Blessings             { return fc.remoteBlessings }
+func (fc *fakeCall) LocalEndpoint() naming.Endpoint                  { return nil }
+func (fc *fakeCall) RemoteEndpoint() naming.Endpoint                 { return nil }
+
+// --------------------------------------
+
+// A principalDesc holds the local state of a single principal in the tests below.
+type principalDesc struct {
+	name                string
+	principal           security.Principal
+	blessings           security.Blessings
+	krl                 *krl.KRL
+	authorBlessingsData *signing.BlessingsData
+	names               []string
+	marshalledBlessings []byte
+	blessingsHash       []byte
+	validatorData       *signing.ValidatorData
+	validatorHash       []byte
+	cache               *signing.ValidationCache
+	data                *signing.DataWithSignature
+}
+
+// makePrincipal() returns a pointer to a newly-initialized principalDesc,
+// with a unique key, and a single blessing named with its own name.
+func makePrincipal(t testing.TB, name string) (desc *principalDesc) {
+	var err error
+	desc = new(principalDesc)
+	desc.name = name
+	desc.principal, err = lib_security.NewPrincipal()
+	if err != nil {
+		t.Fatalf("security.CreatePrincipal %q failed: %v", desc.name, err)
+	}
+	desc.blessings, err = desc.principal.BlessSelf(desc.name)
+	if err != nil {
+		t.Fatalf("principal.BlessSelf %q failed: %v", desc.name, err)
+	}
+	desc.krl = krl.New()
+	desc.cache = signing.NewValidationCache(5 * time.Second)
+	return desc
+}
+
+// makePrincipals() creares one principal per name, and adds
+// the blessings of each to the roots of all.
+func makePrincipals(t testing.TB, names ...string) (principals []*principalDesc) {
+	for i := range names {
+		principals = append(principals, makePrincipal(t, names[i]))
+	}
+	for i := range principals {
+		for j := range principals {
+			principals[j].principal.AddToRoots(principals[i].blessings)
+		}
+	}
+	return principals
+}
+
+// BenchmarkHashData() measures the time taken to do a cryptogrqaphic hash of
+// 1kBytes.
+func BenchmarkHashData(b *testing.B) {
+	var block [1024]byte
+	hasher := sha256.New()
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		hasher.Write(block[:])
+	}
+}
+
+// BenchmarkSignData() measures the time taken to sign something with
+// signing.SignData().
+func BenchmarkSignData(b *testing.B) {
+	ctx, shutdown := test.V23Init()
+	defer shutdown()
+	var err error
+	author := makePrincipal(b, "author")
+	dataToSign := []signing.Item{signing.ItemData{Value: []byte("hello")}}
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		author.data, author.authorBlessingsData, err =
+			signing.SignData(ctx, author.cache, author.principal, author.blessings, dataToSign)
+	}
+	if err != nil {
+		panic(err)
+	}
+}
+
+// BenchmarkSign1000Data() measures the time taken to sign 1000 small data
+// items with signing.SignData().
+func BenchmarkSign1000Data(b *testing.B) {
+	ctx, shutdown := test.V23Init()
+	defer shutdown()
+	var err error
+	author := makePrincipal(b, "author")
+	var dataToSign []signing.Item
+	for i := 0; i != 1000; i++ {
+		dataToSign = append(dataToSign, signing.ItemData{Value: []byte("hello")})
+	}
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		author.data, author.authorBlessingsData, err =
+			signing.SignData(ctx, author.cache, author.principal, author.blessings, dataToSign)
+	}
+	if err != nil {
+		panic(err)
+	}
+}
+
+// BenchmarkSignData() measures the time taken to check a validated signature
+// with DataWithSignature.Check().
+func BenchmarkCheckData(b *testing.B) {
+	ctx, shutdown := test.V23Init()
+	defer shutdown()
+	var err error
+
+	principals := makePrincipals(b, "author", "validator", "checker")
+	author := principals[0]
+	validator := principals[1]
+	checker := principals[2]
+
+	dataToSign := []signing.Item{signing.ItemData{Value: []byte("hello")}}
+	author.data, author.authorBlessingsData, err =
+		signing.SignData(ctx, author.cache, author.principal, author.blessings, dataToSign)
+	if err != nil {
+		panic(err)
+	}
+	callToValidator := fakeCall{
+		localPrincipal:  validator.principal,
+		localBlessings:  validator.blessings,
+		remoteBlessings: author.blessings,
+	}
+	validator.names, err = author.data.Check(ctx, author.cache, &callToValidator, validator.krl, 24*30*time.Hour)
+	if err != nil {
+		panic(err)
+	}
+	callToChecker := fakeCall{
+		localPrincipal:  checker.principal,
+		localBlessings:  checker.blessings,
+		remoteBlessings: validator.blessings,
+	}
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		checker.names, err = author.data.Check(ctx, author.cache, &callToChecker, checker.krl, 24*30*time.Hour)
+	}
+}
+
+// BenchmarkSign1000Data() measures the time taken to check a validated
+// signature over 1000 small data items with DataWithSignature.Check().
+func BenchmarkCheck1000Data(b *testing.B) {
+	ctx, shutdown := test.V23Init()
+	defer shutdown()
+	var err error
+
+	principals := makePrincipals(b, "author", "validator", "checker")
+	author := principals[0]
+	validator := principals[1]
+	checker := principals[2]
+
+	var dataToSign []signing.Item
+	for i := 0; i != 1000; i++ {
+		dataToSign = append(dataToSign, signing.ItemData{Value: []byte("hello")})
+	}
+	author.data, author.authorBlessingsData, err =
+		signing.SignData(ctx, author.cache, author.principal, author.blessings, dataToSign)
+	if err != nil {
+		panic(err)
+	}
+	callToValidator := fakeCall{
+		localPrincipal:  validator.principal,
+		localBlessings:  validator.blessings,
+		remoteBlessings: author.blessings,
+	}
+	validator.names, err = author.data.Check(ctx, author.cache, &callToValidator, validator.krl, 24*30*time.Hour)
+	if err != nil {
+		panic(err)
+	}
+	callToChecker := fakeCall{
+		localPrincipal:  checker.principal,
+		localBlessings:  checker.blessings,
+		remoteBlessings: validator.blessings,
+	}
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		checker.names, err = author.data.Check(ctx, author.cache, &callToChecker, checker.krl, 24*30*time.Hour)
+	}
+}
+
+// BenchmarkMarshallBlessings() measures the time taken to marshal a Blessings.
+func BenchmarkMarshallBlessings(b *testing.B) {
+	var err error
+	author := makePrincipal(b, "author")
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		author.marshalledBlessings, err = vom.Encode(author.blessings)
+	}
+	if err != nil {
+		b.Fatalf("vom.Encode failed: %v", err)
+	}
+}
+
+// BenchmarkUnmarshallBlessings() measures the time taken to unmashal a Blessings.
+func BenchmarkUnmarshallBlessings(b *testing.B) {
+	var err error
+	author := makePrincipal(b, "author")
+	author.marshalledBlessings, err = vom.Encode(author.blessings)
+	if err != nil {
+		b.Fatalf("vom.Encode failed: %v", err)
+	}
+	var blessings security.Blessings
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		err = vom.Decode(author.marshalledBlessings, &blessings)
+	}
+	if err != nil {
+		b.Fatalf("vom.Encode failed: %v", err)
+	}
+}
+
+// BenchmarkMarshallPublicKey() measures the time taken to marshal a PublicKey.
+func BenchmarkMarshallPublicKey(b *testing.B) {
+	var err error
+	author := makePrincipal(b, "author")
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		_, err = author.principal.PublicKey().MarshalBinary()
+	}
+	if err != nil {
+		b.Fatalf("MarshalBinary() failed: %v", err)
+	}
+}
+
+// BenchmarkUnmarshallPublicKey() measures the time taken to unmarshal a PublicKey.
+func BenchmarkUnmarshallPublicKey(b *testing.B) {
+	var err error
+	author := makePrincipal(b, "author")
+	var marshalledKey []byte
+	marshalledKey, err = author.principal.PublicKey().MarshalBinary()
+	if err != nil {
+		b.Fatalf("MarshalBinary() failed: %v", err)
+	}
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		_, err = security.UnmarshalPublicKey(marshalledKey)
+	}
+	if err != nil {
+		b.Fatalf("MarshalBinary() failed: %v", err)
+	}
+}
+
+// TestSignData() tests that a complete flow of signing, validating, and
+// checking works on a DataWithSignature.
+func TestSignData(t *testing.T) {
+	ctx, shutdown := test.V23Init()
+	defer shutdown()
+
+	var err error
+
+	principals := makePrincipals(t, "author", "validator", "checker")
+	author := principals[0]
+	validator := principals[1]
+	checker := principals[2]
+
+	// Add each princpipal's blessings to each principal's roots.
+	pdList := []*principalDesc{author, validator, checker}
+	for i := 0; i != len(pdList); i++ {
+		for j := 0; j != len(pdList); j++ {
+			pdList[j].principal.AddToRoots(pdList[i].blessings)
+		}
+	}
+
+	// --------------------------------------
+	// Author
+	// Sign some data.
+	dataToSign := []signing.Item{
+		signing.ItemData{Value: []byte("hello")},
+		signing.ItemData{Value: []byte("world")},
+		signing.ItemData{Value: []byte("!")},
+	}
+	author.data, author.authorBlessingsData, err =
+		signing.SignData(ctx, author.cache, author.principal, author.blessings, dataToSign)
+	if err != nil {
+		t.Fatalf("signing.SignData failed: %v", err)
+	}
+	if author.data.IsValidated {
+		t.Fatalf("signing.SignData generated data with IsValidated set")
+	}
+
+	// --------------------------------------
+	// Validator
+	callToValidator := fakeCall{
+		localPrincipal:  validator.principal,
+		localBlessings:  validator.blessings,
+		remoteBlessings: author.blessings,
+	}
+	// The validator receives author.data from the author.
+	validator.data = new(signing.DataWithSignature)
+	*validator.data = *author.data
+	// Initially the validator doesn't have the author BlessingsData.
+	validator.authorBlessingsData = validator.cache.LookupBlessingsData(ctx, validator.data.BlessingsHash)
+	if validator.authorBlessingsData != nil {
+		t.Errorf("found non-nil BlessingsData for validator.data.BlessingsHash in validator's ValidationCache")
+	}
+	validator.names, err = validator.data.Check(ctx, validator.cache, &callToValidator, validator.krl, 24*30*time.Hour)
+	if verror.ErrorID(err) != signing.ErrNeedAuthorBlessingsForHash.ID {
+		t.Fatalf("validator.data.Check got err %v, want %s", err, signing.ErrNeedAuthorBlessingsForHash.ID)
+	}
+
+	// The validator receives the author's marshalled blessings from the author.
+	validator.marshalledBlessings = author.authorBlessingsData.MarshalledBlessings
+	validator.blessingsHash, validator.authorBlessingsData, err = validator.cache.AddWireBlessings(ctx, validator.marshalledBlessings)
+	if err != nil {
+		t.Fatalf("validator can't add author's marshalled belssings to its ValidationCache: %v", err)
+	}
+
+	validator.names, err = validator.data.Check(ctx, validator.cache, &callToValidator, validator.krl, 24*30*time.Hour)
+	if err != nil {
+		t.Fatalf("validator error calling Check() on data: %v", err)
+	}
+	if !validator.data.IsValidated {
+		t.Fatalf("signing.Check didn't set IsValidated")
+	}
+	// Validator's cache should now have the author's BlessingData, and the validator's ValidatorData.
+	validator.authorBlessingsData = validator.cache.LookupBlessingsData(ctx, validator.data.BlessingsHash)
+	if validator.authorBlessingsData == nil {
+		t.Errorf("didn't finf BlessingsData for validator.data.BlessingsHash in validator's ValidationCache")
+	}
+	validator.validatorData = validator.cache.LookupValidatorData(ctx, validator.data.ValidatorDataHash)
+
+	// --------------------------------------
+	// Checker
+	callToChecker := fakeCall{
+		localPrincipal:  checker.principal,
+		localBlessings:  checker.blessings,
+		remoteBlessings: validator.blessings,
+	}
+	// The checker recieves validator.data from the validator, except that
+	// data item 1 is replaced by its hash, because (for example) the
+	// check is not allowed to see it.
+	checker.data = new(signing.DataWithSignature)
+	*checker.data = *validator.data
+	checker.data.Data[1] = signing.ItemHash{Value: signing.SumByteVectorWithLength(checker.data.Data[1].(signing.ItemData).Value)}
+
+	// Initially the checker doesn't have the author BlessingsData, or the validator ValidatorData.
+	checker.authorBlessingsData = checker.cache.LookupBlessingsData(ctx, checker.data.BlessingsHash)
+	if checker.authorBlessingsData != nil {
+		t.Errorf("found non-nil blessings data for checker.data.BlessingsHash hash in checker's ValidationCache")
+	}
+	checker.names, err = checker.data.Check(ctx, checker.cache, &callToChecker, checker.krl, 24*30*time.Hour)
+	if verror.ErrorID(err) != signing.ErrNeedAuthorBlessingsAndValidatorDataForHash.ID {
+		t.Fatalf("checker.data.Check got err %v, want %s", err, signing.ErrNeedAuthorBlessingsAndValidatorDataForHash.ID)
+	}
+
+	// The checker receives the author's marshalled blessings from the validator.
+	checker.marshalledBlessings = validator.marshalledBlessings
+	checker.blessingsHash, checker.authorBlessingsData, err = checker.cache.AddWireBlessings(ctx, checker.marshalledBlessings)
+	if err != nil {
+		t.Fatalf("checker can't add author's marshalled belssings to its ValidationCache: %v", err)
+	}
+	checker.names, err = checker.data.Check(ctx, checker.cache, &callToChecker, checker.krl, 24*30*time.Hour)
+	if verror.ErrorID(err) != signing.ErrNeedValidatorDataForHash.ID {
+		t.Fatalf("checker.data.Check got err %v, want %s", err, signing.ErrNeedValidatorDataForHash.ID)
+	}
+
+	// The checker receives the validator's data from the validator, passing through the wire format.
+	wvd := signing.ToWireValidatorData(validator.validatorData)
+	var vd signing.ValidatorData
+	vd, err = signing.FromWireValidatorData(&wvd)
+	if err != nil {
+		t.Fatalf("signing.FromWireValidatorData got error:  %v", err)
+	}
+	checker.validatorData = &vd
+
+	// The checker adds the ValidatorData to its cache.
+	checker.validatorHash = checker.cache.AddValidatorData(ctx, checker.validatorData)
+
+	// And now the Check() operation should work.
+	checker.names, err = checker.data.Check(ctx, checker.cache, &callToChecker, checker.krl, 24*30*time.Hour)
+	if err != nil {
+		t.Fatalf("checker.data.Check got unexpected err %v", err)
+	}
+}
diff --git a/services/syncbase/signing/validationcache.go b/services/syncbase/signing/validationcache.go
new file mode 100644
index 0000000..1c95bd0
--- /dev/null
+++ b/services/syncbase/signing/validationcache.go
@@ -0,0 +1,190 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This module implements a cache of data associated with the
+// signatures, keyed by hash values of the data.  The intent is that
+// communicating devices will refer to the data using hashes, and transmit the
+// data itself only if the device on the other side does not have the data in
+// its cache.
+
+package signing
+
+import "crypto/sha256"
+import "encoding/binary"
+import "time"
+
+import "v.io/syncbase/x/ref/services/syncbase/signing/hashcache"
+import "v.io/v23/context"
+import "v.io/v23/security"
+import "v.io/v23/vom"
+
+// --------------------------------------------
+
+// A BlessingsData contains information about a security.Blessings object.  The
+// object itself is referred to by UnmarshalledBlessings.  The implementation
+// constructs all instances; the client should not modify fields.
+type BlessingsData struct {
+	UnmarshalledBlessings security.Blessings // The Blessings.
+	MarshalledBlessings   []byte             // VOM encoded Blessings.
+	MarshalledPublicKey   []byte             // Value from blessings.PublicKey().MarshalBinary().
+}
+
+// A ValidatorData is the extra data that a validator signs when validating and
+// signing a DataWithSignature.  Clients may construct instances to pass to
+// AddValidatorData(), but should not modify the fields of a constructed
+// ValidatorData.
+type ValidatorData struct {
+	Names               []string           // Names of valid signing blessings in the Blessings referred to by BlessingsHash.
+	PublicKey           security.PublicKey // The key used to create ValidatorSigned.
+	MarshalledPublicKey []byte             // PublicKey, marshalled with MarshalBinary().
+}
+
+// hash() returns the hash of *vd.  This hash should be used in the
+// ValidatorDataHash field of DataWithSignature, and as the cache key of *vd
+// in a ValidationCache.
+func (vd *ValidatorData) hash() []byte {
+	hasher := sha256.New()
+	var buffer [256]byte
+	var buf []byte = buffer[:]
+	binary.LittleEndian.PutUint64(buf[:], uint64(len(vd.Names)))
+	hasher.Write(buf[:8])
+	for i := range vd.Names {
+		if len(vd.Names[i]) > len(buf) {
+			buf = make([]byte, len(vd.Names[i])+256)
+		}
+		hashByteVectorWithLength(hasher, []byte(vd.Names[i]))
+	}
+	hashByteVectorWithLength(hasher, vd.MarshalledPublicKey)
+	return hasher.Sum(nil)[:]
+}
+
+// A ValidationCache records recently-seen instances of BlessingsData and
+// ValidatorData values, keys by hashes of the blessings and validator keys
+// respectively.  Values may expire from the cache if unused for a duration
+// specified with NewValidationCache().
+type ValidationCache struct {
+	blessingsCache *hashcache.Cache
+	validatorCache *hashcache.Cache
+}
+
+// NewValidationCache() returns a pointer to a new, empty ValidationCache with
+// the specified expiry duration..
+func NewValidationCache(expiry time.Duration) *ValidationCache {
+	return &ValidationCache{
+		blessingsCache: hashcache.New(expiry),
+		validatorCache: hashcache.New(expiry)}
+}
+
+// LookupBlessingsData() returns a pointer to the BlessingsData associated with
+// blessingsHash in *vc.  blessingsHash should have been returned by a previous
+// call to AddBlessings() or AddWireBlessings() (possibly on another machine).
+// nil is returned if the data is not present.  The client should not modify
+// *result, since it is shared with *vc.
+func (vc *ValidationCache) LookupBlessingsData(ctx *context.T, blessingsHash []byte) (result *BlessingsData) {
+	value, found := vc.blessingsCache.Lookup(blessingsHash)
+	if found {
+		result = value.(*BlessingsData)
+	}
+	return result
+}
+
+// addBlessings() adds a BlessingsData for blessings to *vc, and returns a hash
+// value, which if passed to LookupBlessingsData() will yield a pointer to the
+// BlessingsData, or a non-nil error.  The fields of BlessingsData other than
+// MarshalledBlessings and UnmarshalledBlessings are constructed by this
+// routine.  Requires that blessings and marshalledBlessings represent the same
+// data, or that marshalledBlessings be nil.
+func (vc *ValidationCache) addBlessings(ctx *context.T, blessings security.Blessings,
+	marshalledBlessings []byte) (blessingsHash []byte, data *BlessingsData, err error) {
+
+	blessingsHash = blessings.UniqueID()
+	if value, found := vc.blessingsCache.Lookup(blessingsHash); found {
+		data = value.(*BlessingsData)
+	} else { // not found
+		var marshalledKey []byte
+		if marshalledBlessings == nil {
+			marshalledBlessings, err = vom.Encode(blessings)
+		}
+		if err == nil {
+			marshalledKey, err = blessings.PublicKey().MarshalBinary()
+		}
+		if err == nil {
+			data = &BlessingsData{
+				UnmarshalledBlessings: blessings,
+				MarshalledBlessings:   marshalledBlessings,
+				MarshalledPublicKey:   marshalledKey}
+			vc.blessingsCache.Add(blessingsHash, data)
+		}
+	}
+	return blessingsHash, data, err
+}
+
+// AddBlessings() adds a BlessingsData for blessings to *cv, and
+// returns a hash value, which if passed to LookupBlessingsData() will yield a
+// pointer to the BlessingsData, or a non-nil error.  The fields of
+// BlessingsData other than UnmarshalledBlessings are constructed by this
+// routine.
+func (vc *ValidationCache) AddBlessings(ctx *context.T, blessings security.Blessings) (blessingsHash []byte, data *BlessingsData, err error) {
+	return vc.addBlessings(ctx, blessings, nil)
+}
+
+// AddWireBlessings() adds a BlessingsData for blessings to *cv, and
+// returns a hash value, which if passed to LookupBlessingsData() will yield a
+// pointer to the BlessingsData, or a non-nil error.  The fields of
+// BlessingsData other than MarshalledBlessings are constructed by this
+// routine.
+func (vc *ValidationCache) AddWireBlessings(ctx *context.T,
+	marshalledBlessings []byte) (blessingsHash []byte, data *BlessingsData, err error) {
+
+	var blessings security.Blessings
+	err = vom.Decode(marshalledBlessings, &blessings)
+	if err == nil {
+		blessingsHash, data, err = vc.addBlessings(ctx, blessings, marshalledBlessings)
+	}
+	return blessingsHash, data, err
+}
+
+// LookupValidatorData() returns a pointer to the ValidatorData associated with
+// hash validatorHash in *vc validatorHash should have been returned by a
+// previous call to AddValidatorData() (possibly on another machine).  nil is
+// returned if the data is not present.  The client should not modifiy *result,
+// since it it shared with *vc.
+func (vc *ValidationCache) LookupValidatorData(ctx *context.T, validatorHash []byte) (result *ValidatorData) {
+	value, found := vc.validatorCache.Lookup(validatorHash)
+	if found {
+		result = value.(*ValidatorData)
+	}
+	return result
+}
+
+// AddValidatorData() adds a ValidatorData *vd to cache *vc, and returns a hash
+// value, which if passed to LookupValidatorData() will yield a pointer to the
+// ValidatorData.  The client should not modify *vd after the call, since it is
+// shared with *vc.
+func (vc *ValidationCache) AddValidatorData(ctx *context.T, vd *ValidatorData) (validatorDataHash []byte) {
+	validatorDataHash = vd.hash()
+	vc.validatorCache.Add(validatorDataHash, vd)
+	return validatorDataHash
+}
+
+// ToWireValidatorData() puts the wire form of ValidatorData *vd in *wvd.
+func ToWireValidatorData(vd *ValidatorData) (wvd WireValidatorData) {
+	wvd.Names = make([]string, len(vd.Names))
+	copy(wvd.Names, vd.Names)
+	wvd.MarshalledPublicKey = make([]byte, len(vd.MarshalledPublicKey))
+	copy(wvd.MarshalledPublicKey, vd.MarshalledPublicKey)
+	return wvd
+}
+
+// FromWireValidatorData() puts the in-memory form of WireValidatorData *wvd in *vd.
+func FromWireValidatorData(wvd *WireValidatorData) (vd ValidatorData, err error) {
+	vd.PublicKey, err = security.UnmarshalPublicKey(wvd.MarshalledPublicKey)
+	if err == nil {
+		vd.Names = make([]string, len(wvd.Names))
+		copy(vd.Names, wvd.Names)
+		vd.MarshalledPublicKey = make([]byte, len(wvd.MarshalledPublicKey))
+		copy(vd.MarshalledPublicKey, wvd.MarshalledPublicKey)
+	}
+	return vd, err
+}
diff --git a/services/syncbase/signing/validationcache_test.go b/services/syncbase/signing/validationcache_test.go
new file mode 100644
index 0000000..a45835b
--- /dev/null
+++ b/services/syncbase/signing/validationcache_test.go
@@ -0,0 +1,189 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file tests the validationcache.go module.
+
+package signing_test
+
+import "bytes"
+import "testing"
+import "time"
+
+import "v.io/syncbase/x/ref/services/syncbase/signing"
+import "v.io/v23/security"
+import "v.io/x/ref/test"
+import _ "v.io/x/ref/runtime/factories/generic"
+import lib_security "v.io/x/ref/lib/security"
+
+// A principalVDesc holds the local state of a single principal in the tests below.
+type principalVDesc struct {
+	name          string
+	principal     security.Principal
+	blessings     security.Blessings
+	blessingsHash []byte
+	blessingsData *signing.BlessingsData
+	validatorHash []byte
+	validatorData *signing.ValidatorData
+	cache         *signing.ValidationCache
+}
+
+// makePrincipalVDesc() returns a pointer to a newly-initialized principalVDesc,
+// with a unique key, and a single blessing named with its own name.
+func makePrincipalVDesc(t *testing.T, name string) (desc *principalVDesc) {
+	var err error
+	desc = new(principalVDesc)
+	desc.name = name
+	desc.principal, err = lib_security.NewPrincipal()
+	if err != nil {
+		t.Fatalf("lib_security.NewPrincipal %q failed: %v", desc.name, err)
+	}
+	desc.blessings, err = desc.principal.BlessSelf(desc.name)
+	if err != nil {
+		t.Fatalf("principal.BlessSelf %q failed: %v", desc.name, err)
+	}
+	desc.cache = signing.NewValidationCache(5 * time.Second)
+	return desc
+}
+
+func TestValidationCache(t *testing.T) {
+	ctx, shutdown := test.V23Init()
+	defer shutdown()
+
+	var err error
+
+	// Make a principalVDesc for each of the author, validator, and checker.
+	// (The author creates a signed change; the validator is a device
+	// author syncs with; the checker is a device a validator syncs with.)
+	author := makePrincipalVDesc(t, "author")
+	validator := makePrincipalVDesc(t, "validator")
+	checker := makePrincipalVDesc(t, "checker")
+
+	// Add each princpipal's blessings to each principal's roots.
+	pdList := []*principalVDesc{author, validator, checker}
+	for i := 0; i != len(pdList); i++ {
+		for j := 0; j != len(pdList); j++ {
+			pdList[j].principal.AddToRoots(pdList[i].blessings)
+		}
+	}
+
+	// --------------------------------------
+	// Author
+	arbitraryBlessingsData := author.cache.LookupBlessingsData(ctx, []byte{0x00})
+	if arbitraryBlessingsData != nil {
+		t.Errorf("found non-nil blessings data for nonsense hash in author's ValidationCache")
+	}
+	author.blessingsHash, author.blessingsData, err = author.cache.AddBlessings(ctx, author.blessings)
+	if err != nil {
+		t.Fatalf("error from author.cache.AddBlessings(): %v", err)
+	}
+	// Check that the author's data is as we expect.
+	if author.cache.LookupBlessingsData(ctx, author.blessingsHash) != author.blessingsData {
+		t.Fatalf("found wrong blessings data for hash in author's ValidationCache: %v vs %v",
+			author.cache.LookupBlessingsData(ctx, author.blessingsHash), author.blessingsData)
+	}
+
+	// --------------------------------------
+	// Validator
+	// The validator receives author.blessingsHash from the author.
+	// Initially the validator doesn't have the author BlessingsData.
+	authorBlessingsData := validator.cache.LookupBlessingsData(ctx, author.blessingsHash)
+	if authorBlessingsData != nil {
+		t.Errorf("found non-nil blessings data for author.blessingsHash hash in validator's ValidationCache")
+	}
+	// The validator receives the author's marshalled blessings from the author.
+	validator.blessingsHash, validator.blessingsData, err =
+		validator.cache.AddWireBlessings(ctx, author.blessingsData.MarshalledBlessings)
+	if err != nil {
+		t.Fatalf("validator can't add author's marshalled blessings to its ValidationCache: %v", err)
+	}
+	if !bytes.Equal(author.blessingsHash, validator.blessingsHash) {
+		t.Errorf("validator's copy of the blessingsHash different from author's")
+	}
+	// Check that we could have got the blessingsData with a lookup if this were the second time.
+	if validator.cache.LookupBlessingsData(ctx, validator.blessingsHash) != validator.blessingsData {
+		t.Fatalf("found wrong blessings data for hash in validator's ValidationCache")
+	}
+	var marshalledPublicKey []byte
+	marshalledPublicKey, err = validator.principal.PublicKey().MarshalBinary()
+	if err != nil {
+		t.Fatalf("validator.principal.PublicKey().MarshalBinary() got error: %v", err)
+	}
+
+	var validatedNames []string
+	validatedNames, _ = security.SigningBlessingNames(ctx, validator.principal,
+		validator.blessingsData.UnmarshalledBlessings)
+	validator.validatorData = &signing.ValidatorData{
+		Names:               validatedNames,
+		PublicKey:           validator.principal.PublicKey(),
+		MarshalledPublicKey: marshalledPublicKey}
+	validator.validatorHash = validator.cache.AddValidatorData(ctx, validator.validatorData)
+	if validator.cache.LookupValidatorData(ctx, validator.validatorHash) != validator.validatorData {
+		t.Fatalf("LookupValidatorData returned wrong ValidatorData pointer in validator")
+	}
+
+	// --------------------------------------
+	// Checker
+	// The checker receives validator.blessingsHash from the validator.
+	// Initially the checker doesn't have the author BlessingsData.
+	authorBlessingsData = checker.cache.LookupBlessingsData(ctx, validator.blessingsHash)
+	if authorBlessingsData != nil {
+		t.Errorf("found non-nil blessings data for author.blessingsHash hash in checker's ValidationCache")
+	}
+	// The checker receives the author's marshalled blessings from the validator.
+	checker.blessingsHash, checker.blessingsData, err =
+		checker.cache.AddWireBlessings(ctx, validator.blessingsData.MarshalledBlessings)
+	if err != nil {
+		t.Fatalf("checker can't add author's marshalled blessings (from validator) to ValidationCache: %v", err)
+	}
+	if !bytes.Equal(author.blessingsHash, checker.blessingsHash) {
+		t.Errorf("checker's copy of the blessingsHash different from author's")
+	}
+	// Check that we could have got the blessingsData with a lookup if this where the second time.
+	if checker.cache.LookupBlessingsData(ctx, checker.blessingsHash) != checker.blessingsData {
+		t.Fatalf("found wrong blessings data for hash in checker's ValidationCache")
+	}
+	// The checker recieves validator.validatorHash from the validator.
+	// Initially the checker doesn't have the ValidatorData.
+	validatorData := checker.cache.LookupValidatorData(ctx, validator.validatorHash)
+	if validatorData != nil {
+		t.Errorf("found non-nil validator data for validator.validatorHash hash in checker's ValidationCache")
+	}
+	// The checker receives the validator's data from the validator (or another checker).
+	checker.validatorHash = checker.cache.AddValidatorData(ctx, validator.validatorData)
+	if !bytes.Equal(validator.validatorHash, checker.validatorHash) {
+		t.Fatalf("checker's copy of the validatorHash different from validator's")
+	}
+	// Get the validatorData
+	checker.validatorData = checker.cache.LookupValidatorData(ctx, checker.validatorHash)
+	if checker.validatorData == nil {
+		t.Fatalf("found nil valdidatorData for checker.validatorHash hash in checker's ValidationCache")
+	}
+}
+
+func TestWireValidatorData(t *testing.T) {
+	var err error
+
+	pDesc := makePrincipalVDesc(t, "some_principal")
+
+	var vd signing.ValidatorData
+	vd.Names = []string{"wombat", "foo"}
+	vd.PublicKey = pDesc.principal.PublicKey()
+	vd.MarshalledPublicKey, err = vd.PublicKey.MarshalBinary()
+	if err != nil {
+		t.Fatalf("failed to marshel public key: %v\n", err)
+	}
+
+	var wvd signing.WireValidatorData
+	var vd2 signing.ValidatorData
+
+	wvd = signing.ToWireValidatorData(&vd)
+	vd2, err = signing.FromWireValidatorData(&wvd)
+	if err != nil {
+		t.Fatalf("FromWireValidatorData failed: %v\n", err)
+	}
+	if len(vd.Names) != len(vd2.Names) {
+		t.Fatalf("ToWireValidatorData/FromWireValidatorData failed to transfer Names list correctly:\nold\n%v\n\nnew\n%v\n\nwire\n%v\n",
+			vd, vd2, wvd)
+	}
+}
diff --git a/services/syncbase/store/benchmark/benchmark.go b/services/syncbase/store/benchmark/benchmark.go
new file mode 100644
index 0000000..c794fa9
--- /dev/null
+++ b/services/syncbase/store/benchmark/benchmark.go
@@ -0,0 +1,144 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package benchmark
+
+import (
+	"fmt"
+	"math/rand"
+	"testing"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+)
+
+// RandomGenerator is a helper for generating random data.
+type RandomGenerator struct {
+	rand.Rand
+	data []byte
+	pos  int
+}
+
+// NewRandomGenerator returns a new generator of pseudo-random byte sequences
+// seeded with the given value. Every N bytes produced by this generator can be
+// compressed to (compressionRatio * N) bytes.
+func NewRandomGenerator(seed int64, compressionRatio float64) *RandomGenerator {
+	gen := &RandomGenerator{
+		*rand.New(rand.NewSource(seed)),
+		[]byte{},
+		0,
+	}
+	for len(gen.data) < 1000*1000 {
+		// We generate compressible byte sequences to test Snappy compression
+		// engine used by LevelDB.
+		gen.data = append(gen.data, gen.compressibleBytes(100, compressionRatio)...)
+	}
+	return gen
+}
+
+// randomBytes generates n pseudo-random bytes from range [' '..'~'].
+func (r *RandomGenerator) randomBytes(n int) (bytes []byte) {
+	for i := 0; i < n; i++ {
+		bytes = append(bytes, byte(' '+r.Intn(95))) // ' ' .. '~'
+	}
+	return
+}
+
+// compressibleBytes generates a sequence of n pseudo-random bytes that can
+// be compressed to ~(compressionRatio * n) bytes.
+func (r *RandomGenerator) compressibleBytes(n int, compressionRatio float64) (bytes []byte) {
+	raw := int(float64(n) * compressionRatio)
+	if raw < 1 {
+		raw = 1
+	}
+	rawData := r.randomBytes(raw)
+	// Duplicate the random data until we have filled n bytes.
+	for len(bytes) < n {
+		bytes = append(bytes, rawData...)
+	}
+	return bytes[0:n]
+}
+
+// generate returns a sequence of n pseudo-random bytes.
+func (r *RandomGenerator) generate(n int) []byte {
+	if r.pos+n > len(r.data) {
+		r.pos = 0
+		if n >= len(r.data) {
+			panic(fmt.Sprintf("length(%d) is too big", n))
+		}
+	}
+	r.pos += n
+	return r.data[r.pos-n : r.pos]
+}
+
+// Config is a set of settings required to run a benchmark.
+type Config struct {
+	Rand *RandomGenerator
+	// St is the database to use. Initially it should be empty.
+	St       store.Store
+	KeyLen   int // size of each key
+	ValueLen int // size of each value
+}
+
+// WriteSequential writes b.N values in sequential key order.
+func WriteSequential(b *testing.B, config *Config) {
+	doWrite(b, config, true)
+}
+
+// WriteRandom writes b.N values in random key order.
+func WriteRandom(b *testing.B, config *Config) {
+	doWrite(b, config, false)
+}
+
+func doWrite(b *testing.B, config *Config, seq bool) {
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		var k int
+		if seq {
+			k = i
+		} else {
+			k = config.Rand.Intn(b.N)
+		}
+		key := []byte(fmt.Sprintf("%0[2]*[1]d", k, config.KeyLen))
+		if err := config.St.Put(key, config.Rand.generate(config.ValueLen)); err != nil {
+			b.Fatalf("put error: %v", err)
+		}
+	}
+}
+
+// ReadSequential reads b.N values in sequential key order.
+func ReadSequential(b *testing.B, config *Config) {
+	WriteSequential(b, config)
+	b.ResetTimer()
+	s := config.St.Scan([]byte("0"), []byte("z"))
+	var key, value []byte
+	for i := 0; i < b.N; i++ {
+		if !s.Advance() {
+			b.Fatalf("can't read next value: %v", s.Err())
+		}
+		key = s.Key(key)
+		value = s.Value(value)
+	}
+	s.Cancel()
+}
+
+// ReadRandom reads b.N values in random key order.
+func ReadRandom(b *testing.B, config *Config) {
+	WriteSequential(b, config)
+	b.ResetTimer()
+	var value []byte
+	var err error
+	for i := 0; i < b.N; i++ {
+		key := []byte(fmt.Sprintf("%0[2]*[1]d", config.Rand.Intn(b.N), config.KeyLen))
+		if value, err = config.St.Get(key, value); err != nil {
+			b.Fatalf("can't read value for key %s: %v", key, err)
+		}
+	}
+}
+
+// Overwrite overwrites b.N values in random key order.
+func Overwrite(b *testing.B, config *Config) {
+	WriteSequential(b, config)
+	b.ResetTimer()
+	WriteRandom(b, config)
+}
diff --git a/services/syncbase/store/constants.go b/services/syncbase/store/constants.go
new file mode 100644
index 0000000..26551fa
--- /dev/null
+++ b/services/syncbase/store/constants.go
@@ -0,0 +1,15 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package store
+
+// TODO(sadovsky): Maybe define verrors for these.
+const (
+	ErrMsgClosedStore     = "closed store"
+	ErrMsgAbortedSnapshot = "aborted snapshot"
+	ErrMsgCanceledStream  = "canceled stream"
+	ErrMsgCommittedTxn    = "already called commit"
+	ErrMsgAbortedTxn      = "already called abort"
+	ErrMsgExpiredTxn      = "expired transaction"
+)
diff --git a/services/syncbase/store/invalid_types.go b/services/syncbase/store/invalid_types.go
new file mode 100644
index 0000000..a230684
--- /dev/null
+++ b/services/syncbase/store/invalid_types.go
@@ -0,0 +1,121 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package store
+
+import (
+	"v.io/v23/verror"
+)
+
+// InvalidSnapshot is a Snapshot for which all methods return errors.
+type InvalidSnapshot struct {
+	SnapshotSpecImpl
+	Error error // returned by all methods
+}
+
+// InvalidStream is a Stream for which all methods return errors.
+type InvalidStream struct {
+	Error error // returned by all methods
+}
+
+// InvalidTransaction is a Transaction for which all methods return errors.
+type InvalidTransaction struct {
+	Error error // returned by all methods
+}
+
+var (
+	_ Snapshot    = (*InvalidSnapshot)(nil)
+	_ Stream      = (*InvalidStream)(nil)
+	_ Transaction = (*InvalidTransaction)(nil)
+)
+
+////////////////////////////////////////////////////////////
+// InvalidSnapshot
+
+// Abort implements the store.Snapshot interface.
+func (s *InvalidSnapshot) Abort() error {
+	return convertError(s.Error)
+}
+
+// Get implements the store.StoreReader interface.
+func (s *InvalidSnapshot) Get(key, valbuf []byte) ([]byte, error) {
+	return valbuf, convertError(s.Error)
+}
+
+// Scan implements the store.StoreReader interface.
+func (s *InvalidSnapshot) Scan(start, limit []byte) Stream {
+	return &InvalidStream{s.Error}
+}
+
+////////////////////////////////////////////////////////////
+// InvalidStream
+
+// Advance implements the store.Stream interface.
+func (s *InvalidStream) Advance() bool {
+	return false
+}
+
+// Key implements the store.Stream interface.
+func (s *InvalidStream) Key(keybuf []byte) []byte {
+	panic(s.Error)
+}
+
+// Value implements the store.Stream interface.
+func (s *InvalidStream) Value(valbuf []byte) []byte {
+	panic(s.Error)
+}
+
+// Err implements the store.Stream interface.
+func (s *InvalidStream) Err() error {
+	return convertError(s.Error)
+}
+
+// Cancel implements the store.Stream interface.
+func (s *InvalidStream) Cancel() {
+}
+
+////////////////////////////////////////////////////////////
+// InvalidTransaction
+
+// ResetForRetry implements the store.Transaction interface.
+func (tx *InvalidTransaction) ResetForRetry() {
+	panic(tx.Error)
+}
+
+// Get implements the store.StoreReader interface.
+func (tx *InvalidTransaction) Get(key, valbuf []byte) ([]byte, error) {
+	return valbuf, convertError(tx.Error)
+}
+
+// Scan implements the store.StoreReader interface.
+func (tx *InvalidTransaction) Scan(start, limit []byte) Stream {
+	return &InvalidStream{tx.Error}
+}
+
+// Put implements the store.StoreWriter interface.
+func (tx *InvalidTransaction) Put(key, value []byte) error {
+	return convertError(tx.Error)
+}
+
+// Delete implements the store.StoreWriter interface.
+func (tx *InvalidTransaction) Delete(key []byte) error {
+	return convertError(tx.Error)
+}
+
+// Commit implements the store.Transaction interface.
+func (tx *InvalidTransaction) Commit() error {
+	return convertError(tx.Error)
+}
+
+// Abort implements the store.Transaction interface.
+func (tx *InvalidTransaction) Abort() error {
+	return convertError(tx.Error)
+}
+
+////////////////////////////////////////////////////////////
+// Internal helpers
+
+func convertError(err error) error {
+	return verror.Convert(verror.IDAction{}, nil, err)
+}
diff --git a/services/syncbase/store/leveldb/benchmark_test.go b/services/syncbase/store/leveldb/benchmark_test.go
new file mode 100644
index 0000000..7de0062
--- /dev/null
+++ b/services/syncbase/store/leveldb/benchmark_test.go
@@ -0,0 +1,52 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package leveldb
+
+import (
+	"testing"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/syncbase/x/ref/services/syncbase/store/benchmark"
+)
+
+func testConfig(db store.Store) *benchmark.Config {
+	return &benchmark.Config{
+		Rand:     benchmark.NewRandomGenerator(23917, 0.5),
+		St:       db,
+		KeyLen:   20,
+		ValueLen: 100,
+	}
+}
+
+func runBenchmark(b *testing.B, f func(*testing.B, *benchmark.Config)) {
+	db, dbPath := newDB()
+	defer destroyDB(db, dbPath)
+	f(b, testConfig(db))
+}
+
+// BenchmarkWriteSequential writes b.N values in sequential key order.
+func BenchmarkWriteSequential(b *testing.B) {
+	runBenchmark(b, benchmark.WriteSequential)
+}
+
+// BenchmarkWriteRandom writes b.N values in random key order.
+func BenchmarkWriteRandom(b *testing.B) {
+	runBenchmark(b, benchmark.WriteRandom)
+}
+
+// BenchmarkOverwrite overwrites b.N values in random key order.
+func BenchmarkOverwrite(b *testing.B) {
+	runBenchmark(b, benchmark.Overwrite)
+}
+
+// BenchmarkReadSequential reads b.N values in sequential key order.
+func BenchmarkReadSequential(b *testing.B) {
+	runBenchmark(b, benchmark.ReadSequential)
+}
+
+// BenchmarkReadRandom reads b.N values in random key order.
+func BenchmarkReadRandom(b *testing.B) {
+	runBenchmark(b, benchmark.ReadRandom)
+}
diff --git a/services/syncbase/store/leveldb/db.go b/services/syncbase/store/leveldb/db.go
new file mode 100644
index 0000000..0430561
--- /dev/null
+++ b/services/syncbase/store/leveldb/db.go
@@ -0,0 +1,176 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package leveldb provides a LevelDB-based implementation of store.Store.
+package leveldb
+
+// #cgo LDFLAGS: -lleveldb -lsnappy
+// #include <stdlib.h>
+// #include "leveldb/c.h"
+// #include "syncbase_leveldb.h"
+import "C"
+import (
+	"fmt"
+	"sync"
+	"unsafe"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/syncbase/x/ref/services/syncbase/store/transactions"
+	"v.io/v23/verror"
+)
+
+// db is a wrapper around LevelDB that implements the transactions.BatchStore
+// interface.
+type db struct {
+	// mu protects the state of the db.
+	mu   sync.RWMutex
+	node *store.ResourceNode
+	cDb  *C.leveldb_t
+	// Default read/write options.
+	readOptions  *C.leveldb_readoptions_t
+	writeOptions *C.leveldb_writeoptions_t
+	err          error
+}
+
+type OpenOptions struct {
+	CreateIfMissing bool
+	ErrorIfExists   bool
+}
+
+// Open opens the database located at the given path.
+func Open(path string, opts OpenOptions) (store.Store, error) {
+	var cError *C.char
+	cPath := C.CString(path)
+	defer C.free(unsafe.Pointer(cPath))
+
+	var cOptsCreateIfMissing, cOptsErrorIfExists C.uchar
+	if opts.CreateIfMissing {
+		cOptsCreateIfMissing = 1
+	}
+	if opts.ErrorIfExists {
+		cOptsErrorIfExists = 1
+	}
+
+	cOpts := C.leveldb_options_create()
+	C.leveldb_options_set_create_if_missing(cOpts, cOptsCreateIfMissing)
+	C.leveldb_options_set_error_if_exists(cOpts, cOptsErrorIfExists)
+	C.leveldb_options_set_paranoid_checks(cOpts, 1)
+	defer C.leveldb_options_destroy(cOpts)
+
+	cDb := C.leveldb_open(cOpts, cPath, &cError)
+	if err := goError(cError); err != nil {
+		return nil, err
+	}
+	readOptions := C.leveldb_readoptions_create()
+	C.leveldb_readoptions_set_verify_checksums(readOptions, 1)
+	return transactions.Wrap(&db{
+		node:         store.NewResourceNode(),
+		cDb:          cDb,
+		readOptions:  readOptions,
+		writeOptions: C.leveldb_writeoptions_create(),
+	}), nil
+}
+
+// Close implements the store.Store interface.
+func (d *db) Close() error {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	if d.err != nil {
+		return store.ConvertError(d.err)
+	}
+	d.node.Close()
+	C.leveldb_close(d.cDb)
+	d.cDb = nil
+	C.leveldb_readoptions_destroy(d.readOptions)
+	d.readOptions = nil
+	C.leveldb_writeoptions_destroy(d.writeOptions)
+	d.writeOptions = nil
+	d.err = verror.New(verror.ErrCanceled, nil, store.ErrMsgClosedStore)
+	return nil
+}
+
+// Destroy removes all physical data of the database located at the given path.
+func Destroy(path string) error {
+	var cError *C.char
+	cPath := C.CString(path)
+	defer C.free(unsafe.Pointer(cPath))
+	cOpts := C.leveldb_options_create()
+	defer C.leveldb_options_destroy(cOpts)
+	C.leveldb_destroy_db(cOpts, cPath, &cError)
+	return goError(cError)
+}
+
+// Get implements the store.StoreReader interface.
+func (d *db) Get(key, valbuf []byte) ([]byte, error) {
+	return d.getWithOpts(key, valbuf, d.readOptions)
+}
+
+// Scan implements the store.StoreReader interface.
+func (d *db) Scan(start, limit []byte) store.Stream {
+	d.mu.RLock()
+	defer d.mu.RUnlock()
+	if d.err != nil {
+		return &store.InvalidStream{Error: d.err}
+	}
+	return newStream(d, d.node, start, limit, d.readOptions)
+}
+
+// NewSnapshot implements the store.Store interface.
+func (d *db) NewSnapshot() store.Snapshot {
+	d.mu.RLock()
+	defer d.mu.RUnlock()
+	if d.err != nil {
+		return &store.InvalidSnapshot{Error: d.err}
+	}
+	return newSnapshot(d, d.node)
+}
+
+// WriteBatch implements the transactions.BatchStore interface.
+func (d *db) WriteBatch(batch ...transactions.WriteOp) error {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	if d.err != nil {
+		return d.err
+	}
+	cBatch := C.leveldb_writebatch_create()
+	defer C.leveldb_writebatch_destroy(cBatch)
+	for _, write := range batch {
+		switch write.T {
+		case transactions.PutOp:
+			cKey, cKeyLen := cSlice(write.Key)
+			cVal, cValLen := cSlice(write.Value)
+			C.leveldb_writebatch_put(cBatch, cKey, cKeyLen, cVal, cValLen)
+		case transactions.DeleteOp:
+			cKey, cKeyLen := cSlice(write.Key)
+			C.leveldb_writebatch_delete(cBatch, cKey, cKeyLen)
+		default:
+			panic(fmt.Sprintf("unknown write operation type: %v", write.T))
+		}
+	}
+	var cError *C.char
+	C.leveldb_write(d.cDb, d.writeOptions, cBatch, &cError)
+	return goError(cError)
+}
+
+// getWithOpts returns the value for the given key.
+// cOpts may contain a pointer to a snapshot.
+func (d *db) getWithOpts(key, valbuf []byte, cOpts *C.leveldb_readoptions_t) ([]byte, error) {
+	d.mu.RLock()
+	defer d.mu.RUnlock()
+	if d.err != nil {
+		return valbuf, store.ConvertError(d.err)
+	}
+	var cError *C.char
+	var valLen C.size_t
+	cStr, cLen := cSlice(key)
+	val := C.leveldb_get(d.cDb, cOpts, cStr, cLen, &valLen, &cError)
+	if err := goError(cError); err != nil {
+		return valbuf, err
+	}
+	if val == nil {
+		return valbuf, verror.New(store.ErrUnknownKey, nil, string(key))
+	}
+	defer C.leveldb_free(unsafe.Pointer(val))
+	return store.CopyBytes(valbuf, goBytes(val, valLen)), nil
+}
diff --git a/services/syncbase/store/leveldb/db_test.go b/services/syncbase/store/leveldb/db_test.go
new file mode 100644
index 0000000..88cddc2
--- /dev/null
+++ b/services/syncbase/store/leveldb/db_test.go
@@ -0,0 +1,123 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package leveldb
+
+import (
+	"fmt"
+	"io/ioutil"
+	"runtime"
+	"testing"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/syncbase/x/ref/services/syncbase/store/test"
+)
+
+func init() {
+	runtime.GOMAXPROCS(10)
+}
+
+func TestStream(t *testing.T) {
+	runTest(t, test.RunStreamTest)
+}
+
+func TestSnapshot(t *testing.T) {
+	runTest(t, test.RunSnapshotTest)
+}
+
+func TestStoreState(t *testing.T) {
+	runTest(t, test.RunStoreStateTest)
+}
+
+func TestClose(t *testing.T) {
+	runTest(t, test.RunCloseTest)
+}
+
+func TestReadWriteBasic(t *testing.T) {
+	runTest(t, test.RunReadWriteBasicTest)
+}
+
+func TestReadWriteRandom(t *testing.T) {
+	runTest(t, test.RunReadWriteRandomTest)
+}
+
+func TestConcurrentTransactions(t *testing.T) {
+	runTest(t, test.RunConcurrentTransactionsTest)
+}
+
+func TestTransactionState(t *testing.T) {
+	runTest(t, test.RunTransactionStateTest)
+}
+
+func TestTransactionsWithGet(t *testing.T) {
+	runTest(t, test.RunTransactionsWithGetTest)
+}
+
+func TestOpenOptions(t *testing.T) {
+	path, err := ioutil.TempDir("", "syncbase_leveldb")
+	if err != nil {
+		t.Fatalf("can't create temp dir: %v", err)
+	}
+	// DB is missing => call should fail.
+	st, err := Open(path, OpenOptions{CreateIfMissing: false, ErrorIfExists: false})
+	if err == nil {
+		t.Fatalf("open should've failed")
+	}
+	// DB is missing => call should succeed.
+	st, err = Open(path, OpenOptions{CreateIfMissing: true, ErrorIfExists: false})
+	if err != nil {
+		t.Fatalf("open failed: %v", err)
+	}
+	st.Close()
+	// DB exists => call should succeed.
+	st, err = Open(path, OpenOptions{CreateIfMissing: false, ErrorIfExists: false})
+	if err != nil {
+		t.Fatalf("open failed: %v", err)
+	}
+	st.Close()
+	// DB exists => call should fail.
+	st, err = Open(path, OpenOptions{CreateIfMissing: false, ErrorIfExists: true})
+	if err == nil {
+		t.Fatalf("open should've failed")
+	}
+	// DB exists => call should fail.
+	st, err = Open(path, OpenOptions{CreateIfMissing: true, ErrorIfExists: true})
+	if err == nil {
+		t.Fatalf("open should've failed")
+	}
+	// DB exists => call should succeed.
+	st, err = Open(path, OpenOptions{CreateIfMissing: true, ErrorIfExists: false})
+	if err != nil {
+		t.Fatalf("open failed: %v", err)
+	}
+	st.Close()
+	if err := Destroy(path); err != nil {
+		t.Fatalf("destroy failed: %v", err)
+	}
+}
+
+func runTest(t *testing.T, f func(t *testing.T, st store.Store)) {
+	st, dbPath := newDB()
+	defer destroyDB(st, dbPath)
+	f(t, st)
+}
+
+func newDB() (store.Store, string) {
+	path, err := ioutil.TempDir("", "syncbase_leveldb")
+	if err != nil {
+		panic(fmt.Sprintf("can't create temp dir: %v", err))
+	}
+	st, err := Open(path, OpenOptions{CreateIfMissing: true, ErrorIfExists: true})
+	if err != nil {
+		panic(fmt.Sprintf("can't open db at %v: %v", path, err))
+	}
+	return st, path
+}
+
+func destroyDB(st store.Store, path string) {
+	st.Close()
+	if err := Destroy(path); err != nil {
+		panic(fmt.Sprintf("can't destroy db at %v: %v", path, err))
+	}
+}
diff --git a/services/syncbase/store/leveldb/snapshot.go b/services/syncbase/store/leveldb/snapshot.go
new file mode 100644
index 0000000..a403127
--- /dev/null
+++ b/services/syncbase/store/leveldb/snapshot.go
@@ -0,0 +1,82 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package leveldb
+
+// #include "leveldb/c.h"
+import "C"
+import (
+	"sync"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/verror"
+)
+
+// snapshot is a wrapper around LevelDB snapshot that implements
+// the store.Snapshot interface.
+type snapshot struct {
+	store.SnapshotSpecImpl
+	// mu protects the state of the snapshot.
+	mu        sync.RWMutex
+	node      *store.ResourceNode
+	d         *db
+	cSnapshot *C.leveldb_snapshot_t
+	cOpts     *C.leveldb_readoptions_t
+	err       error
+}
+
+var _ store.Snapshot = (*snapshot)(nil)
+
+func newSnapshot(d *db, parent *store.ResourceNode) *snapshot {
+	cSnapshot := C.leveldb_create_snapshot(d.cDb)
+	cOpts := C.leveldb_readoptions_create()
+	C.leveldb_readoptions_set_verify_checksums(cOpts, 1)
+	C.leveldb_readoptions_set_snapshot(cOpts, cSnapshot)
+	s := &snapshot{
+		node:      store.NewResourceNode(),
+		d:         d,
+		cSnapshot: cSnapshot,
+		cOpts:     cOpts,
+	}
+	parent.AddChild(s.node, func() {
+		s.Abort()
+	})
+	return s
+}
+
+// Abort implements the store.Snapshot interface.
+func (s *snapshot) Abort() error {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	if s.err != nil {
+		return store.ConvertError(s.err)
+	}
+	s.node.Close()
+	C.leveldb_readoptions_destroy(s.cOpts)
+	s.cOpts = nil
+	C.leveldb_release_snapshot(s.d.cDb, s.cSnapshot)
+	s.cSnapshot = nil
+	s.err = verror.New(verror.ErrCanceled, nil, store.ErrMsgAbortedSnapshot)
+	return nil
+}
+
+// Get implements the store.StoreReader interface.
+func (s *snapshot) Get(key, valbuf []byte) ([]byte, error) {
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+	if s.err != nil {
+		return valbuf, store.ConvertError(s.err)
+	}
+	return s.d.getWithOpts(key, valbuf, s.cOpts)
+}
+
+// Scan implements the store.StoreReader interface.
+func (s *snapshot) Scan(start, limit []byte) store.Stream {
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+	if s.err != nil {
+		return &store.InvalidStream{Error: s.err}
+	}
+	return newStream(s.d, s.node, start, limit, s.cOpts)
+}
diff --git a/services/syncbase/store/leveldb/stream.go b/services/syncbase/store/leveldb/stream.go
new file mode 100644
index 0000000..2d592b4
--- /dev/null
+++ b/services/syncbase/store/leveldb/stream.go
@@ -0,0 +1,150 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package leveldb
+
+// #include "leveldb/c.h"
+// #include "syncbase_leveldb.h"
+import "C"
+import (
+	"bytes"
+	"sync"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/verror"
+)
+
+// stream is a wrapper around LevelDB iterator that implements
+// the store.Stream interface.
+type stream struct {
+	// mu protects the state of the stream.
+	mu    sync.Mutex
+	node  *store.ResourceNode
+	cIter *C.syncbase_leveldb_iterator_t
+	limit []byte
+
+	hasAdvanced bool
+	err         error
+
+	// hasValue is true iff a value has been staged. If hasValue is true,
+	// key and value point to the staged key/value pair. The underlying buffers
+	// of key and value are allocated on the C heap until Cancel is called,
+	// at which point they are copied to the Go heap.
+	hasValue bool
+	key      []byte
+	value    []byte
+}
+
+var _ store.Stream = (*stream)(nil)
+
+func newStream(d *db, parent *store.ResourceNode, start, limit []byte, cOpts *C.leveldb_readoptions_t) *stream {
+	cStr, size := cSlice(start)
+	cIter := C.syncbase_leveldb_create_iterator(d.cDb, cOpts, cStr, size)
+	s := &stream{
+		node:  store.NewResourceNode(),
+		cIter: cIter,
+		limit: limit,
+	}
+	parent.AddChild(s.node, func() {
+		s.Cancel()
+	})
+	return s
+}
+
+// destroyLeveldbIter destroys the underlying C iterator.
+// Assumes mu is held.
+func (s *stream) destroyLeveldbIter() {
+	s.node.Close()
+	C.syncbase_leveldb_iter_destroy(s.cIter)
+	s.cIter = nil
+}
+
+// Advance implements the store.Stream interface.
+func (s *stream) Advance() bool {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.hasValue = false
+	if s.cIter == nil {
+		return false
+	}
+	// The C iterator starts out initialized, pointing at the first value; we
+	// shouldn't move it during the first Advance() call.
+	if !s.hasAdvanced {
+		s.hasAdvanced = true
+	} else {
+		C.syncbase_leveldb_iter_next(s.cIter)
+	}
+	if s.cIter.is_valid != 0 && (len(s.limit) == 0 || bytes.Compare(s.cKey(), s.limit) < 0) {
+		s.hasValue = true
+		s.key = s.cKey()
+		s.value = s.cVal()
+		return true
+	}
+
+	var cError *C.char
+	C.syncbase_leveldb_iter_get_error(s.cIter, &cError)
+	s.err = goError(cError)
+	s.destroyLeveldbIter()
+	return false
+}
+
+// Key implements the store.Stream interface.
+func (s *stream) Key(keybuf []byte) []byte {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	if !s.hasValue {
+		panic("nothing staged")
+	}
+	return store.CopyBytes(keybuf, s.key)
+}
+
+// Value implements the store.Stream interface.
+func (s *stream) Value(valbuf []byte) []byte {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	if !s.hasValue {
+		panic("nothing staged")
+	}
+	return store.CopyBytes(valbuf, s.value)
+}
+
+// Err implements the store.Stream interface.
+func (s *stream) Err() error {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	return store.ConvertError(s.err)
+}
+
+// Cancel implements the store.Stream interface.
+// TODO(rogulenko): make Cancel non-blocking.
+func (s *stream) Cancel() {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	if s.cIter == nil {
+		return
+	}
+	// s.hasValue will be false if Advance has never been called.
+	if s.hasValue {
+		// We copy the key and the value from the C heap to the Go heap before
+		// deallocating the C iterator.
+		s.key = store.CopyBytes(nil, s.cKey())
+		s.value = store.CopyBytes(nil, s.cVal())
+	}
+	s.err = verror.New(verror.ErrCanceled, nil, store.ErrMsgCanceledStream)
+	s.destroyLeveldbIter()
+}
+
+// cKey returns the current key.
+// The returned []byte points to a buffer allocated on the C heap. This buffer
+// is valid until the next call to Advance or Cancel.
+func (it *stream) cKey() []byte {
+	return goBytes(it.cIter.key, it.cIter.key_len)
+}
+
+// cVal returns the current value.
+// The returned []byte points to a buffer allocated on the C heap. This buffer
+// is valid until the next call to Advance or Cancel.
+func (it *stream) cVal() []byte {
+	return goBytes(it.cIter.val, it.cIter.val_len)
+}
diff --git a/services/syncbase/store/leveldb/syncbase_leveldb.cc b/services/syncbase/store/leveldb/syncbase_leveldb.cc
new file mode 100644
index 0000000..8c6f7e6
--- /dev/null
+++ b/services/syncbase/store/leveldb/syncbase_leveldb.cc
@@ -0,0 +1,47 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file is intended to be C++ so that we can access C++ LevelDB interface
+// directly if necessary.
+
+#include "syncbase_leveldb.h"
+
+extern "C" {
+
+static void PopulateIteratorFields(syncbase_leveldb_iterator_t* iter) {
+  iter->is_valid = leveldb_iter_valid(iter->rep);
+  if (!iter->is_valid) {
+    return;
+  }
+  iter->key = leveldb_iter_key(iter->rep, &iter->key_len);
+  iter->val = leveldb_iter_value(iter->rep, &iter->val_len);
+}
+
+syncbase_leveldb_iterator_t* syncbase_leveldb_create_iterator(
+    leveldb_t* db,
+    const leveldb_readoptions_t* options,
+    const char* start, size_t start_len) {
+  syncbase_leveldb_iterator_t* result = new syncbase_leveldb_iterator_t;
+  result->rep = leveldb_create_iterator(db, options);
+  leveldb_iter_seek(result->rep, start, start_len);
+  PopulateIteratorFields(result);
+  return result;
+}
+
+void syncbase_leveldb_iter_destroy(syncbase_leveldb_iterator_t* iter) {
+  leveldb_iter_destroy(iter->rep);
+  delete iter;
+}
+
+void syncbase_leveldb_iter_next(syncbase_leveldb_iterator_t* iter) {
+  leveldb_iter_next(iter->rep);
+  PopulateIteratorFields(iter);
+}
+
+void syncbase_leveldb_iter_get_error(
+    const syncbase_leveldb_iterator_t* iter, char** errptr) {
+  leveldb_iter_get_error(iter->rep, errptr);
+}
+
+}  // end extern "C"
diff --git a/services/syncbase/store/leveldb/syncbase_leveldb.h b/services/syncbase/store/leveldb/syncbase_leveldb.h
new file mode 100644
index 0000000..d2faa82
--- /dev/null
+++ b/services/syncbase/store/leveldb/syncbase_leveldb.h
@@ -0,0 +1,61 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+//
+// This file contains helpers to minimize the number of cgo calls, which have
+// some overhead.
+// Some conventions:
+//
+// Errors are represented by a null-terminated C string. NULL means no error.
+// All operations that can raise an error are passed a "char** errptr" as the
+// last argument. *errptr should be NULL.
+// On failure, leveldb sets *errptr to a malloc()ed error message.
+//
+// All of the pointer arguments must be non-NULL.
+
+#ifndef V_IO_SYNCBASE_X_REF_SERVICES_SYNCBASE_STORE_LEVELDB_SYNCBASE_LEVELDB_H_
+#define V_IO_SYNCBASE_X_REF_SERVICES_SYNCBASE_STORE_LEVELDB_SYNCBASE_LEVELDB_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "leveldb/c.h"
+
+// Fields of this struct are accessed from go directly without cgo calls.
+struct syncbase_leveldb_iterator_t {
+  leveldb_iterator_t* rep;
+  unsigned char is_valid;
+  char const* key;
+  size_t key_len;
+  char const* val;
+  size_t val_len;
+};
+
+typedef struct syncbase_leveldb_iterator_t syncbase_leveldb_iterator_t;
+
+// Returns iterator that points to first key that is not less than |start|.
+// The returned iterator must be passed to |syncbase_leveldb_iter_destroy|
+// when finished.
+syncbase_leveldb_iterator_t* syncbase_leveldb_create_iterator(
+    leveldb_t* db,
+    const leveldb_readoptions_t* options,
+    const char* start, size_t start_len);
+
+// Deallocates iterator returned by |syncbase_leveldb_create_iterator|.
+void syncbase_leveldb_iter_destroy(syncbase_leveldb_iterator_t*);
+
+// Moves to the next entry in the source. After this call, |is_valid| is
+// true iff the iterator was not positioned at the last entry in the source.
+// REQUIRES: |is_valid| is true.
+void syncbase_leveldb_iter_next(syncbase_leveldb_iterator_t* iter);
+
+// Returns a non-nil error iff the iterator encountered any errors.
+void syncbase_leveldb_iter_get_error(
+    const syncbase_leveldb_iterator_t* iter, char** errptr);
+
+#ifdef __cplusplus
+}  // end extern "C"
+#endif
+
+#endif  // V_IO_SYNCBASE_X_REF_SERVICES_SYNCBASE_STORE_LEVELDB_SYNCBASE_LEVELDB_H_
diff --git a/services/syncbase/store/leveldb/util.go b/services/syncbase/store/leveldb/util.go
new file mode 100644
index 0000000..dce69bb
--- /dev/null
+++ b/services/syncbase/store/leveldb/util.go
@@ -0,0 +1,46 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package leveldb
+
+// #include "leveldb/c.h"
+import "C"
+import (
+	"reflect"
+	"unsafe"
+
+	"v.io/v23/verror"
+)
+
+// goError copies C error into Go heap and frees C buffer.
+func goError(cError *C.char) error {
+	if cError == nil {
+		return nil
+	}
+	err := verror.New(verror.ErrInternal, nil, C.GoString(cError))
+	C.leveldb_free(unsafe.Pointer(cError))
+	return err
+}
+
+// cSlice converts Go []byte to C string without copying the data.
+// This function behaves similarly to standard Go slice copying or sub-slicing,
+// in that the caller need not worry about ownership or garbage collection.
+func cSlice(str []byte) (*C.char, C.size_t) {
+	if len(str) == 0 {
+		return nil, 0
+	}
+	data := unsafe.Pointer((*reflect.StringHeader)(unsafe.Pointer(&str)).Data)
+	return (*C.char)(data), C.size_t(len(str))
+}
+
+// goBytes converts C string to Go []byte without copying the data.
+// This function behaves similarly to cSlice.
+func goBytes(str *C.char, size C.size_t) []byte {
+	ptr := unsafe.Pointer(&reflect.SliceHeader{
+		Data: uintptr(unsafe.Pointer(str)),
+		Len:  int(size),
+		Cap:  int(size),
+	})
+	return *(*[]byte)(ptr)
+}
diff --git a/services/syncbase/store/memstore/snapshot.go b/services/syncbase/store/memstore/snapshot.go
new file mode 100644
index 0000000..310f6e2
--- /dev/null
+++ b/services/syncbase/store/memstore/snapshot.go
@@ -0,0 +1,74 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package memstore
+
+import (
+	"sync"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/verror"
+)
+
+type snapshot struct {
+	store.SnapshotSpecImpl
+	mu   sync.Mutex
+	node *store.ResourceNode
+	data map[string][]byte
+	err  error
+}
+
+var _ store.Snapshot = (*snapshot)(nil)
+
+// Assumes st lock is held.
+func newSnapshot(st *memstore, parent *store.ResourceNode) *snapshot {
+	dataCopy := make(map[string][]byte, len(st.data))
+	for k, v := range st.data {
+		dataCopy[k] = v
+	}
+	s := &snapshot{
+		node: store.NewResourceNode(),
+		data: dataCopy,
+	}
+	parent.AddChild(s.node, func() {
+		s.Abort()
+	})
+	return s
+}
+
+// Abort implements the store.Snapshot interface.
+func (s *snapshot) Abort() error {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	if s.err != nil {
+		return store.ConvertError(s.err)
+	}
+	s.node.Close()
+	s.err = verror.New(verror.ErrCanceled, nil, store.ErrMsgAbortedSnapshot)
+	return nil
+}
+
+// Get implements the store.StoreReader interface.
+func (s *snapshot) Get(key, valbuf []byte) ([]byte, error) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	if s.err != nil {
+		return valbuf, store.ConvertError(s.err)
+	}
+	value, ok := s.data[string(key)]
+	if !ok {
+		return valbuf, verror.New(store.ErrUnknownKey, nil, string(key))
+	}
+	return store.CopyBytes(valbuf, value), nil
+}
+
+// Scan implements the store.StoreReader interface.
+func (s *snapshot) Scan(start, limit []byte) store.Stream {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	if s.err != nil {
+		return &store.InvalidStream{Error: s.err}
+	}
+	return newStream(s, s.node, start, limit)
+}
diff --git a/services/syncbase/store/memstore/store.go b/services/syncbase/store/memstore/store.go
new file mode 100644
index 0000000..15a2988
--- /dev/null
+++ b/services/syncbase/store/memstore/store.go
@@ -0,0 +1,98 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package memstore provides a simple, in-memory implementation of store.Store.
+// Since it's a prototype implementation, it makes no attempt to be performant.
+package memstore
+
+import (
+	"fmt"
+	"sync"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/syncbase/x/ref/services/syncbase/store/transactions"
+	"v.io/v23/verror"
+)
+
+type memstore struct {
+	mu   sync.Mutex
+	node *store.ResourceNode
+	data map[string][]byte
+	err  error
+}
+
+// New creates a new memstore.
+func New() store.Store {
+	return transactions.Wrap(&memstore{
+		data: map[string][]byte{},
+		node: store.NewResourceNode(),
+	})
+}
+
+// Close implements the store.Store interface.
+func (st *memstore) Close() error {
+	st.mu.Lock()
+	defer st.mu.Unlock()
+	if st.err != nil {
+		return store.ConvertError(st.err)
+	}
+	st.node.Close()
+	st.err = verror.New(verror.ErrCanceled, nil, store.ErrMsgClosedStore)
+	return nil
+}
+
+// Get implements the store.StoreReader interface.
+func (st *memstore) Get(key, valbuf []byte) ([]byte, error) {
+	st.mu.Lock()
+	defer st.mu.Unlock()
+	if st.err != nil {
+		return valbuf, store.ConvertError(st.err)
+	}
+	value, ok := st.data[string(key)]
+	if !ok {
+		return valbuf, verror.New(store.ErrUnknownKey, nil, string(key))
+	}
+	return store.CopyBytes(valbuf, value), nil
+}
+
+// Scan implements the store.StoreReader interface.
+func (st *memstore) Scan(start, limit []byte) store.Stream {
+	st.mu.Lock()
+	defer st.mu.Unlock()
+	if st.err != nil {
+		return &store.InvalidStream{Error: st.err}
+	}
+	// TODO(sadovsky): Close snapshot once stream is closed or canceled.
+	return newSnapshot(st, st.node).Scan(start, limit)
+}
+
+// NewSnapshot implements the store.Store interface.
+func (st *memstore) NewSnapshot() store.Snapshot {
+	st.mu.Lock()
+	defer st.mu.Unlock()
+	if st.err != nil {
+		return &store.InvalidSnapshot{Error: st.err}
+	}
+	return newSnapshot(st, st.node)
+}
+
+// WriteBatch implements the transactions.BatchStore interface.
+func (st *memstore) WriteBatch(batch ...transactions.WriteOp) error {
+	st.mu.Lock()
+	defer st.mu.Unlock()
+	if st.err != nil {
+		return store.ConvertError(st.err)
+	}
+	for _, write := range batch {
+		switch write.T {
+		case transactions.PutOp:
+			st.data[string(write.Key)] = write.Value
+		case transactions.DeleteOp:
+			delete(st.data, string(write.Key))
+		default:
+			panic(fmt.Sprintf("unknown write operation type: %v", write.T))
+		}
+	}
+	return nil
+}
diff --git a/services/syncbase/store/memstore/store_test.go b/services/syncbase/store/memstore/store_test.go
new file mode 100644
index 0000000..0b04032
--- /dev/null
+++ b/services/syncbase/store/memstore/store_test.go
@@ -0,0 +1,59 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package memstore
+
+import (
+	"runtime"
+	"testing"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/syncbase/x/ref/services/syncbase/store/test"
+)
+
+func init() {
+	runtime.GOMAXPROCS(10)
+}
+
+func TestStream(t *testing.T) {
+	runTest(t, test.RunStreamTest)
+}
+
+func TestSnapshot(t *testing.T) {
+	runTest(t, test.RunSnapshotTest)
+}
+
+func TestStoreState(t *testing.T) {
+	runTest(t, test.RunStoreStateTest)
+}
+
+func TestClose(t *testing.T) {
+	runTest(t, test.RunCloseTest)
+}
+
+func TestReadWriteBasic(t *testing.T) {
+	runTest(t, test.RunReadWriteBasicTest)
+}
+
+func TestReadWriteRandom(t *testing.T) {
+	runTest(t, test.RunReadWriteRandomTest)
+}
+
+func TestConcurrentTransactions(t *testing.T) {
+	runTest(t, test.RunConcurrentTransactionsTest)
+}
+
+func TestTransactionState(t *testing.T) {
+	runTest(t, test.RunTransactionStateTest)
+}
+
+func TestTransactionsWithGet(t *testing.T) {
+	runTest(t, test.RunTransactionsWithGetTest)
+}
+
+func runTest(t *testing.T, f func(t *testing.T, st store.Store)) {
+	st := New()
+	defer st.Close()
+	f(t, st)
+}
diff --git a/services/syncbase/store/memstore/stream.go b/services/syncbase/store/memstore/stream.go
new file mode 100644
index 0000000..345ea93
--- /dev/null
+++ b/services/syncbase/store/memstore/stream.go
@@ -0,0 +1,103 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package memstore
+
+import (
+	"sort"
+	"sync"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/verror"
+)
+
+type stream struct {
+	mu        sync.Mutex
+	node      *store.ResourceNode
+	sn        *snapshot
+	keys      []string
+	currIndex int
+	currKey   *string
+	err       error
+	done      bool
+}
+
+var _ store.Stream = (*stream)(nil)
+
+func newStream(sn *snapshot, parent *store.ResourceNode, start, limit []byte) *stream {
+	keys := []string{}
+	for k := range sn.data {
+		if k >= string(start) && (len(limit) == 0 || k < string(limit)) {
+			keys = append(keys, k)
+		}
+	}
+	sort.Strings(keys)
+	s := &stream{
+		node:      store.NewResourceNode(),
+		sn:        sn,
+		keys:      keys,
+		currIndex: -1,
+	}
+	parent.AddChild(s.node, func() {
+		s.Cancel()
+	})
+	return s
+}
+
+// Advance implements the store.Stream interface.
+func (s *stream) Advance() bool {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.currKey = nil
+	if s.done {
+		return false
+	}
+	s.currIndex++
+	if s.currIndex < len(s.keys) {
+		s.currKey = &s.keys[s.currIndex]
+	} else {
+		s.done = true
+		s.currKey = nil
+	}
+	return !s.done
+}
+
+// Key implements the store.Stream interface.
+func (s *stream) Key(keybuf []byte) []byte {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	if s.currKey == nil {
+		panic("nothing staged")
+	}
+	return store.CopyBytes(keybuf, []byte(*s.currKey))
+}
+
+// Value implements the store.Stream interface.
+func (s *stream) Value(valbuf []byte) []byte {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	if s.currKey == nil {
+		panic("nothing staged")
+	}
+	return store.CopyBytes(valbuf, s.sn.data[*s.currKey])
+}
+
+// Err implements the store.Stream interface.
+func (s *stream) Err() error {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	return store.ConvertError(s.err)
+}
+
+// Cancel implements the store.Stream interface.
+func (s *stream) Cancel() {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	if s.done {
+		return
+	}
+	s.done = true
+	s.node.Close()
+	s.err = verror.New(verror.ErrCanceled, nil, store.ErrMsgCanceledStream)
+}
diff --git a/services/syncbase/store/model.go b/services/syncbase/store/model.go
new file mode 100644
index 0000000..be7265d
--- /dev/null
+++ b/services/syncbase/store/model.go
@@ -0,0 +1,147 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package store defines the API for the syncbase storage engine.
+// Currently, this API and its implementations are meant to be internal.
+package store
+
+// TODO(sadovsky): Decide whether to defensively copy passed-in []byte's vs.
+// requiring clients not to modify passed-in []byte's.
+
+// StoreReader reads data from a CRUD-capable storage engine.
+type StoreReader interface {
+	// Get returns the value for the given key. The returned slice may be a
+	// sub-slice of valbuf if valbuf was large enough to hold the entire value.
+	// Otherwise, a newly allocated slice will be returned. It is valid to pass a
+	// nil valbuf.
+	// If the given key is unknown, valbuf is returned unchanged and the function
+	// fails with ErrUnknownKey.
+	Get(key, valbuf []byte) ([]byte, error)
+
+	// Scan returns all rows with keys in range [start, limit). If limit is "",
+	// all rows with keys >= start are included.
+	// Concurrency semantics: It is legal to perform writes concurrently with
+	// Scan. The returned stream may or may not reflect subsequent writes to keys
+	// not yet reached by the stream.
+	Scan(start, limit []byte) Stream
+}
+
+// StoreWriter writes data to a CRUD-capable storage engine.
+type StoreWriter interface {
+	// Put writes the given value for the given key.
+	Put(key, value []byte) error
+
+	// Delete deletes the entry for the given key.
+	// Succeeds (no-op) if the given key is unknown.
+	Delete(key []byte) error
+}
+
+// storeReadWriter combines StoreReader and StoreWriter.
+type storeReadWriter interface {
+	StoreReader
+	StoreWriter
+}
+
+// Store is a CRUD-capable storage engine that supports transactions.
+type Store interface {
+	storeReadWriter
+
+	// Close closes the store.
+	Close() error
+
+	// NewTransaction creates a transaction.
+	// TODO(rogulenko): add transaction options.
+	NewTransaction() Transaction
+
+	// NewSnapshot creates a snapshot.
+	// TODO(rogulenko): add snapshot options.
+	NewSnapshot() Snapshot
+}
+
+// SnapshotOrTransaction represents a Snapshot or a Transaction.
+type SnapshotOrTransaction interface {
+	StoreReader
+
+	// Abort closes the snapshot or transaction.
+	// Any subsequent method calls will fail.
+	// NOTE: this method is also used to distinguish between StoreReader and
+	// SnapshotOrTransaction.
+	Abort() error
+}
+
+// Snapshot is a handle to particular state in time of a Store.
+//
+// All read operations are executed against a consistent view of Store commit
+// history. Snapshots don't acquire locks and thus don't block transactions.
+type Snapshot interface {
+	SnapshotOrTransaction
+
+	// __snapshotSpec is a utility method to distinguish between Snapshot and
+	// SnapshotOrTransaction. This is a no-op.
+	__snapshotSpec()
+}
+
+// Transaction provides a mechanism for atomic reads and writes. Instead of
+// calling this function directly, clients are encouraged to use the
+// RunInTransaction() helper function, which detects "concurrent transaction"
+// errors and handles retries internally.
+//
+// Default concurrency semantics:
+// - Reads (e.g. gets, scans) inside a transaction operate over a consistent
+//   snapshot taken during NewTransaction(), and will see the effects of prior
+//   writes performed inside the transaction.
+// - Commit() may fail with ErrConcurrentTransaction, indicating that after
+//   NewTransaction() but before Commit(), some concurrent routine wrote to a
+//   key that matches a key or row-range read inside this transaction.
+// - Other methods will never fail with error ErrConcurrentTransaction, even if
+//   it is known that Commit() will fail with this error.
+//
+// Once a transaction has been committed or aborted, subsequent method calls
+// will fail with no effect.
+type Transaction interface {
+	SnapshotOrTransaction
+	StoreWriter
+
+	// Commit commits the transaction.
+	// Fails if writes from outside this transaction conflict with reads from
+	// within this transaction.
+	Commit() error
+}
+
+// Stream is an interface for iterating through a collection of key-value pairs.
+type Stream interface {
+	// Advance stages an element so the client can retrieve it with Key or Value.
+	// Advance returns true iff there is an element to retrieve. The client must
+	// call Advance before calling Key or Value. The client must call Cancel if it
+	// does not iterate through all elements (i.e. until Advance returns false).
+	// Advance may block if an element is not immediately available.
+	Advance() bool
+
+	// Key returns the key of the element that was staged by Advance. The returned
+	// slice may be a sub-slice of keybuf if keybuf was large enough to hold the
+	// entire key. Otherwise, a newly allocated slice will be returned. It is
+	// valid to pass a nil keybuf.
+	// Key may panic if Advance returned false or was not called at all.
+	// Key does not block.
+	Key(keybuf []byte) []byte
+
+	// Value returns the value of the element that was staged by Advance. The
+	// returned slice may be a sub-slice of valbuf if valbuf was large enough to
+	// hold the entire value. Otherwise, a newly allocated slice will be returned.
+	// It is valid to pass a nil valbuf.
+	// Value may panic if Advance returned false or was not called at all.
+	// Value does not block.
+	Value(valbuf []byte) []byte
+
+	// Err returns a non-nil error iff the stream encountered any errors. Err does
+	// not block.
+	Err() error
+
+	// Cancel notifies the stream provider that it can stop producing elements.
+	// The client must call Cancel if it does not iterate through all elements
+	// (i.e. until Advance returns false). Cancel is idempotent and can be called
+	// concurrently with a goroutine that is iterating via Advance/Key/Value.
+	// Cancel causes Advance to subsequently return false. Cancel does not block.
+	Cancel()
+}
diff --git a/services/syncbase/store/model.vdl b/services/syncbase/store/model.vdl
new file mode 100644
index 0000000..6a56e66
--- /dev/null
+++ b/services/syncbase/store/model.vdl
@@ -0,0 +1,14 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package store
+
+error (
+	// ConcurrentTransaction means that the current transaction failed to commit
+	// because its read set was invalidated by some other transaction.
+	ConcurrentTransaction() {"en":"Concurrent transaction{:_}"}
+
+	// UnknownKey means the given key does not exist in the store.
+	UnknownKey() {"en":"Unknown key{:_}"}
+)
diff --git a/services/syncbase/store/model.vdl.go b/services/syncbase/store/model.vdl.go
new file mode 100644
index 0000000..eec8747
--- /dev/null
+++ b/services/syncbase/store/model.vdl.go
@@ -0,0 +1,38 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file was auto-generated by the vanadium vdl tool.
+// Source: model.vdl
+
+package store
+
+import (
+	// VDL system imports
+	"v.io/v23/context"
+	"v.io/v23/i18n"
+	"v.io/v23/verror"
+)
+
+var (
+	// ConcurrentTransaction means that the current transaction failed to commit
+	// because its read set was invalidated by some other transaction.
+	ErrConcurrentTransaction = verror.Register("v.io/syncbase/x/ref/services/syncbase/store.ConcurrentTransaction", verror.NoRetry, "{1:}{2:} Concurrent transaction{:_}")
+	// UnknownKey means the given key does not exist in the store.
+	ErrUnknownKey = verror.Register("v.io/syncbase/x/ref/services/syncbase/store.UnknownKey", verror.NoRetry, "{1:}{2:} Unknown key{:_}")
+)
+
+func init() {
+	i18n.Cat().SetWithBase(i18n.LangID("en"), i18n.MsgID(ErrConcurrentTransaction.ID), "{1:}{2:} Concurrent transaction{:_}")
+	i18n.Cat().SetWithBase(i18n.LangID("en"), i18n.MsgID(ErrUnknownKey.ID), "{1:}{2:} Unknown key{:_}")
+}
+
+// NewErrConcurrentTransaction returns an error with the ErrConcurrentTransaction ID.
+func NewErrConcurrentTransaction(ctx *context.T) error {
+	return verror.New(ErrConcurrentTransaction, ctx)
+}
+
+// NewErrUnknownKey returns an error with the ErrUnknownKey ID.
+func NewErrUnknownKey(ctx *context.T) error {
+	return verror.New(ErrUnknownKey, ctx)
+}
diff --git a/services/syncbase/store/resource_node.go b/services/syncbase/store/resource_node.go
new file mode 100644
index 0000000..c7228b2
--- /dev/null
+++ b/services/syncbase/store/resource_node.go
@@ -0,0 +1,73 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package store
+
+import (
+	"sync"
+)
+
+// ResourceNode is a node in a dependency graph. This graph is used to ensure
+// that when a resource is freed, downstream resources are also freed. For
+// example, closing a store closes all downstream transactions, snapshots and
+// streams.
+type ResourceNode struct {
+	mu       sync.Mutex
+	parent   *ResourceNode
+	children map[*ResourceNode]func()
+}
+
+// NewResourceNode creates a new isolated node in the dependency graph.
+func NewResourceNode() *ResourceNode {
+	return &ResourceNode{
+		children: make(map[*ResourceNode]func()),
+	}
+}
+
+// AddChild adds a parent-child relation between this node and the provided
+// node. The provided function is called to close the child when this node is
+// closed.
+func (r *ResourceNode) AddChild(node *ResourceNode, closefn func()) {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	if r.children == nil {
+		panic("already closed")
+	}
+	node.parent = r
+	r.children[node] = closefn
+}
+
+// removeChild removes the parent-child relation between this node and the
+// provided node, enabling Go's garbage collector to free the resources
+// associated with the child node if there are no more references to it.
+func (r *ResourceNode) removeChild(node *ResourceNode) {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	if r.children == nil {
+		// Already closed.
+		return
+	}
+	delete(r.children, node)
+}
+
+// Close closes this node and detaches it from its parent. All of this node's
+// children are closed using close functions provided to AddChild.
+func (r *ResourceNode) Close() {
+	r.mu.Lock()
+	if r.parent != nil {
+		// If there is a node V with parent P and we decide to explicitly close V,
+		// then we need to remove V from P's children list so that we don't close
+		// V again when P is closed.
+		r.parent.removeChild(r)
+		r.parent = nil
+	}
+	// Copy the children map to a local variable so that the removeChild step
+	// executed from children won't affect the map while we iterate through it.
+	children := r.children
+	r.children = nil
+	r.mu.Unlock()
+	for _, closefn := range children {
+		closefn()
+	}
+}
diff --git a/services/syncbase/store/test/snapshot.go b/services/syncbase/store/test/snapshot.go
new file mode 100644
index 0000000..04dee18
--- /dev/null
+++ b/services/syncbase/store/test/snapshot.go
@@ -0,0 +1,42 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package test
+
+import (
+	"testing"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/verror"
+)
+
+// RunSnapshotTest verifies store.Snapshot operations.
+func RunSnapshotTest(t *testing.T, st store.Store) {
+	key1, value1 := []byte("key1"), []byte("value1")
+	st.Put(key1, value1)
+	snapshot := st.NewSnapshot()
+	key2, value2 := []byte("key2"), []byte("value2")
+	st.Put(key2, value2)
+
+	// Test Get and Scan.
+	verifyGet(t, snapshot, key1, value1)
+	verifyGet(t, snapshot, key2, nil)
+	s := snapshot.Scan([]byte("a"), []byte("z"))
+	verifyAdvance(t, s, key1, value1)
+	verifyAdvance(t, s, nil, nil)
+
+	// Test functions after Abort.
+	if err := snapshot.Abort(); err != nil {
+		t.Fatalf("can't abort the snapshot: %v", err)
+	}
+	expectedErrMsg := store.ErrMsgAbortedSnapshot
+	verifyError(t, snapshot.Abort(), verror.ErrCanceled.ID, expectedErrMsg)
+
+	_, err := snapshot.Get(key1, nil)
+	verifyError(t, err, verror.ErrCanceled.ID, expectedErrMsg)
+
+	s = snapshot.Scan([]byte("a"), []byte("z"))
+	verifyAdvance(t, s, nil, nil)
+	verifyError(t, s.Err(), verror.ErrCanceled.ID, expectedErrMsg)
+}
diff --git a/services/syncbase/store/test/store.go b/services/syncbase/store/test/store.go
new file mode 100644
index 0000000..48022f9
--- /dev/null
+++ b/services/syncbase/store/test/store.go
@@ -0,0 +1,239 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package test
+
+import (
+	"fmt"
+	"math/rand"
+	"testing"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/verror"
+)
+
+type operation int
+
+const (
+	Put    operation = 0
+	Delete operation = 1
+)
+
+type testStep struct {
+	op  operation
+	key int
+}
+
+func randomBytes(rnd *rand.Rand, length int) []byte {
+	var res []byte
+	for i := 0; i < length; i++ {
+		res = append(res, '0'+byte(rnd.Intn(10)))
+	}
+	return res
+}
+
+// storeState is the in-memory representation of the store state.
+type storeState struct {
+	// We assume that the database has keys [0..size).
+	size     int
+	rnd      *rand.Rand
+	memtable map[string][]byte
+}
+
+func newStoreState(size int) *storeState {
+	return &storeState{
+		size,
+		rand.New(rand.NewSource(239017)),
+		make(map[string][]byte),
+	}
+}
+
+func (s *storeState) clone() *storeState {
+	other := &storeState{
+		s.size,
+		s.rnd,
+		make(map[string][]byte),
+	}
+	for k, v := range s.memtable {
+		other.memtable[k] = v
+	}
+	return other
+}
+
+// nextKey returns the smallest key in the store that is not less than the
+// provided key. If there is no such key, returns size.
+func (s *storeState) lowerBound(key int) int {
+	for key < s.size {
+		if _, ok := s.memtable[fmt.Sprintf("%05d", key)]; ok {
+			return key
+		}
+		key++
+	}
+	return key
+}
+
+// verify checks that various read operations on store.Store and memtable return
+// the same results.
+func (s *storeState) verify(t *testing.T, st store.StoreReader) {
+	// Verify Get().
+	for i := 0; i < s.size; i++ {
+		keystr := fmt.Sprintf("%05d", i)
+		answer, ok := s.memtable[keystr]
+		if ok {
+			verifyGet(t, st, []byte(keystr), answer)
+		} else {
+			verifyGet(t, st, []byte(keystr), nil)
+		}
+	}
+	// Verify 10 random Scan() calls.
+	for i := 0; i < 10; i++ {
+		start, limit := s.rnd.Intn(s.size), s.rnd.Intn(s.size)
+		if start > limit {
+			start, limit = limit, start
+		}
+		limit++
+		stream := st.Scan([]byte(fmt.Sprintf("%05d", start)), []byte(fmt.Sprintf("%05d", limit)))
+		for start = s.lowerBound(start); start < limit; start = s.lowerBound(start + 1) {
+			keystr := fmt.Sprintf("%05d", start)
+			verifyAdvance(t, stream, []byte(keystr), s.memtable[keystr])
+		}
+		verifyAdvance(t, stream, nil, nil)
+	}
+}
+
+// runReadWriteTest verifies read/write/snapshot operations.
+func runReadWriteTest(t *testing.T, st store.Store, size int, steps []testStep) {
+	s := newStoreState(size)
+	// We verify database state no more than ~100 times to prevent the test from
+	// being slow.
+	frequency := (len(steps) + 99) / 100
+	var states []*storeState
+	var snapshots []store.Snapshot
+	for i, step := range steps {
+		if step.key < 0 || step.key >= s.size {
+			t.Fatalf("invalid test step %v", step)
+		}
+		key := fmt.Sprintf("%05d", step.key)
+		switch step.op {
+		case Put:
+			value := randomBytes(s.rnd, 100)
+			s.memtable[key] = value
+			st.Put([]byte(key), value)
+		case Delete:
+			if _, ok := s.memtable[key]; ok {
+				delete(s.memtable, key)
+				st.Delete([]byte(key))
+			}
+		default:
+			t.Fatalf("invalid test step %v", step)
+		}
+		if i%frequency == 0 {
+			s.verify(t, st)
+			states = append(states, s.clone())
+			snapshots = append(snapshots, st.NewSnapshot())
+		}
+	}
+	s.verify(t, st)
+	for i := 0; i < len(states); i++ {
+		states[i].verify(t, snapshots[i])
+		snapshots[i].Abort()
+	}
+}
+
+// RunReadWriteBasicTest runs a basic test that verifies reads, writes and
+// snapshots.
+func RunReadWriteBasicTest(t *testing.T, st store.Store) {
+	runReadWriteTest(t, st, 3, []testStep{
+		testStep{Put, 1},
+		testStep{Put, 2},
+		testStep{Delete, 1},
+		testStep{Put, 1},
+		testStep{Put, 2},
+	})
+}
+
+// RunReadWriteRandomTest runs a random-generated test that verifies reads,
+// writes and snapshots.
+func RunReadWriteRandomTest(t *testing.T, st store.Store) {
+	rnd := rand.New(rand.NewSource(239017))
+	var steps []testStep
+	size := 50
+	for i := 0; i < 10000; i++ {
+		steps = append(steps, testStep{operation(rnd.Intn(2)), rnd.Intn(size)})
+	}
+	runReadWriteTest(t, st, size, steps)
+}
+
+// RunStoreStateTest verifies operations that modify the state of a store.Store.
+func RunStoreStateTest(t *testing.T, st store.Store) {
+	key1, value1 := []byte("key1"), []byte("value1")
+	st.Put(key1, value1)
+	key2 := []byte("key2")
+
+	// Test Get and Scan.
+	verifyGet(t, st, key1, value1)
+	verifyGet(t, st, key2, nil)
+	s := st.Scan([]byte("a"), []byte("z"))
+	verifyAdvance(t, s, key1, value1)
+	verifyAdvance(t, s, nil, nil)
+
+	// Test functions after Close.
+	if err := st.Close(); err != nil {
+		t.Fatalf("can't close the store: %v", err)
+	}
+	expectedErrMsg := store.ErrMsgClosedStore
+	verifyError(t, st.Close(), verror.ErrCanceled.ID, expectedErrMsg)
+
+	s = st.Scan([]byte("a"), []byte("z"))
+	verifyAdvance(t, s, nil, nil)
+	verifyError(t, s.Err(), verror.ErrCanceled.ID, expectedErrMsg)
+
+	snapshot := st.NewSnapshot()
+	_, err := snapshot.Get(key1, nil)
+	verifyError(t, err, verror.ErrCanceled.ID, expectedErrMsg)
+
+	tx := st.NewTransaction()
+	_, err = tx.Get(key1, nil)
+	verifyError(t, err, verror.ErrCanceled.ID, expectedErrMsg)
+
+	_, err = st.Get(key1, nil)
+	verifyError(t, err, verror.ErrCanceled.ID, expectedErrMsg)
+	verifyError(t, st.Put(key1, value1), verror.ErrCanceled.ID, expectedErrMsg)
+	verifyError(t, st.Delete(key1), verror.ErrCanceled.ID, expectedErrMsg)
+}
+
+// RunCloseTest verifies that child objects are closed when the parent object is
+// closed.
+func RunCloseTest(t *testing.T, st store.Store) {
+	key1, value1 := []byte("key1"), []byte("value1")
+	st.Put(key1, value1)
+
+	var streams []store.Stream
+	var snapshots []store.Snapshot
+	var transactions []store.Transaction
+	for i := 0; i < 10; i++ {
+		streams = append(streams, st.Scan([]byte("a"), []byte("z")))
+		snapshot := st.NewSnapshot()
+		tx := st.NewTransaction()
+		for j := 0; j < 10; j++ {
+			streams = append(streams, snapshot.Scan([]byte("a"), []byte("z")))
+			streams = append(streams, tx.Scan([]byte("a"), []byte("z")))
+		}
+		snapshots = append(snapshots, snapshot)
+		transactions = append(transactions, tx)
+	}
+	st.Close()
+
+	for _, stream := range streams {
+		verifyError(t, stream.Err(), verror.ErrCanceled.ID, store.ErrMsgCanceledStream)
+	}
+	for _, snapshot := range snapshots {
+		_, err := snapshot.Get(key1, nil)
+		verifyError(t, err, verror.ErrCanceled.ID, store.ErrMsgAbortedSnapshot)
+	}
+	for _, tx := range transactions {
+		_, err := tx.Get(key1, nil)
+		verifyError(t, err, verror.ErrCanceled.ID, store.ErrMsgAbortedTxn)
+	}
+}
diff --git a/services/syncbase/store/test/stream.go b/services/syncbase/store/test/stream.go
new file mode 100644
index 0000000..e058fc0
--- /dev/null
+++ b/services/syncbase/store/test/stream.go
@@ -0,0 +1,53 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package test
+
+import (
+	"bytes"
+	"testing"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/verror"
+)
+
+// RunStreamTest verifies store.Stream operations.
+func RunStreamTest(t *testing.T, st store.Store) {
+	// Test that advancing or canceling a stream that has reached its end
+	// doesn't cause a panic.
+	s := st.Scan([]byte("a"), []byte("z"))
+	verifyAdvance(t, s, nil, nil)
+	verifyAdvance(t, s, nil, nil)
+	if s.Err() != nil {
+		t.Fatalf("unexpected error: %v", s.Err())
+	}
+	s.Cancel()
+	if s.Err() != nil {
+		t.Fatalf("unexpected error: %v", s.Err())
+	}
+
+	key1, value1 := []byte("key1"), []byte("value1")
+	st.Put(key1, value1)
+	key2, value2 := []byte("key2"), []byte("value2")
+	st.Put(key2, value2)
+	key3, value3 := []byte("key3"), []byte("value3")
+	st.Put(key3, value3)
+	s = st.Scan([]byte("a"), []byte("z"))
+	verifyAdvance(t, s, key1, value1)
+	if !s.Advance() {
+		t.Fatalf("can't advance the stream")
+	}
+	s.Cancel()
+	for i := 0; i < 2; i++ {
+		var key, value []byte
+		if key = s.Key(key); !bytes.Equal(key, key2) {
+			t.Fatalf("unexpected key: got %q, want %q", key, key2)
+		}
+		if value = s.Value(value); !bytes.Equal(value, value2) {
+			t.Fatalf("unexpected value: got %q, want %q", value, value2)
+		}
+	}
+	verifyAdvance(t, s, nil, nil)
+	verifyError(t, s.Err(), verror.ErrCanceled.ID, store.ErrMsgCanceledStream)
+}
diff --git a/services/syncbase/store/test/transaction.go b/services/syncbase/store/test/transaction.go
new file mode 100644
index 0000000..6cf26e8
--- /dev/null
+++ b/services/syncbase/store/test/transaction.go
@@ -0,0 +1,216 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package test
+
+import (
+	"bytes"
+	"fmt"
+	"math/rand"
+	"strconv"
+	"sync"
+	"testing"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/verror"
+)
+
+// RunTransactionStateTest verifies operations that modify the state of a
+// store.Transaction.
+func RunTransactionStateTest(t *testing.T, st store.Store) {
+	finalizeFns := []func(t *testing.T, tx store.Transaction) (verror.ID, string){
+		func(t *testing.T, tx store.Transaction) (verror.ID, string) {
+			if err := tx.Abort(); err != nil {
+				Fatalf(t, "can't abort the transaction: %v", err)
+			}
+			return verror.ErrCanceled.ID, store.ErrMsgAbortedTxn
+		},
+		func(t *testing.T, tx store.Transaction) (verror.ID, string) {
+			if err := tx.Commit(); err != nil {
+				Fatalf(t, "can't commit the transaction: %v", err)
+			}
+			return verror.ErrBadState.ID, store.ErrMsgCommittedTxn
+		},
+	}
+	for _, fn := range finalizeFns {
+		key1, value1 := []byte("key1"), []byte("value1")
+		st.Put(key1, value1)
+		key2 := []byte("key2")
+		tx := st.NewTransaction()
+
+		// Test Get and Scan.
+		verifyGet(t, tx, key1, value1)
+		verifyGet(t, tx, key2, nil)
+		s := tx.Scan([]byte("a"), []byte("z"))
+		verifyAdvance(t, s, key1, value1)
+		verifyAdvance(t, s, nil, nil)
+
+		// Test Put then Get & Scan inside the transaction.
+		key3, value3 := []byte("key3"), []byte("value3")
+		tx.Put(key3, value3)
+		verifyGet(t, tx, key3, value3)
+		s = tx.Scan([]byte("a"), []byte("z"))
+		verifyAdvance(t, s, key1, value1)
+		verifyAdvance(t, s, key3, value3)
+		verifyAdvance(t, s, nil, nil)
+
+		// Test Delete of old key then Get inside the transaction.
+		tx.Delete(key1)
+		verifyGet(t, tx, key1, nil)
+
+		// Test Delete of new key then Get inside the transaction.
+		tx.Delete(key3)
+		verifyGet(t, tx, key3, nil)
+
+		// Test functions after finalize.
+		expectedID, expectedErrMsg := fn(t, tx)
+		verifyError(t, tx.Abort(), expectedID, expectedErrMsg)
+		verifyError(t, tx.Commit(), expectedID, expectedErrMsg)
+
+		s = tx.Scan([]byte("a"), []byte("z"))
+		verifyAdvance(t, s, nil, nil)
+		verifyError(t, s.Err(), expectedID, expectedErrMsg)
+
+		_, err := tx.Get(key1, nil)
+		verifyError(t, err, expectedID, expectedErrMsg)
+		verifyError(t, tx.Put(key1, value1), expectedID, expectedErrMsg)
+		verifyError(t, tx.Delete(key1), expectedID, expectedErrMsg)
+	}
+}
+
+// RunConcurrentTransactionsTest verifies that concurrent transactions
+// invalidate each other as expected.
+func RunConcurrentTransactionsTest(t *testing.T, st store.Store) {
+	st.Put([]byte("a"), []byte("0"))
+	st.Put([]byte("b"), []byte("0"))
+	st.Put([]byte("c"), []byte("0"))
+	// Test Get fails.
+	txA := st.NewTransaction()
+	txB := st.NewTransaction()
+	txA.Get([]byte("a"), nil)
+	txB.Get([]byte("a"), nil)
+	txA.Put([]byte("a"), []byte("a"))
+	txB.Put([]byte("a"), []byte("b"))
+	if err := txA.Commit(); err != nil {
+		t.Fatalf("can't commit the transaction: %v", err)
+	}
+	if err := txB.Commit(); verror.ErrorID(err) != store.ErrConcurrentTransaction.ID {
+		t.Fatalf("unexpected commit error: %v", err)
+	}
+	if value, _ := st.Get([]byte("a"), nil); !bytes.Equal(value, []byte("a")) {
+		t.Fatalf("unexpected value: got %q, want %q", value, "a")
+	}
+	// Test Scan fails.
+	txA = st.NewTransaction()
+	txB = st.NewTransaction()
+	txA.Scan([]byte("a"), []byte("z"))
+	txB.Scan([]byte("a"), []byte("z"))
+	txA.Put([]byte("aa"), []byte("a"))
+	txB.Put([]byte("bb"), []byte("b"))
+	if err := txA.Commit(); err != nil {
+		t.Fatalf("can't commit the transaction: %v", err)
+	}
+	if err := txB.Commit(); verror.ErrorID(err) != store.ErrConcurrentTransaction.ID {
+		t.Fatalf("unexpected commit error: %v", err)
+	}
+	if value, _ := st.Get([]byte("aa"), nil); !bytes.Equal(value, []byte("a")) {
+		t.Fatalf("unexpected value: got %q, want %q", value, "a")
+	}
+	// Test Get and Scan OK.
+	txA = st.NewTransaction()
+	txB = st.NewTransaction()
+	txA.Scan([]byte("a"), []byte("b"))
+	txB.Scan([]byte("b"), []byte("c"))
+	txA.Get([]byte("c"), nil)
+	txB.Get([]byte("c"), nil)
+	txA.Put([]byte("a"), []byte("a"))
+	txB.Put([]byte("b"), []byte("b"))
+	if err := txA.Commit(); err != nil {
+		t.Fatalf("can't commit the transaction: %v", err)
+	}
+	if err := txB.Commit(); err != nil {
+		t.Fatalf("can't commit the transaction: %v", err)
+	}
+}
+
+// RunTransactionsWithGetTest tests transactions that use Put and Get
+// operations.
+// NOTE: consider setting GOMAXPROCS to something greater than 1.
+func RunTransactionsWithGetTest(t *testing.T, st store.Store) {
+	// Invariant: value mapped to n is sum of values of 0..n-1.
+	// Each of k transactions takes m distinct random values from 0..n-1, adds 1
+	// to each and m to value mapped to n.
+	// The correctness of sums is checked after all transactions have been
+	// committed.
+	n, m, k := 10, 3, 100
+	for i := 0; i <= n; i++ {
+		if err := st.Put([]byte(fmt.Sprintf("%05d", i)), []byte{'0'}); err != nil {
+			t.Fatalf("can't write to database")
+		}
+	}
+	var wg sync.WaitGroup
+	wg.Add(k)
+	for i := 0; i < k; i++ {
+		go func(idx int) {
+			rnd := rand.New(rand.NewSource(239017 * int64(idx)))
+			perm := rnd.Perm(n)
+			if err := store.RunInTransaction(st, func(tx store.Transaction) error {
+				for j := 0; j <= m; j++ {
+					var keystr string
+					if j < m {
+						keystr = fmt.Sprintf("%05d", perm[j])
+					} else {
+						keystr = fmt.Sprintf("%05d", n)
+					}
+					key := []byte(keystr)
+					val, err := tx.Get(key, nil)
+					if err != nil {
+						return fmt.Errorf("can't get key %q: %v", key, err)
+					}
+					intValue, err := strconv.ParseInt(string(val), 10, 64)
+					if err != nil {
+						return fmt.Errorf("can't parse int from %q: %v", val, err)
+					}
+					var newValue int64
+					if j < m {
+						newValue = intValue + 1
+					} else {
+						newValue = intValue + int64(m)
+					}
+					if err := tx.Put(key, []byte(fmt.Sprintf("%d", newValue))); err != nil {
+						return fmt.Errorf("can't put {%q: %v}: %v", key, newValue, err)
+					}
+				}
+				return nil
+			}); err != nil {
+				panic(fmt.Errorf("can't commit transaction: %v", err))
+			}
+			wg.Done()
+		}(i)
+	}
+	wg.Wait()
+	var sum int64
+	for j := 0; j <= n; j++ {
+		keystr := fmt.Sprintf("%05d", j)
+		key := []byte(keystr)
+		val, err := st.Get(key, nil)
+		if err != nil {
+			t.Fatalf("can't get key %q: %v", key, err)
+		}
+		intValue, err := strconv.ParseInt(string(val), 10, 64)
+		if err != nil {
+			t.Fatalf("can't parse int from %q: %v", val, err)
+		}
+		if j < n {
+			sum += intValue
+		} else {
+			if intValue != int64(m*k) {
+				t.Fatalf("invalid sum value in the database: got %d, want %d", intValue, m*k)
+			}
+		}
+	}
+	if sum != int64(m*k) {
+		t.Fatalf("invalid sum of values in the database: got %d, want %d", sum, m*k)
+	}
+}
diff --git a/services/syncbase/store/test/util.go b/services/syncbase/store/test/util.go
new file mode 100644
index 0000000..55b886a
--- /dev/null
+++ b/services/syncbase/store/test/util.go
@@ -0,0 +1,79 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package test
+
+import (
+	"bytes"
+	"runtime/debug"
+	"strings"
+	"testing"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/verror"
+)
+
+// verifyGet verifies that st.Get(key) == value. If value is nil, verifies that
+// the key is not found.
+func verifyGet(t *testing.T, st store.StoreReader, key, value []byte) {
+	valbuf := []byte("tmp")
+	var err error
+	if value != nil {
+		if valbuf, err = st.Get(key, valbuf); err != nil {
+			Fatalf(t, "can't get value of %q: %v", key, err)
+		}
+		if !bytes.Equal(valbuf, value) {
+			Fatalf(t, "unexpected value: got %q, want %q", valbuf, value)
+		}
+	} else {
+		valbuf, err = st.Get(key, valbuf)
+		verifyError(t, err, store.ErrUnknownKey.ID, string(key))
+		valcopy := []byte("tmp")
+		// Verify that valbuf is not modified if the key is not found.
+		if !bytes.Equal(valbuf, valcopy) {
+			Fatalf(t, "unexpected value: got %q, want %q", valbuf, valcopy)
+		}
+	}
+}
+
+// verifyGet verifies the next key/value pair of the provided stream.
+// If key is nil, verifies that next Advance call on the stream returns false.
+func verifyAdvance(t *testing.T, s store.Stream, key, value []byte) {
+	ok := s.Advance()
+	if key == nil {
+		if ok {
+			Fatalf(t, "advance returned true unexpectedly")
+		}
+		return
+	}
+	if !ok {
+		Fatalf(t, "can't advance the stream")
+	}
+	var k, v []byte
+	for i := 0; i < 2; i++ {
+		if k = s.Key(k); !bytes.Equal(k, key) {
+			Fatalf(t, "unexpected key: got %q, want %q", k, key)
+		}
+		if v = s.Value(v); !bytes.Equal(v, value) {
+			Fatalf(t, "unexpected value: got %q, want %q", v, value)
+		}
+	}
+}
+
+// verifyError verifies that the given error has the given errorID and that the
+// error string contains the given substr. Pass an empty substr to skip the
+// substr check.
+func verifyError(t *testing.T, err error, errorID verror.ID, substr string) {
+	if got := verror.ErrorID(err); got != errorID {
+		Fatalf(t, "unexpected error ID: got %v, want %v", got, errorID)
+	}
+	if !strings.Contains(err.Error(), substr) {
+		Fatalf(t, "unexpected error: %q not found in %q", substr, err)
+	}
+}
+
+func Fatalf(t *testing.T, format string, args ...interface{}) {
+	debug.PrintStack()
+	t.Fatalf(format, args...)
+}
diff --git a/services/syncbase/store/transactions/manager.go b/services/syncbase/store/transactions/manager.go
new file mode 100644
index 0000000..254812f
--- /dev/null
+++ b/services/syncbase/store/transactions/manager.go
@@ -0,0 +1,194 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package transactions
+
+import (
+	"container/list"
+	"sync"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/verror"
+)
+
+// BatchStore is a CRUD-capable storage engine that supports atomic batch
+// writes. BatchStore doesn't support transactions.
+// This interface is a Go version of the C++ LevelDB interface. It serves as
+// an intermediate interface between store.Store and the LevelDB interface.
+type BatchStore interface {
+	store.StoreReader
+
+	// WriteBatch atomically writes a list of write operations to the database.
+	WriteBatch(batch ...WriteOp) error
+
+	// Close closes the store.
+	Close() error
+
+	// NewSnapshot creates a snapshot.
+	NewSnapshot() store.Snapshot
+}
+
+// manager handles transaction-related operations of the store.
+type manager struct {
+	BatchStore
+	// mu protects the variables below, and is also held during transaction
+	// commits. It must always be acquired before the store-level lock.
+	mu sync.Mutex
+	// events is a queue of create/commit transaction events.
+	events *list.List
+	seq    uint64
+	// txTable is a set of keys written by recent transactions. This set
+	// includes all write sets of transactions committed after the oldest living
+	// (in-flight) transaction.
+	txTable *trie
+}
+
+// commitedTransaction is only used as an element of manager.events.
+type commitedTransaction struct {
+	seq   uint64
+	batch [][]byte
+}
+
+// Wrap wraps the BatchStore with transaction functionality.
+func Wrap(bs BatchStore) store.Store {
+	return &manager{
+		BatchStore: bs,
+		events:     list.New(),
+		txTable:    newTrie(),
+	}
+}
+
+// Close implements the store.Store interface.
+func (mg *manager) Close() error {
+	mg.mu.Lock()
+	defer mg.mu.Unlock()
+	if mg.txTable == nil {
+		return verror.New(verror.ErrCanceled, nil, store.ErrMsgClosedStore)
+	}
+	mg.BatchStore.Close()
+	for event := mg.events.Front(); event != nil; event = event.Next() {
+		if tx, ok := event.Value.(*transaction); ok {
+			// tx.Abort() internally removes tx from the mg.events list under
+			// the mg.mu lock. To brake the cyclic dependency, we set tx.event
+			// to nil.
+			tx.mu.Lock()
+			tx.event = nil
+			tx.mu.Unlock()
+			tx.Abort()
+		}
+	}
+	mg.events = nil
+	mg.txTable = nil
+	return nil
+}
+
+// NewTransaction implements the store.Store interface.
+func (mg *manager) NewTransaction() store.Transaction {
+	mg.mu.Lock()
+	defer mg.mu.Unlock()
+	if mg.txTable == nil {
+		return &store.InvalidTransaction{
+			Error: verror.New(verror.ErrCanceled, nil, store.ErrMsgClosedStore),
+		}
+	}
+	return newTransaction(mg)
+}
+
+// Put implements the store.StoreWriter interface.
+func (mg *manager) Put(key, value []byte) error {
+	mg.mu.Lock()
+	defer mg.mu.Unlock()
+	if mg.txTable == nil {
+		return verror.New(verror.ErrCanceled, nil, store.ErrMsgClosedStore)
+	}
+	write := WriteOp{
+		T:     PutOp,
+		Key:   key,
+		Value: value,
+	}
+	if err := mg.BatchStore.WriteBatch(write); err != nil {
+		return err
+	}
+	mg.trackBatch(write)
+	return nil
+}
+
+// Delete implements the store.StoreWriter interface.
+func (mg *manager) Delete(key []byte) error {
+	mg.mu.Lock()
+	defer mg.mu.Unlock()
+	if mg.txTable == nil {
+		return verror.New(verror.ErrCanceled, nil, store.ErrMsgClosedStore)
+	}
+	write := WriteOp{
+		T:   DeleteOp,
+		Key: key,
+	}
+	if err := mg.BatchStore.WriteBatch(write); err != nil {
+		return err
+	}
+	mg.trackBatch(write)
+	return nil
+}
+
+// trackBatch writes the batch to txTable and adds a commit event to
+// the events queue.
+// Assumes mu is held.
+func (mg *manager) trackBatch(batch ...WriteOp) {
+	if mg.events.Len() == 0 {
+		return
+	}
+	// TODO(rogulenko): do GC.
+	mg.seq++
+	var keys [][]byte
+	for _, write := range batch {
+		mg.txTable.add(write.Key, mg.seq)
+		keys = append(keys, write.Key)
+	}
+	tx := &commitedTransaction{
+		seq:   mg.seq,
+		batch: keys,
+	}
+	mg.events.PushBack(tx)
+}
+
+//////////////////////////////////////////////////////////////
+// Read and Write types used for storing transcation reads
+// and uncommitted writes.
+
+type WriteType int
+
+const (
+	PutOp WriteType = iota
+	DeleteOp
+)
+
+type WriteOp struct {
+	T     WriteType
+	Key   []byte
+	Value []byte
+}
+
+type scanRange struct {
+	Start, Limit []byte
+}
+
+type readSet struct {
+	Keys   [][]byte
+	Ranges []scanRange
+}
+
+type writeOpArray []WriteOp
+
+func (a writeOpArray) Len() int {
+	return len(a)
+}
+
+func (a writeOpArray) Less(i, j int) bool {
+	return string(a[i].Key) < string(a[j].Key)
+}
+
+func (a writeOpArray) Swap(i, j int) {
+	a[i], a[j] = a[j], a[i]
+}
diff --git a/services/syncbase/store/transactions/merged_stream.go b/services/syncbase/store/transactions/merged_stream.go
new file mode 100644
index 0000000..4ab10be
--- /dev/null
+++ b/services/syncbase/store/transactions/merged_stream.go
@@ -0,0 +1,149 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package transactions
+
+import (
+	"sort"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+)
+
+//////////////////////////////////////////////////////////////
+// mergedStream implementation of Stream
+//
+// This implementation of Stream must take into account writes
+// which have occurred since the snapshot was taken on the
+// transaction.
+//
+// The MergeWritesWithStream() function requires uncommitted
+// changes to be passed in as an array of WriteOp.
+
+// Create a new stream which merges a snapshot stream with an array of write operations.
+func mergeWritesWithStream(sn store.Snapshot, w []WriteOp, start, limit []byte) store.Stream {
+	// Collect writes with the range specified, then sort them.
+	// Note: Writes could contain more than one write for a given key.
+	//       The last write is the current state.
+	writesMap := map[string]WriteOp{}
+	for _, write := range w {
+		if string(write.Key) >= string(start) && (string(limit) == "" || string(write.Key) < string(limit)) {
+			writesMap[string(write.Key)] = write
+		}
+	}
+	var writesArray writeOpArray
+	for _, writeOp := range writesMap {
+		writesArray = append(writesArray, writeOp)
+	}
+	sort.Sort(writesArray)
+	return &mergedStream{
+		snapshotStream:      sn.Scan(start, limit),
+		writesArray:         writesArray,
+		writesCursor:        0,
+		unusedSnapshotValue: false,
+		snapshotStreamEOF:   false,
+		hasValue:            false,
+	}
+}
+
+type mergedStream struct {
+	snapshotStream      store.Stream
+	writesArray         []WriteOp
+	writesCursor        int
+	unusedSnapshotValue bool
+	snapshotStreamEOF   bool
+	hasValue            bool // if true, Key() and Value() can be called
+	key                 []byte
+	value               []byte
+}
+
+// Convenience function to check EOF on writesArray
+func (s *mergedStream) writesArrayEOF() bool {
+	return s.writesCursor >= len(s.writesArray)
+}
+
+// If a kv from the snapshot isn't on deck, call
+// Advance on the snapshot and set unusedSnapshotValue.
+// If EOF encountered, set snapshotStreamEOF.
+// If error encountered, return it.
+func (s *mergedStream) stageSnapshotKeyValue() error {
+	if !s.snapshotStreamEOF && !s.unusedSnapshotValue {
+		if !s.snapshotStream.Advance() {
+			s.snapshotStreamEOF = true
+			if err := s.snapshotStream.Err(); err != nil {
+				return err
+			}
+		}
+		s.unusedSnapshotValue = true
+	}
+	return nil
+}
+
+// Pick a kv from either the snapshot or the uncommited writes array.
+// If an uncommited write is picked advance past it and return false (also, advance the snapshot
+// stream if its current key is equal to the ucommitted delete).
+func (s *mergedStream) pickKeyValue() bool {
+	if !s.snapshotStreamEOF && (s.writesArrayEOF() || string(s.writesArray[s.writesCursor].Key) > string(s.snapshotStream.Key(nil))) {
+		s.key = s.snapshotStream.Key(s.key)
+		s.value = s.snapshotStream.Value(s.value)
+		s.unusedSnapshotValue = false
+		return true
+	}
+	if !s.snapshotStreamEOF && string(s.writesArray[s.writesCursor].Key) == string(s.snapshotStream.Key(nil)) {
+		s.unusedSnapshotValue = false
+	}
+	if s.writesArrayEOF() || s.writesArray[s.writesCursor].T == DeleteOp {
+		s.writesCursor++
+		return false
+	}
+	s.key = store.CopyBytes(s.key, s.writesArray[s.writesCursor].Key)
+	s.value = store.CopyBytes(s.value, s.writesArray[s.writesCursor].Value)
+	s.writesCursor++
+	return true
+}
+
+func (s *mergedStream) Advance() bool {
+	s.hasValue = false
+	for true {
+		if err := s.stageSnapshotKeyValue(); err != nil {
+			return false
+		}
+		if s.snapshotStreamEOF && s.writesArrayEOF() {
+			return false
+		}
+		if s.pickKeyValue() {
+			s.hasValue = true
+			return true
+		}
+	}
+	return false // compiler insists on this line
+}
+
+// Key implements the Stream interface.
+func (s *mergedStream) Key(keybuf []byte) []byte {
+	if !s.hasValue {
+		panic("nothing staged")
+	}
+	return store.CopyBytes(keybuf, s.key)
+}
+
+// Value implements the Stream interface.
+func (s *mergedStream) Value(valbuf []byte) []byte {
+	if !s.hasValue {
+		panic("nothing staged")
+	}
+	return store.CopyBytes(valbuf, s.value)
+}
+
+// Err implements the Stream interface.
+func (s *mergedStream) Err() error {
+	return s.snapshotStream.Err()
+}
+
+// Cancel implements the Stream interface.
+func (s *mergedStream) Cancel() {
+	s.snapshotStream.Cancel()
+	s.hasValue = false
+	s.snapshotStreamEOF = true
+	s.writesCursor = len(s.writesArray)
+}
diff --git a/services/syncbase/store/transactions/transaction.go b/services/syncbase/store/transactions/transaction.go
new file mode 100644
index 0000000..dcf7569
--- /dev/null
+++ b/services/syncbase/store/transactions/transaction.go
@@ -0,0 +1,193 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package transactions
+
+import (
+	"bytes"
+	"container/list"
+	"sync"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/verror"
+	"v.io/x/lib/vlog"
+)
+
+// transaction is a wrapper on top of a BatchWriter and a store.Snapshot that
+// implements the store.Transaction interface.
+type transaction struct {
+	// mu protects the state of the transaction.
+	mu       sync.Mutex
+	mg       *manager
+	seq      uint64
+	event    *list.Element // pointer to element of db.txEvents
+	snapshot store.Snapshot
+	reads    readSet
+	writes   []WriteOp
+	err      error
+}
+
+var _ store.Transaction = (*transaction)(nil)
+
+func newTransaction(mg *manager) *transaction {
+	tx := &transaction{
+		mg:       mg,
+		snapshot: mg.BatchStore.NewSnapshot(),
+		seq:      mg.seq,
+	}
+	tx.event = mg.events.PushFront(tx)
+	return tx
+}
+
+// close removes this transaction from the mg.events queue and aborts
+// the underlying snapshot.
+// Assumes mu is held.
+func (tx *transaction) close() {
+	tx.removeEvent()
+	tx.snapshot.Abort()
+}
+
+// removeEvent removes this transaction from the mg.events queue.
+// Assumes mu is held.
+func (tx *transaction) removeEvent() {
+	// This can happen if the transaction was committed, since Commit()
+	// explicitly calls removeEvent().
+	if tx.event == nil {
+		return
+	}
+	tx.mg.mu.Lock()
+	tx.mg.events.Remove(tx.event)
+	tx.mg.mu.Unlock()
+	tx.event = nil
+}
+
+// Get implements the store.StoreReader interface.
+func (tx *transaction) Get(key, valbuf []byte) ([]byte, error) {
+	tx.mu.Lock()
+	defer tx.mu.Unlock()
+	if tx.err != nil {
+		return valbuf, store.ConvertError(tx.err)
+	}
+	tx.reads.Keys = append(tx.reads.Keys, key)
+
+	// Reflect the state of the transaction: the "writes" (puts and
+	// deletes) override the values in the transaction snapshot.
+	// Find the last "writes" entry for this key, if one exists.
+	// Note: this step could be optimized by using maps (puts and
+	// deletes) instead of an array.
+	for i := len(tx.writes) - 1; i >= 0; i-- {
+		op := &tx.writes[i]
+		if bytes.Equal(op.Key, key) {
+			if op.T == PutOp {
+				return op.Value, nil
+			}
+			return valbuf, verror.New(store.ErrUnknownKey, nil, string(key))
+		}
+	}
+
+	return tx.snapshot.Get(key, valbuf)
+}
+
+// Scan implements the store.StoreReader interface.
+func (tx *transaction) Scan(start, limit []byte) store.Stream {
+	tx.mu.Lock()
+	defer tx.mu.Unlock()
+	if tx.err != nil {
+		return &store.InvalidStream{Error: tx.err}
+	}
+
+	tx.reads.Ranges = append(tx.reads.Ranges, scanRange{
+		Start: start,
+		Limit: limit,
+	})
+
+	// Return a stream which merges the snaphot stream with the uncommitted changes.
+	return mergeWritesWithStream(tx.snapshot, tx.writes, start, limit)
+}
+
+// Put implements the store.StoreWriter interface.
+func (tx *transaction) Put(key, value []byte) error {
+	tx.mu.Lock()
+	defer tx.mu.Unlock()
+	if tx.err != nil {
+		return store.ConvertError(tx.err)
+	}
+	tx.writes = append(tx.writes, WriteOp{
+		T:     PutOp,
+		Key:   key,
+		Value: value,
+	})
+	return nil
+}
+
+// Delete implements the store.StoreWriter interface.
+func (tx *transaction) Delete(key []byte) error {
+	tx.mu.Lock()
+	defer tx.mu.Unlock()
+	if tx.err != nil {
+		return store.ConvertError(tx.err)
+	}
+	tx.writes = append(tx.writes, WriteOp{
+		T:   DeleteOp,
+		Key: key,
+	})
+	return nil
+}
+
+// validateReadSet returns true iff the read set of this transaction has not
+// been invalidated by other transactions.
+// Assumes tx.mg.mu is held.
+func (tx *transaction) validateReadSet() bool {
+	for _, key := range tx.reads.Keys {
+		if tx.mg.txTable.get(key) > tx.seq {
+			vlog.VI(3).Infof("key conflict: %q", key)
+			return false
+		}
+	}
+	for _, r := range tx.reads.Ranges {
+		if tx.mg.txTable.rangeMax(r.Start, r.Limit) > tx.seq {
+			vlog.VI(3).Infof("range conflict: {%q, %q}", r.Start, r.Limit)
+			return false
+		}
+
+	}
+	return true
+}
+
+// Commit implements the store.Transaction interface.
+func (tx *transaction) Commit() error {
+	tx.mu.Lock()
+	defer tx.mu.Unlock()
+	if tx.err != nil {
+		return store.ConvertError(tx.err)
+	}
+	tx.err = verror.New(verror.ErrBadState, nil, store.ErrMsgCommittedTxn)
+	// Explicitly remove this transaction from the event queue. If this was the
+	// only active transaction, the event queue becomes empty and writeLocked will
+	// not add this transaction's write set to txTable.
+	tx.removeEvent()
+	defer tx.close()
+	tx.mg.mu.Lock()
+	defer tx.mg.mu.Unlock()
+	if !tx.validateReadSet() {
+		return store.NewErrConcurrentTransaction(nil)
+	}
+	if err := tx.mg.BatchStore.WriteBatch(tx.writes...); err != nil {
+		return err
+	}
+	tx.mg.trackBatch(tx.writes...)
+	return nil
+}
+
+// Abort implements the store.Transaction interface.
+func (tx *transaction) Abort() error {
+	tx.mu.Lock()
+	defer tx.mu.Unlock()
+	if tx.err != nil {
+		return store.ConvertError(tx.err)
+	}
+	tx.err = verror.New(verror.ErrCanceled, nil, store.ErrMsgAbortedTxn)
+	tx.close()
+	return nil
+}
diff --git a/services/syncbase/store/transactions/trie.go b/services/syncbase/store/transactions/trie.go
new file mode 100644
index 0000000..51a99a3
--- /dev/null
+++ b/services/syncbase/store/transactions/trie.go
@@ -0,0 +1,75 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package transactions
+
+import (
+	"fmt"
+)
+
+// trie is an in-memory data structure that keeps track of recently-written
+// keys, and exposes an interface for asking when a key or key range was most
+// recently written to. It is used to check whether the read set of a
+// transaction pending commit is still valid. The transaction can be committed
+// iff its read set is valid.
+// TODO(rogulenko): replace this dummy implementation with an actual trie.
+type trie struct {
+	seqs map[string]uint64
+}
+
+func newTrie() *trie {
+	return &trie{
+		seqs: make(map[string]uint64),
+	}
+}
+
+// add updates the given key to the given seq, which must be greater than the
+// current seq (if one exists). Seqs of subsequent calls must be in
+// ascending order.
+func (t *trie) add(key []byte, seq uint64) {
+	keystr := string(key)
+	if oldSeq, ok := t.seqs[keystr]; ok && seq < oldSeq {
+		panic(fmt.Sprintf("seq for key %q should be at least %d, but got %d", key, oldSeq, seq))
+	}
+	t.seqs[keystr] = seq
+}
+
+// remove reverts effect of add(key, seq).
+// Seqs of subsequent calls must be in ascending order.
+func (t *trie) remove(key []byte, seq uint64) {
+	keystr := string(key)
+	oldSeq, ok := t.seqs[keystr]
+	if !ok {
+		panic(fmt.Sprintf("key %q was not found", key))
+	}
+	if oldSeq > seq {
+		return
+	} else if oldSeq == seq {
+		delete(t.seqs, keystr)
+	} else {
+		panic(fmt.Sprintf("seq for key %q is too big: got %v, want %v", keystr, seq, oldSeq))
+	}
+}
+
+// get returns the seq associated with the given key.
+func (t *trie) get(key []byte) uint64 {
+	keystr := string(key)
+	if seq, ok := t.seqs[keystr]; ok {
+		return seq
+	}
+	return 0
+}
+
+// rangeMax returns the max seq associated with keys in range
+// [start, limit). Empty limit means no limit.
+func (t *trie) rangeMax(start, limit []byte) uint64 {
+	var result uint64 = 0
+	s, e := string(start), string(limit)
+	for key, seq := range t.seqs {
+		if key >= s && (e == "" || key < e) && seq > result {
+			result = seq
+		}
+	}
+	return result
+}
diff --git a/services/syncbase/store/util.go b/services/syncbase/store/util.go
new file mode 100644
index 0000000..19695ed
--- /dev/null
+++ b/services/syncbase/store/util.go
@@ -0,0 +1,61 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package store
+
+import (
+	"v.io/v23/verror"
+)
+
+type SnapshotSpecImpl struct{}
+
+func (s *SnapshotSpecImpl) __snapshotSpec() {}
+
+// RunInTransaction runs the given fn in a transaction, managing retries and
+// commit/abort.
+func RunInTransaction(st Store, fn func(tx Transaction) error) error {
+	// TODO(rogulenko): Make the number of attempts configurable.
+	// TODO(rogulenko): Change the default number of attempts to 3. Currently,
+	// some storage engine tests fail when the number of attempts is that low.
+	var err error
+	for i := 0; i < 100; i++ {
+		// TODO(sadovsky): Should NewTransaction return an error? If not, how will
+		// we deal with RPC errors when talking to remote storage engines? (Note,
+		// client-side BeginBatch returns an error.)
+		tx := st.NewTransaction()
+		if err = fn(tx); err != nil {
+			tx.Abort()
+			return err
+		}
+		// TODO(sadovsky): Commit() can fail for a number of reasons, e.g. RPC
+		// failure or ErrConcurrentTransaction. Depending on the cause of failure,
+		// it may be desirable to retry the Commit() and/or to call Abort().
+		if err = tx.Commit(); verror.ErrorID(err) != ErrConcurrentTransaction.ID {
+			return err
+		}
+	}
+	return err
+}
+
+// CopyBytes copies elements from a source slice into a destination slice.
+// The returned slice may be a sub-slice of dst if dst was large enough to hold
+// src. Otherwise, a newly allocated slice will be returned.
+// TODO(rogulenko): add some tests.
+func CopyBytes(dst, src []byte) []byte {
+	if cap(dst) < len(src) {
+		newlen := cap(dst)*2 + 2
+		if newlen < len(src) {
+			newlen = len(src)
+		}
+		dst = make([]byte, newlen)
+	}
+	dst = dst[:len(src)]
+	copy(dst, src)
+	return dst
+}
+
+// ConvertError returns a copy of the verror, appending the current stack to it.
+func ConvertError(err error) error {
+	return verror.Convert(verror.IDAction{}, nil, err)
+}
diff --git a/services/syncbase/syncbased/main.go b/services/syncbase/syncbased/main.go
new file mode 100644
index 0000000..8def540
--- /dev/null
+++ b/services/syncbase/syncbased/main.go
@@ -0,0 +1,81 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+	"flag"
+
+	"v.io/syncbase/x/ref/services/syncbase/server"
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/rpc"
+	"v.io/v23/security"
+	"v.io/v23/security/access"
+	"v.io/x/lib/vlog"
+	"v.io/x/ref/lib/security/securityflag"
+	_ "v.io/x/ref/runtime/factories/roaming"
+)
+
+var (
+	name    = flag.String("name", "", "Name to mount at.")
+	rootDir = flag.String("root-dir", "/var/lib/syncbase", "Root dir for storage engines and other data")
+	engine  = flag.String("engine", "leveldb", "Storage engine to use. Currently supported: memstore and leveldb.")
+)
+
+// defaultPerms returns a permissions object that grants all permissions to the
+// provided blessing patterns.
+func defaultPerms(blessingPatterns []security.BlessingPattern) access.Permissions {
+	perms := access.Permissions{}
+	for _, tag := range access.AllTypicalTags() {
+		for _, bp := range blessingPatterns {
+			perms.Add(bp, string(tag))
+		}
+	}
+	return perms
+}
+
+// TODO(sadovsky): We return rpc.Server and rpc.Dispatcher as a quick hack to
+// support Mojo.
+func Serve(ctx *context.T) (rpc.Server, rpc.Dispatcher) {
+	s, err := v23.NewServer(ctx)
+	if err != nil {
+		vlog.Fatal("v23.NewServer() failed: ", err)
+	}
+	if _, err := s.Listen(v23.GetListenSpec(ctx)); err != nil {
+		vlog.Fatal("s.Listen() failed: ", err)
+	}
+
+	perms, err := securityflag.PermissionsFromFlag()
+	if err != nil {
+		vlog.Fatal("securityflag.PermissionsFromFlag() failed: ", err)
+	}
+	if perms != nil {
+		vlog.Info("Using perms from command line flag.")
+	} else {
+		vlog.Info("Perms flag not set. Giving local principal all perms.")
+		perms = defaultPerms(security.DefaultBlessingPatterns(v23.GetPrincipal(ctx)))
+	}
+	vlog.Infof("Perms: %v", perms)
+	service, err := server.NewService(ctx, nil, server.ServiceOptions{
+		Perms:   perms,
+		RootDir: *rootDir,
+		Engine:  *engine,
+		Server:  s,
+	})
+	if err != nil {
+		vlog.Fatal("server.NewService() failed: ", err)
+	}
+	d := server.NewDispatcher(service)
+
+	// Publish the service in the mount table.
+	if err := s.ServeDispatcher(*name, d); err != nil {
+		vlog.Fatal("s.ServeDispatcher() failed: ", err)
+	}
+	if *name != "" {
+		vlog.Info("Mounted at: ", *name)
+	}
+
+	return s, d
+}
diff --git a/services/syncbase/syncbased/mojo_main.go b/services/syncbase/syncbased/mojo_main.go
new file mode 100644
index 0000000..74a247f
--- /dev/null
+++ b/services/syncbase/syncbased/mojo_main.go
@@ -0,0 +1,79 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build mojo
+
+package main
+
+// To build:
+// cd $V23_ROOT/experimental/projects/ether
+// make build
+
+import (
+	"log"
+
+	"mojo/public/go/application"
+	"mojo/public/go/bindings"
+	"mojo/public/go/system"
+
+	"mojom/syncbase"
+
+	"v.io/syncbase/x/ref/services/syncbase/server"
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/rpc"
+)
+
+//#include "mojo/public/c/system/types.h"
+import "C"
+
+type delegate struct {
+	ctx   *context.T
+	srv   rpc.Server
+	disp  rpc.Dispatcher
+	stubs []*bindings.Stub
+}
+
+func (d *delegate) Initialize(ctx application.Context) {
+	d.srv, d.disp = Serve(d.ctx)
+}
+
+func (d *delegate) Create(req syncbase.Syncbase_Request) {
+	impl := server.NewMojoImpl(d.ctx, d.srv, d.disp)
+	stub := syncbase.NewSyncbaseStub(req, impl, 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(&syncbase.Syncbase_ServiceFactory{d})
+}
+
+func (d *delegate) Quit() {
+	for _, stub := range d.stubs {
+		stub.Close()
+	}
+}
+
+//export MojoMain
+func MojoMain(handle C.MojoHandle) C.MojoResult {
+	ctx, shutdown := v23.Init()
+	defer shutdown()
+	application.Run(&delegate{ctx: ctx}, system.MojoHandle(handle))
+	return C.MOJO_RESULT_OK
+}
+
+// NOTE(nlacasse): Mojo runs Go code by calling MojoMain().  The main() method
+// below is still needed because the Go tool won't build without it.
+func main() {}
diff --git a/services/syncbase/syncbased/v23_main.go b/services/syncbase/syncbased/v23_main.go
new file mode 100644
index 0000000..1651dc1
--- /dev/null
+++ b/services/syncbase/syncbased/v23_main.go
@@ -0,0 +1,23 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build !mojo
+
+// syncbased is a syncbase daemon.
+package main
+
+// Example invocation:
+// syncbased --veyron.tcp.address="127.0.0.1:0" --name=syncbased
+
+import (
+	"v.io/v23"
+	"v.io/x/ref/lib/signals"
+)
+
+func main() {
+	ctx, shutdown := v23.Init()
+	defer shutdown()
+	Serve(ctx)
+	<-signals.ShutdownOnSignals(ctx)
+}
diff --git a/services/syncbase/vsync/blob.go b/services/syncbase/vsync/blob.go
new file mode 100644
index 0000000..ff04066
--- /dev/null
+++ b/services/syncbase/vsync/blob.go
@@ -0,0 +1,429 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+import (
+	"io"
+
+	wire "v.io/syncbase/v23/services/syncbase/nosql"
+	blob "v.io/syncbase/x/ref/services/syncbase/localblobstore"
+	"v.io/syncbase/x/ref/services/syncbase/server/interfaces"
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/v23/context"
+	"v.io/v23/naming"
+	"v.io/v23/rpc"
+	"v.io/v23/verror"
+	"v.io/x/lib/vlog"
+)
+
+const (
+	chunkSize = 8 * 1024
+)
+
+// blobLocInfo contains the location information about a BlobRef. This location
+// information is merely a hint used to search for the blob.
+type blobLocInfo struct {
+	peer   string                          // Syncbase from which the presence of this BlobRef was first learned.
+	source string                          // Syncbase that originated this blob.
+	sgIds  map[interfaces.GroupId]struct{} // SyncGroups through which the BlobRef was learned.
+}
+
+////////////////////////////////////////////////////////////
+// RPCs for managing blobs between Syncbase and its clients.
+
+func (sd *syncDatabase) CreateBlob(ctx *context.T, call rpc.ServerCall) (wire.BlobRef, error) {
+	vlog.VI(2).Infof("sync: CreateBlob: begin")
+	defer vlog.VI(2).Infof("sync: CreateBlob: end")
+
+	// Get this Syncbase's blob store handle.
+	ss := sd.sync.(*syncService)
+	bst := ss.bst
+
+	writer, err := bst.NewBlobWriter(ctx, "")
+	if err != nil {
+		return wire.NullBlobRef, err
+	}
+	defer writer.CloseWithoutFinalize()
+
+	name := writer.Name()
+	vlog.VI(4).Infof("sync: CreateBlob: blob ref %s", name)
+	return wire.BlobRef(name), nil
+}
+
+func (sd *syncDatabase) PutBlob(ctx *context.T, call wire.BlobManagerPutBlobServerCall, br wire.BlobRef) error {
+	vlog.VI(2).Infof("sync: PutBlob: begin br %v", br)
+	defer vlog.VI(2).Infof("sync: PutBlob: end br %v", br)
+
+	// Get this Syncbase's blob store handle.
+	ss := sd.sync.(*syncService)
+	bst := ss.bst
+
+	writer, err := bst.ResumeBlobWriter(ctx, string(br))
+	if err != nil {
+		return err
+	}
+	defer writer.CloseWithoutFinalize()
+
+	stream := call.RecvStream()
+	for stream.Advance() {
+		item := blob.BlockOrFile{Block: stream.Value()}
+		if err = writer.AppendFragment(item); err != nil {
+			return err
+		}
+	}
+	return stream.Err()
+}
+
+func (sd *syncDatabase) CommitBlob(ctx *context.T, call rpc.ServerCall, br wire.BlobRef) error {
+	vlog.VI(2).Infof("sync: CommitBlob: begin br %v", br)
+	defer vlog.VI(2).Infof("sync: CommitBlob: end br %v", br)
+
+	// Get this Syncbase's blob store handle.
+	ss := sd.sync.(*syncService)
+	bst := ss.bst
+
+	writer, err := bst.ResumeBlobWriter(ctx, string(br))
+	if err != nil {
+		return err
+	}
+	return writer.Close()
+}
+
+func (sd *syncDatabase) GetBlobSize(ctx *context.T, call rpc.ServerCall, br wire.BlobRef) (int64, error) {
+	vlog.VI(2).Infof("sync: GetBlobSize: begin br %v", br)
+	defer vlog.VI(2).Infof("sync: GetBlobSize: end br %v", br)
+
+	// Get this Syncbase's blob store handle.
+	ss := sd.sync.(*syncService)
+	bst := ss.bst
+
+	reader, err := bst.NewBlobReader(ctx, string(br))
+	if err != nil {
+		return 0, err
+	}
+	defer reader.Close()
+
+	return reader.Size(), nil
+}
+
+func (sd *syncDatabase) DeleteBlob(ctx *context.T, call rpc.ServerCall, br wire.BlobRef) error {
+	return verror.NewErrNotImplemented(ctx)
+}
+
+func (sd *syncDatabase) GetBlob(ctx *context.T, call wire.BlobManagerGetBlobServerCall, br wire.BlobRef, offset int64) error {
+	vlog.VI(2).Infof("sync: GetBlob: begin br %v", br)
+	defer vlog.VI(2).Infof("sync: GetBlob: end br %v", br)
+
+	// First get the blob locally if available.
+	ss := sd.sync.(*syncService)
+	err := getLocalBlob(ctx, call.SendStream(), ss.bst, br, offset)
+	if err == nil || verror.ErrorID(err) == wire.ErrBlobNotCommitted.ID {
+		return err
+	}
+
+	return sd.fetchBlobRemote(ctx, br, nil, call, offset)
+}
+
+func (sd *syncDatabase) FetchBlob(ctx *context.T, call wire.BlobManagerFetchBlobServerCall, br wire.BlobRef, priority uint64) error {
+	vlog.VI(2).Infof("sync: FetchBlob: begin br %v", br)
+	defer vlog.VI(2).Infof("sync: FetchBlob: end br %v", br)
+
+	clientStream := call.SendStream()
+
+	// Check if BlobRef already exists locally.
+	ss := sd.sync.(*syncService)
+	bst := ss.bst
+
+	bReader, err := bst.NewBlobReader(ctx, string(br))
+	if err == nil {
+		finalized := bReader.IsFinalized()
+		bReader.Close()
+
+		if !finalized {
+			return wire.NewErrBlobNotCommitted(ctx)
+		}
+		clientStream.Send(wire.BlobFetchStatus{State: wire.BlobFetchStateDone})
+		return nil
+	}
+
+	// Wait for this blob's turn.
+	// TODO(hpucha): Implement a blob queue.
+	clientStream.Send(wire.BlobFetchStatus{State: wire.BlobFetchStatePending})
+
+	return sd.fetchBlobRemote(ctx, br, call, nil, 0)
+}
+
+func (sd *syncDatabase) PinBlob(ctx *context.T, call rpc.ServerCall, br wire.BlobRef) error {
+	return verror.NewErrNotImplemented(ctx)
+}
+
+func (sd *syncDatabase) UnpinBlob(ctx *context.T, call rpc.ServerCall, br wire.BlobRef) error {
+	return verror.NewErrNotImplemented(ctx)
+}
+
+func (sd *syncDatabase) KeepBlob(ctx *context.T, call rpc.ServerCall, br wire.BlobRef, rank uint64) error {
+	return verror.NewErrNotImplemented(ctx)
+}
+
+////////////////////////////////////////////////////////////
+// RPC for blob fetch between Syncbases.
+
+func (s *syncService) FetchBlob(ctx *context.T, call interfaces.SyncFetchBlobServerCall, br wire.BlobRef) error {
+	vlog.VI(2).Infof("sync: FetchBlob: sb-sb begin br %v", br)
+	defer vlog.VI(2).Infof("sync: FetchBlob: sb-sb end br %v", br)
+	return getLocalBlob(ctx, call.SendStream(), s.bst, br, 0)
+}
+
+func (s *syncService) HaveBlob(ctx *context.T, call rpc.ServerCall, br wire.BlobRef) (int64, error) {
+	vlog.VI(2).Infof("sync: HaveBlob: begin br %v", br)
+	defer vlog.VI(2).Infof("sync: HaveBlob: end br %v", br)
+
+	bReader, err := s.bst.NewBlobReader(ctx, string(br))
+	if err != nil {
+		return 0, err
+	}
+	defer bReader.Close()
+	if !bReader.IsFinalized() {
+		return 0, wire.NewErrBlobNotCommitted(ctx)
+	}
+	return bReader.Size(), nil
+}
+
+func (s *syncService) FetchBlobRecipe(ctx *context.T, call interfaces.SyncFetchBlobRecipeServerCall, br wire.BlobRef) error {
+	return verror.NewErrNotImplemented(ctx)
+}
+
+func (s *syncService) FetchChunks(ctx *context.T, call interfaces.SyncFetchChunksServerCall) error {
+	return verror.NewErrNotImplemented(ctx)
+}
+
+////////////////////////////////////////////////////////////
+// Helpers.
+
+type byteStream interface {
+	Send(item []byte) error
+}
+
+// getLocalBlob looks for a blob in the local store and, if found, reads the
+// blob and sends it to the client.  If the blob is found, it starts reading it
+// from the given offset and sends its bytes into the client stream.
+func getLocalBlob(ctx *context.T, stream byteStream, bst blob.BlobStore, br wire.BlobRef, offset int64) error {
+	vlog.VI(4).Infof("sync: getLocalBlob: begin br %v, offset %v", br, offset)
+	defer vlog.VI(4).Infof("sync: getLocalBlob: end br %v, offset %v", br, offset)
+
+	reader, err := bst.NewBlobReader(ctx, string(br))
+	if err != nil {
+		return err
+	}
+	defer reader.Close()
+
+	if !reader.IsFinalized() {
+		return wire.NewErrBlobNotCommitted(ctx)
+	}
+
+	buf := make([]byte, chunkSize)
+	for {
+		nbytes, err := reader.ReadAt(buf, offset)
+		if err != nil && err != io.EOF {
+			return err
+		}
+		if nbytes <= 0 {
+			break
+		}
+		offset += int64(nbytes)
+		stream.Send(buf[:nbytes])
+		if err == io.EOF {
+			break
+		}
+	}
+
+	return nil
+}
+
+func (sd *syncDatabase) fetchBlobRemote(ctx *context.T, br wire.BlobRef, statusCall wire.BlobManagerFetchBlobServerCall, dataCall wire.BlobManagerGetBlobServerCall, offset int64) error {
+	vlog.VI(4).Infof("sync: fetchBlobRemote: begin br %v, offset %v", br, offset)
+	defer vlog.VI(4).Infof("sync: fetchBlobRemote: end br %v, offset %v", br, offset)
+
+	var sendStatus, sendData bool
+	var statusStream interface {
+		Send(item wire.BlobFetchStatus) error
+	}
+	var dataStream interface {
+		Send(item []byte) error
+	}
+
+	if statusCall != nil {
+		sendStatus = true
+		statusStream = statusCall.SendStream()
+	}
+	if dataCall != nil {
+		sendData = true
+		dataStream = dataCall.SendStream()
+	}
+
+	if sendStatus {
+		// Start blob source discovery.
+		statusStream.Send(wire.BlobFetchStatus{State: wire.BlobFetchStateLocating})
+	}
+
+	// Locate blob.
+	peer, size, err := sd.locateBlob(ctx, br)
+	if err != nil {
+		return err
+	}
+
+	// Start blob fetching.
+	status := wire.BlobFetchStatus{State: wire.BlobFetchStateFetching, Total: size}
+	if sendStatus {
+		statusStream.Send(status)
+	}
+
+	ss := sd.sync.(*syncService)
+	bst := ss.bst
+
+	bWriter, err := bst.NewBlobWriter(ctx, string(br))
+	if err != nil {
+		return err
+	}
+
+	c := interfaces.SyncClient(peer)
+	ctxPeer, cancel := context.WithRootCancel(ctx)
+	stream, err := c.FetchBlob(ctxPeer, br)
+	if err == nil {
+		peerStream := stream.RecvStream()
+		for peerStream.Advance() {
+			item := blob.BlockOrFile{Block: peerStream.Value()}
+			if err = bWriter.AppendFragment(item); err != nil {
+				break
+			}
+			curSize := int64(len(item.Block))
+			status.Received += curSize
+			if sendStatus {
+				statusStream.Send(status)
+			}
+			if sendData {
+				if curSize <= offset {
+					offset -= curSize
+				} else if offset != 0 {
+					dataStream.Send(item.Block[offset:])
+					offset = 0
+				} else {
+					dataStream.Send(item.Block)
+				}
+			}
+		}
+
+		if err != nil {
+			cancel()
+			stream.Finish()
+		} else {
+			err = peerStream.Err()
+			if terr := stream.Finish(); err == nil {
+				err = terr
+			}
+			cancel()
+		}
+	}
+
+	bWriter.Close()
+	if err != nil {
+		// Clean up the blob with failed download, so that it can be
+		// downloaded again. Ignore any error from deletion.
+		bst.DeleteBlob(ctx, string(br))
+	} else {
+		status := wire.BlobFetchStatus{State: wire.BlobFetchStateDone}
+		if sendStatus {
+			statusStream.Send(status)
+		}
+	}
+	return err
+}
+
+// TODO(hpucha): Add syncgroup driven blob discovery.
+func (sd *syncDatabase) locateBlob(ctx *context.T, br wire.BlobRef) (string, int64, error) {
+	vlog.VI(4).Infof("sync: locateBlob: begin br %v", br)
+	defer vlog.VI(4).Infof("sync: locateBlob: end br %v", br)
+
+	ss := sd.sync.(*syncService)
+	loc, err := ss.getBlobLocInfo(ctx, br)
+	if err != nil {
+		return "", 0, err
+	}
+
+	// Search for blob amongst the source peer and peer learned from.
+	var peers = []string{loc.source, loc.peer}
+	for _, p := range peers {
+		vlog.VI(4).Infof("sync: locateBlob: attempting %s", p)
+		// Get the mounttables for this peer.
+		mtTables, err := sd.getMountTables(ctx, p)
+		if err != nil {
+			continue
+		}
+
+		for mt := range mtTables {
+			absName := naming.Join(mt, p, util.SyncbaseSuffix)
+			c := interfaces.SyncClient(absName)
+			size, err := c.HaveBlob(ctx, br)
+			if err == nil {
+				vlog.VI(4).Infof("sync: locateBlob: found blob on %s", absName)
+				return absName, size, nil
+			}
+		}
+	}
+
+	return "", 0, verror.New(verror.ErrInternal, ctx, "blob not found")
+
+}
+
+func (sd *syncDatabase) getMountTables(ctx *context.T, peer string) (map[string]struct{}, error) {
+	ss := sd.sync.(*syncService)
+	mInfo := ss.copyMemberInfo(ctx, peer)
+
+	mtTables := make(map[string]struct{})
+	for gdbName, sgInfo := range mInfo.db2sg {
+		appName, dbName, err := splitAppDbName(ctx, gdbName)
+		if err != nil {
+			return nil, err
+		}
+		st, err := ss.getDbStore(ctx, nil, appName, dbName)
+		if err != nil {
+			return nil, err
+		}
+
+		for id := range sgInfo {
+			sg, err := getSyncGroupById(ctx, st, id)
+			if err != nil {
+				continue
+			}
+			if _, ok := sg.Joiners[peer]; !ok {
+				// Peer is no longer part of the SyncGroup.
+				continue
+			}
+			for _, mt := range sg.Spec.MountTables {
+				mtTables[mt] = struct{}{}
+			}
+		}
+	}
+	return mtTables, nil
+}
+
+// TODO(hpucha): Persist the blob directory periodically.
+func (s *syncService) addBlobLocInfo(ctx *context.T, br wire.BlobRef, info *blobLocInfo) error {
+	s.blobDirLock.Lock()
+	defer s.blobDirLock.Unlock()
+
+	s.blobDirectory[br] = info
+	return nil
+}
+
+func (s *syncService) getBlobLocInfo(ctx *context.T, br wire.BlobRef) (*blobLocInfo, error) {
+	s.blobDirLock.Lock()
+	defer s.blobDirLock.Unlock()
+
+	if info, ok := s.blobDirectory[br]; ok {
+		return info, nil
+	}
+	return nil, verror.New(verror.ErrInternal, ctx, "blob state not found", br)
+}
diff --git a/services/syncbase/vsync/conflict_resolution.go b/services/syncbase/vsync/conflict_resolution.go
new file mode 100644
index 0000000..a8e41f6
--- /dev/null
+++ b/services/syncbase/vsync/conflict_resolution.go
@@ -0,0 +1,144 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+import (
+	"v.io/v23/context"
+	"v.io/v23/verror"
+	"v.io/x/lib/vlog"
+)
+
+// Policies for conflict resolution.
+// TODO(hpucha): Move relevant parts to client-facing vdl.
+const (
+	// Resolves conflicts by picking the update with the most recent timestamp.
+	useTime = iota
+
+	// TODO(hpucha): implement other policies.
+	// Resolves conflicts by using the app conflict resolver callbacks via store.
+	useCallback
+)
+
+var (
+	// conflictResolutionPolicy is the policy used to resolve conflicts.
+	conflictResolutionPolicy = useTime
+)
+
+// resolutionType represents how a conflict is resolved.
+type resolutionType byte
+
+const (
+	pickLocal  resolutionType = iota // local update was chosen as the resolution.
+	pickRemote                       // remote update was chosen as the resolution.
+	createNew                        // new update was created as the resolution.
+)
+
+// conflictResolution represents the state of a conflict resolution.
+type conflictResolution struct {
+	ty  resolutionType
+	rec *localLogRec // Valid only if ty == createNew.
+	val []byte       // Valid only if ty == createNew.
+}
+
+// resolveConflicts resolves conflicts for updated objects. Conflicts may be
+// resolved by adding new versions or picking either the local or the remote
+// version.
+func (iSt *initiationState) resolveConflicts(ctx *context.T) error {
+	for obj, st := range iSt.updObjects {
+		if !st.isConflict {
+			continue
+		}
+
+		// TODO(hpucha): Look up policy from the schema. Currently,
+		// hardcoded to time.
+		var err error
+		st.res, err = iSt.resolveObjConflict(ctx, obj, st.oldHead, st.newHead, st.ancestor)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// resolveObjConflict resolves a conflict for an object given its ID and the 3
+// versions that express the conflict: the object's local version, its remote
+// version (from the device contacted), and the closest common ancestor (see
+// dag.go on how the ancestor is chosen). The function returns the new object
+// value according to the conflict resolution policy.
+func (iSt *initiationState) resolveObjConflict(ctx *context.T, oid, local, remote, ancestor string) (*conflictResolution, error) {
+	// Fetch the log records of the 3 object versions.
+	versions := []string{local, remote, ancestor}
+	lrecs, err := iSt.getLogRecsBatch(ctx, oid, versions)
+	if err != nil {
+		return nil, err
+	}
+
+	// The local and remote records must exist, however it is valid for the
+	// common ancestor to not exist.  This happens when two Syncbases create
+	// separately their first versions for the same object (key).
+	locRec, remRec, ancRec := lrecs[0], lrecs[1], lrecs[2]
+	if locRec == nil || remRec == nil {
+		vlog.Fatalf("sync: resolveObjConflict: oid %s: invalid local (%s: %v) or remote recs (%s: %v)",
+			oid, local, locRec, remote, remRec)
+	}
+
+	// Resolve the conflict according to the resolution policy.
+	switch conflictResolutionPolicy {
+	case useTime:
+		return iSt.resolveObjConflictByTime(ctx, oid, locRec, remRec, ancRec)
+	default:
+		return nil, verror.New(verror.ErrInternal, ctx, "unknown conflict resolution policy", conflictResolutionPolicy)
+	}
+}
+
+// resolveObjConflictByTime resolves conflicts using the timestamps of the
+// conflicting mutations.  It picks a mutation with the larger timestamp,
+// i.e. the most recent update.  If the timestamps are equal, it uses the
+// mutation version numbers as a tie-breaker, picking the mutation with the
+// larger version.  Instead of creating a new version that resolves the
+// conflict, we pick an existing version as the conflict resolution.
+func (iSt *initiationState) resolveObjConflictByTime(ctx *context.T, oid string, local, remote, ancestor *localLogRec) (*conflictResolution, error) {
+	var res conflictResolution
+	switch {
+	case local.Metadata.UpdTime.After(remote.Metadata.UpdTime):
+		res.ty = pickLocal
+	case local.Metadata.UpdTime.Before(remote.Metadata.UpdTime):
+		res.ty = pickRemote
+	case local.Metadata.CurVers > remote.Metadata.CurVers:
+		res.ty = pickLocal
+	case local.Metadata.CurVers < remote.Metadata.CurVers:
+		res.ty = pickRemote
+	default:
+		vlog.Fatalf("sync: resolveObjConflictByTime: local and remote update times and versions are the same, local %v remote %v", local, remote)
+	}
+
+	return &res, nil
+}
+
+// getLogRecsBatch gets the log records for an array of versions for a given object.
+func (iSt *initiationState) getLogRecsBatch(ctx *context.T, obj string, versions []string) ([]*localLogRec, error) {
+	lrecs := make([]*localLogRec, len(versions))
+	for p, v := range versions {
+		if v == NoVersion {
+			lrecs[p] = nil
+			continue
+		}
+
+		logKey, err := getLogRecKey(ctx, iSt.tx, obj, v)
+		if err != nil {
+			return nil, err
+		}
+		dev, gen, err := splitLogRecKey(ctx, logKey)
+		if err != nil {
+			return nil, err
+		}
+		lrecs[p], err = getLogRec(ctx, iSt.tx, dev, gen)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return lrecs, nil
+}
diff --git a/services/syncbase/vsync/dag.go b/services/syncbase/vsync/dag.go
new file mode 100644
index 0000000..121f594
--- /dev/null
+++ b/services/syncbase/vsync/dag.go
@@ -0,0 +1,855 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Syncbase DAG (directed acyclic graph) utility functions.
+//
+// The DAG is used to track the version history of synced objects in order
+// to detect and resolve conflicts (concurrent changes on different devices).
+//
+// Note: the sync code uses the words "object" and "object ID" (oid) as a
+// generic way to refer to syncable entities, whether they are actual user data
+// (table row and its row key), prefix-ACLs (permission entry and its prefix),
+// or other metadata such as SyncGroups (SyncGroup value and its internal key
+// based on the SyncGroup ID).
+//
+// * Object IDs are globally unique across all devices.
+// * Syncable objects have version numbers associated with their mutations.
+// * For a given object ID, the version number is globally unique across all
+//   devices, i.e. the (oid, version) tuple is globally unique.
+// * Each (oid, version) tuple is represented by a node in the DAG.
+// * The previous version of an object is its parent in the DAG, i.e. the
+//   new version is derived from that parent.
+// * DAG nodes have child-to-parent pointers.
+// * When there are no conflicts, the parent node has a single child node
+//   that points to it.
+// * When a parent node has more than one child, this indicates concurrent
+//   mutations which are treated as a conflict to be resolved.
+// * When a conflict is resolved, the new version has pointers back to each of
+//   the two parents to indicate that it is derived from both nodes.
+// * During a sync operation from a source device to a target device, the
+//   target receives a DAG fragment from the source.  That fragment has to
+//   be incorporated (grafted) into the target device's DAG.  It may be a
+//   continuation of the DAG of an object, with the attachment (graft) point
+//   being the current head of DAG, in which case there are no conflicts.
+//   Or the graft point(s) may be older nodes, which means the new fragment
+//   is a divergence in the graph causing a conflict that must be resolved
+//   in order to re-converge the two DAG fragments.
+//
+// In the diagrams below:
+// (h) represents the head node in the local device.
+// (nh) represents the new head node received from the remote device.
+// (g) represents a graft node, where new nodes attach to the existing DAG.
+// <- represents a derived-from mutation, i.e. a child-to-parent pointer
+//
+// a- No-conflict example: the new nodes (v4, v5) attach to the head node (v3).
+//    In this case the new head becomes the head node, the new DAG fragment
+//    being a continuation of the existing DAG.
+//
+//    Before:
+//    v1 <- v2 <- v3(h)
+//
+//    Sync updates applied, no conflict detected:
+//    v1 <- v2 <- v3(h,g) <- v4 <- v5 (nh)
+//
+//    After:
+//    v1 <- v2 <- v3 <- v4 <- v5 (h)
+//
+// b- Conflict example: the new nodes (v4, v5) attach to an old node (v2).
+//    The current head node (v3) and the new head node (v5) are divergent
+//    (concurrent) mutations that need to be resolved.  The conflict
+//    resolution function is passed the old head (v3), new head (v5), and
+//    the common ancestor (v2).  It resolves the conflict with (v6) which
+//    is represented in the DAG as derived from both v3 and v5 (2 parents).
+//
+//    Before:
+//    v1 <- v2 <- v3(h)
+//
+//    Sync updates applied, conflict detected (v3 not a graft node):
+//    v1 <- v2(g) <- v3(h)
+//                <- v4 <- v5 (nh)
+//
+//    After: conflict resolver creates v6 having 2 parents (v3, v5):
+//    v1 <- v2(g) <- v3 <------- v6(h)
+//                <- v4 <- v5 <-
+//
+// The DAG does not grow indefinitely.  During a sync operation each device
+// learns what the other device already knows -- where it's at in the version
+// history for the objects.  When a device determines that all devices that
+// sync an object (members of matching SyncGroups) have moved past some version
+// for that object, the DAG for that object can be pruned up to that common
+// version, deleting all prior (ancestor) nodes.
+//
+// The DAG contains three tables persisted to disk (nodes, heads, batches):
+//
+//   * nodes:   one entry per (oid, version) with references to parent node(s)
+//              it is derived from, a reference to the log record identifying
+//              that mutation, a reference to its write batch (if any), and a
+//              boolean to indicate whether this was an object deletion.
+//
+//   * heads:   one entry per object pointing to its most recent version.
+//
+//   * batches: one entry per batch ID containing the set of objects in the
+//              write batch and their versions.
+
+import (
+	"container/list"
+	"fmt"
+
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/context"
+	"v.io/v23/verror"
+	"v.io/x/lib/vlog"
+)
+
+const (
+	NoVersion = ""
+	NoBatchId = uint64(0)
+)
+
+// dagNode holds the information on a object mutation in the DAG.
+// Note: the batch ID and deleted flag are copies of information in the log
+// record.  They are also stored in the DAG node to improve DAG traversal for
+// conflict resolution and pruning without having to fetch the full log record
+// every time.
+type dagNode struct {
+	Level   uint64   // node distance from root
+	Parents []string // references to parent versions
+	Logrec  string   // reference to log record
+	BatchId uint64   // ID of a write batch
+	Deleted bool     // true if the change was a delete
+}
+
+// batchSet holds information on a set of write batches.
+type batchSet map[uint64]*batchInfo
+
+// batchInfo holds the information on a write batch:
+// - The map of syncable (versioned) objects: {oid: version}
+// - The total count of batch objects, including non-syncable ones.
+// TODO(rdaoud): add support to track the read and scan sets.
+type batchInfo struct {
+	Objects map[string]string
+	Count   uint64
+}
+
+// graftMap holds the state of DAG node grafting (attaching) per object.
+type graftMap map[string]*graftInfo
+
+// graftInfo holds the state of an object's node grafting in the DAG.
+// It is ephemeral (in-memory), used during a single sync operation to track
+// where the new DAG fragments are attached to the existing DAG for the object:
+// - newNodes:    the set of newly added nodes; used to detect the type of edges
+//                between nodes (new-node to old-node or vice versa).
+// - newHeads:    the set of new candidate head nodes; used to detect conflicts.
+// - graftNodes:  the set of old nodes on which new nodes were added, and their
+//                level in the DAG; used to find common ancestors for conflicts.
+// - oldHeadSnap: snapshot of the current local head known by sync, used in
+//                conflict detection, particularly when conflict detection needs
+//                to be retried due to sync dag state being stale compared
+//                to local store.
+//
+// After the received mutations are applied, if there are two heads in the
+// newHeads set, there is a conflict to be resolved for the object.  Otherwise,
+// if there is one head, no conflict was triggered and the new head becomes the
+// current object version.  In case of conflict, the graftNodes set is used to
+// select a common ancestor.
+// TODO(rdaoud): support open DAGs to handle delayed conflict resolution by
+// tracking multiple dangling remote heads in addition to the local head node.
+type graftInfo struct {
+	newNodes    map[string]bool
+	newHeads    map[string]bool
+	graftNodes  map[string]uint64
+	oldHeadSnap string
+}
+
+// newBatchInfo allocates and initializes a batch info entry.
+func newBatchInfo() *batchInfo {
+	return &batchInfo{Objects: make(map[string]string), Count: 0}
+}
+
+// startBatch marks the start of a batch.  It generates a batch ID and returns
+// it to the caller, if an ID is not given.  The batch ID is used to track DAG
+// nodes that are part of the same batch and it is stored in the log records.
+// If a batch ID is given by the caller, its information is accessed.
+func (s *syncService) startBatch(ctx *context.T, st store.StoreReader, btid uint64) uint64 {
+	s.batchesLock.Lock()
+	defer s.batchesLock.Unlock()
+
+	// If no batch ID is given, generate a new unused one.
+	if btid == NoBatchId {
+		for (btid == NoBatchId) || (s.batches[btid] != nil) {
+			btid = rand64()
+		}
+
+		s.batches[btid] = newBatchInfo()
+		return btid
+	}
+
+	// Use the given batch ID and, if needed, refetch its in-memory entry
+	// from the store.  It is OK not to find it in the store; it means sync
+	// is learning about this batch ID the first time from another sync.
+	if s.batches[btid] == nil {
+		info, err := getBatch(ctx, st, btid)
+		if err != nil {
+			info = newBatchInfo()
+		}
+		s.batches[btid] = info
+	}
+
+	return btid
+}
+
+// addNodeToBatch adds a node (oid, version) to a batch under construction.
+func (s *syncService) addNodeToBatch(ctx *context.T, btid uint64, oid, version string) error {
+	s.batchesLock.Lock()
+	defer s.batchesLock.Unlock()
+
+	if btid == NoBatchId {
+		return verror.New(verror.ErrInternal, ctx, "invalid batch id", btid)
+	}
+
+	info := s.batches[btid]
+	if info == nil {
+		return verror.New(verror.ErrInternal, ctx, "unknown batch id", btid)
+	}
+
+	info.Objects[oid] = version
+	return nil
+}
+
+// endBatch marks the end of a given batch.  The batch information is persisted
+// to the store and removed from the temporary in-memory entry.
+func (s *syncService) endBatch(ctx *context.T, tx store.Transaction, btid, count uint64) error {
+	s.batchesLock.Lock()
+	defer s.batchesLock.Unlock()
+
+	if btid == NoBatchId || count == 0 {
+		return verror.New(verror.ErrInternal, ctx, "invalid batch info", btid, count)
+	}
+
+	info := s.batches[btid]
+	if info == nil {
+		return verror.New(verror.ErrInternal, ctx, "unknown batch id", btid)
+	}
+
+	// The first time a batch is ended, info.Count is zero.  Subsequently,
+	// if this batch ID is started and ended again, info.Count should be
+	// the same as the "count" value given.
+	if info.Count != 0 && info.Count != count {
+		return verror.New(verror.ErrInternal, ctx, "wrong counts for batch", btid, info.Count, count)
+	}
+
+	// Only save non-empty batches.
+	if len(info.Objects) > 0 {
+		info.Count = count
+		if err := setBatch(ctx, tx, btid, info); err != nil {
+			return err
+		}
+	}
+
+	delete(s.batches, btid)
+	return nil
+}
+
+// addNode adds a new node for a DAG object, linking it to its parent nodes.
+// It verifies that the node does not exist and its parent nodes are valid.
+// If a batch ID is given, track the node membership in the batch.
+//
+// Note: an in-memory grafting structure is passed to track DAG attachments
+// during a sync operation.  This is needed when nodes are being added due to
+// remote changes fetched by the sync protocol.  The Initiator allocates a
+// grafting structure at the start of a sync operation and passes it across
+// calls to addNode() to update the DAG grafting state:
+// - If a parent node is not new, mark it as a DAG graft point.
+// - Mark this version as a new node.
+// - Update the new head node pointer of the grafted DAG.
+//
+// The grafting structure is not needed when nodes are being added locally by
+// the Watcher, passing a nil grafting structure.
+func (s *syncService) addNode(ctx *context.T, tx store.Transaction, oid, version, logrec string, deleted bool, parents []string, btid uint64, graft graftMap) error {
+	if parents != nil {
+		if len(parents) > 2 {
+			return verror.New(verror.ErrInternal, ctx, "cannot have more than 2 parents")
+		}
+		if len(parents) == 0 {
+			parents = nil // replace an empty array with a nil
+		}
+	}
+
+	// The new node must not exist.
+	if ok, err := hasNode(ctx, tx, oid, version); err != nil {
+		return err
+	} else if ok {
+		return verror.New(verror.ErrInternal, ctx, "DAG node already exists", oid, version)
+	}
+
+	// Verify the parents, determine the node level.  Also save the levels
+	// of the parent nodes for later in this function in graft updates.
+	parentLevels := make(map[string]uint64)
+	var level uint64
+	for _, parent := range parents {
+		pnode, err := getNode(ctx, tx, oid, parent)
+		if err != nil {
+			return err
+		}
+		parentLevels[parent] = pnode.Level
+		if level <= pnode.Level {
+			level = pnode.Level + 1
+		}
+	}
+
+	// If a batch ID is given, add the node to that batch.
+	if btid != NoBatchId {
+		if err := s.addNodeToBatch(ctx, btid, oid, version); err != nil {
+			return err
+		}
+	}
+
+	// Add the node entry to the DAG.
+	node := &dagNode{
+		Level:   level,
+		Parents: parents,
+		Logrec:  logrec,
+		BatchId: btid,
+		Deleted: deleted,
+	}
+	if err := setNode(ctx, tx, oid, version, node); err != nil {
+		return err
+	}
+
+	// We are done if grafting is not being tracked (a local node add).
+	if graft == nil {
+		return nil
+	}
+
+	// Get the object's graft info entry in order to update it.  It happens
+	// when addNode() is called by the sync Initiator and the DAG is updated
+	// with new nodes fetched from other devices.
+	//
+	// During a sync operation, each mutated object gets new nodes added in
+	// its DAG.  These new nodes are either derived from nodes that were
+	// previously known on this device (i.e. their parent nodes are pre-
+	// existing, or they have no parents (new root nodes)), or they are
+	// derived from other new DAG nodes being discovered during this sync
+	// (i.e. their parent nodes were also just added to the DAG).
+	//
+	// To detect a conflict and find the most recent common ancestor to
+	// pass to the conflict resolver, the DAG graft info keeps track of the
+	// new nodes that have old parent nodes.  These old-to-new edges are
+	// points where new DAG fragments are attached (grafted) onto the
+	// existing DAG.  The old nodes are the graft nodes forming the set of
+	// common ancestors to use in conflict resolution:
+	//
+	// 1- A conflict happens when the current "head node" for an object is
+	//    not in the set of graft nodes.  It means the object mutations
+	//    were not derived from what the device knows, but are divergent
+	//    changes at a prior point.
+	//
+	// 2- The most recent common ancestor to use in resolving the conflict
+	//    is the object graft node with the deepest level (furthest from
+	//    the root node), representing the most up-to-date common knowledge
+	//    between the devices.
+	info := getObjectGraftInfo(ctx, tx, graft, oid)
+
+	for _, parent := range parents {
+		// If this parent is an old node, it's a graft point.
+		if !info.newNodes[parent] {
+			info.graftNodes[parent] = parentLevels[parent]
+		}
+
+		// A parent cannot be a candidate for a new head.
+		delete(info.newHeads, parent)
+	}
+
+	// This new node is a candidate for new head version.
+	info.newNodes[version] = true
+	info.newHeads[version] = true
+	return nil
+}
+
+// addParent adds to the DAG node (oid, version) linkage to this parent node.
+//
+// Note: as with the addNode() call, an in-memory grafting structure is passed
+// to track DAG attachements during a sync operation.  It is not needed if the
+// parent linkage is due to a local change (from conflict resolution selecting
+// an existing version).
+func (s *syncService) addParent(ctx *context.T, tx store.Transaction, oid, version, parent string, graft graftMap) error {
+	if version == parent {
+		return verror.New(verror.ErrInternal, ctx, "object", oid, version, "cannot be its own parent")
+	}
+
+	node, err := getNode(ctx, tx, oid, version)
+	if err != nil {
+		return err
+	}
+	pnode, err := getNode(ctx, tx, oid, parent)
+	if err != nil {
+		return err
+	}
+
+	// Check if the parent is already linked to this node.
+	found := false
+	for _, p := range node.Parents {
+		if p == parent {
+			found = true
+			break
+		}
+	}
+
+	// Add the parent if it is not yet linked.
+	if !found {
+		// Make sure that adding the link does not create a DAG cycle.
+		// Verify that the node is not an ancestor of the parent that
+		// it is being linked to.
+		err = forEachAncestor(ctx, tx, oid, pnode.Parents, func(v string, nd *dagNode) error {
+			if v == version {
+				return verror.New(verror.ErrInternal, ctx, "cycle on object",
+					oid, ": node", version, "is ancestor of parent", parent)
+			}
+			return nil
+		})
+		if err != nil {
+			return err
+		}
+		node.Parents = append(node.Parents, parent)
+		if err = setNode(ctx, tx, oid, version, node); err != nil {
+			return err
+		}
+	}
+
+	// If no grafting structure is given (i.e. local changes), we are done.
+	if graft == nil {
+		return nil
+	}
+
+	// Update graft: if the node and its parent are new/old or old/new then
+	// add the parent as a graft point (a potential common ancestor).
+	info := getObjectGraftInfo(ctx, tx, graft, oid)
+
+	_, nodeNew := info.newNodes[version]
+	_, parentNew := info.newNodes[parent]
+	if (nodeNew && !parentNew) || (!nodeNew && parentNew) {
+		info.graftNodes[parent] = pnode.Level
+	}
+
+	// The parent node can no longer be a candidate for a new head version.
+	delete(info.newHeads, parent)
+	return nil
+}
+
+// moveHead moves the object head node in the DAG.
+func moveHead(ctx *context.T, tx store.Transaction, oid, head string) error {
+	// Verify that the node exists.
+	if ok, err := hasNode(ctx, tx, oid, head); err != nil {
+		return err
+	} else if !ok {
+		return verror.New(verror.ErrInternal, ctx, "node", oid, head, "does not exist")
+	}
+
+	return setHead(ctx, tx, oid, head)
+}
+
+// hasConflict determines if an object has a conflict between its new and old
+// head nodes.
+// - Yes: return (true, newHead, oldHead, ancestor)   -- from a common past
+// - Yes: return (true, newHead, oldHead, NoVersion)  -- from disjoint pasts
+// - No:  return (false, newHead, oldHead, NoVersion) -- no conflict
+//
+// A conflict exists when there are two new-head nodes in the graft structure.
+// It means the newly added object versions are not derived in part from this
+// device's current knowledge. A conflict also exists if the snapshotted local
+// head is different from the current local head. If there is a single new-head
+// and the snapshot head is the same as the current local head, the object
+// changes were applied without triggering a conflict.
+func hasConflict(ctx *context.T, st store.StoreReader, oid string, graft graftMap) (isConflict bool, newHead, oldHead, ancestor string, err error) {
+	isConflict = false
+	oldHead = NoVersion
+	newHead = NoVersion
+	ancestor = NoVersion
+	err = nil
+
+	if graft == nil {
+		err = verror.New(verror.ErrInternal, ctx, "no DAG graft map given")
+		return
+	}
+
+	info := graft[oid]
+	if info == nil {
+		err = verror.New(verror.ErrInternal, ctx, "node", oid, "has no DAG graft info")
+		return
+	}
+
+	numHeads := len(info.newHeads)
+	if numHeads < 1 || numHeads > 2 {
+		err = verror.New(verror.ErrInternal, ctx, "node", oid, "invalid count of new heads", numHeads)
+		return
+	}
+
+	// Fetch the current head for this object if it exists.  The error from
+	// getHead() is ignored because a newly received object is not yet known
+	// on this device and will not trigger a conflict.
+	oldHead, _ = getHead(ctx, st, oid)
+
+	// If there is only one new head node and the snapshotted old head is
+	// still unchanged, there is no conflict. The new head is that single
+	// one, even if it might also be the same old node.
+	if numHeads == 1 {
+		for head := range info.newHeads {
+			newHead = head
+		}
+		if newHead == info.oldHeadSnap {
+			// Only link log records could've been received.
+			newHead = oldHead
+			return
+		} else if oldHead == info.oldHeadSnap {
+			return
+		}
+	}
+
+	// The new head is the non-old one.
+	for head := range info.newHeads {
+		if head != info.oldHeadSnap {
+			newHead = head
+			break
+		}
+	}
+
+	// There wasn't a conflict at the old snapshot, but now there is. The
+	// snapshotted head is the common ancestor.
+	isConflict = true
+	if numHeads == 1 {
+		vlog.VI(4).Infof("sync: hasConflict: old graft snapshot %v, head %s", graft, oldHead)
+		ancestor = info.oldHeadSnap
+		return
+	}
+
+	// There is a conflict: the best choice ancestor is the graft node with
+	// the largest level (farthest from the root).  It is possible in some
+	// corner cases to have multiple graft nodes at the same level.  This
+	// would still be a single conflict, but the multiple same-level graft
+	// nodes representing equivalent conflict resolutions on different
+	// devices that are now merging their resolutions.  In such a case it
+	// does not matter which node is chosen as the ancestor because the
+	// conflict resolver function is assumed to be convergent.  However it
+	// is nicer to make that selection deterministic so all devices see the
+	// same choice: the version number is used as a tie-breaker.
+	// Note: for the case of a conflict from disjoint pasts, there are no
+	// graft nodes (empty set) and thus no common ancestor because the two
+	// DAG fragments were created from distinct root nodes.  The "NoVersion"
+	// value is returned as the ancestor.
+	var maxLevel uint64
+	for node, level := range info.graftNodes {
+		if maxLevel < level || (maxLevel == level && ancestor < node) {
+			maxLevel = level
+			ancestor = node
+		}
+	}
+	return
+}
+
+// newGraft allocates a graftMap to track DAG node grafting during sync.
+func newGraft() graftMap {
+	return make(graftMap)
+}
+
+// getObjectGraft returns the graftInfo for an object ID.  If the graftMap is
+// nil, a nil graftInfo is returned because grafting is not being tracked.
+func getObjectGraftInfo(ctx *context.T, sntx store.SnapshotOrTransaction, graft graftMap, oid string) *graftInfo {
+	if graft == nil {
+		return nil
+	}
+	if info := graft[oid]; info != nil {
+		return info
+	}
+
+	info := &graftInfo{
+		newNodes:   make(map[string]bool),
+		newHeads:   make(map[string]bool),
+		graftNodes: make(map[string]uint64),
+	}
+
+	// If the object has a head node, include it in the set of new heads.
+	if head, err := getHead(ctx, sntx, oid); err == nil {
+		info.newHeads[head] = true
+		info.oldHeadSnap = head
+	}
+
+	graft[oid] = info
+	return info
+}
+
+// forEachAncestor loops over the DAG ancestor nodes of an object in a breadth-
+// first traversal starting from given version nodes.  It calls the given
+// callback function once for each ancestor node.
+func forEachAncestor(ctx *context.T, st store.StoreReader, oid string, startVersions []string, callback func(version string, node *dagNode) error) error {
+	visited := make(map[string]bool)
+	queue := list.New()
+	for _, version := range startVersions {
+		queue.PushBack(version)
+		visited[version] = true
+	}
+
+	for queue.Len() > 0 {
+		version := queue.Remove(queue.Front()).(string)
+		node, err := getNode(ctx, st, oid, version)
+		if err != nil {
+			// Ignore it, the parent was previously pruned.
+			continue
+		}
+		for _, parent := range node.Parents {
+			if !visited[parent] {
+				queue.PushBack(parent)
+				visited[parent] = true
+			}
+		}
+		if err = callback(version, node); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// newBatchPruning allocates an in-memory structure to track batches affected
+// by a DAG pruning operation across objects.
+func newBatchPruning() batchSet {
+	return make(batchSet)
+}
+
+// prune trims the DAG of an object at a given version (node) by deleting all
+// its ancestor nodes, making it the new root node.  For each deleted node it
+// calls the given callback function to delete its log record.
+//
+// Note: this function should only be used when sync determines that all devices
+// that know about this object have gotten past this version.
+//
+// The batch set passed is used to track batches affected by the deletion of DAG
+// objects across multiple calls to prune().  It is later given to pruneDone()
+// to do GC on these batches.
+func prune(ctx *context.T, tx store.Transaction, oid, version string, batches batchSet, delLogRec func(ctx *context.T, tx store.Transaction, logrec string) error) error {
+	if batches == nil {
+		return verror.New(verror.ErrInternal, ctx, "missing batch set")
+	}
+
+	// Get the node at the pruning point and set its parents to nil.
+	// It will become the oldest DAG node (root) for the object.
+	node, err := getNode(ctx, tx, oid, version)
+	if err != nil {
+		return err
+	}
+	if node.Parents == nil {
+		// Nothing to do, this node is already the root.
+		return nil
+	}
+
+	parents := node.Parents
+	node.Parents = nil
+	if err = setNode(ctx, tx, oid, version, node); err != nil {
+		return err
+	}
+
+	// Delete all ancestor nodes and their log records. Delete as many as
+	// possible and track the error counts.  Update the batch set to track
+	// their pruning.
+	nodeErrs, logErrs := 0, 0
+	forEachAncestor(ctx, tx, oid, parents, func(v string, nd *dagNode) error {
+		if btid := nd.BatchId; btid != NoBatchId {
+			if batches[btid] == nil {
+				batches[btid] = newBatchInfo()
+			}
+			batches[btid].Objects[oid] = v
+		}
+
+		if err := delLogRec(ctx, tx, nd.Logrec); err != nil {
+			logErrs++
+		}
+		if err := delNode(ctx, tx, oid, v); err != nil {
+			nodeErrs++
+		}
+		return nil
+	})
+	if nodeErrs != 0 || logErrs != 0 {
+		return verror.New(verror.ErrInternal, ctx,
+			"prune failed to delete nodes and logs:", nodeErrs, logErrs)
+	}
+	return nil
+}
+
+// pruneDone is called when object pruning is finished within a single pass of
+// the sync garbage collector.  It updates the batch sets affected by objects
+// deleted by prune().
+func pruneDone(ctx *context.T, tx store.Transaction, batches batchSet) error {
+	// Update batch sets by removing the pruned objects from them.
+	for btid, pruneInfo := range batches {
+		info, err := getBatch(ctx, tx, btid)
+		if err != nil {
+			return err
+		}
+
+		for oid := range pruneInfo.Objects {
+			delete(info.Objects, oid)
+		}
+
+		if len(info.Objects) > 0 {
+			err = setBatch(ctx, tx, btid, info)
+		} else {
+			err = delBatch(ctx, tx, btid)
+		}
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// getLogRecKey returns the key of the log record for a given object version.
+func getLogRecKey(ctx *context.T, st store.StoreReader, oid, version string) (string, error) {
+	node, err := getNode(ctx, st, oid, version)
+	if err != nil {
+		return "", err
+	}
+	return node.Logrec, nil
+}
+
+// Low-level utility functions to access DB entries without tracking their
+// relationships.  Use the functions above to manipulate the DAG.
+
+// nodeKey returns the key used to access a DAG node (oid, version).
+func nodeKey(oid, version string) string {
+	return util.JoinKeyParts(util.SyncPrefix, dagPrefix, "n", oid, version)
+}
+
+// setNode stores the DAG node entry.
+func setNode(ctx *context.T, tx store.Transaction, oid, version string, node *dagNode) error {
+	if version == NoVersion {
+		return verror.New(verror.ErrInternal, ctx, "invalid version", version)
+	}
+
+	return util.Put(ctx, tx, nodeKey(oid, version), node)
+}
+
+// getNode retrieves the DAG node entry for the given (oid, version).
+func getNode(ctx *context.T, st store.StoreReader, oid, version string) (*dagNode, error) {
+	if version == NoVersion {
+		return nil, verror.New(verror.ErrInternal, ctx, "invalid version", version)
+	}
+
+	var node dagNode
+	key := nodeKey(oid, version)
+	if err := util.Get(ctx, st, key, &node); err != nil {
+		return nil, err
+	}
+	return &node, nil
+}
+
+// delNode deletes the DAG node entry.
+func delNode(ctx *context.T, tx store.Transaction, oid, version string) error {
+	if version == NoVersion {
+		return verror.New(verror.ErrInternal, ctx, "invalid version", version)
+	}
+
+	return util.Delete(ctx, tx, nodeKey(oid, version))
+}
+
+// hasNode returns true if the node (oid, version) exists in the DAG.
+func hasNode(ctx *context.T, st store.StoreReader, oid, version string) (bool, error) {
+	// TODO(rdaoud): optimize to avoid the unneeded fetch/decode of the data.
+	if _, err := getNode(ctx, st, oid, version); err != nil {
+		if verror.ErrorID(err) == verror.ErrNoExist.ID {
+			err = nil
+		}
+		return false, err
+	}
+	return true, nil
+}
+
+// headKey returns the key used to access the DAG object head.
+func headKey(oid string) string {
+	return util.JoinKeyParts(util.SyncPrefix, dagPrefix, "h", oid)
+}
+
+// setHead stores version as the DAG object head.
+func setHead(ctx *context.T, tx store.Transaction, oid, version string) error {
+	if version == NoVersion {
+		return verror.New(verror.ErrInternal, ctx, fmt.Errorf("invalid version: %s", version))
+	}
+
+	return util.Put(ctx, tx, headKey(oid), version)
+}
+
+// getHead retrieves the DAG object head.
+func getHead(ctx *context.T, st store.StoreReader, oid string) (string, error) {
+	var version string
+	key := headKey(oid)
+	if err := util.Get(ctx, st, key, &version); err != nil {
+		return NoVersion, err
+	}
+	return version, nil
+}
+
+// delHead deletes the DAG object head.
+func delHead(ctx *context.T, tx store.Transaction, oid string) error {
+	return util.Delete(ctx, tx, headKey(oid))
+}
+
+// batchKey returns the key used to access the DAG batch info.
+func batchKey(btid uint64) string {
+	return util.JoinKeyParts(util.SyncPrefix, dagPrefix, "b", fmt.Sprintf("%d", btid))
+}
+
+// setBatch stores the DAG batch entry.
+func setBatch(ctx *context.T, tx store.Transaction, btid uint64, info *batchInfo) error {
+	if btid == NoBatchId {
+		return verror.New(verror.ErrInternal, ctx, "invalid batch id", btid)
+	}
+
+	return util.Put(ctx, tx, batchKey(btid), info)
+}
+
+// getBatch retrieves the DAG batch entry.
+func getBatch(ctx *context.T, st store.StoreReader, btid uint64) (*batchInfo, error) {
+	if btid == NoBatchId {
+		return nil, verror.New(verror.ErrInternal, ctx, "invalid batch id", btid)
+	}
+
+	var info batchInfo
+	key := batchKey(btid)
+	if err := util.Get(ctx, st, key, &info); err != nil {
+		return nil, err
+	}
+	return &info, nil
+}
+
+// delBatch deletes the DAG batch entry.
+func delBatch(ctx *context.T, tx store.Transaction, btid uint64) error {
+	if btid == NoBatchId {
+		return verror.New(verror.ErrInternal, ctx, "invalid batch id", btid)
+	}
+
+	return util.Delete(ctx, tx, batchKey(btid))
+}
+
+// getParentMap is a testing and debug helper function that returns for an
+// object a map of its DAG (node-to-parents relations).  If a graft structure
+// is given, include its fragments in the map.
+func getParentMap(ctx *context.T, st store.StoreReader, oid string, graft graftMap) map[string][]string {
+	parentMap := make(map[string][]string)
+	var start []string
+
+	if head, err := getHead(ctx, st, oid); err == nil {
+		start = append(start, head)
+	}
+	if graft != nil && graft[oid] != nil {
+		for v := range graft[oid].newHeads {
+			start = append(start, v)
+		}
+	}
+
+	forEachAncestor(ctx, st, oid, start, func(v string, nd *dagNode) error {
+		parentMap[v] = nd.Parents
+		return nil
+	})
+	return parentMap
+}
diff --git a/services/syncbase/vsync/dag_test.go b/services/syncbase/vsync/dag_test.go
new file mode 100644
index 0000000..9ef43f7
--- /dev/null
+++ b/services/syncbase/vsync/dag_test.go
@@ -0,0 +1,1632 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Tests for the Syncbase DAG.
+
+import (
+	"errors"
+	"fmt"
+	"reflect"
+	"strconv"
+	"testing"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/context"
+	_ "v.io/x/ref/runtime/factories/generic"
+)
+
+// TestSetNode tests setting and getting a DAG node.
+func TestSetNode(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+
+	oid, version := "1111", "1"
+
+	node, err := getNode(nil, st, oid, version)
+	if err == nil || node != nil {
+		t.Errorf("found non-existent object %s:%s: %v", oid, version, node)
+	}
+
+	if ok, err := hasNode(nil, st, oid, version); err != nil || ok {
+		t.Errorf("hasNode() found non-existent object %s:%s", oid, version)
+	}
+
+	if logrec, err := getLogRecKey(nil, st, oid, version); err == nil || logrec != "" {
+		t.Errorf("non-existent object %s:%s has a logrec: %v", oid, version, logrec)
+	}
+
+	node = &dagNode{Level: 15, Parents: []string{"444", "555"}, Logrec: "logrec-23"}
+
+	tx := st.NewTransaction()
+	if err = setNode(nil, tx, oid, version, node); err != nil {
+		t.Fatalf("cannot set object %s:%s (%v): %v", oid, version, node, err)
+	}
+	tx.Commit()
+
+	node2, err := getNode(nil, st, oid, version)
+	if err != nil || node2 == nil {
+		t.Errorf("cannot find stored object %s:%s: %v", oid, version, err)
+	}
+
+	if ok, err := hasNode(nil, st, oid, version); err != nil || !ok {
+		t.Errorf("hasNode() did not find object %s:%s", oid, version)
+	}
+
+	if !reflect.DeepEqual(node, node2) {
+		t.Errorf("object %s:%s has wrong data: %v instead of %v", oid, version, node2, node)
+	}
+
+	if logrec, err := getLogRecKey(nil, st, oid, version); err != nil || logrec != "logrec-23" {
+		t.Errorf("object %s:%s has wrong logrec: %s", oid, version, logrec)
+	}
+}
+
+// TestDelNode tests deleting a DAG node.
+func TestDelNode(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+
+	oid, version := "2222", "2"
+
+	node := &dagNode{Level: 123, Parents: []string{"333"}, Logrec: "logrec-789"}
+
+	tx := st.NewTransaction()
+	if err := setNode(nil, tx, oid, version, node); err != nil {
+		t.Fatalf("cannot set object %s:%s (%v): %v", oid, version, node, err)
+	}
+	tx.Commit()
+
+	tx = st.NewTransaction()
+	if err := delNode(nil, tx, oid, version); err != nil {
+		t.Fatalf("cannot delete object %s:%s: %v", oid, version, err)
+	}
+	tx.Commit()
+
+	node2, err := getNode(nil, st, oid, version)
+	if err == nil || node2 != nil {
+		t.Errorf("found deleted object %s:%s (%v)", oid, version, node2)
+	}
+
+	if ok, err := hasNode(nil, st, oid, version); err != nil || ok {
+		t.Errorf("hasNode() found deleted object %s:%s", oid, version)
+	}
+
+	if logrec, err := getLogRecKey(nil, st, oid, version); err == nil || logrec != "" {
+		t.Errorf("deleted object %s:%s has logrec: %s", oid, version, logrec)
+	}
+}
+
+// TestAddParent tests adding parents to a DAG node.
+func TestAddParent(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+	s := svc.sync
+
+	oid, version := "foo1", "7"
+
+	tx := st.NewTransaction()
+	if err := s.addParent(nil, tx, oid, version, "haha", nil); err == nil {
+		t.Errorf("addParent() did not fail for an unknown object %s:%s", oid, version)
+	}
+	tx.Abort()
+
+	if _, err := s.dagReplayCommands(nil, "local-init-00.log.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	node := &dagNode{Level: 15, Logrec: "logrec-22"}
+
+	tx = st.NewTransaction()
+	if err := setNode(nil, tx, oid, version, node); err != nil {
+		t.Fatalf("cannot set object %s:%s (%v): %v", oid, version, node, err)
+	}
+	tx.Commit()
+
+	graft := newGraft()
+	tx = st.NewTransaction()
+	if err := s.addParent(nil, tx, oid, version, version, graft); err == nil {
+		t.Errorf("addParent() did not fail on a self-parent for object %s:%s", oid, version)
+	}
+	tx.Abort()
+
+	remote := true
+	expParents := []string{"4", "5", "6"}
+
+	for _, parent := range expParents {
+		tx = st.NewTransaction()
+		if err := s.addParent(nil, tx, oid, version, parent, graft); err == nil {
+			t.Errorf("addParent() did not reject invalid parent %s for object %s:%s",
+				parent, oid, version)
+		}
+		tx.Abort()
+
+		pnode := &dagNode{Level: 11, Logrec: fmt.Sprintf("logrec-%s", parent), Parents: []string{"3"}}
+
+		tx = st.NewTransaction()
+		if err := setNode(nil, tx, oid, parent, pnode); err != nil {
+			t.Fatalf("cannot set parent object %s:%s (%v): %v", oid, parent, pnode, err)
+		}
+		tx.Commit()
+
+		var g graftMap
+		if remote {
+			g = graft
+		}
+
+		// addParent() twice to verify it is idempotent.
+		for i := 0; i < 2; i++ {
+			tx = st.NewTransaction()
+			if err := s.addParent(nil, tx, oid, version, parent, g); err != nil {
+				t.Errorf("addParent() failed on parent %s, remote %t (i=%d) for %s:%s: %v",
+					parent, remote, i, oid, version, err)
+			}
+			tx.Commit()
+		}
+
+		remote = !remote
+	}
+
+	node2, err := getNode(nil, st, oid, version)
+	if err != nil || node2 == nil {
+		t.Errorf("cannot find object %s:%s: %v", oid, version, err)
+	}
+
+	if !reflect.DeepEqual(node2.Parents, expParents) {
+		t.Errorf("invalid parents for object %s:%s: %v instead of %v",
+			oid, version, node2.Parents, expParents)
+	}
+
+	// Creating cycles should fail.
+	for v := 1; v < 7; v++ {
+		ver := fmt.Sprintf("%d", v)
+		tx = st.NewTransaction()
+		if err = s.addParent(nil, tx, oid, ver, version, nil); err == nil {
+			t.Errorf("addParent() failed to reject a cycle for %s: from ancestor %s to node %s",
+				oid, ver, version)
+		}
+		tx.Abort()
+	}
+}
+
+// TestSetHead tests setting and getting a DAG head node.
+func TestSetHead(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+
+	oid := "3333"
+
+	version, err := getHead(nil, st, oid)
+	if err == nil {
+		t.Errorf("found non-existent object head %s:%s", oid, version)
+	}
+
+	for i := 0; i < 2; i++ {
+		version = fmt.Sprintf("v%d", 555+i)
+
+		tx := st.NewTransaction()
+		if err = setHead(nil, tx, oid, version); err != nil {
+			t.Fatalf("cannot set object head %s:%s (i=%d)", oid, version, i)
+		}
+		tx.Commit()
+
+		version2, err := getHead(nil, st, oid)
+		if err != nil {
+			t.Errorf("cannot find stored object head %s (i=%d)", oid, i)
+		}
+		if version != version2 {
+			t.Errorf("object %s has wrong head data (i=%d): %s instead of %s",
+				oid, i, version2, version)
+		}
+	}
+}
+
+// TestLocalUpdates tests the sync handling of initial local updates: an object
+// is created (v1) and updated twice (v2, v3) on this device.  The DAG should
+// show: v1 -> v2 -> v3 and the head should point to v3.
+func TestLocalUpdates(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+	s := svc.sync
+
+	oid := "foo1"
+
+	if _, err := s.dagReplayCommands(nil, "local-init-00.log.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	// The head must have moved to v3 and the parent map shows the updated DAG.
+	if head, err := getHead(nil, st, oid); err != nil || head != "3" {
+		t.Errorf("invalid object %s head: %s", oid, head)
+	}
+
+	pmap := getParentMap(nil, st, oid, nil)
+
+	exp := map[string][]string{"1": nil, "2": {"1"}, "3": {"2"}}
+
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("invalid object %s parent map: (%v) instead of (%v)", oid, pmap, exp)
+	}
+
+	// Make sure an existing node cannot be added again.
+	tx := st.NewTransaction()
+	if err := s.addNode(nil, tx, oid, "2", "foo", false, []string{"1", "3"}, NoBatchId, nil); err == nil {
+		t.Errorf("addNode() did not fail when given an existing node")
+	}
+
+	// Make sure a new node cannot have more than 2 parents.
+	if err := s.addNode(nil, tx, oid, "4", "foo", false, []string{"1", "2", "3"}, NoBatchId, nil); err == nil {
+		t.Errorf("addNode() did not fail when given 3 parents")
+	}
+
+	// Make sure a new node cannot have an invalid parent.
+	if err := s.addNode(nil, tx, oid, "4", "foo", false, []string{"1", "555"}, NoBatchId, nil); err == nil {
+		t.Errorf("addNode() did not fail when using an invalid parent")
+	}
+
+	// Make sure a new root node (no parents) can be added once a root exists.
+	// For the parents array, check both the "nil" and the empty array as input.
+	if err := s.addNode(nil, tx, oid, "6789", "foo", false, nil, NoBatchId, nil); err != nil {
+		t.Errorf("cannot add another root node (nil parents) for object %s: %v", oid, err)
+	}
+	if err := s.addNode(nil, tx, oid, "9999", "foo", false, []string{}, NoBatchId, nil); err != nil {
+		t.Errorf("cannot add another root node (empty parents) for object %s: %v", oid, err)
+	}
+
+	tx.Abort()
+}
+
+// TestRemoteUpdates tests the sync handling of initial remote updates:
+// an object is created (v1) and updated twice (v2, v3) on another device and
+// we learn about it during sync.  The updated DAG should show: v1 -> v2 -> v3
+// and report no conflicts with the new head pointing at v3.
+func TestRemoteUpdates(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+	s := svc.sync
+
+	oid := "foo1"
+
+	graft, err := s.dagReplayCommands(nil, "remote-init-00.log.sync")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// The head must not have moved (i.e. still undefined) and the parent
+	// map shows the newly grafted DAG fragment.
+	if head, err := getHead(nil, st, oid); err == nil {
+		t.Errorf("object %s head found: %s", oid, head)
+	}
+
+	pmap := getParentMap(nil, st, oid, graft)
+
+	exp := map[string][]string{"1": nil, "2": {"1"}, "3": {"2"}}
+
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("invalid object %s parent map: (%v) instead of (%v)", oid, pmap, exp)
+	}
+
+	// Verify the grafting of remote nodes.
+	g := graft[oid]
+
+	expNewHeads := map[string]bool{"3": true}
+
+	if !reflect.DeepEqual(g.newHeads, expNewHeads) {
+		t.Errorf("object %s has invalid newHeads: (%v) instead of (%v)", oid, g.newHeads, expNewHeads)
+	}
+
+	expGrafts := map[string]uint64{}
+	if !reflect.DeepEqual(g.graftNodes, expGrafts) {
+		t.Errorf("invalid object %s graft: (%v) instead of (%v)", oid, g.graftNodes, expGrafts)
+	}
+
+	// There should be no conflict.
+	isConflict, newHead, oldHead, ancestor, errConflict := hasConflict(nil, st, oid, graft)
+	if !(!isConflict && newHead == "3" && oldHead == NoVersion && ancestor == NoVersion && errConflict == nil) {
+		t.Errorf("object %s: wrong conflict info: flag %t, newHead %s, oldHead %s, ancestor %s, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+
+	if logrec, err := getLogRecKey(nil, st, oid, newHead); err != nil || logrec != "$sync:log:11:3" {
+		t.Errorf("invalid logrec for newhead object %s:%s: %v", oid, newHead, logrec)
+	}
+
+	// Make sure an unknown node cannot become the new head.
+	tx := st.NewTransaction()
+	if err := moveHead(nil, tx, oid, "55"); err == nil {
+		t.Errorf("moveHead() did not fail on an invalid node")
+	}
+	tx.Abort()
+
+	// Then move the head.
+	tx = st.NewTransaction()
+	if err := moveHead(nil, tx, oid, newHead); err != nil {
+		t.Errorf("object %s cannot move head to %s: %v", oid, newHead, err)
+	}
+	tx.Commit()
+}
+
+// TestRemoteNoConflict tests sync of remote updates on top of a local initial
+// state without conflict.  An object is created locally and updated twice
+// (v1 -> v2 -> v3).  Another device, having gotten this info, makes 3 updates
+// on top of that (v3 -> v4 -> v5 -> v6) and sends this info in a later sync.
+// The updated DAG should show (v1 -> v2 -> v3 -> v4 -> v5 -> v6) and report
+// no conflicts with the new head pointing at v6.  It should also report v3 as
+// the graft point on which the new fragment (v4 -> v5 -> v6) gets attached.
+func TestRemoteNoConflict(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+	s := svc.sync
+
+	oid := "foo1"
+
+	if _, err := s.dagReplayCommands(nil, "local-init-00.log.sync"); err != nil {
+		t.Fatal(err)
+	}
+	graft, err := s.dagReplayCommands(nil, "remote-noconf-00.log.sync")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// The head must not have moved (i.e. still at v3) and the parent map
+	// shows the newly grafted DAG fragment on top of the prior DAG.
+	if head, err := getHead(nil, st, oid); err != nil || head != "3" {
+		t.Errorf("object %s has wrong head: %s", oid, head)
+	}
+
+	pmap := getParentMap(nil, st, oid, graft)
+
+	exp := map[string][]string{"1": nil, "2": {"1"}, "3": {"2"}, "4": {"3"}, "5": {"4"}, "6": {"5"}}
+
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("invalid object %s parent map: (%v) instead of (%v)", oid, pmap, exp)
+	}
+
+	// Verify the grafting of remote nodes.
+	g := graft[oid]
+
+	expNewHeads := map[string]bool{"6": true}
+	if !reflect.DeepEqual(g.newHeads, expNewHeads) {
+		t.Errorf("object %s has invalid newHeads: (%v) instead of (%v)", oid, g.newHeads, expNewHeads)
+	}
+
+	expGrafts := map[string]uint64{"3": 2}
+	if !reflect.DeepEqual(g.graftNodes, expGrafts) {
+		t.Errorf("invalid object %s graft: (%v) instead of (%v)", oid, g.graftNodes, expGrafts)
+	}
+
+	// There should be no conflict.
+	isConflict, newHead, oldHead, ancestor, errConflict := hasConflict(nil, st, oid, graft)
+	if !(!isConflict && newHead == "6" && oldHead == "3" && ancestor == NoVersion && errConflict == nil) {
+		t.Errorf("object %s: wrong conflict info: flag %t, newHead %s, oldHead %s, ancestor %s, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+
+	if logrec, err := getLogRecKey(nil, st, oid, oldHead); err != nil || logrec != "$sync:log:10:3" {
+		t.Errorf("invalid logrec for oldhead object %s:%s: %v", oid, oldHead, logrec)
+	}
+	if logrec, err := getLogRecKey(nil, st, oid, newHead); err != nil || logrec != "$sync:log:11:3" {
+		t.Errorf("invalid logrec for newhead object %s:%s: %v", oid, newHead, logrec)
+	}
+
+	// Then move the head.
+	tx := st.NewTransaction()
+	if err := moveHead(nil, tx, oid, newHead); err != nil {
+		t.Errorf("object %s cannot move head to %s: %v", oid, newHead, err)
+	}
+	tx.Commit()
+
+	// Verify that hasConflict() fails without graft data.
+	isConflict, newHead, oldHead, ancestor, errConflict = hasConflict(nil, st, oid, nil)
+	if errConflict == nil {
+		t.Errorf("hasConflict() on %s did not fail w/o graft data: flag %t, newHead %s, oldHead %s, ancestor %s, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+}
+
+// TestRemoteConflict tests sync handling of remote updates that build on the
+// local initial state and trigger a conflict.  An object is created locally
+// and updated twice (v1 -> v2 -> v3).  Another device, having only gotten
+// the v1 -> v2 history, makes 3 updates on top of v2 (v2 -> v4 -> v5 -> v6)
+// and sends this info during a later sync.  Separately, the local device
+// makes a conflicting (concurrent) update v2 -> v3.  The updated DAG should
+// show the branches: (v1 -> v2 -> v3) and (v1 -> v2 -> v4 -> v5 -> v6) and
+// report the conflict between v3 and v6 (current and new heads).  It should
+// also report v2 as the graft point and the common ancestor in the conflict.
+// The conflict is resolved locally by creating v7 that is derived from both
+// v3 and v6 and it becomes the new head.
+func TestRemoteConflict(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+	s := svc.sync
+
+	oid := "foo1"
+
+	if _, err := s.dagReplayCommands(nil, "local-init-00.log.sync"); err != nil {
+		t.Fatal(err)
+	}
+	graft, err := s.dagReplayCommands(nil, "remote-conf-00.log.sync")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// The head must not have moved (i.e. still at v3) and the parent map
+	// shows the newly grafted DAG fragment on top of the prior DAG.
+	if head, err := getHead(nil, st, oid); err != nil || head != "3" {
+		t.Errorf("object %s has wrong head: %s", oid, head)
+	}
+
+	pmap := getParentMap(nil, st, oid, graft)
+
+	exp := map[string][]string{"1": nil, "2": {"1"}, "3": {"2"}, "4": {"2"}, "5": {"4"}, "6": {"5"}}
+
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("invalid object %s parent map: (%v) instead of (%v)", oid, pmap, exp)
+	}
+
+	// Verify the grafting of remote nodes.
+	g := graft[oid]
+
+	expNewHeads := map[string]bool{"3": true, "6": true}
+	if !reflect.DeepEqual(g.newHeads, expNewHeads) {
+		t.Errorf("object %s has invalid newHeads: (%v) instead of (%v)", oid, g.newHeads, expNewHeads)
+	}
+
+	expGrafts := map[string]uint64{"2": 1}
+	if !reflect.DeepEqual(g.graftNodes, expGrafts) {
+		t.Errorf("invalid object %s graft: (%v) instead of (%v)", oid, g.graftNodes, expGrafts)
+	}
+
+	// There should be a conflict between v3 and v6 with v2 as ancestor.
+	isConflict, newHead, oldHead, ancestor, errConflict := hasConflict(nil, st, oid, graft)
+	if !(isConflict && newHead == "6" && oldHead == "3" && ancestor == "2" && errConflict == nil) {
+		t.Errorf("object %s: wrong conflict info: flag %t, newHead %s, oldHead %s, ancestor %s, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+
+	if logrec, err := getLogRecKey(nil, st, oid, oldHead); err != nil || logrec != "$sync:log:10:3" {
+		t.Errorf("invalid logrec for oldhead object %s:%s: %s", oid, oldHead, logrec)
+	}
+	if logrec, err := getLogRecKey(nil, st, oid, newHead); err != nil || logrec != "$sync:log:11:3" {
+		t.Errorf("invalid logrec for newhead object %s:%s: %s", oid, newHead, logrec)
+	}
+	if logrec, err := getLogRecKey(nil, st, oid, ancestor); err != nil || logrec != "$sync:log:10:2" {
+		t.Errorf("invalid logrec for ancestor object %s:%s: %s", oid, ancestor, logrec)
+	}
+
+	// Resolve the conflict by adding a new local v7 derived from v3 and v6 (this replay moves the head).
+	if _, err := s.dagReplayCommands(nil, "local-resolve-00.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	// Verify that the head moved to v7 and the parent map shows the resolution.
+	if head, err := getHead(nil, st, oid); err != nil || head != "7" {
+		t.Errorf("object %s has wrong head after conflict resolution: %s", oid, head)
+	}
+
+	exp["7"] = []string{"3", "6"}
+	pmap = getParentMap(nil, st, oid, nil)
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("invalid object %s parent map after conflict resolution: (%v) instead of (%v)",
+			oid, pmap, exp)
+	}
+}
+
+// TestRemoteConflictTwoGrafts tests sync handling of remote updates that build
+// on the local initial state and trigger a conflict with 2 graft points.
+// An object is created locally and updated twice (v1 -> v2 -> v3).  Another
+// device, first learns about v1 and makes it own conflicting update v1 -> v4.
+// That remote device later learns about v2 and resolves the v2/v4 confict by
+// creating v5.  Then it makes a last v5 -> v6 update -- which will conflict
+// with v3 but it doesn't know that.
+// Now the sync order is reversed and the local device learns all of what
+// happened on the remote device.  The local DAG should get be augmented by
+// a subtree with 2 graft points: v1 and v2.  It receives this new branch:
+// v1 -> v4 -> v5 -> v6.  Note that v5 is also derived from v2 as a remote
+// conflict resolution.  This should report a conflict between v3 and v6
+// (current and new heads), with v1 and v2 as graft points, and v2 as the
+// most-recent common ancestor for that conflict.  The conflict is resolved
+// locally by creating v7, derived from both v3 and v6, becoming the new head.
+func TestRemoteConflictTwoGrafts(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+	s := svc.sync
+
+	oid := "foo1"
+
+	if _, err := s.dagReplayCommands(nil, "local-init-00.log.sync"); err != nil {
+		t.Fatal(err)
+	}
+	graft, err := s.dagReplayCommands(nil, "remote-conf-01.log.sync")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// The head must not have moved (i.e. still at v3) and the parent map
+	// shows the newly grafted DAG fragment on top of the prior DAG.
+	if head, err := getHead(nil, st, oid); err != nil || head != "3" {
+		t.Errorf("object %s has wrong head: %s", oid, head)
+	}
+
+	pmap := getParentMap(nil, st, oid, graft)
+
+	exp := map[string][]string{"1": nil, "2": {"1"}, "3": {"2"}, "4": {"1"}, "5": {"2", "4"}, "6": {"5"}}
+
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("invalid object %s parent map: (%v) instead of (%v)", oid, pmap, exp)
+	}
+
+	// Verify the grafting of remote nodes.
+	g := graft[oid]
+
+	expNewHeads := map[string]bool{"3": true, "6": true}
+	if !reflect.DeepEqual(g.newHeads, expNewHeads) {
+		t.Errorf("object %s has invalid newHeads: (%v) instead of (%v)", oid, g.newHeads, expNewHeads)
+	}
+
+	expGrafts := map[string]uint64{"1": 0, "2": 1}
+	if !reflect.DeepEqual(g.graftNodes, expGrafts) {
+		t.Errorf("invalid object %s graft: (%v) instead of (%v)", oid, g.graftNodes, expGrafts)
+	}
+
+	// There should be a conflict between v3 and v6 with v2 as ancestor.
+	isConflict, newHead, oldHead, ancestor, errConflict := hasConflict(nil, st, oid, graft)
+	if !(isConflict && newHead == "6" && oldHead == "3" && ancestor == "2" && errConflict == nil) {
+		t.Errorf("object %s: wrong conflict info: flag %t, newHead %s, oldHead %s, ancestor %s, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+
+	if logrec, err := getLogRecKey(nil, st, oid, oldHead); err != nil || logrec != "$sync:log:10:3" {
+		t.Errorf("invalid logrec for oldhead object %s:%s: %s", oid, oldHead, logrec)
+	}
+	if logrec, err := getLogRecKey(nil, st, oid, newHead); err != nil || logrec != "$sync:log:11:2" {
+		t.Errorf("invalid logrec for newhead object %s:%s: %s", oid, newHead, logrec)
+	}
+	if logrec, err := getLogRecKey(nil, st, oid, ancestor); err != nil || logrec != "$sync:log:10:2" {
+		t.Errorf("invalid logrec for ancestor object %s:%s: %s", oid, ancestor, logrec)
+	}
+
+	// Resolve the conflict by adding a new local v7 derived from v3 and v6 (this replay moves the head).
+	if _, err := s.dagReplayCommands(nil, "local-resolve-00.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	// Verify that the head moved to v7 and the parent map shows the resolution.
+	if head, err := getHead(nil, st, oid); err != nil || head != "7" {
+		t.Errorf("object %s has wrong head after conflict resolution: %s", oid, head)
+	}
+
+	exp["7"] = []string{"3", "6"}
+	pmap = getParentMap(nil, st, oid, nil)
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("invalid object %s parent map after conflict resolution: (%v) instead of (%v)",
+			oid, pmap, exp)
+	}
+}
+
+// TestRemoteConflictNoAncestor tests sync handling of remote updates that create
+// the same object independently from local initial state (no common past) and
+// trigger a conflict with no common ancestors (no graft points).  An object is
+// created locally and updated twice (v1 -> v2 -> v3).  Another device creates
+// the same object from scratch and updates it twice (v4 -> v5 -> v6).  When
+// the local device learns of what happened on the remote device, it should
+// detect a conflict between v3 and v6 with no common ancestor.  The conflict
+// is resolved locally by creating v7, derived from both v3 and v6, becoming
+// the new head.
+func TestRemoteConflictNoAncestor(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+	s := svc.sync
+
+	oid := "foo1"
+
+	if _, err := s.dagReplayCommands(nil, "local-init-00.log.sync"); err != nil {
+		t.Fatal(err)
+	}
+	graft, err := s.dagReplayCommands(nil, "remote-conf-03.log.sync")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// The head must not have moved (i.e. still at v3) and the parent map
+	// shows the newly grafted DAG fragment on top of the prior DAG.
+	if head, err := getHead(nil, st, oid); err != nil || head != "3" {
+		t.Errorf("object %s has wrong head: %s", oid, head)
+	}
+
+	pmap := getParentMap(nil, st, oid, graft)
+
+	exp := map[string][]string{"1": nil, "2": {"1"}, "3": {"2"}, "4": nil, "5": {"4"}, "6": {"5"}}
+
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("invalid object %s parent map: (%v) instead of (%v)", oid, pmap, exp)
+	}
+
+	// Verify the grafting of remote nodes.
+	g := graft[oid]
+
+	expNewHeads := map[string]bool{"3": true, "6": true}
+	if !reflect.DeepEqual(g.newHeads, expNewHeads) {
+		t.Errorf("object %s has invalid newHeads: (%v) instead of (%v)", oid, g.newHeads, expNewHeads)
+	}
+
+	expGrafts := map[string]uint64{}
+	if !reflect.DeepEqual(g.graftNodes, expGrafts) {
+		t.Errorf("invalid object %s graft: (%v) instead of (%v)", oid, g.graftNodes, expGrafts)
+	}
+
+	// There should be a conflict between v3 and v6 with no ancestor.
+	isConflict, newHead, oldHead, ancestor, errConflict := hasConflict(nil, st, oid, graft)
+	if !(isConflict && newHead == "6" && oldHead == "3" && ancestor == NoVersion && errConflict == nil) {
+		t.Errorf("object %s: wrong conflict info: flag %t, newHead %s, oldHead %s, ancestor %s, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+
+	if logrec, err := getLogRecKey(nil, st, oid, oldHead); err != nil || logrec != "$sync:log:10:3" {
+		t.Errorf("invalid logrec for oldhead object %s:%s: %s", oid, oldHead, logrec)
+	}
+	if logrec, err := getLogRecKey(nil, st, oid, newHead); err != nil || logrec != "$sync:log:11:3" {
+		t.Errorf("invalid logrec for newhead object %s:%s: %s", oid, newHead, logrec)
+	}
+
+	// Resolve the conflict by adding a new local v7 derived from v3 and v6 (this replay moves the head).
+	if _, err := s.dagReplayCommands(nil, "local-resolve-00.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	// Verify that the head moved to v7 and the parent map shows the resolution.
+	if head, err := getHead(nil, st, oid); err != nil || head != "7" {
+		t.Errorf("object %s has wrong head after conflict resolution: %s", oid, head)
+	}
+
+	exp["7"] = []string{"3", "6"}
+	pmap = getParentMap(nil, st, oid, nil)
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("invalid object %s parent map after conflict resolution: (%v) instead of (%v)",
+			oid, pmap, exp)
+	}
+}
+
+// TestAncestorIterator checks that the iterator goes over the correct set
+// of ancestor nodes for an object given a starting node.  It should traverse
+// reconvergent DAG branches only visiting each ancestor once:
+// v1 -> v2 -> v3 -> v5 -> v6 -> v8 -> v9
+//        |--> v4 ---|           |
+//        +--> v7 ---------------+
+// - Starting at v1 it should only cover v1.
+// - Starting at v3 it should only cover v1-v3.
+// - Starting at v6 it should only cover v1-v6.
+// - Starting at v9 it should cover all nodes (v1-v9).
+func TestAncestorIterator(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+	s := svc.sync
+
+	oid := "1234"
+
+	if _, err := s.dagReplayCommands(nil, "local-init-01.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	// Loop checking the iteration behavior for different starting nodes.
+	for _, start := range []int{1, 3, 6, 9} {
+		visitCount := make(map[string]int)
+		vstart := fmt.Sprintf("%d", start)
+		forEachAncestor(nil, st, oid, []string{vstart}, func(v string, nd *dagNode) error {
+			visitCount[v]++
+			return nil
+		})
+
+		// Check that all prior nodes are visited only once.
+		for i := 1; i < (start + 1); i++ {
+			vv := fmt.Sprintf("%d", i)
+			if visitCount[vv] != 1 {
+				t.Errorf("wrong visit count on object %s:%s starting from %s: %d instead of 1",
+					oid, vv, vstart, visitCount[vv])
+			}
+		}
+	}
+
+	// Make sure an error in the callback is returned.
+	cbErr := errors.New("callback error")
+	err := forEachAncestor(nil, st, oid, []string{"9"}, func(v string, nd *dagNode) error {
+		if v == "1" {
+			return cbErr
+		}
+		return nil
+	})
+	if err != cbErr {
+		t.Errorf("wrong error returned from callback: %v instead of %v", err, cbErr)
+	}
+}
+
+// TestPruning tests sync pruning of the DAG for an object with 3 concurrent
+// updates (i.e. 2 conflict resolution convergent points).  The pruning must
+// get rid of the DAG branches across the reconvergence points:
+// v1 -> v2 -> v3 -> v5 -> v6 -> v8 -> v9
+//        |--> v4 ---|           |
+//        +--> v7 ---------------+
+// By pruning at v1, nothing is deleted.
+// Then by pruning at v2, only v1 is deleted.
+// Then by pruning at v6, v2-v5 are deleted leaving v6 and "v7 -> v8 -> v9".
+// Then by pruning at v8, v6-v7 are deleted leaving "v8 -> v9".
+// Then by pruning at v9, v8 is deleted leaving v9 as the head.
+// Then by pruning again at v9 nothing changes.
+func TestPruning(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+	s := svc.sync
+
+	oid := "1234"
+
+	if _, err := s.dagReplayCommands(nil, "local-init-01.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	exp := map[string][]string{"1": nil, "2": {"1"}, "3": {"2"}, "4": {"2"}, "5": {"3", "4"}, "6": {"5"}, "7": {"2"}, "8": {"6", "7"}, "9": {"8"}}
+
+	// Loop pruning at an invalid version (333) then at different valid versions.
+	testVersions := []string{"333", "1", "2", "6", "8", "9", "9"}
+	delCounts := []int{0, 0, 1, 4, 2, 1, 0}
+	which := "prune-snip-"
+	remain := 9
+
+	for i, version := range testVersions {
+		batches := newBatchPruning()
+		tx := st.NewTransaction()
+		del := 0
+		err := prune(nil, tx, oid, version, batches,
+			func(ctx *context.T, tx store.Transaction, lr string) error {
+				del++
+				return nil
+			})
+		tx.Commit()
+
+		if i == 0 && err == nil {
+			t.Errorf("pruning non-existent object %s:%s did not fail", oid, version)
+		} else if i > 0 && err != nil {
+			t.Errorf("pruning object %s:%s failed: %v", oid, version, err)
+		}
+
+		if del != delCounts[i] {
+			t.Errorf("pruning object %s:%s deleted %d log records instead of %d",
+				oid, version, del, delCounts[i])
+		}
+
+		which += "*"
+		remain -= del
+
+		if head, err := getHead(nil, st, oid); err != nil || head != "9" {
+			t.Errorf("object %s has wrong head: %s", oid, head)
+		}
+
+		tx = st.NewTransaction()
+		err = pruneDone(nil, tx, batches)
+		if err != nil {
+			t.Errorf("pruneDone() failed: %v", err)
+		}
+		tx.Commit()
+
+		// Remove pruned nodes from the expected parent map used to validate
+		// and set the parents of the pruned node to nil.
+		intVersion, err := strconv.ParseInt(version, 10, 32)
+		if err != nil {
+			t.Errorf("invalid version: %s", version)
+		}
+
+		if intVersion < 10 {
+			for j := int64(0); j < intVersion; j++ {
+				delete(exp, fmt.Sprintf("%d", j))
+			}
+			exp[version] = nil
+		}
+
+		pmap := getParentMap(nil, st, oid, nil)
+		if !reflect.DeepEqual(pmap, exp) {
+			t.Errorf("invalid object %s parent map: (%v) instead of (%v)", oid, pmap, exp)
+		}
+	}
+}
+
+// TestPruningCallbackError tests sync pruning of the DAG when the callback
+// function returns an error.  The pruning must try to delete as many nodes
+// and log records as possible and properly adjust the parent pointers of
+// the pruning node.  The object DAG is:
+// v1 -> v2 -> v3 -> v5 -> v6 -> v8 -> v9
+//        |--> v4 ---|           |
+//        +--> v7 ---------------+
+// By pruning at v9 and having the callback function fail for v4, all other
+// nodes must be deleted and only v9 remains as the head.
+func TestPruningCallbackError(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+	s := svc.sync
+
+	oid := "1234"
+
+	if _, err := s.dagReplayCommands(nil, "local-init-01.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	exp := map[string][]string{"9": nil}
+
+	// Prune at v9 with a callback function that fails for v4.
+	del, expDel := 0, 8
+	version := "9"
+
+	batches := newBatchPruning()
+	tx := st.NewTransaction()
+	err := prune(nil, tx, oid, version, batches,
+		func(ctx *context.T, tx store.Transaction, lr string) error {
+			del++
+			if lr == "logrec-03" {
+				return fmt.Errorf("refuse to delete %s", lr)
+			}
+			return nil
+		})
+	tx.Commit()
+
+	if err == nil {
+		t.Errorf("pruning object %s:%s did not fail", oid, version)
+	}
+	if del != expDel {
+		t.Errorf("pruning object %s:%s deleted %d log records instead of %d", oid, version, del, expDel)
+	}
+
+	tx = st.NewTransaction()
+	err = pruneDone(nil, tx, batches)
+	if err != nil {
+		t.Errorf("pruneDone() failed: %v", err)
+	}
+	tx.Commit()
+
+	if head, err := getHead(nil, st, oid); err != nil || head != version {
+		t.Errorf("object %s has wrong head: %s", oid, head)
+	}
+
+	pmap := getParentMap(nil, st, oid, nil)
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("invalid object %s parent map: (%v) instead of (%v)", oid, pmap, exp)
+	}
+}
+
+// TestRemoteLinkedNoConflictSameHead tests sync of remote updates that contain
+// linked nodes (conflict resolution by selecting an existing version) on top of
+// a local initial state without conflict.  An object is created locally and
+// updated twice (v1 -> v2 -> v3).  Another device learns about v1, then creates
+// (v1 -> v4), then learns about (v1 -> v2) and resolves the (v2/v4) conflict by
+// selecting v2 over v4.  It sends that new info (v4 and the v2/v4 link) back to
+// the original (local) device.  Instead of a v3/v4 conflict, the device sees
+// that v2 was chosen over v4 and resolves it as a no-conflict case.
+func TestRemoteLinkedNoConflictSameHead(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+	s := svc.sync
+
+	oid := "foo1"
+
+	if _, err := s.dagReplayCommands(nil, "local-init-00.log.sync"); err != nil {
+		t.Fatal(err)
+	}
+	graft, err := s.dagReplayCommands(nil, "remote-noconf-link-00.log.sync")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// The head must not have moved (i.e. still at v3) and the parent map
+	// shows the newly grafted DAG fragment on top of the prior DAG.
+	if head, err := getHead(nil, st, oid); err != nil || head != "3" {
+		t.Errorf("object %s has wrong head: %s", oid, head)
+	}
+
+	pmap := getParentMap(nil, st, oid, graft)
+
+	exp := map[string][]string{"1": nil, "2": {"1", "4"}, "3": {"2"}, "4": {"1"}}
+
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("invalid object %s parent map: (%v) instead of (%v)", oid, pmap, exp)
+	}
+
+	// Verify the grafting of remote nodes.
+	g := graft[oid]
+
+	expNewHeads := map[string]bool{"3": true}
+	if !reflect.DeepEqual(g.newHeads, expNewHeads) {
+		t.Errorf("object %s has invalid newHeads: (%v) instead of (%v)", oid, g.newHeads, expNewHeads)
+	}
+
+	expGrafts := map[string]uint64{"1": 0, "4": 1}
+	if !reflect.DeepEqual(g.graftNodes, expGrafts) {
+		t.Errorf("invalid object %s graft: (%v) instead of (%v)", oid, g.graftNodes, expGrafts)
+	}
+
+	// There should be no conflict.
+	isConflict, newHead, oldHead, ancestor, errConflict := hasConflict(nil, st, oid, graft)
+	if !(!isConflict && newHead == "3" && oldHead == "3" && ancestor == NoVersion && errConflict == nil) {
+		t.Errorf("object %s: wrong conflict info: flag %t, newHead %s, oldHead %s, ancestor %s, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+
+	// Verify that hasConflict() fails with a nil or empty graft map.
+	isConflict, newHead, oldHead, ancestor, errConflict = hasConflict(nil, st, oid, nil)
+	if errConflict == nil {
+		t.Errorf("hasConflict() on %v did not fail with a nil graft map: flag %t, newHead %s, oldHead %s, ancestor %s, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+	isConflict, newHead, oldHead, ancestor, errConflict = hasConflict(nil, st, oid, newGraft())
+	if errConflict == nil {
+		t.Errorf("hasConflict() on %v did not fail with an empty graft map: flag %t, newHead %s, oldHead %s, ancestor %s, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+}
+
+// TestRemoteLinkedConflict tests sync of remote updates that contain linked
+// nodes (conflict resolution by selecting an existing version) on top of a local
+// initial state triggering a local conflict.  An object is created locally and
+// updated twice (v1 -> v2 -> v3).  Another device has along the way learned
+// about v1, created (v1 -> v4), then learned about (v1 -> v2) and resolved that
+// conflict by selecting v4 over v2.  Now it sends that new info (v4 and the
+// v4/v2 link) back to the original (local) device which sees a v3/v4 conflict.
+func TestRemoteLinkedConflict(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+	s := svc.sync
+
+	oid := "foo1"
+
+	if _, err := s.dagReplayCommands(nil, "local-init-00.log.sync"); err != nil {
+		t.Fatal(err)
+	}
+	graft, err := s.dagReplayCommands(nil, "remote-conf-link.log.sync")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// The head must not have moved (i.e. still at v2) and the parent map
+	// shows the newly grafted DAG fragment on top of the prior DAG.
+	if head, err := getHead(nil, st, oid); err != nil || head != "3" {
+		t.Errorf("object %s has wrong head: %s", oid, head)
+	}
+
+	pmap := getParentMap(nil, st, oid, graft)
+
+	exp := map[string][]string{"1": nil, "2": {"1"}, "3": {"2"}, "4": {"1", "2"}}
+
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("invalid object %s parent map: (%v) instead of (%v)", oid, pmap, exp)
+	}
+
+	// Verify the grafting of remote nodes.
+	g := graft[oid]
+
+	expNewHeads := map[string]bool{"3": true, "4": true}
+	if !reflect.DeepEqual(g.newHeads, expNewHeads) {
+		t.Errorf("object %s has invalid newHeads: (%v) instead of (%v)", oid, g.newHeads, expNewHeads)
+	}
+
+	expGrafts := map[string]uint64{"1": 0, "2": 1}
+	if !reflect.DeepEqual(g.graftNodes, expGrafts) {
+		t.Errorf("invalid object %s graft: (%v) instead of (%v)", oid, g.graftNodes, expGrafts)
+	}
+
+	// There should be a conflict.
+	isConflict, newHead, oldHead, ancestor, errConflict := hasConflict(nil, st, oid, graft)
+	if !(isConflict && newHead == "4" && oldHead == "3" && ancestor == "2" && errConflict == nil) {
+		t.Errorf("object %s: wrong conflict info: flag %t, newHead %s, oldHead %s, ancestor %s, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+}
+
+// TestRemoteLinkedNoConflictNewHead tests sync of remote updates that contain
+// linked nodes (conflict resolution by selecting an existing version) on top of
+// a local initial state without conflict, but moves the head node to a new one.
+// An object is created locally and updated twice (v1 -> v2 -> v3).  Another
+// device has along the way learned about v1, created (v1 -> v4), then learned
+// about (v1 -> v2 -> v3) and resolved that conflict by selecting v4 over v3.
+// Now it sends that new info (v4 and the v4/v3 link) back to the original
+// (local) device.  The device sees that the new head v4 is "derived" from v3
+// thus no conflict.
+func TestRemoteLinkedConflictNewHead(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+	s := svc.sync
+
+	oid := "foo1"
+
+	if _, err := s.dagReplayCommands(nil, "local-init-00.log.sync"); err != nil {
+		t.Fatal(err)
+	}
+	graft, err := s.dagReplayCommands(nil, "remote-noconf-link-01.log.sync")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// The head must not have moved (i.e. still at v2) and the parent map
+	// shows the newly grafted DAG fragment on top of the prior DAG.
+	if head, err := getHead(nil, st, oid); err != nil || head != "3" {
+		t.Errorf("object %s has wrong head: %s", oid, head)
+	}
+
+	pmap := getParentMap(nil, st, oid, graft)
+
+	exp := map[string][]string{"1": nil, "2": {"1"}, "3": {"2"}, "4": {"1", "3"}}
+
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("invalid object %s parent map: (%v) instead of (%v)", oid, pmap, exp)
+	}
+
+	// Verify the grafting of remote nodes.
+	g := graft[oid]
+
+	expNewHeads := map[string]bool{"4": true}
+	if !reflect.DeepEqual(g.newHeads, expNewHeads) {
+		t.Errorf("object %s has invalid newHeads: (%v) instead of (%v)", oid, g.newHeads, expNewHeads)
+	}
+
+	expGrafts := map[string]uint64{"1": 0, "3": 2}
+	if !reflect.DeepEqual(g.graftNodes, expGrafts) {
+		t.Errorf("invalid object %s graft: (%v) instead of (%v)", oid, g.graftNodes, expGrafts)
+	}
+
+	// There should be no conflict.
+	isConflict, newHead, oldHead, ancestor, errConflict := hasConflict(nil, st, oid, graft)
+	if !(!isConflict && newHead == "4" && oldHead == "3" && ancestor == NoVersion && errConflict == nil) {
+		t.Errorf("object %s: wrong conflict info: flag %t, newHead %s, oldHead %s, ancestor %s, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+}
+
+// TestRemoteLinkedNoConflictNewHeadOvertake tests sync of remote updates that
+// contain linked nodes (conflict resolution by selecting an existing version)
+// on top of a local initial state without conflict, but moves the head node
+// to a new one that overtook the linked node.
+//
+// An object is created locally and updated twice (v1 -> v2 -> v3).  Another
+// device has along the way learned about v1, created (v1 -> v4), then learned
+// about (v1 -> v2 -> v3) and resolved that conflict by selecting v3 over v4.
+// Then it creates a new update v5 from v3 (v3 -> v5).  Now it sends that new
+// info (v4, the v3/v4 link, and v5) back to the original (local) device.
+// The device sees that the new head v5 is "derived" from v3 thus no conflict.
+func TestRemoteLinkedConflictNewHeadOvertake(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+	s := svc.sync
+
+	oid := "foo1"
+
+	if _, err := s.dagReplayCommands(nil, "local-init-00.log.sync"); err != nil {
+		t.Fatal(err)
+	}
+	graft, err := s.dagReplayCommands(nil, "remote-noconf-link-02.log.sync")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// The head must not have moved (i.e. still at v2) and the parent map
+	// shows the newly grafted DAG fragment on top of the prior DAG.
+	if head, err := getHead(nil, st, oid); err != nil || head != "3" {
+		t.Errorf("object %s has wrong head: %s", oid, head)
+	}
+
+	pmap := getParentMap(nil, st, oid, graft)
+
+	exp := map[string][]string{"1": nil, "2": {"1"}, "3": {"2", "4"}, "4": {"1"}, "5": {"3"}}
+
+	if !reflect.DeepEqual(pmap, exp) {
+		t.Errorf("invalid object %s parent map: (%v) instead of (%v)", oid, pmap, exp)
+	}
+
+	// Verify the grafting of remote nodes.
+	g := graft[oid]
+
+	expNewHeads := map[string]bool{"5": true}
+	if !reflect.DeepEqual(g.newHeads, expNewHeads) {
+		t.Errorf("object %s has invalid newHeads: (%v) instead of (%v)", oid, g.newHeads, expNewHeads)
+	}
+
+	expGrafts := map[string]uint64{"1": 0, "3": 2, "4": 1}
+	if !reflect.DeepEqual(g.graftNodes, expGrafts) {
+		t.Errorf("invalid object %s graft: (%v) instead of (%v)", oid, g.graftNodes, expGrafts)
+	}
+
+	// There should be no conflict.
+	isConflict, newHead, oldHead, ancestor, errConflict := hasConflict(nil, st, oid, graft)
+	if !(!isConflict && newHead == "5" && oldHead == "3" && ancestor == NoVersion && errConflict == nil) {
+		t.Errorf("object %s: wrong conflict info: flag %t, newHead %s, oldHead %s, ancestor %s, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+
+	// Move the head.
+	tx := st.NewTransaction()
+	if err = moveHead(nil, tx, oid, newHead); err != nil {
+		t.Errorf("object %s cannot move head to %s: %v", oid, newHead, err)
+	}
+	tx.Commit()
+
+	// Now new info comes from another device repeating the v2/v3 link.
+	// Verify that it is a NOP (no changes).
+	graft, err = s.dagReplayCommands(nil, "remote-noconf-link-repeat.log.sync")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if head, err := getHead(nil, st, oid); err != nil || head != "5" {
+		t.Errorf("object %s has wrong head: %s", oid, head)
+	}
+
+	g = graft[oid]
+
+	if !reflect.DeepEqual(g.newHeads, expNewHeads) {
+		t.Errorf("object %s has invalid newHeads: (%v) instead of (%v)", oid, g.newHeads, expNewHeads)
+	}
+
+	expGrafts = map[string]uint64{}
+	if !reflect.DeepEqual(g.graftNodes, expGrafts) {
+		t.Errorf("invalid object %s graft: (%v) instead of (%v)", oid, g.graftNodes, expGrafts)
+	}
+
+	isConflict, newHead, oldHead, ancestor, errConflict = hasConflict(nil, st, oid, graft)
+	if !(!isConflict && newHead == "5" && oldHead == "5" && ancestor == NoVersion && errConflict == nil) {
+		t.Errorf("object %s: wrong conflict info: flag %t, newHead %s, oldHead %s, ancestor %s, err %v",
+			oid, isConflict, newHead, oldHead, ancestor, errConflict)
+	}
+}
+
+// TestAddNodeBatch tests adding multiple DAG nodes grouped within a batch.
+func TestAddNodeBatch(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+	s := svc.sync
+
+	if _, err := s.dagReplayCommands(nil, "local-init-02.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	oid_a, oid_b, oid_c := "1234", "6789", "2222"
+
+	tx := st.NewTransaction()
+
+	// Verify NoBatchId is reported as an error.
+	if err := s.endBatch(nil, tx, NoBatchId, 0); err == nil {
+		t.Errorf("endBatch() did not fail for invalid 'NoBatchId' value")
+	}
+	if _, err := getBatch(nil, st, NoBatchId); err == nil {
+		t.Errorf("getBatch() did not fail for invalid 'NoBatchId' value")
+	}
+	if err := setBatch(nil, tx, NoBatchId, nil); err == nil {
+		t.Errorf("setBatch() did not fail for invalid 'NoBatchId' value")
+	}
+	if err := delBatch(nil, tx, NoBatchId); err == nil {
+		t.Errorf("delBatch() did not fail for invalid 'NoBatchId' value")
+	}
+
+	// Mutate 2 objects within a batch.
+	btid_1 := s.startBatch(nil, st, NoBatchId)
+	if btid_1 == NoBatchId {
+		t.Fatal("cannot start 1st DAG batch")
+	}
+	if err := s.endBatch(nil, tx, btid_1, 0); err == nil {
+		t.Errorf("endBatch() did not fail for a zero-count batch")
+	}
+
+	info := s.batches[btid_1]
+	if info == nil {
+		t.Errorf("batches state for ID %v not found", btid_1)
+	}
+	if n := len(info.Objects); n != 0 {
+		t.Errorf("batch info map for ID %v has length %d instead of 0", btid_1, n)
+	}
+
+	if err := s.addNode(nil, tx, oid_a, "3", "logrec-a-03", false, []string{"2"}, btid_1, nil); err != nil {
+		t.Errorf("cannot addNode() on object %s and batch ID %v: %v", oid_a, btid_1, err)
+	}
+
+	if id := s.startBatch(nil, st, btid_1); id != btid_1 {
+		t.Fatalf("restarting batch failed: got %v instead of %v", id, btid_1)
+	}
+
+	if err := s.addNode(nil, tx, oid_b, "3", "logrec-b-03", false, []string{"2"}, btid_1, nil); err != nil {
+		t.Errorf("cannot addNode() on object %s and batch ID %v: %v", oid_b, btid_1, err)
+	}
+
+	// At the same time mutate the 3rd object in another batch.
+	btid_2 := s.startBatch(nil, st, NoBatchId)
+	if btid_2 == NoBatchId {
+		t.Fatal("cannot start 2nd DAG batch")
+	}
+
+	info = s.batches[btid_2]
+	if info == nil {
+		t.Errorf("batches state for ID %v not found", btid_2)
+	}
+	if n := len(info.Objects); n != 0 {
+		t.Errorf("batch info map for ID %v has length %d instead of 0", btid_2, n)
+	}
+
+	if err := s.addNode(nil, tx, oid_c, "2", "logrec-c-02", false, []string{"1"}, btid_2, nil); err != nil {
+		t.Errorf("cannot addNode() on object %s and batch ID %v: %v", oid_c, btid_2, err)
+	}
+
+	// Verify the in-memory batch sets constructed.
+	info = s.batches[btid_1]
+	if info == nil {
+		t.Errorf("batches state for ID %v not found", btid_1)
+	}
+
+	expInfo := &batchInfo{map[string]string{oid_a: "3", oid_b: "3"}, 0}
+	if !reflect.DeepEqual(info, expInfo) {
+		t.Errorf("invalid batch info for ID %v: %v instead of %v", btid_1, info, expInfo)
+	}
+
+	info = s.batches[btid_2]
+	if info == nil {
+		t.Errorf("batches state for ID %v not found", btid_2)
+	}
+
+	expInfo = &batchInfo{map[string]string{oid_c: "2"}, 0}
+	if !reflect.DeepEqual(info, expInfo) {
+		t.Errorf("invalid batch info for ID %v: %v instead of %v", btid_2, info, expInfo)
+	}
+
+	// Verify failing to use a batch ID not returned by startBatch().
+	bad_btid := btid_1 + 1
+	for bad_btid == NoBatchId || bad_btid == btid_2 {
+		bad_btid++
+	}
+
+	if err := s.addNode(nil, tx, oid_c, "3", "logrec-c-03", false, []string{"2"}, bad_btid, nil); err == nil {
+		t.Errorf("addNode() did not fail on object %s for a bad batch ID %v", oid_c, bad_btid)
+	}
+	if err := s.endBatch(nil, tx, bad_btid, 1); err == nil {
+		t.Errorf("endBatch() did not fail for a bad batch ID %v", bad_btid)
+	}
+
+	// End the 1st batch and verify the in-memory and in-store data.
+	if err := s.endBatch(nil, tx, btid_1, 2); err != nil {
+		t.Errorf("cannot endBatch() for ID %v: %v", btid_1, err)
+	}
+	tx.Commit()
+
+	if info = s.batches[btid_1]; info != nil {
+		t.Errorf("batch info for ID %v still exists", btid_1)
+	}
+
+	info, err := getBatch(nil, st, btid_1)
+	if err != nil {
+		t.Errorf("cannot getBatch() for ID %v: %v", btid_1, err)
+	}
+
+	expInfo = &batchInfo{map[string]string{oid_a: "3", oid_b: "3"}, 2}
+	if !reflect.DeepEqual(info, expInfo) {
+		t.Errorf("invalid batch info from DAG storage for ID %v: %v instead of %v",
+			btid_1, info, expInfo)
+	}
+
+	info = s.batches[btid_2]
+	if info == nil {
+		t.Errorf("batches state for ID %v not found", btid_2)
+	}
+
+	expInfo = &batchInfo{map[string]string{oid_c: "2"}, 0}
+	if !reflect.DeepEqual(info, expInfo) {
+		t.Errorf("invalid batch info for ID %v: %v instead of %v", btid_2, info, expInfo)
+	}
+
+	// End the 2nd batch and re-verify the in-memory and in-store data.
+	tx = st.NewTransaction()
+	if err := s.endBatch(nil, tx, btid_2, 1); err != nil {
+		t.Errorf("cannot endBatch() for ID %v: %v", btid_2, err)
+	}
+	tx.Commit()
+
+	if info = s.batches[btid_2]; info != nil {
+		t.Errorf("batch info for ID %v still exists", btid_2)
+	}
+
+	info, err = getBatch(nil, st, btid_2)
+	if err != nil {
+		t.Errorf("cannot getBatch() for ID %v: %v", btid_2, err)
+	}
+
+	expInfo = &batchInfo{map[string]string{oid_c: "2"}, 1}
+	if !reflect.DeepEqual(info, expInfo) {
+		t.Errorf("invalid batch info from DAG storage for ID %v: %v instead of %v", btid_2, info, expInfo)
+	}
+
+	if n := len(s.batches); n != 0 {
+		t.Errorf("batches set in-memory: %d entries found, should be empty", n)
+	}
+
+	// Test incrementally filling up a batch.
+	btid_3 := uint64(100)
+	if s.batches[btid_3] != nil {
+		t.Errorf("batch info for ID %v found", btid_3)
+	}
+
+	if id := s.startBatch(nil, st, btid_3); id != btid_3 {
+		t.Fatalf("cannot start batch %v", btid_3)
+	}
+
+	info = s.batches[btid_3]
+	if info == nil {
+		t.Errorf("batches state for ID %v not found", btid_3)
+	}
+	if n := len(info.Objects); n != 0 {
+		t.Errorf("batch info map for ID %v has length %d instead of 0", btid_3, n)
+	}
+
+	tx = st.NewTransaction()
+	if err := s.addNode(nil, tx, oid_a, "4", "logrec-a-04", false, []string{"3"}, btid_3, nil); err != nil {
+		t.Errorf("cannot addNode() on object %s and batch ID %v: %v", oid_a, btid_3, err)
+	}
+
+	if err := s.endBatch(nil, tx, btid_3, 2); err != nil {
+		t.Errorf("cannot endBatch() for ID %v: %v", btid_3, err)
+	}
+	tx.Commit()
+
+	if s.batches[btid_3] != nil {
+		t.Errorf("batch info for ID %v still exists", btid_3)
+	}
+
+	info, err = getBatch(nil, st, btid_3)
+	if err != nil {
+		t.Errorf("cannot getBatch() for ID %v: %v", btid_3, err)
+	}
+
+	expInfo = &batchInfo{map[string]string{oid_a: "4"}, 2}
+	if !reflect.DeepEqual(info, expInfo) {
+		t.Errorf("invalid batch info from DAG storage for ID %v: %v instead of %v",
+			btid_3, info, expInfo)
+	}
+
+	if id := s.startBatch(nil, st, btid_3); id != btid_3 {
+		t.Fatalf("cannot start batch %v", btid_3)
+	}
+
+	info = s.batches[btid_3]
+	if info == nil {
+		t.Errorf("batch state for ID %v not found", btid_3)
+	}
+
+	if !reflect.DeepEqual(info, expInfo) {
+		t.Errorf("invalid batch info from DAG storage for ID %v: %v instead of %v",
+			btid_3, info, expInfo)
+	}
+
+	tx = st.NewTransaction()
+	if err := s.addNode(nil, tx, oid_b, "4", "logrec-b-04", false, []string{"3"}, btid_3, nil); err != nil {
+		t.Errorf("cannot addNode() on object %s and batch ID %v: %v", oid_b, btid_3, err)
+	}
+
+	if err := s.endBatch(nil, tx, btid_3, 3); err == nil {
+		t.Errorf("endBatch() didn't fail for ID %v: %v", btid_3, err)
+	}
+
+	if err := s.endBatch(nil, tx, btid_3, 2); err != nil {
+		t.Errorf("cannot endBatch() for ID %v: %v", btid_3, err)
+	}
+	tx.Commit()
+
+	info, err = getBatch(nil, st, btid_3)
+	if err != nil {
+		t.Errorf("cannot getBatch() for ID %v: %v", btid_3, err)
+	}
+
+	expInfo = &batchInfo{map[string]string{oid_a: "4", oid_b: "4"}, 2}
+	if !reflect.DeepEqual(info, expInfo) {
+		t.Errorf("invalid batch state from DAG storage for ID %v: %v instead of %v",
+			btid_3, info, expInfo)
+	}
+
+	// Get the 3 new nodes from the DAG and verify their batch IDs.
+	type nodeTest struct {
+		oid     string
+		version string
+		btid    uint64
+	}
+	tests := []nodeTest{
+		{oid_a, "3", btid_1},
+		{oid_a, "4", btid_3},
+		{oid_b, "3", btid_1},
+		{oid_b, "4", btid_3},
+		{oid_c, "2", btid_2},
+	}
+
+	for _, test := range tests {
+		node, err := getNode(nil, st, test.oid, test.version)
+		if err != nil {
+			t.Errorf("cannot find object %s:%s: %v", test.oid, test.version, err)
+		}
+		if node.BatchId != test.btid {
+			t.Errorf("invalid batch ID for object %s:%s: %v instead of %v",
+				test.oid, test.version, node.BatchId, test.btid)
+		}
+	}
+}
+
+// TestPruningBatches tests pruning DAG nodes grouped within batches.
+func TestPruningBatches(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+	s := svc.sync
+
+	if _, err := s.dagReplayCommands(nil, "local-init-02.sync"); err != nil {
+		t.Fatal(err)
+	}
+
+	oid_a, oid_b, oid_c := "1234", "6789", "2222"
+
+	// Mutate objects in 2 batches then add non-batch mutations to act as
+	// the pruning points.  Before pruning the DAG is:
+	// a1 -- a2 -- (a3) --- a4
+	// b1 -- b2 -- (b3) -- (b4) -- b5
+	// c1 ---------------- (c2)
+	// Now by pruning at (a4, b5, c2), the new DAG should be:
+	// a4
+	// b5
+	// (c2)
+	// Batch 1 (a3, b3) gets deleted, but batch 2 (b4, c2) still has (c2)
+	// dangling waiting for a future pruning.
+	btid_1 := s.startBatch(nil, st, NoBatchId)
+	if btid_1 == NoBatchId {
+		t.Fatal("cannot start 1st DAG addNode() batch")
+	}
+
+	tx := st.NewTransaction()
+	if err := s.addNode(nil, tx, oid_a, "3", "logrec-a-03", false, []string{"2"}, btid_1, nil); err != nil {
+		t.Errorf("cannot addNode() on object %s and batch ID %v: %v", oid_a, btid_1, err)
+	}
+	if err := s.addNode(nil, tx, oid_b, "3", "logrec-b-03", false, []string{"2"}, btid_1, nil); err != nil {
+		t.Errorf("cannot addNode() on object %s and batch ID %v: %v", oid_b, btid_1, err)
+	}
+	if err := s.endBatch(nil, tx, btid_1, 2); err != nil {
+		t.Errorf("cannot endBatch() for ID %v: %v", btid_1, err)
+	}
+	tx.Commit()
+
+	btid_2 := s.startBatch(nil, st, NoBatchId)
+	if btid_2 == NoBatchId {
+		t.Fatal("cannot start 2nd DAG addNode() batch")
+	}
+
+	tx = st.NewTransaction()
+	if err := s.addNode(nil, tx, oid_b, "4", "logrec-b-04", false, []string{"3"}, btid_2, nil); err != nil {
+		t.Errorf("cannot addNode() on object %s and batch ID %v: %v", oid_b, btid_2, err)
+	}
+	if err := s.addNode(nil, tx, oid_c, "2", "logrec-c-02", false, []string{"1"}, btid_2, nil); err != nil {
+		t.Errorf("cannot addNode() on object %s and batch ID %v: %v", oid_c, btid_2, err)
+	}
+	if err := s.endBatch(nil, tx, btid_2, 2); err != nil {
+		t.Errorf("cannot endBatch() for ID %v: %v", btid_2, err)
+	}
+	tx.Commit()
+
+	tx = st.NewTransaction()
+	if err := s.addNode(nil, tx, oid_a, "4", "logrec-a-04", false, []string{"3"}, NoBatchId, nil); err != nil {
+		t.Errorf("cannot addNode() on object %s: %v", oid_a, err)
+	}
+	if err := s.addNode(nil, tx, oid_b, "5", "logrec-b-05", false, []string{"4"}, NoBatchId, nil); err != nil {
+		t.Errorf("cannot addNode() on object %s: %v", oid_b, err)
+	}
+
+	if err := moveHead(nil, tx, oid_a, "4"); err != nil {
+		t.Errorf("object %s cannot move head: %v", oid_a, err)
+	}
+	if err := moveHead(nil, tx, oid_b, "5"); err != nil {
+		t.Errorf("object %s cannot move head: %v", oid_b, err)
+	}
+	if err := moveHead(nil, tx, oid_c, "2"); err != nil {
+		t.Errorf("object %s cannot move head: %v", oid_c, err)
+	}
+	tx.Commit()
+
+	// Verify the batch sets.
+	info, err := getBatch(nil, st, btid_1)
+	if err != nil {
+		t.Errorf("cannot getBatch() for ID %v: %v", btid_1, err)
+	}
+
+	expInfo := &batchInfo{map[string]string{oid_a: "3", oid_b: "3"}, 2}
+	if !reflect.DeepEqual(info, expInfo) {
+		t.Errorf("invalid batch info from DAG storage for ID %v: %v instead of %v",
+			btid_1, info, expInfo)
+	}
+
+	info, err = getBatch(nil, st, btid_2)
+	if err != nil {
+		t.Errorf("cannot getBatch() for ID %v: %v", btid_2, err)
+	}
+
+	expInfo = &batchInfo{map[string]string{oid_b: "4", oid_c: "2"}, 2}
+	if !reflect.DeepEqual(info, expInfo) {
+		t.Errorf("invalid batch info from DAG storage for ID %v: %v instead of %v",
+			btid_2, info, expInfo)
+	}
+
+	// Prune the 3 objects at their head nodes.
+	batches := newBatchPruning()
+	tx = st.NewTransaction()
+	for _, oid := range []string{oid_a, oid_b, oid_c} {
+		head, err := getHead(nil, st, oid)
+		if err != nil {
+			t.Errorf("cannot getHead() on object %s: %v", oid, err)
+		}
+		err = prune(nil, tx, oid, head, batches,
+			func(ctx *context.T, itx store.Transaction, lr string) error {
+				return nil
+			})
+		if err != nil {
+			t.Errorf("cannot prune() on object %s: %v", oid, err)
+		}
+	}
+
+	if err = pruneDone(nil, tx, batches); err != nil {
+		t.Errorf("pruneDone() failed: %v", err)
+	}
+	tx.Commit()
+
+	// Verify that batch-1 was deleted and batch-2 still has c2 in it.
+	info, err = getBatch(nil, st, btid_1)
+	if err == nil {
+		t.Errorf("getBatch() did not fail for ID %v: %v", btid_1, info)
+	}
+
+	info, err = getBatch(nil, st, btid_2)
+	if err != nil {
+		t.Errorf("cannot getBatch() for ID %v: %v", btid_2, err)
+	}
+
+	expInfo = &batchInfo{map[string]string{oid_c: "2"}, 2}
+	if !reflect.DeepEqual(info, expInfo) {
+		t.Errorf("invalid batch info for ID %v: %v instead of %v", btid_2, info, expInfo)
+	}
+
+	// Add c3 as a new head and prune at that point.  This should GC batch-2.
+	tx = st.NewTransaction()
+	if err := s.addNode(nil, tx, oid_c, "3", "logrec-c-03", false, []string{"2"}, NoBatchId, nil); err != nil {
+		t.Errorf("cannot addNode() on object %s: %v", oid_c, err)
+	}
+	if err = moveHead(nil, tx, oid_c, "3"); err != nil {
+		t.Errorf("object %s cannot move head: %v", oid_c, err)
+	}
+
+	batches = newBatchPruning()
+	err = prune(nil, tx, oid_c, "3", batches,
+		func(ctx *context.T, itx store.Transaction, lr string) error {
+			return nil
+		})
+	if err != nil {
+		t.Errorf("cannot prune() on object %s: %v", oid_c, err)
+	}
+	if err = pruneDone(nil, tx, batches); err != nil {
+		t.Errorf("pruneDone() #2 failed: %v", err)
+	}
+	tx.Commit()
+
+	info, err = getBatch(nil, st, btid_2)
+	if err == nil {
+		t.Errorf("getBatch() did not fail for ID %v: %v", btid_2, info)
+	}
+}
diff --git a/services/syncbase/vsync/initiator.go b/services/syncbase/vsync/initiator.go
new file mode 100644
index 0000000..cccf217
--- /dev/null
+++ b/services/syncbase/vsync/initiator.go
@@ -0,0 +1,968 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Initiator is a goroutine that periodically picks a peer from all the known
+// remote peers, and requests deltas from that peer for all the SyncGroups in
+// common across all apps/databases. It then modifies the sync metadata (DAG and
+// local log records) based on the deltas, detects and resolves conflicts if
+// any, and suitably updates the local Databases.
+
+import (
+	"sort"
+	"strings"
+	"time"
+
+	"v.io/syncbase/v23/services/syncbase/nosql"
+	"v.io/syncbase/x/ref/services/syncbase/server/interfaces"
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/syncbase/x/ref/services/syncbase/server/watchable"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/context"
+	"v.io/v23/naming"
+	"v.io/v23/vdl"
+	"v.io/v23/verror"
+	"v.io/v23/vom"
+	"v.io/x/lib/vlog"
+)
+
+// Policies to pick a peer to sync with.
+const (
+	// Picks a peer at random from the available set.
+	selectRandom = iota
+
+	// TODO(hpucha): implement other policies.
+	// Picks a peer with most differing generations.
+	selectMostDiff
+
+	// Picks a peer that was synced with the furthest in the past.
+	selectOldest
+)
+
+var (
+	// peerSyncInterval is the duration between two consecutive peer
+	// contacts. During every peer contact, the initiator obtains any
+	// pending updates from that peer.
+	peerSyncInterval = 50 * time.Millisecond
+
+	// peerSelectionPolicy is the policy used to select a peer when
+	// the initiator gets a chance to sync.
+	peerSelectionPolicy = selectRandom
+)
+
+// syncer wakes up every peerSyncInterval to do work: (1) Act as an initiator
+// for SyncGroup metadata by selecting a SyncGroup Admin, and syncing Syncgroup
+// metadata with it (getting updates from the remote peer, detecting and
+// resolving conflicts); (2) Refresh memberView if needed and act as an
+// initiator for data by selecting a peer, and syncing data corresponding to all
+// common SyncGroups across all Databases; (3) Act as a SyncGroup publisher to
+// publish pending SyncGroups; (4) Garbage collect older generations.
+//
+// TODO(hpucha): Currently only does initiation. Add rest.
+func (s *syncService) syncer(ctx *context.T) {
+	defer s.pending.Done()
+
+	ticker := time.NewTicker(peerSyncInterval)
+	defer ticker.Stop()
+
+	for {
+		select {
+		case <-s.closed:
+			vlog.VI(1).Info("sync: syncer: channel closed, stop work and exit")
+			return
+
+		case <-ticker.C:
+		}
+
+		// TODO(hpucha): Cut a gen for the responder even if there is no
+		// one to initiate to?
+
+		// Do work.
+		peer, err := s.pickPeer(ctx)
+		if err != nil {
+			continue
+		}
+		s.getDeltasFromPeer(ctx, peer)
+	}
+}
+
+// getDeltasFromPeer performs an initiation round to the specified
+// peer. An initiation round consists of:
+// * Contacting the peer to receive all the deltas based on the local genvector.
+// * Processing those deltas to discover objects which have been updated.
+// * Processing updated objects to detect and resolve any conflicts if needed.
+// * Communicating relevant object updates to the Database, and updating local
+// genvector to catch up to the received remote genvector.
+//
+// The processing of the deltas is done one Database at a time. If a local error
+// is encountered during the processing of a Database, that Database is skipped
+// and the initiator continues on to the next one. If the connection to the peer
+// encounters an error, this initiation round is aborted. Note that until the
+// local genvector is updated based on the received deltas (the last step in an
+// initiation round), the work done by the initiator is idempotent.
+//
+// TODO(hpucha): Check the idempotence, esp in addNode in DAG.
+func (s *syncService) getDeltasFromPeer(ctxIn *context.T, peer string) {
+	vlog.VI(2).Infof("sync: getDeltasFromPeer: begin: contacting peer %s", peer)
+	defer vlog.VI(2).Infof("sync: getDeltasFromPeer: end: contacting peer %s", peer)
+
+	ctx, cancel := context.WithRootCancel(ctxIn)
+
+	info := s.copyMemberInfo(ctx, peer)
+	if info == nil {
+		vlog.Fatalf("sync: getDeltasFromPeer: missing information in member view for %q", peer)
+	}
+	connected := false
+	var stream interfaces.SyncGetDeltasClientCall
+
+	// Sync each Database that may have SyncGroups common with this peer,
+	// one at a time.
+	for gdbName, sgInfo := range info.db2sg {
+
+		// Initialize initiation state for syncing this Database.
+		iSt, err := newInitiationState(ctx, s, peer, gdbName, sgInfo)
+		if err != nil {
+			vlog.Errorf("sync: getDeltasFromPeer: couldn't initialize initiator state for peer %s, gdb %s, err %v", peer, gdbName, err)
+			continue
+		}
+
+		if len(iSt.sgIds) == 0 || len(iSt.sgPfxs) == 0 {
+			vlog.Errorf("sync: getDeltasFromPeer: didn't find any SyncGroups for peer %s, gdb %s, err %v", peer, gdbName, err)
+			continue
+		}
+
+		// Make contact with the peer once.
+		if !connected {
+			stream, connected = iSt.connectToPeer(ctx)
+			if !connected {
+				// Try a different Database. Perhaps there are
+				// different mount tables.
+				continue
+			}
+		}
+
+		// Create local genvec so that it contains knowledge only about common prefixes.
+		if err := iSt.createLocalGenVec(ctx); err != nil {
+			vlog.Errorf("sync: getDeltasFromPeer: error creating local genvec for gdb %s, err %v", gdbName, err)
+			continue
+		}
+
+		iSt.stream = stream
+		req := interfaces.DeltaReq{
+			AppName: iSt.appName,
+			DbName:  iSt.dbName,
+			SgIds:   iSt.sgIds,
+			InitVec: iSt.local,
+		}
+
+		vlog.VI(3).Infof("sync: getDeltasFromPeer: send request: %v", req)
+		sender := iSt.stream.SendStream()
+		sender.Send(req)
+
+		// Obtain deltas from the peer over the network.
+		if err := iSt.recvAndProcessDeltas(ctx); err != nil {
+			vlog.Errorf("sync: getDeltasFromPeer: error receiving deltas for gdb %s, err %v", gdbName, err)
+			// Returning here since something could be wrong with
+			// the connection, and no point in attempting the next
+			// Database.
+			cancel()
+			stream.Finish()
+			return
+		}
+		vlog.VI(3).Infof("sync: getDeltasFromPeer: got reply: %v", iSt.remote)
+
+		if err := iSt.processUpdatedObjects(ctx); err != nil {
+			vlog.Errorf("sync: getDeltasFromPeer: error processing objects for gdb %s, err %v", gdbName, err)
+			// Move to the next Database even if processing updates
+			// failed.
+			continue
+		}
+	}
+
+	if connected {
+		stream.Finish()
+	}
+	cancel()
+}
+
+type sgSet map[interfaces.GroupId]struct{}
+
+// initiationState is accumulated for each Database during an initiation round.
+type initiationState struct {
+	// Relative name of the peer to sync with.
+	peer string
+
+	// Collection of mount tables that this peer may have registered with.
+	mtTables map[string]struct{}
+
+	// SyncGroups being requested in the initiation round.
+	sgIds sgSet
+
+	// SyncGroup prefixes being requested in the initiation round, and their
+	// corresponding SyncGroup ids.
+	sgPfxs map[string]sgSet
+
+	// Local generation vector.
+	local interfaces.GenVector
+
+	// Generation vector from the remote peer.
+	remote interfaces.GenVector
+
+	// Updated local generation vector at the end of the initiation round.
+	updLocal interfaces.GenVector
+
+	// State to track updated objects during a log replay.
+	updObjects map[string]*objConflictState
+
+	// DAG state that tracks conflicts and common ancestors.
+	dagGraft graftMap
+
+	sync    *syncService
+	appName string
+	dbName  string
+	st      store.Store                        // Store handle to the Database.
+	stream  interfaces.SyncGetDeltasClientCall // Stream handle for the GetDeltas RPC.
+
+	// Transaction handle for the initiation round. Used during the update
+	// of objects in the Database.
+	tx store.Transaction
+}
+
+// objConflictState contains the conflict state for an object that is updated
+// during an initiator round.
+type objConflictState struct {
+	isConflict bool
+	newHead    string
+	oldHead    string
+	ancestor   string
+	res        *conflictResolution
+}
+
+// newInitiationState creates new initiation state.
+func newInitiationState(ctx *context.T, s *syncService, peer string, name string, sgInfo sgMemberInfo) (*initiationState, error) {
+	iSt := &initiationState{}
+	iSt.peer = peer
+	iSt.updObjects = make(map[string]*objConflictState)
+	iSt.dagGraft = newGraft()
+	iSt.sync = s
+
+	// TODO(hpucha): Would be nice to standardize on the combined "app:db"
+	// name across sync (not syncbase) so we only join split/join them at
+	// the boundary with the store part.
+	var err error
+	iSt.appName, iSt.dbName, err = splitAppDbName(ctx, name)
+	if err != nil {
+		return nil, err
+	}
+
+	// TODO(hpucha): nil rpc.ServerCall ok?
+	iSt.st, err = s.getDbStore(ctx, nil, iSt.appName, iSt.dbName)
+	if err != nil {
+		return nil, err
+	}
+
+	iSt.peerMtTblsAndSgInfo(ctx, peer, sgInfo)
+
+	return iSt, nil
+}
+
+// peerMtTblsAndSgInfo computes the possible mount tables, the SyncGroup Ids and
+// prefixes common with a remote peer in a particular Database by consulting the
+// SyncGroups in the specified Database.
+func (iSt *initiationState) peerMtTblsAndSgInfo(ctx *context.T, peer string, info sgMemberInfo) {
+	iSt.mtTables = make(map[string]struct{})
+	iSt.sgIds = make(sgSet)
+	iSt.sgPfxs = make(map[string]sgSet)
+
+	for id := range info {
+		sg, err := getSyncGroupById(ctx, iSt.st, id)
+		if err != nil {
+			continue
+		}
+		if _, ok := sg.Joiners[peer]; !ok {
+			// Peer is no longer part of the SyncGroup.
+			continue
+		}
+		for _, mt := range sg.Spec.MountTables {
+			iSt.mtTables[mt] = struct{}{}
+		}
+		iSt.sgIds[id] = struct{}{}
+
+		for _, p := range sg.Spec.Prefixes {
+			sgs, ok := iSt.sgPfxs[p]
+			if !ok {
+				sgs = make(sgSet)
+				iSt.sgPfxs[p] = sgs
+			}
+			sgs[id] = struct{}{}
+		}
+	}
+}
+
+// connectToPeer attempts to connect to the remote peer using the mount tables
+// obtained from the SyncGroups being synced in the current Database.
+func (iSt *initiationState) connectToPeer(ctx *context.T) (interfaces.SyncGetDeltasClientCall, bool) {
+	if len(iSt.mtTables) < 1 {
+		vlog.Errorf("sync: connectToPeer: no mount tables found to connect to peer %s, app %s db %s", iSt.peer, iSt.appName, iSt.dbName)
+		return nil, false
+	}
+	for mt := range iSt.mtTables {
+		absName := naming.Join(mt, iSt.peer, util.SyncbaseSuffix)
+		c := interfaces.SyncClient(absName)
+		stream, err := c.GetDeltas(ctx, iSt.sync.name)
+		if err == nil {
+			vlog.VI(3).Infof("sync: connectToPeer: established on %s", absName)
+			return stream, true
+		}
+	}
+	return nil, false
+}
+
+// createLocalGenVec creates the generation vector with local knowledge for the
+// initiator to send to the responder.
+//
+// TODO(hpucha): Refactor this code with computeDelta code in sync_state.go.
+func (iSt *initiationState) createLocalGenVec(ctx *context.T) error {
+	iSt.sync.thLock.Lock()
+	defer iSt.sync.thLock.Unlock()
+
+	// Freeze the most recent batch of local changes before fetching
+	// remote changes from a peer. This frozen state is used by the
+	// responder when responding to GetDeltas RPC.
+	//
+	// We only allow an initiator to freeze local generations (not
+	// responders/watcher) in order to maintain a static baseline
+	// for the duration of a sync. This addresses the following race
+	// condition: If we allow responders to use newer local
+	// generations while the initiator is in progress, they may beat
+	// the initiator and send these new generations to remote
+	// devices.  These remote devices in turn can send these
+	// generations back to the initiator in progress which was
+	// started with older generation information.
+	if err := iSt.sync.checkptLocalGen(ctx, iSt.appName, iSt.dbName); err != nil {
+		return err
+	}
+
+	local, lgen, err := iSt.sync.copyDbGenInfo(ctx, iSt.appName, iSt.dbName)
+	if err != nil {
+		return err
+	}
+	localPfxs := extractAndSortPrefixes(local)
+
+	sgPfxs := make([]string, len(iSt.sgPfxs))
+	i := 0
+	for p := range iSt.sgPfxs {
+		sgPfxs[i] = p
+		i++
+	}
+	sort.Strings(sgPfxs)
+
+	iSt.local = make(interfaces.GenVector)
+
+	if len(sgPfxs) == 0 {
+		return verror.New(verror.ErrInternal, ctx, "no syncgroups for syncing")
+	}
+
+	pfx := sgPfxs[0]
+	for _, p := range sgPfxs {
+		if strings.HasPrefix(p, pfx) && p != pfx {
+			continue
+		}
+
+		// Process this prefix as this is the start of a new set of
+		// nested prefixes.
+		pfx = p
+		var lpStart string
+		for _, lp := range localPfxs {
+			if !strings.HasPrefix(lp, pfx) && !strings.HasPrefix(pfx, lp) {
+				// No relationship with pfx.
+				continue
+			}
+			if strings.HasPrefix(pfx, lp) {
+				lpStart = lp
+			} else {
+				iSt.local[lp] = local[lp]
+			}
+		}
+		// Deal with the starting point.
+		if lpStart == "" {
+			// No matching prefixes for pfx were found.
+			iSt.local[pfx] = make(interfaces.PrefixGenVector)
+			iSt.local[pfx][iSt.sync.id] = lgen
+		} else {
+			iSt.local[pfx] = local[lpStart]
+		}
+	}
+	return nil
+}
+
+// recvAndProcessDeltas first receives the log records and generation vector
+// from the GetDeltas RPC and puts them in the Database. It also replays the
+// entire log stream as the log records arrive. These records span multiple
+// generations from different devices. It does not perform any conflict
+// resolution during replay.  This avoids resolving conflicts that have already
+// been resolved by other devices.
+func (iSt *initiationState) recvAndProcessDeltas(ctx *context.T) error {
+	iSt.sync.thLock.Lock()
+	defer iSt.sync.thLock.Unlock()
+
+	// TODO(hpucha): This works for now, but figure out a long term solution
+	// as this may be implementation dependent. It currently works because
+	// the RecvStream call is stateless, and grabbing a handle to it
+	// repeatedly doesn't affect what data is seen next.
+	rcvr := iSt.stream.RecvStream()
+	start, finish := false, false
+
+	// TODO(hpucha): See if we can avoid committing the entire delta stream
+	// as one batch. Currently the dependency is between the log records and
+	// the batch info.
+	tx := iSt.st.NewTransaction()
+	committed := false
+
+	defer func() {
+		if !committed {
+			tx.Abort()
+		}
+	}()
+
+	// Track received batches (BatchId --> BatchCount mapping).
+	batchMap := make(map[uint64]uint64)
+
+	for rcvr.Advance() {
+		resp := rcvr.Value()
+		switch v := resp.(type) {
+		case interfaces.DeltaRespStart:
+			if start {
+				return verror.New(verror.ErrInternal, ctx, "received start followed by start in delta response stream")
+			}
+			start = true
+
+		case interfaces.DeltaRespFinish:
+			if finish {
+				return verror.New(verror.ErrInternal, ctx, "received finish followed by finish in delta response stream")
+			}
+			finish = true
+
+		case interfaces.DeltaRespRespVec:
+			iSt.remote = v.Value
+
+		case interfaces.DeltaRespRec:
+			// Insert log record in Database.
+			// TODO(hpucha): Should we reserve more positions in a batch?
+			// TODO(hpucha): Handle if SyncGroup is left/destroyed while sync is in progress.
+			pos := iSt.sync.reservePosInDbLog(ctx, iSt.appName, iSt.dbName, 1)
+			rec := &localLogRec{Metadata: v.Value.Metadata, Pos: pos}
+			batchId := rec.Metadata.BatchId
+			if batchId != NoBatchId {
+				if cnt, ok := batchMap[batchId]; !ok {
+					if iSt.sync.startBatch(ctx, tx, batchId) != batchId {
+						return verror.New(verror.ErrInternal, ctx, "failed to create batch info")
+					}
+					batchMap[batchId] = rec.Metadata.BatchCount
+				} else if cnt != rec.Metadata.BatchCount {
+					return verror.New(verror.ErrInternal, ctx, "inconsistent counts for tid", batchId, cnt, rec.Metadata.BatchCount)
+				}
+			}
+
+			vlog.VI(4).Infof("sync: recvAndProcessDeltas: processing rec %v", rec)
+			if err := iSt.insertRecInLogDagAndDb(ctx, rec, batchId, v.Value.Value, tx); err != nil {
+				return err
+			}
+
+			// Check for BlobRefs, and process them.
+			if err := iSt.processBlobRefs(ctx, &rec.Metadata, v.Value.Value); err != nil {
+				return err
+			}
+
+			// Mark object dirty.
+			iSt.updObjects[rec.Metadata.ObjId] = &objConflictState{}
+		}
+
+		// Break out of the stream.
+		if finish {
+			break
+		}
+	}
+
+	if !(start && finish) {
+		return verror.New(verror.ErrInternal, ctx, "didn't receive start/finish delimiters in delta response stream")
+	}
+
+	if err := rcvr.Err(); err != nil {
+		return err
+	}
+
+	// End the started batches if any.
+	for bid, cnt := range batchMap {
+		if err := iSt.sync.endBatch(ctx, tx, bid, cnt); err != nil {
+			return err
+		}
+	}
+
+	// Commit this transaction. We do not retry this transaction since it
+	// should not conflict with any other keys. So if it fails, it is a
+	// non-retriable error.
+	err := tx.Commit()
+	if verror.ErrorID(err) == store.ErrConcurrentTransaction.ID {
+		// Note: This might be triggered with memstore until it handles
+		// transactions in a more fine-grained fashion.
+		vlog.Fatalf("sync: recvAndProcessDeltas: encountered concurrent transaction")
+	}
+	if err == nil {
+		committed = true
+	}
+	return err
+}
+
+func (iSt *initiationState) processBlobRefs(ctx *context.T, m *interfaces.LogRecMetadata, valbuf []byte) error {
+	objid := m.ObjId
+	srcPeer := syncbaseIdToName(m.Id)
+
+	vlog.VI(4).Infof("sync: processBlobRefs: begin processing blob refs for objid %s", objid)
+	defer vlog.VI(4).Infof("sync: processBlobRefs: end processing blob refs for objid %s", objid)
+
+	if valbuf == nil {
+		return nil
+	}
+
+	var val *vdl.Value
+	if err := vom.Decode(valbuf, &val); err != nil {
+		return err
+	}
+
+	brs := make(map[nosql.BlobRef]struct{})
+	if err := extractBlobRefs(val, brs); err != nil {
+		return err
+	}
+	sgIds := make(sgSet)
+	for br := range brs {
+		for p, sgs := range iSt.sgPfxs {
+			if strings.HasPrefix(extractAppKey(objid), p) {
+				for sg := range sgs {
+					sgIds[sg] = struct{}{}
+				}
+			}
+		}
+		vlog.VI(4).Infof("sync: processBlobRefs: Found blobref %v peer %v, source %v, sgs %v", br, iSt.peer, srcPeer, sgIds)
+		info := &blobLocInfo{peer: iSt.peer, source: srcPeer, sgIds: sgIds}
+		if err := iSt.sync.addBlobLocInfo(ctx, br, info); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// TODO(hpucha): Handle blobrefs part of list, map, any.
+func extractBlobRefs(val *vdl.Value, brs map[nosql.BlobRef]struct{}) error {
+	if val == nil {
+		return nil
+	}
+	switch val.Kind() {
+	case vdl.String:
+		// Could be a BlobRef.
+		var br nosql.BlobRef
+		if val.Type() == vdl.TypeOf(br) {
+			brs[nosql.BlobRef(val.RawString())] = struct{}{}
+		}
+	case vdl.Struct:
+		for i := 0; i < val.Type().NumField(); i++ {
+			v := val.StructField(i)
+			if err := extractBlobRefs(v, brs); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+// insertRecInLogDagAndDb adds a new log record to log and dag data structures,
+// and inserts the versioned value in the Database.
+func (iSt *initiationState) insertRecInLogDagAndDb(ctx *context.T, rec *localLogRec, batchId uint64, valbuf []byte, tx store.Transaction) error {
+	if err := putLogRec(ctx, tx, rec); err != nil {
+		return err
+	}
+
+	m := rec.Metadata
+	logKey := logRecKey(m.Id, m.Gen)
+
+	var err error
+	switch m.RecType {
+	case interfaces.NodeRec:
+		err = iSt.sync.addNode(ctx, tx, m.ObjId, m.CurVers, logKey, m.Delete, m.Parents, m.BatchId, iSt.dagGraft)
+	case interfaces.LinkRec:
+		err = iSt.sync.addParent(ctx, tx, m.ObjId, m.CurVers, m.Parents[0], iSt.dagGraft)
+	default:
+		err = verror.New(verror.ErrInternal, ctx, "unknown log record type")
+	}
+
+	if err != nil {
+		return err
+	}
+	// TODO(hpucha): Hack right now. Need to change Database's handling of
+	// deleted objects. Currently, the initiator needs to treat deletions
+	// specially since deletions do not get a version number or a special
+	// value in the Database.
+	if !rec.Metadata.Delete && rec.Metadata.RecType == interfaces.NodeRec {
+		return watchable.PutAtVersion(ctx, tx, []byte(m.ObjId), valbuf, []byte(m.CurVers))
+	}
+	return nil
+}
+
+// processUpdatedObjects processes all the updates received by the initiator,
+// one object at a time. Conflict detection and resolution is carried out after
+// the entire delta of log records is replayed, instead of incrementally after
+// each record/batch is replayed, to avoid repeating conflict resolution already
+// performed by other peers.
+//
+// For each updated object, we first check if the object has any conflicts,
+// resulting in three possibilities:
+//
+// * There is no conflict, and no updates are needed to the Database
+// (isConflict=false, newHead == oldHead). All changes received convey
+// information that still keeps the local head as the most recent version. This
+// occurs when conflicts are resolved by picking the existing local version.
+//
+// * There is no conflict, but a remote version is discovered that builds on the
+// local head (isConflict=false, newHead != oldHead). In this case, we generate
+// a Database update to simply update the Database to the latest value.
+//
+// * There is a conflict and we call into the app or use a well-known policy to
+// resolve the conflict, resulting in three possibilties: (a) conflict was
+// resolved by picking the local version. In this case, Database need not be
+// updated, but a link is added to record the choice. (b) conflict was resolved
+// by picking the remote version. In this case, Database is updated with the
+// remote version and a link is added as well. (c) conflict was resolved by
+// generating a new Database update. In this case, Database is updated with the
+// new version.
+//
+// We collect all the updates to the Database in a transaction. In addition, as
+// part of the same transaction, we update the log and dag state suitably (move
+// the head ptr of the object in the dag to the latest version, and create a new
+// log record reflecting conflict resolution if any). Finally, we update the
+// sync state first on storage. This transaction's commit can fail since
+// preconditions on the objects may have been violated. In this case, we wait to
+// get the latest versions of objects from the Database, and recheck if the object
+// has any conflicts and repeat the above steps, until the transaction commits
+// successfully. Upon commit, we also update the in-memory sync state of the
+// Database.
+func (iSt *initiationState) processUpdatedObjects(ctx *context.T) error {
+	// Note that the tx handle in initiation state is cached for the scope of
+	// this function only as different stages in the pipeline add to the
+	// transaction.
+	committed := false
+	defer func() {
+		if !committed {
+			iSt.tx.Abort()
+		}
+	}()
+
+	for {
+		vlog.VI(3).Infof("sync: processUpdatedObjects: begin: %d objects updated", len(iSt.updObjects))
+
+		iSt.tx = iSt.st.NewTransaction()
+		watchable.SetTransactionFromSync(iSt.tx) // for echo-suppression
+
+		if count, err := iSt.detectConflicts(ctx); err != nil {
+			return err
+		} else {
+			vlog.VI(3).Infof("sync: processUpdatedObjects: %d conflicts detected", count)
+		}
+
+		if err := iSt.resolveConflicts(ctx); err != nil {
+			return err
+		}
+
+		err := iSt.updateDbAndSyncSt(ctx)
+		if err == nil {
+			err = iSt.tx.Commit()
+		}
+		if err == nil {
+			committed = true
+			// Update in-memory genvector since commit is successful.
+			if err := iSt.sync.putDbGenInfoRemote(ctx, iSt.appName, iSt.dbName, iSt.updLocal); err != nil {
+				vlog.Fatalf("sync: processUpdatedObjects: putting geninfo in memory failed for app %s db %s, err %v", iSt.appName, iSt.dbName, err)
+			}
+			vlog.VI(3).Info("sync: processUpdatedObjects: end: changes committed")
+			return nil
+		}
+		if verror.ErrorID(err) != store.ErrConcurrentTransaction.ID {
+			return err
+		}
+
+		// Either updateDbAndSyncSt() or tx.Commit() detected a
+		// concurrent transaction. Retry processing the remote updates.
+		//
+		// TODO(hpucha): Sleeping and retrying is a temporary
+		// solution. Next iteration will have coordination with watch
+		// thread to intelligently retry. Hence this value is not a
+		// config param.
+		vlog.VI(3).Info("sync: processUpdatedObjects: retry due to local mutations")
+		iSt.tx.Abort()
+		time.Sleep(1 * time.Second)
+	}
+}
+
+// detectConflicts iterates through all the updated objects to detect conflicts.
+func (iSt *initiationState) detectConflicts(ctx *context.T) (int, error) {
+	count := 0
+	for objid, confSt := range iSt.updObjects {
+		// Check if object has a conflict.
+		var err error
+		confSt.isConflict, confSt.newHead, confSt.oldHead, confSt.ancestor, err = hasConflict(ctx, iSt.tx, objid, iSt.dagGraft)
+		if err != nil {
+			return 0, err
+		}
+
+		if !confSt.isConflict {
+			if confSt.newHead == confSt.oldHead {
+				confSt.res = &conflictResolution{ty: pickLocal}
+			} else {
+				confSt.res = &conflictResolution{ty: pickRemote}
+			}
+		} else {
+			count++
+		}
+	}
+	return count, nil
+}
+
+// updateDbAndSync updates the Database, and if that is successful, updates log,
+// dag and genvector data structures as needed.
+func (iSt *initiationState) updateDbAndSyncSt(ctx *context.T) error {
+	for objid, confSt := range iSt.updObjects {
+		// If the local version is picked, no further updates to the
+		// Database are needed. If the remote version is picked or if a
+		// new version is created, we put it in the Database.
+		if confSt.res.ty != pickLocal {
+
+			// TODO(hpucha): Hack right now. Need to change Database's
+			// handling of deleted objects.
+			oldVersDeleted := true
+			if confSt.oldHead != NoVersion {
+				oldDagNode, err := getNode(ctx, iSt.tx, objid, confSt.oldHead)
+				if err != nil {
+					return err
+				}
+				oldVersDeleted = oldDagNode.Deleted
+			}
+
+			var newVersion string
+			var newVersDeleted bool
+			switch confSt.res.ty {
+			case pickRemote:
+				newVersion = confSt.newHead
+				newDagNode, err := getNode(ctx, iSt.tx, objid, newVersion)
+				if err != nil {
+					return err
+				}
+				newVersDeleted = newDagNode.Deleted
+			case createNew:
+				newVersion = confSt.res.rec.Metadata.CurVers
+				newVersDeleted = confSt.res.rec.Metadata.Delete
+			}
+
+			// Skip delete followed by a delete.
+			if oldVersDeleted && newVersDeleted {
+				continue
+			}
+
+			if !oldVersDeleted {
+				// Read current version to enter it in the readset of the transaction.
+				version, err := watchable.GetVersion(ctx, iSt.tx, []byte(objid))
+				if err != nil {
+					return err
+				}
+				if string(version) != confSt.oldHead {
+					vlog.VI(4).Infof("sync: updateDbAndSyncSt: concurrent updates %s %s", version, confSt.oldHead)
+					return store.NewErrConcurrentTransaction(ctx)
+				}
+			} else {
+				// Ensure key doesn't exist.
+				if _, err := watchable.GetVersion(ctx, iSt.tx, []byte(objid)); verror.ErrorID(err) != store.ErrUnknownKey.ID {
+					return store.NewErrConcurrentTransaction(ctx)
+				}
+			}
+
+			if !newVersDeleted {
+				if confSt.res.ty == createNew {
+					vlog.VI(4).Infof("sync: updateDbAndSyncSt: PutAtVersion %s %s", objid, newVersion)
+					if err := watchable.PutAtVersion(ctx, iSt.tx, []byte(objid), confSt.res.val, []byte(newVersion)); err != nil {
+						return err
+					}
+				}
+				vlog.VI(4).Infof("sync: updateDbAndSyncSt: PutVersion %s %s", objid, newVersion)
+				if err := watchable.PutVersion(ctx, iSt.tx, []byte(objid), []byte(newVersion)); err != nil {
+					return err
+				}
+			} else {
+				vlog.VI(4).Infof("sync: updateDbAndSyncSt: Deleting obj %s", objid)
+				if err := iSt.tx.Delete([]byte(objid)); err != nil {
+					return err
+				}
+			}
+		}
+		// Always update sync state irrespective of local/remote/new
+		// versions being picked.
+		if err := iSt.updateLogAndDag(ctx, objid); err != nil {
+			return err
+		}
+	}
+
+	return iSt.updateSyncSt(ctx)
+}
+
+// updateLogAndDag updates the log and dag data structures.
+func (iSt *initiationState) updateLogAndDag(ctx *context.T, obj string) error {
+	confSt, ok := iSt.updObjects[obj]
+	if !ok {
+		return verror.New(verror.ErrInternal, ctx, "object state not found", obj)
+	}
+	var newVersion string
+
+	if !confSt.isConflict {
+		newVersion = confSt.newHead
+	} else {
+		// Object had a conflict. Create a log record to reflect resolution.
+		var rec *localLogRec
+
+		switch {
+		case confSt.res.ty == pickLocal:
+			// Local version was picked as the conflict resolution.
+			rec = iSt.createLocalLinkLogRec(ctx, obj, confSt.oldHead, confSt.newHead)
+			newVersion = confSt.oldHead
+		case confSt.res.ty == pickRemote:
+			// Remote version was picked as the conflict resolution.
+			rec = iSt.createLocalLinkLogRec(ctx, obj, confSt.newHead, confSt.oldHead)
+			newVersion = confSt.newHead
+		default:
+			// New version was created to resolve the conflict.
+			rec = confSt.res.rec
+			newVersion = confSt.res.rec.Metadata.CurVers
+		}
+
+		if err := putLogRec(ctx, iSt.tx, rec); err != nil {
+			return err
+		}
+
+		// Add a new DAG node.
+		var err error
+		m := rec.Metadata
+		switch m.RecType {
+		case interfaces.NodeRec:
+			err = iSt.sync.addNode(ctx, iSt.tx, obj, m.CurVers, logRecKey(m.Id, m.Gen), m.Delete, m.Parents, NoBatchId, nil)
+		case interfaces.LinkRec:
+			err = iSt.sync.addParent(ctx, iSt.tx, obj, m.CurVers, m.Parents[0], nil)
+		default:
+			return verror.New(verror.ErrInternal, ctx, "unknown log record type")
+		}
+		if err != nil {
+			return err
+		}
+	}
+
+	// Move the head. This should be idempotent. We may move head to the
+	// local head in some cases.
+	return moveHead(ctx, iSt.tx, obj, newVersion)
+}
+
+func (iSt *initiationState) createLocalLinkLogRec(ctx *context.T, obj, vers, par string) *localLogRec {
+	gen, pos := iSt.sync.reserveGenAndPosInDbLog(ctx, iSt.appName, iSt.dbName, 1)
+
+	vlog.VI(4).Infof("sync: createLocalLinkLogRec: obj %s vers %s par %s", obj, vers, par)
+
+	rec := &localLogRec{
+		Metadata: interfaces.LogRecMetadata{
+			Id:      iSt.sync.id,
+			Gen:     gen,
+			RecType: interfaces.LinkRec,
+
+			ObjId:      obj,
+			CurVers:    vers,
+			Parents:    []string{par},
+			UpdTime:    time.Now().UTC(),
+			BatchId:    NoBatchId,
+			BatchCount: 1,
+			// TODO(hpucha): What is its batchid and count?
+		},
+		Pos: pos,
+	}
+	return rec
+}
+
+// updateSyncSt updates local sync state at the end of an initiator cycle.
+func (iSt *initiationState) updateSyncSt(ctx *context.T) error {
+	// Get the current local sync state.
+	dsInMem, err := iSt.sync.copyDbSyncStateInMem(ctx, iSt.appName, iSt.dbName)
+	if err != nil {
+		return err
+	}
+	ds := &dbSyncState{
+		Gen:        dsInMem.gen,
+		CheckptGen: dsInMem.checkptGen,
+		GenVec:     dsInMem.genvec,
+	}
+
+	// remote can be a subset of local.
+	for rpfx, respgv := range iSt.remote {
+		for lpfx, lpgv := range ds.GenVec {
+			if strings.HasPrefix(lpfx, rpfx) {
+				mergePrefixGenVectors(lpgv, respgv)
+			}
+		}
+		if _, ok := ds.GenVec[rpfx]; !ok {
+			ds.GenVec[rpfx] = respgv
+		}
+	}
+
+	iSt.updLocal = ds.GenVec
+	// Clean the genvector of any local state. Note that local state is held
+	// in gen/ckPtGen in sync state struct.
+	for _, pgv := range iSt.updLocal {
+		delete(pgv, iSt.sync.id)
+	}
+
+	// TODO(hpucha): Add knowledge compaction.
+
+	return putDbSyncState(ctx, iSt.tx, ds)
+}
+
+// mergePrefixGenVectors merges responder prefix genvector into local genvector.
+func mergePrefixGenVectors(lpgv, respgv interfaces.PrefixGenVector) {
+	for devid, rgen := range respgv {
+		gen, ok := lpgv[devid]
+		if !ok || gen < rgen {
+			lpgv[devid] = rgen
+		}
+	}
+}
+
+////////////////////////////////////////
+// Peer selection policies.
+
+// pickPeer picks a Syncbase to sync with.
+func (s *syncService) pickPeer(ctx *context.T) (string, error) {
+	switch peerSelectionPolicy {
+	case selectRandom:
+		members := s.getMembers(ctx)
+		// Remove myself from the set.
+		delete(members, s.name)
+		if len(members) == 0 {
+			return "", verror.New(verror.ErrInternal, ctx, "no useful peer")
+		}
+
+		// Pick a peer at random.
+		ind := randIntn(len(members))
+		for m := range members {
+			if ind == 0 {
+				return m, nil
+			}
+			ind--
+		}
+		return "", verror.New(verror.ErrInternal, ctx, "random selection didn't succeed")
+	default:
+		return "", verror.New(verror.ErrInternal, ctx, "unknown peer selection policy")
+	}
+}
diff --git a/services/syncbase/vsync/initiator_test.go b/services/syncbase/vsync/initiator_test.go
new file mode 100644
index 0000000..af09ce1
--- /dev/null
+++ b/services/syncbase/vsync/initiator_test.go
@@ -0,0 +1,477 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// The initiator tests below are driven by replaying the state from the log
+// files (in testdata directory). These log files may mimic watching the
+// Database locally (addl commands in the log file) or obtaining log records and
+// generation vector from a remote peer (addr, genvec commands). The log files
+// contain the metadata of log records. The log files are only used to set up
+// the state. The tests verify that given a particular local state and a stream
+// of remote deltas, the initiator behaves as expected.
+
+package vsync
+
+import (
+	"fmt"
+	"reflect"
+	"testing"
+	"time"
+
+	"v.io/syncbase/v23/services/syncbase/nosql"
+	"v.io/syncbase/x/ref/services/syncbase/server/interfaces"
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/syncbase/x/ref/services/syncbase/server/watchable"
+	"v.io/v23/vdl"
+	"v.io/v23/vom"
+	_ "v.io/x/ref/runtime/factories/generic"
+)
+
+func TestExtractBlobRefs(t *testing.T) {
+	var tests [][]byte
+	br := nosql.BlobRef("123")
+
+	// BlobRef is the value.
+	buf0, err := vom.Encode(br)
+	if err != nil {
+		t.Fatalf("Encode(BlobRef) failed, err %v", err)
+	}
+	tests = append(tests, buf0)
+
+	// Struct contains BlobRef.
+	type test1Struct struct {
+		A int64
+		B string
+		C nosql.BlobRef
+	}
+	v1 := test1Struct{A: 10, B: "foo", C: br}
+	buf1, err := vom.Encode(v1)
+	if err != nil {
+		t.Fatalf("Encode(test1Struct) failed, err %v", err)
+	}
+	tests = append(tests, buf1)
+
+	// Nested struct contains BlobRef.
+	type test2Struct struct {
+		A int64
+		B string
+		C test1Struct
+	}
+	v2 := test2Struct{A: 10, B: "foo", C: v1}
+	buf2, err := vom.Encode(v2)
+	if err != nil {
+		t.Fatalf("Encode(test2Struct) failed, err %v", err)
+	}
+	tests = append(tests, buf2)
+
+	for i, buf := range tests {
+		var val *vdl.Value
+		if err := vom.Decode(buf, &val); err != nil {
+			t.Fatalf("Decode failed (test %d), err %v", i, err)
+		}
+
+		gotbrs := make(map[nosql.BlobRef]struct{})
+		if err := extractBlobRefs(val, gotbrs); err != nil {
+			t.Fatalf("extractBlobRefs failed (test %d), err %v", i, err)
+		}
+		wantbrs := map[nosql.BlobRef]struct{}{br: struct{}{}}
+		if !reflect.DeepEqual(gotbrs, wantbrs) {
+			t.Fatalf("Data mismatch in blobrefs (test %d), got %v, want %v", i, gotbrs, wantbrs)
+		}
+	}
+}
+
+// TestLogStreamRemoteOnly tests processing of a remote log stream. Commands are
+// in file testdata/remote-init-00.log.sync.
+func TestLogStreamRemoteOnly(t *testing.T) {
+	svc, iSt, cleanup := testInit(t, "", "remote-init-00.log.sync")
+	defer cleanup(t, svc)
+
+	// Check all log records.
+	objid := util.JoinKeyParts(util.RowPrefix, "foo1")
+	var gen uint64
+	var parents []string
+	for gen = 1; gen < 4; gen++ {
+		gotRec, err := getLogRec(nil, svc.St(), 11, gen)
+		if err != nil || gotRec == nil {
+			t.Fatalf("getLogRec can not find object 11 %d, err %v", gen, err)
+		}
+		vers := fmt.Sprintf("%d", gen)
+		wantRec := &localLogRec{
+			Metadata: interfaces.LogRecMetadata{
+				Id:         11,
+				Gen:        gen,
+				RecType:    interfaces.NodeRec,
+				ObjId:      objid,
+				CurVers:    vers,
+				Parents:    parents,
+				UpdTime:    constTime,
+				BatchCount: 1,
+			},
+			Pos: gen - 1,
+		}
+
+		if !reflect.DeepEqual(gotRec, wantRec) {
+			t.Fatalf("Data mismatch in log record got %v, want %v", gotRec, wantRec)
+		}
+		// Verify DAG state.
+		if _, err := getNode(nil, svc.St(), objid, vers); err != nil {
+			t.Fatalf("getNode can not find object %s vers %s in DAG, err %v", objid, vers, err)
+		}
+		// Verify Database state.
+		tx := svc.St().NewTransaction()
+		if _, err := watchable.GetAtVersion(nil, tx, []byte(objid), nil, []byte(vers)); err != nil {
+			t.Fatalf("GetAtVersion can not find object %s vers %s in Database, err %v", objid, vers, err)
+		}
+		tx.Abort()
+		parents = []string{vers}
+	}
+
+	// Verify conflict state.
+	if len(iSt.updObjects) != 1 {
+		t.Fatalf("Unexpected number of updated objects %d", len(iSt.updObjects))
+	}
+	st := iSt.updObjects[objid]
+	if st.isConflict {
+		t.Fatalf("Detected a conflict %v", st)
+	}
+	if st.newHead != "3" || st.oldHead != NoVersion {
+		t.Fatalf("Conflict detection didn't succeed %v", st)
+	}
+
+	// Verify genvec state.
+	wantVec := interfaces.GenVector{
+		"foo1": interfaces.PrefixGenVector{11: 3},
+		"bar":  interfaces.PrefixGenVector{11: 0},
+	}
+	if !reflect.DeepEqual(iSt.updLocal, wantVec) {
+		t.Fatalf("Final local gen vec mismatch got %v, want %v", iSt.updLocal, wantVec)
+	}
+
+	// Verify DAG state.
+	if head, err := getHead(nil, svc.St(), objid); err != nil || head != "3" {
+		t.Fatalf("Invalid object %s head in DAG %v, err %v", objid, head, err)
+	}
+
+	// Verify Database state.
+	valbuf, err := svc.St().Get([]byte(objid), nil)
+	var val string
+	if err := vom.Decode(valbuf, &val); err != nil {
+		t.Fatalf("Value decode failed, err %v", err)
+	}
+	if err != nil || val != "abc" {
+		t.Fatalf("Invalid object %s in Database %v, err %v", objid, val, err)
+	}
+	tx := svc.St().NewTransaction()
+	version, err := watchable.GetVersion(nil, tx, []byte(objid))
+	if err != nil || string(version) != "3" {
+		t.Fatalf("Invalid object %s head in Database %v, err %v", objid, string(version), err)
+	}
+	tx.Abort()
+}
+
+// TestLogStreamNoConflict tests that a local and a remote log stream can be
+// correctly applied (when there are no conflicts). Commands are in files
+// testdata/<local-init-00.log.sync,remote-noconf-00.log.sync>.
+func TestLogStreamNoConflict(t *testing.T) {
+	svc, iSt, cleanup := testInit(t, "local-init-00.log.sync", "remote-noconf-00.log.sync")
+	defer cleanup(t, svc)
+
+	objid := util.JoinKeyParts(util.RowPrefix, "foo1")
+
+	// Check all log records.
+	var version uint64 = 1
+	var parents []string
+	for _, devid := range []uint64{10, 11} {
+		var gen uint64
+		for gen = 1; gen < 4; gen++ {
+			gotRec, err := getLogRec(nil, svc.St(), devid, gen)
+			if err != nil || gotRec == nil {
+				t.Fatalf("getLogRec can not find object %d:%d, err %v",
+					devid, gen, err)
+			}
+			vers := fmt.Sprintf("%d", version)
+			wantRec := &localLogRec{
+				Metadata: interfaces.LogRecMetadata{
+					Id:         devid,
+					Gen:        gen,
+					RecType:    interfaces.NodeRec,
+					ObjId:      objid,
+					CurVers:    vers,
+					Parents:    parents,
+					UpdTime:    constTime,
+					BatchCount: 1,
+				},
+				Pos: gen - 1,
+			}
+
+			if !reflect.DeepEqual(gotRec, wantRec) {
+				t.Fatalf("Data mismatch in log record got %v, want %v", gotRec, wantRec)
+			}
+
+			// Verify DAG state.
+			if _, err := getNode(nil, svc.St(), objid, vers); err != nil {
+				t.Fatalf("getNode can not find object %s vers %s in DAG, err %v", objid, vers, err)
+			}
+			// Verify Database state.
+			tx := svc.St().NewTransaction()
+			if _, err := watchable.GetAtVersion(nil, tx, []byte(objid), nil, []byte(vers)); err != nil {
+				t.Fatalf("GetAtVersion can not find object %s vers %s in Database, err %v", objid, vers, err)
+			}
+			tx.Abort()
+			parents = []string{vers}
+			version++
+		}
+	}
+
+	// Verify conflict state.
+	if len(iSt.updObjects) != 1 {
+		t.Fatalf("Unexpected number of updated objects %d", len(iSt.updObjects))
+	}
+	st := iSt.updObjects[objid]
+	if st.isConflict {
+		t.Fatalf("Detected a conflict %v", st)
+	}
+	if st.newHead != "6" || st.oldHead != "3" {
+		t.Fatalf("Conflict detection didn't succeed %v", st)
+	}
+
+	// Verify genvec state.
+	wantVec := interfaces.GenVector{
+		"foo1": interfaces.PrefixGenVector{11: 3},
+		"bar":  interfaces.PrefixGenVector{11: 0},
+	}
+	if !reflect.DeepEqual(iSt.updLocal, wantVec) {
+		t.Fatalf("Final local gen vec failed got %v, want %v", iSt.updLocal, wantVec)
+	}
+
+	// Verify DAG state.
+	if head, err := getHead(nil, svc.St(), objid); err != nil || head != "6" {
+		t.Fatalf("Invalid object %s head in DAG %v, err %v", objid, head, err)
+	}
+
+	// Verify Database state.
+	valbuf, err := svc.St().Get([]byte(objid), nil)
+	var val string
+	if err := vom.Decode(valbuf, &val); err != nil {
+		t.Fatalf("Value decode failed, err %v", err)
+	}
+	if err != nil || val != "abc" {
+		t.Fatalf("Invalid object %s in Database %v, err %v", objid, val, err)
+	}
+	tx := svc.St().NewTransaction()
+	versbuf, err := watchable.GetVersion(nil, tx, []byte(objid))
+	if err != nil || string(versbuf) != "6" {
+		t.Fatalf("Invalid object %s head in Database %v, err %v", objid, string(versbuf), err)
+	}
+	tx.Abort()
+}
+
+// TestLogStreamConflict tests that a local and a remote log stream can be
+// correctly applied when there are conflicts. Commands are in files
+// testdata/<local-init-00.log.sync,remote-conf-00.log.sync>.
+func TestLogStreamConflict(t *testing.T) {
+	svc, iSt, cleanup := testInit(t, "local-init-00.log.sync", "remote-conf-00.log.sync")
+	defer cleanup(t, svc)
+
+	objid := util.JoinKeyParts(util.RowPrefix, "foo1")
+
+	// Verify conflict state.
+	if len(iSt.updObjects) != 1 {
+		t.Fatalf("Unexpected number of updated objects %d", len(iSt.updObjects))
+	}
+	st := iSt.updObjects[objid]
+	if !st.isConflict {
+		t.Fatalf("Didn't detect a conflict %v", st)
+	}
+	if st.newHead != "6" || st.oldHead != "3" || st.ancestor != "2" {
+		t.Fatalf("Conflict detection didn't succeed %v", st)
+	}
+	if st.res.ty != pickRemote {
+		t.Fatalf("Conflict resolution did not pick remote: %v", st.res.ty)
+	}
+
+	// Verify DAG state.
+	if head, err := getHead(nil, svc.St(), objid); err != nil || head != "6" {
+		t.Fatalf("Invalid object %s head in DAG %v, err %v", objid, head, err)
+	}
+
+	// Verify Database state.
+	valbuf, err := svc.St().Get([]byte(objid), nil)
+	var val string
+	if err := vom.Decode(valbuf, &val); err != nil {
+		t.Fatalf("Value decode failed, err %v", err)
+	}
+	if err != nil || val != "abc" {
+		t.Fatalf("Invalid object %s in Database %v, err %v", objid, string(valbuf), err)
+	}
+	tx := svc.St().NewTransaction()
+	versbuf, err := watchable.GetVersion(nil, tx, []byte(objid))
+	if err != nil || string(versbuf) != "6" {
+		t.Fatalf("Invalid object %s head in Database %v, err %v", objid, string(versbuf), err)
+	}
+	tx.Abort()
+}
+
+// TestLogStreamConflictNoAncestor tests that a local and a remote log stream
+// can be correctly applied when there are conflicts from the start where the
+// two versions of an object have no common ancestor. Commands are in files
+// testdata/<local-init-00.log.sync,remote-conf-03.log.sync>.
+func TestLogStreamConflictNoAncestor(t *testing.T) {
+	svc, iSt, cleanup := testInit(t, "local-init-00.log.sync", "remote-conf-03.log.sync")
+	defer cleanup(t, svc)
+
+	objid := util.JoinKeyParts(util.RowPrefix, "foo1")
+
+	// Verify conflict state.
+	if len(iSt.updObjects) != 1 {
+		t.Fatalf("Unexpected number of updated objects %d", len(iSt.updObjects))
+	}
+	st := iSt.updObjects[objid]
+	if !st.isConflict {
+		t.Fatalf("Didn't detect a conflict %v", st)
+	}
+	if st.newHead != "6" || st.oldHead != "3" || st.ancestor != "" {
+		t.Fatalf("Conflict detection didn't succeed %v", st)
+	}
+	if st.res.ty != pickRemote {
+		t.Fatalf("Conflict resolution did not pick remote: %v", st.res.ty)
+	}
+
+	// Verify DAG state.
+	if head, err := getHead(nil, svc.St(), objid); err != nil || head != "6" {
+		t.Fatalf("Invalid object %s head in DAG %v, err %v", objid, head, err)
+	}
+
+	// Verify Database state.
+	valbuf, err := svc.St().Get([]byte(objid), nil)
+	var val string
+	if err := vom.Decode(valbuf, &val); err != nil {
+		t.Fatalf("Value decode failed, err %v", err)
+	}
+	if err != nil || val != "abc" {
+		t.Fatalf("Invalid object %s in Database %v, err %v", objid, string(valbuf), err)
+	}
+	tx := svc.St().NewTransaction()
+	versbuf, err := watchable.GetVersion(nil, tx, []byte(objid))
+	if err != nil || string(versbuf) != "6" {
+		t.Fatalf("Invalid object %s head in Database %v, err %v", objid, string(versbuf), err)
+	}
+	tx.Abort()
+}
+
+//////////////////////////////
+// Helpers.
+
+func testInit(t *testing.T, lfile, rfile string) (*mockService, *initiationState, func(*testing.T, *mockService)) {
+	// Set a large value to prevent the initiator from running.
+	peerSyncInterval = 1 * time.Hour
+	conflictResolutionPolicy = useTime
+	svc := createService(t)
+	cleanup := destroyService
+	s := svc.sync
+	s.id = 10 // initiator
+
+	sgId1 := interfaces.GroupId(1234)
+	nullInfo := nosql.SyncGroupMemberInfo{}
+	sgInfo := sgMemberInfo{
+		sgId1: nullInfo,
+	}
+
+	sg1 := &interfaces.SyncGroup{
+		Name:        "sg1",
+		Id:          sgId1,
+		AppName:     "mockapp",
+		DbName:      "mockdb",
+		Creator:     "mockCreator",
+		SpecVersion: "etag-0",
+		Spec: nosql.SyncGroupSpec{
+			Prefixes:    []string{"foo", "bar"},
+			MountTables: []string{"1/2/3/4", "5/6/7/8"},
+		},
+		Joiners: map[string]nosql.SyncGroupMemberInfo{
+			"a": nullInfo,
+			"b": nullInfo,
+		},
+	}
+
+	tx := svc.St().NewTransaction()
+	if err := addSyncGroup(nil, tx, sg1); err != nil {
+		t.Fatalf("cannot add SyncGroup ID %d, err %v", sg1.Id, err)
+	}
+	if err := tx.Commit(); err != nil {
+		t.Fatalf("cannot commit adding SyncGroup ID %d, err %v", sg1.Id, err)
+	}
+
+	if lfile != "" {
+		replayLocalCommands(t, svc, lfile)
+	}
+
+	if rfile == "" {
+		return svc, nil, cleanup
+	}
+
+	gdb := appDbName("mockapp", "mockdb")
+	iSt, err := newInitiationState(nil, s, "b", gdb, sgInfo)
+	if err != nil {
+		t.Fatalf("newInitiationState failed with err %v", err)
+	}
+
+	testIfSgPfxsEqual(t, iSt.sgPfxs, sg1.Spec.Prefixes)
+	testIfMapArrEqual(t, iSt.mtTables, sg1.Spec.MountTables)
+
+	s.initDbSyncStateInMem(nil, "mockapp", "mockdb")
+
+	// Create local genvec so that it contains knowledge only about common prefixes.
+	if err := iSt.createLocalGenVec(nil); err != nil {
+		t.Fatalf("createLocalGenVec failed with err %v", err)
+	}
+
+	wantVec := interfaces.GenVector{
+		"foo": interfaces.PrefixGenVector{10: 0},
+		"bar": interfaces.PrefixGenVector{10: 0},
+	}
+	if !reflect.DeepEqual(iSt.local, wantVec) {
+		t.Fatalf("createLocalGenVec failed got %v, want %v", iSt.local, wantVec)
+	}
+
+	iSt.stream = createReplayStream(t, rfile)
+
+	if err := iSt.recvAndProcessDeltas(nil); err != nil {
+		t.Fatalf("recvAndProcessDeltas failed with err %v", err)
+	}
+
+	if err := iSt.processUpdatedObjects(nil); err != nil {
+		t.Fatalf("processUpdatedObjects failed with err %v", err)
+	}
+	return svc, iSt, cleanup
+}
+
+func testIfSgPfxsEqual(t *testing.T, m map[string]sgSet, a []string) {
+	aMap := arrToMap(a)
+
+	if len(aMap) != len(m) {
+		t.Fatalf("testIfSgPfxsEqual diff lengths, got %v want %v", aMap, m)
+	}
+
+	for p := range aMap {
+		if _, ok := m[p]; !ok {
+			t.Fatalf("testIfSgPfxsEqual want %v", p)
+		}
+	}
+}
+
+func testIfMapArrEqual(t *testing.T, m map[string]struct{}, a []string) {
+	aMap := arrToMap(a)
+	if !reflect.DeepEqual(m, aMap) {
+		t.Fatalf("testIfMapArrEqual failed map %v, arr %v", m, aMap)
+	}
+}
+
+func arrToMap(a []string) map[string]struct{} {
+	m := make(map[string]struct{})
+	for _, s := range a {
+		m[s] = struct{}{}
+	}
+	return m
+}
diff --git a/services/syncbase/vsync/replay_test.go b/services/syncbase/vsync/replay_test.go
new file mode 100644
index 0000000..03d6dc6
--- /dev/null
+++ b/services/syncbase/vsync/replay_test.go
@@ -0,0 +1,399 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Used to ease the setup of sync test scenarios.
+// Parses a sync command file and returns a vector of commands to execute.
+// dagReplayCommands() executes the parsed commands at the DAG API level.
+
+import (
+	"bufio"
+	"container/list"
+	"fmt"
+	"os"
+	"strconv"
+	"strings"
+	"testing"
+	"time"
+
+	"v.io/syncbase/x/ref/services/syncbase/server/interfaces"
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/syncbase/x/ref/services/syncbase/server/watchable"
+	"v.io/v23/context"
+	"v.io/v23/vom"
+)
+
+const (
+	addLocal = iota
+	addRemote
+	linkLocal
+	linkRemote
+	genvec
+)
+
+var (
+	constTime = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
+)
+
+type syncCommand struct {
+	cmd        int
+	oid        string
+	version    string
+	parents    []string
+	logrec     string
+	deleted    bool
+	batchId    uint64
+	batchCount uint64
+	genVec     interfaces.GenVector
+}
+
+// parseSyncCommands parses a sync test file and returns its commands.
+func parseSyncCommands(file string) ([]syncCommand, error) {
+	cmds := []syncCommand{}
+
+	sf, err := os.Open("testdata/" + file)
+	if err != nil {
+		return nil, err
+	}
+	defer sf.Close()
+
+	scanner := bufio.NewScanner(sf)
+	lineno := 0
+	for scanner.Scan() {
+		lineno++
+		line := strings.TrimSpace(scanner.Text())
+		if line == "" || line[0] == '#' {
+			continue
+		}
+
+		args := strings.Split(line, "|")
+		nargs := len(args)
+
+		switch args[0] {
+		case "addl", "addr":
+			expNargs := 9
+			if nargs != expNargs {
+				return nil, fmt.Errorf("%s:%d: need %d args instead of %d",
+					file, lineno, expNargs, nargs)
+			}
+			var parents []string
+			for i := 3; i <= 4; i++ {
+				if args[i] != "" {
+					parents = append(parents, args[i])
+				}
+			}
+
+			batchId, err := strconv.ParseUint(args[6], 10, 64)
+			if err != nil {
+				return nil, fmt.Errorf("%s:%d: invalid batchId: %s", file, lineno, args[6])
+			}
+			batchCount, err := strconv.ParseUint(args[7], 10, 64)
+			if err != nil {
+				return nil, fmt.Errorf("%s:%d: invalid batch count: %s", file, lineno, args[7])
+			}
+			del, err := strconv.ParseBool(args[8])
+			if err != nil {
+				return nil, fmt.Errorf("%s:%d: invalid deleted bit: %s", file, lineno, args[8])
+			}
+			cmd := syncCommand{
+				oid:        args[1],
+				version:    args[2],
+				parents:    parents,
+				logrec:     args[5],
+				batchId:    batchId,
+				batchCount: batchCount,
+				deleted:    del,
+			}
+			if args[0] == "addl" {
+				cmd.cmd = addLocal
+			} else {
+				cmd.cmd = addRemote
+			}
+			cmds = append(cmds, cmd)
+
+		case "linkl", "linkr":
+			expNargs := 6
+			if nargs != expNargs {
+				return nil, fmt.Errorf("%s:%d: need %d args instead of %d",
+					file, lineno, expNargs, nargs)
+			}
+
+			if args[3] == "" {
+				return nil, fmt.Errorf("%s:%d: parent version not specified", file, lineno)
+			}
+			if args[4] != "" {
+				return nil, fmt.Errorf("%s:%d: cannot specify a 2nd parent: %s",
+					file, lineno, args[4])
+			}
+
+			cmd := syncCommand{
+				oid:     args[1],
+				version: args[2],
+				parents: []string{args[3]},
+				logrec:  args[5],
+			}
+			if args[0] == "linkl" {
+				cmd.cmd = linkLocal
+			} else {
+				cmd.cmd = linkRemote
+			}
+			cmds = append(cmds, cmd)
+
+		case "genvec":
+			cmd := syncCommand{
+				cmd:    genvec,
+				genVec: make(interfaces.GenVector),
+			}
+			for i := 1; i < len(args); i = i + 2 {
+				pfx := args[i]
+				genVec := make(interfaces.PrefixGenVector)
+				for _, elem := range strings.Split(args[i+1], ",") {
+					kv := strings.Split(elem, ":")
+					if len(kv) != 2 {
+						return nil, fmt.Errorf("%s:%d: invalid gen vector key/val: %s", file, lineno, elem)
+					}
+					dev, err := strconv.ParseUint(kv[0], 10, 64)
+					if err != nil {
+						return nil, fmt.Errorf("%s:%d: invalid devid: %s", file, lineno, args[i+1])
+					}
+					gen, err := strconv.ParseUint(kv[1], 10, 64)
+					if err != nil {
+						return nil, fmt.Errorf("%s:%d: invalid gen: %s", file, lineno, args[i+1])
+					}
+					genVec[dev] = gen
+				}
+				cmd.genVec[pfx] = genVec
+			}
+			cmds = append(cmds, cmd)
+
+		default:
+			return nil, fmt.Errorf("%s:%d: invalid operation: %s", file, lineno, args[0])
+		}
+	}
+
+	err = scanner.Err()
+	return cmds, err
+}
+
+// dagReplayCommands parses a sync test file and replays its commands, updating
+// the DAG structures associated with the sync service.
+func (s *syncService) dagReplayCommands(ctx *context.T, syncfile string) (graftMap, error) {
+	cmds, err := parseSyncCommands(syncfile)
+	if err != nil {
+		return nil, err
+	}
+
+	st := s.sv.St()
+	graft := newGraft()
+
+	for _, cmd := range cmds {
+		tx := st.NewTransaction()
+
+		switch cmd.cmd {
+		case addLocal:
+			err = s.addNode(ctx, tx, cmd.oid, cmd.version, cmd.logrec,
+				cmd.deleted, cmd.parents, NoBatchId, nil)
+			if err != nil {
+				return nil, fmt.Errorf("cannot add local node %s:%s: %v",
+					cmd.oid, cmd.version, err)
+			}
+
+			if err = moveHead(ctx, tx, cmd.oid, cmd.version); err != nil {
+				return nil, fmt.Errorf("cannot move head to %s:%s: %v",
+					cmd.oid, cmd.version, err)
+			}
+
+		case addRemote:
+			err = s.addNode(ctx, tx, cmd.oid, cmd.version, cmd.logrec,
+				cmd.deleted, cmd.parents, NoBatchId, graft)
+			if err != nil {
+				return nil, fmt.Errorf("cannot add remote node %s:%s: %v",
+					cmd.oid, cmd.version, err)
+			}
+
+		case linkLocal:
+			if err = s.addParent(ctx, tx, cmd.oid, cmd.version, cmd.parents[0], nil); err != nil {
+				return nil, fmt.Errorf("cannot add local parent %s to node %s:%s: %v",
+					cmd.parents[0], cmd.oid, cmd.version, err)
+			}
+
+		case linkRemote:
+			if err = s.addParent(ctx, tx, cmd.oid, cmd.version, cmd.parents[0], graft); err != nil {
+				return nil, fmt.Errorf("cannot add remote parent %s to node %s:%s: %v",
+					cmd.parents[0], cmd.oid, cmd.version, err)
+			}
+		}
+
+		tx.Commit()
+	}
+
+	return graft, nil
+}
+
+// dummyStream emulates stream of log records received from RPC.
+type dummyStream struct {
+	l     *list.List
+	entry interfaces.DeltaResp
+}
+
+func newStream() *dummyStream {
+	ds := &dummyStream{
+		l: list.New(),
+	}
+	return ds
+}
+
+func (ds *dummyStream) add(entry interfaces.DeltaResp) {
+	ds.l.PushBack(entry)
+}
+
+func (ds *dummyStream) Advance() bool {
+	if ds.l.Len() > 0 {
+		ds.entry = ds.l.Remove(ds.l.Front()).(interfaces.DeltaResp)
+		return true
+	}
+	return false
+}
+
+func (ds *dummyStream) Value() interfaces.DeltaResp {
+	return ds.entry
+}
+
+func (ds *dummyStream) RecvStream() interface {
+	Advance() bool
+	Value() interfaces.DeltaResp
+	Err() error
+} {
+	return ds
+}
+
+func (*dummyStream) Err() error { return nil }
+
+func (ds *dummyStream) Finish() error {
+	return nil
+}
+
+func (ds *dummyStream) Cancel() {
+}
+
+func (ds *dummyStream) SendStream() interface {
+	Send(item interfaces.DeltaReq) error
+	Close() error
+} {
+	return ds
+}
+
+func (ds *dummyStream) Send(item interfaces.DeltaReq) error {
+	return nil
+}
+
+func (ds *dummyStream) Close() error {
+	return nil
+}
+
+// replayLocalCommands replays local log records parsed from the input file.
+func replayLocalCommands(t *testing.T, s *mockService, syncfile string) {
+	cmds, err := parseSyncCommands(syncfile)
+	if err != nil {
+		t.Fatalf("parseSyncCommands failed with err %v", err)
+	}
+
+	tx := s.St().NewTransaction()
+	var pos uint64
+	for _, cmd := range cmds {
+		switch cmd.cmd {
+		case addLocal:
+			rec := &localLogRec{
+				Metadata: createMetadata(t, interfaces.NodeRec, cmd),
+				Pos:      pos,
+			}
+			err = s.sync.processLocalLogRec(nil, tx, rec)
+			if err != nil {
+				t.Fatalf("processLocalLogRec failed with err %v", err)
+			}
+
+			// Add to Store.
+			err = watchable.PutVersion(nil, tx, []byte(rec.Metadata.ObjId), []byte(rec.Metadata.CurVers))
+			if err != nil {
+				t.Fatalf("PutVersion failed with err %v", err)
+			}
+			err = watchable.PutAtVersion(nil, tx, []byte(rec.Metadata.ObjId), []byte("abc"), []byte(rec.Metadata.CurVers))
+			if err != nil {
+				t.Fatalf("PutAtVersion failed with err %v", err)
+			}
+
+		default:
+			t.Fatalf("replayLocalCommands failed with unknown command %v", cmd)
+		}
+		pos++
+	}
+	if err := tx.Commit(); err != nil {
+		t.Fatalf("cannot commit local log records %s, err %v", syncfile, err)
+	}
+}
+
+// createReplayStream creates a dummy stream of log records parsed from the input file.
+func createReplayStream(t *testing.T, syncfile string) *dummyStream {
+	cmds, err := parseSyncCommands(syncfile)
+	if err != nil {
+		t.Fatalf("parseSyncCommands failed with err %v", err)
+	}
+
+	stream := newStream()
+	start := interfaces.DeltaRespStart{true}
+	stream.add(start)
+
+	for _, cmd := range cmds {
+		var ty byte
+		switch cmd.cmd {
+		case genvec:
+			gv := interfaces.DeltaRespRespVec{cmd.genVec}
+			stream.add(gv)
+			continue
+		case addRemote:
+			ty = interfaces.NodeRec
+		case linkRemote:
+			ty = interfaces.LinkRec
+		default:
+			t.Fatalf("createReplayStream unknown command %v", cmd)
+		}
+
+		var val string = "abc"
+		valbuf, err := vom.Encode(val)
+		if err != nil {
+			t.Fatalf("createReplayStream encode failed, err %v", err)
+		}
+
+		rec := interfaces.DeltaRespRec{interfaces.LogRec{
+			Metadata: createMetadata(t, ty, cmd),
+			Value:    valbuf,
+		}}
+
+		stream.add(rec)
+	}
+	fin := interfaces.DeltaRespFinish{true}
+	stream.add(fin)
+	return stream
+}
+
+func createMetadata(t *testing.T, ty byte, cmd syncCommand) interfaces.LogRecMetadata {
+	id, gen, err := splitLogRecKey(nil, cmd.logrec)
+	if err != nil {
+		t.Fatalf("createReplayStream splitLogRecKey failed, key %s, err %v", cmd.logrec, gen)
+	}
+	m := interfaces.LogRecMetadata{
+		Id:         id,
+		Gen:        gen,
+		RecType:    ty,
+		ObjId:      util.JoinKeyParts(util.RowPrefix, cmd.oid),
+		CurVers:    cmd.version,
+		Parents:    cmd.parents,
+		UpdTime:    constTime,
+		Delete:     cmd.deleted,
+		BatchId:    cmd.batchId,
+		BatchCount: cmd.batchCount,
+	}
+	return m
+}
diff --git a/services/syncbase/vsync/responder.go b/services/syncbase/vsync/responder.go
new file mode 100644
index 0000000..c417eca
--- /dev/null
+++ b/services/syncbase/vsync/responder.go
@@ -0,0 +1,516 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+import (
+	"container/heap"
+	"sort"
+	"strings"
+
+	wire "v.io/syncbase/v23/services/syncbase/nosql"
+	"v.io/syncbase/x/ref/services/syncbase/server/interfaces"
+	"v.io/syncbase/x/ref/services/syncbase/server/watchable"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/context"
+	"v.io/v23/verror"
+	"v.io/x/lib/vlog"
+)
+
+// GetDeltas implements the responder side of the GetDeltas RPC.
+func (s *syncService) GetDeltas(ctx *context.T, call interfaces.SyncGetDeltasServerCall, initiator string) error {
+	vlog.VI(2).Infof("sync: GetDeltas: begin: from initiator %s", initiator)
+	defer vlog.VI(2).Infof("sync: GetDeltas: end: from initiator %s", initiator)
+
+	recvr := call.RecvStream()
+	for recvr.Advance() {
+		req := recvr.Value()
+		// Ignoring errors since if one Database fails for any reason,
+		// it is fine to continue to the next one. In fact, sometimes
+		// the failure might be genuine. For example, the responder is
+		// no longer part of the requested SyncGroups, or the app/db is
+		// locally deleted, or a permission change has denied access.
+		rSt := newResponderState(ctx, call, s, req, initiator)
+		rSt.sendDeltasPerDatabase(ctx)
+	}
+
+	// TODO(hpucha): Is there a need to call finish or some such?
+	return recvr.Err()
+}
+
+// responderState is state accumulated per Database by the responder during an
+// initiation round.
+type responderState struct {
+	req       interfaces.DeltaReq
+	call      interfaces.SyncGetDeltasServerCall // Stream handle for the GetDeltas RPC.
+	initiator string
+	errState  error // Captures the error from the first two phases of the responder.
+	sync      *syncService
+	st        store.Store // Store handle to the Database.
+	diff      genRangeVector
+	outVec    interfaces.GenVector
+}
+
+func newResponderState(ctx *context.T, call interfaces.SyncGetDeltasServerCall, sync *syncService, req interfaces.DeltaReq, initiator string) *responderState {
+	rSt := &responderState{call: call, sync: sync, req: req, initiator: initiator}
+	return rSt
+}
+
+// sendDeltasPerDatabase sends to an initiator all the missing generations
+// corresponding to the prefixes requested for this Database, and a genvector
+// summarizing the knowledge transferred from the responder to the
+// initiator. This happens in three phases:
+//
+// In the first phase, the initiator is checked against the SyncGroup ACLs of
+// all the SyncGroups it is requesting, and only those prefixes that belong to
+// allowed SyncGroups are carried forward.
+//
+// In the second phase, for a given set of nested prefixes from the initiator,
+// the shortest prefix in that set is extracted. The initiator's prefix
+// genvector for this shortest prefix represents the lower bound on its
+// knowledge for the entire set of nested prefixes. This prefix genvector
+// (representing the lower bound) is diffed with all the responder prefix
+// genvectors corresponding to same or deeper prefixes compared to the initiator
+// prefix. This diff produces a bound on the missing knowledge. For example, say
+// the initiator is interested in prefixes {foo, foobar}, where each prefix is
+// associated with a prefix genvector. Since the initiator strictly has as much
+// or more knowledge for prefix "foobar" as it has for prefix "foo", "foo"'s
+// prefix genvector is chosen as the lower bound for the initiator's
+// knowledge. Similarly, say the responder has knowledge on prefixes {f,
+// foobarX, foobarY, bar}. The responder diffs the prefix genvectors for
+// prefixes f, foobarX and foobarY with the initiator's prefix genvector to
+// compute a bound on missing generations (all responder's prefixes that match
+// "foo". Note that since the responder doesn't have a prefix genvector at
+// "foo", its knowledge at "f" is applicable to "foo").
+//
+// Since the second phase outputs an aggressive calculation of missing
+// generations containing more generation entries than strictly needed by the
+// initiator, in the third phase, each missing generation is sent to the
+// initiator only if the initiator is eligible for it and is not aware of
+// it. The generations are sent to the initiator in the same order as the
+// responder learned them so that the initiator can reconstruct the DAG for the
+// objects by learning older nodes first.
+func (rSt *responderState) sendDeltasPerDatabase(ctx *context.T) error {
+	// TODO(rdaoud): for such vlog.VI() calls where the function name is
+	// embedded, consider using a helper function to auto-fill it instead
+	// (see http://goo.gl/mEa4L0) but only incur that overhead when the
+	// logging level specified is enabled.
+	vlog.VI(3).Infof("sync: sendDeltasPerDatabase: %s, %s: sgids %v, genvec %v",
+		rSt.req.AppName, rSt.req.DbName, rSt.req.SgIds, rSt.req.InitVec)
+
+	// Phase 1 of sendDeltas: Authorize the initiator and respond to the
+	// caller only for the SyncGroups that allow access.
+	rSt.authorizeAndFilterSyncGroups(ctx)
+
+	// Phase 2 of sendDeltas: diff contains the bound on the
+	// generations missing from the initiator per device.
+	rSt.computeDeltaBound(ctx)
+
+	// Phase 3 of sendDeltas: Process the diff, filtering out records that
+	// are not needed, and send the remainder on the wire ordered.
+	return rSt.filterAndSendDeltas(ctx)
+}
+
+// authorizeAndFilterSyncGroups authorizes the initiator against the requested
+// SyncGroups and filters the initiator's prefixes to only include those from
+// allowed SyncGroups (phase 1 of sendDeltas).
+func (rSt *responderState) authorizeAndFilterSyncGroups(ctx *context.T) {
+	rSt.st, rSt.errState = rSt.sync.getDbStore(ctx, nil, rSt.req.AppName, rSt.req.DbName)
+	if rSt.errState != nil {
+		return
+	}
+
+	allowedPfxs := make(map[string]struct{})
+	for sgid := range rSt.req.SgIds {
+		// Check permissions for the SyncGroup.
+		var sg *interfaces.SyncGroup
+		sg, rSt.errState = getSyncGroupById(ctx, rSt.st, sgid)
+		if rSt.errState != nil {
+			return
+		}
+		rSt.errState = authorize(ctx, rSt.call.Security(), sg)
+		if verror.ErrorID(rSt.errState) == verror.ErrNoAccess.ID {
+			continue
+		} else if rSt.errState != nil {
+			return
+		}
+
+		for _, p := range sg.Spec.Prefixes {
+			allowedPfxs[p] = struct{}{}
+		}
+
+		// Add the initiator to the SyncGroup membership if not already
+		// in it.  It is a temporary solution until SyncGroup metadata
+		// is synchronized peer to peer.
+		// TODO(rdaoud): remove this when SyncGroups are synced.
+		rSt.addInitiatorToSyncGroup(ctx, sgid)
+	}
+
+	// Filter the initiator's prefixes to what is allowed.
+	for pfx := range rSt.req.InitVec {
+		if _, ok := allowedPfxs[pfx]; ok {
+			continue
+		}
+		allowed := false
+		for p := range allowedPfxs {
+			if strings.HasPrefix(pfx, p) {
+				allowed = true
+			}
+		}
+
+		if !allowed {
+			delete(rSt.req.InitVec, pfx)
+		}
+	}
+	return
+}
+
+// addInitiatorToSyncGroup adds the request initiator to the membership of the
+// given SyncGroup if the initiator is not already a member.  It is a temporary
+// solution until SyncGroup metadata starts being synchronized, at which time
+// peers will learn of new members through mutations of the SyncGroup metadata
+// by the SyncGroup administrators.
+// Note: the joiner metadata is fake because the responder does not have it.
+func (rSt *responderState) addInitiatorToSyncGroup(ctx *context.T, gid interfaces.GroupId) {
+	if rSt.initiator == "" {
+		return
+	}
+
+	err := store.RunInTransaction(rSt.st, func(tx store.Transaction) error {
+		sg, err := getSyncGroupById(ctx, tx, gid)
+		if err != nil {
+			return err
+		}
+
+		// If the initiator is already a member of the SyncGroup abort
+		// the transaction with a special error code.
+		if _, ok := sg.Joiners[rSt.initiator]; ok {
+			return verror.New(verror.ErrExist, ctx, "member already in SyncGroup")
+		}
+
+		vlog.VI(4).Infof("sync: addInitiatorToSyncGroup: add %s to sgid %d", rSt.initiator, gid)
+		sg.Joiners[rSt.initiator] = wire.SyncGroupMemberInfo{SyncPriority: 1}
+		return setSGDataEntry(ctx, tx, gid, sg)
+	})
+
+	if err != nil && verror.ErrorID(err) != verror.ErrExist.ID {
+		vlog.Errorf("sync: addInitiatorToSyncGroup: initiator %s, sgid %d: %v", rSt.initiator, gid, err)
+	}
+}
+
+// computeDeltaBound computes the bound on missing generations across all
+// requested prefixes (phase 2 of sendDeltas).
+func (rSt *responderState) computeDeltaBound(ctx *context.T) {
+	// Check error from phase 1.
+	if rSt.errState != nil {
+		return
+	}
+
+	if len(rSt.req.InitVec) == 0 {
+		rSt.errState = verror.New(verror.ErrInternal, ctx, "empty initiator generation vector")
+		return
+	}
+
+	var respVec interfaces.GenVector
+	var respGen uint64
+	respVec, respGen, rSt.errState = rSt.sync.copyDbGenInfo(ctx, rSt.req.AppName, rSt.req.DbName)
+	if rSt.errState != nil {
+		return
+	}
+	respPfxs := extractAndSortPrefixes(respVec)
+	initPfxs := extractAndSortPrefixes(rSt.req.InitVec)
+
+	rSt.outVec = make(interfaces.GenVector)
+	rSt.diff = make(genRangeVector)
+	pfx := initPfxs[0]
+
+	for _, p := range initPfxs {
+		if strings.HasPrefix(p, pfx) && p != pfx {
+			continue
+		}
+
+		// Process this prefix as this is the start of a new set of
+		// nested prefixes.
+		pfx = p
+
+		// Lower bound on initiator's knowledge for this prefix set.
+		initpgv := rSt.req.InitVec[pfx]
+
+		// Find the relevant responder prefixes and add the corresponding knowledge.
+		var respgv interfaces.PrefixGenVector
+		var rpStart string
+		for _, rp := range respPfxs {
+			if !strings.HasPrefix(rp, pfx) && !strings.HasPrefix(pfx, rp) {
+				// No relationship with pfx.
+				continue
+			}
+
+			if strings.HasPrefix(pfx, rp) {
+				// If rp is a prefix of pfx, remember it because
+				// it may be a potential starting point for the
+				// responder's knowledge. The actual starting
+				// point is the deepest prefix where rp is a
+				// prefix of pfx.
+				//
+				// Say the initiator is looking for "foo", and
+				// the responder has knowledge for "f" and "fo",
+				// the responder's starting point will be the
+				// prefix genvector for "fo". Similarly, if the
+				// responder has knowledge for "foo", the
+				// starting point will be the prefix genvector
+				// for "foo".
+				rpStart = rp
+			} else {
+				// If pfx is a prefix of rp, this knowledge must
+				// be definitely sent to the initiator. Diff the
+				// prefix genvectors to adjust the delta bound and
+				// include in outVec.
+				respgv = respVec[rp]
+				rSt.diffPrefixGenVectors(respgv, initpgv)
+				rSt.outVec[rp] = respgv
+			}
+		}
+
+		// Deal with the starting point.
+		if rpStart == "" {
+			// No matching prefixes for pfx were found.
+			respgv = make(interfaces.PrefixGenVector)
+			respgv[rSt.sync.id] = respGen
+		} else {
+			respgv = respVec[rpStart]
+		}
+		rSt.diffPrefixGenVectors(respgv, initpgv)
+		rSt.outVec[pfx] = respgv
+	}
+
+	vlog.VI(3).Infof("sync: computeDeltaBound: %s, %s: diff %v, outvec %v",
+		rSt.req.AppName, rSt.req.DbName, rSt.diff, rSt.outVec)
+	return
+}
+
+// filterAndSendDeltas filters the computed delta to remove records already
+// known by the initiator, and sends the resulting records to the initiator
+// (phase 3 of sendDeltas).
+func (rSt *responderState) filterAndSendDeltas(ctx *context.T) error {
+	// Always send a start and finish response so that the initiator can
+	// move on to the next Database.
+	//
+	// TODO(hpucha): Although ok for now to call SendStream once per
+	// Database, would like to make this implementation agnostic.
+	sender := rSt.call.SendStream()
+	sender.Send(interfaces.DeltaRespStart{true})
+	defer sender.Send(interfaces.DeltaRespFinish{true})
+
+	// Check error from phase 2.
+	if rSt.errState != nil {
+		return rSt.errState
+	}
+
+	// First two phases were successful. So now on to phase 3. We now visit
+	// every log record in the generation range as obtained from phase 1 in
+	// their log order. We use a heap to incrementally sort the log records
+	// as per their position in the log.
+	//
+	// Init the min heap, one entry per device in the diff.
+	mh := make(minHeap, 0, len(rSt.diff))
+	for dev, r := range rSt.diff {
+		r.cur = r.min
+		rec, err := getNextLogRec(ctx, rSt.st, dev, r)
+		if err != nil {
+			return err
+		}
+		if rec != nil {
+			mh = append(mh, rec)
+		} else {
+			delete(rSt.diff, dev)
+		}
+	}
+	heap.Init(&mh)
+
+	// Process the log records in order.
+	initPfxs := extractAndSortPrefixes(rSt.req.InitVec)
+	for mh.Len() > 0 {
+		rec := heap.Pop(&mh).(*localLogRec)
+
+		if !filterLogRec(rec, rSt.req.InitVec, initPfxs) {
+			// Send on the wire.
+			wireRec, err := makeWireLogRec(ctx, rSt.st, rec)
+			if err != nil {
+				return err
+			}
+			sender.Send(interfaces.DeltaRespRec{*wireRec})
+		}
+
+		// Add a new record from the same device if not done.
+		dev := rec.Metadata.Id
+		rec, err := getNextLogRec(ctx, rSt.st, dev, rSt.diff[dev])
+		if err != nil {
+			return err
+		}
+		if rec != nil {
+			heap.Push(&mh, rec)
+		} else {
+			delete(rSt.diff, dev)
+		}
+	}
+
+	sender.Send(interfaces.DeltaRespRespVec{rSt.outVec})
+	return nil
+}
+
+// genRange represents a range of generations (min and max inclusive).
+type genRange struct {
+	min uint64
+	max uint64
+	cur uint64
+}
+
+type genRangeVector map[uint64]*genRange
+
+// diffPrefixGenVectors diffs two generation vectors, belonging to the responder
+// and the initiator, and updates the range of generations per device known to
+// the responder but not known to the initiator. "gens" (generation range) is
+// passed in as an input argument so that it can be incrementally updated as the
+// range of missing generations grows when different responder prefix genvectors
+// are used to compute the diff.
+//
+// For example: Generation vector for responder is say RVec = {A:10, B:5, C:1},
+// Generation vector for initiator is say IVec = {A:5, B:10, D:2}. Diffing these
+// two vectors returns: {A:[6-10], C:[1-1]}.
+//
+// TODO(hpucha): Add reclaimVec for GCing.
+func (rSt *responderState) diffPrefixGenVectors(respPVec, initPVec interfaces.PrefixGenVector) {
+	// Compute missing generations for devices that are in both initiator's and responder's vectors.
+	for devid, gen := range initPVec {
+		rgen, ok := respPVec[devid]
+		if ok {
+			updateDevRange(devid, rgen, gen, rSt.diff)
+		}
+	}
+
+	// Compute missing generations for devices not in initiator's vector but in responder's vector.
+	for devid, rgen := range respPVec {
+		if _, ok := initPVec[devid]; !ok {
+			updateDevRange(devid, rgen, 0, rSt.diff)
+		}
+	}
+}
+
+func updateDevRange(devid, rgen, gen uint64, gens genRangeVector) {
+	if gen < rgen {
+		// Need to include all generations in the interval [gen+1,rgen], gen+1 and rgen inclusive.
+		if r, ok := gens[devid]; !ok {
+			gens[devid] = &genRange{min: gen + 1, max: rgen}
+		} else {
+			if gen+1 < r.min {
+				r.min = gen + 1
+			}
+			if rgen > r.max {
+				r.max = rgen
+			}
+		}
+	}
+}
+
+func extractAndSortPrefixes(vec interfaces.GenVector) []string {
+	pfxs := make([]string, len(vec))
+	i := 0
+	for p := range vec {
+		pfxs[i] = p
+		i++
+	}
+	sort.Strings(pfxs)
+	return pfxs
+}
+
+// TODO(hpucha): This can be optimized using a scan instead of "gets" in a for
+// loop.
+func getNextLogRec(ctx *context.T, st store.Store, dev uint64, r *genRange) (*localLogRec, error) {
+	for i := r.cur; i <= r.max; i++ {
+		rec, err := getLogRec(ctx, st, dev, i)
+		if err == nil {
+			r.cur = i + 1
+			return rec, nil
+		}
+		if verror.ErrorID(err) != verror.ErrNoExist.ID {
+			return nil, err
+		}
+	}
+	return nil, nil
+}
+
+// Note: initPfxs is sorted.
+func filterLogRec(rec *localLogRec, initVec interfaces.GenVector, initPfxs []string) bool {
+	// The key starts with one of the store's reserved prefixes for managed
+	// namespaces (e.g. $row, $perms).  Remove that prefix before comparing
+	// it with the SyncGroup prefixes which are defined by the application.
+	key := extractAppKey(rec.Metadata.ObjId)
+
+	filter := true
+	var maxGen uint64
+	for _, p := range initPfxs {
+		if strings.HasPrefix(key, p) {
+			// Do not filter. Initiator is interested in this
+			// prefix.
+			filter = false
+
+			// Track if the initiator knows of this record.
+			gen := initVec[p][rec.Metadata.Id]
+			if maxGen < gen {
+				maxGen = gen
+			}
+		}
+	}
+
+	// Filter this record if the initiator already has it.
+	if maxGen >= rec.Metadata.Gen {
+		filter = true
+	}
+
+	return filter
+}
+
+// makeWireLogRec creates a sync log record to send on the wire from a given
+// local sync record.
+func makeWireLogRec(ctx *context.T, st store.Store, rec *localLogRec) (*interfaces.LogRec, error) {
+	// Get the object value at the required version.
+	key, version := rec.Metadata.ObjId, rec.Metadata.CurVers
+	var value []byte
+	if !rec.Metadata.Delete {
+		var err error
+		value, err = watchable.GetAtVersion(ctx, st, []byte(key), nil, []byte(version))
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	wireRec := &interfaces.LogRec{Metadata: rec.Metadata, Value: value}
+	return wireRec, nil
+}
+
+// A minHeap implements heap.Interface and holds local log records.
+type minHeap []*localLogRec
+
+func (mh minHeap) Len() int { return len(mh) }
+
+func (mh minHeap) Less(i, j int) bool {
+	return mh[i].Pos < mh[j].Pos
+}
+
+func (mh minHeap) Swap(i, j int) {
+	mh[i], mh[j] = mh[j], mh[i]
+}
+
+func (mh *minHeap) Push(x interface{}) {
+	item := x.(*localLogRec)
+	*mh = append(*mh, item)
+}
+
+func (mh *minHeap) Pop() interface{} {
+	old := *mh
+	n := len(old)
+	item := old[n-1]
+	*mh = old[0 : n-1]
+	return item
+}
diff --git a/services/syncbase/vsync/responder_test.go b/services/syncbase/vsync/responder_test.go
new file mode 100644
index 0000000..986f9b3
--- /dev/null
+++ b/services/syncbase/vsync/responder_test.go
@@ -0,0 +1,520 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+import (
+	"fmt"
+	"math/rand"
+	"reflect"
+	"testing"
+	"time"
+
+	"v.io/syncbase/x/ref/services/syncbase/server/interfaces"
+	"v.io/syncbase/x/ref/services/syncbase/server/watchable"
+	"v.io/v23/naming"
+	"v.io/v23/rpc"
+	"v.io/v23/security"
+	_ "v.io/x/ref/runtime/factories/generic"
+)
+
+// TestDiffPrefixGenVectors tests diffing prefix gen vectors.
+func TestDiffPrefixGenVectors(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	s := svc.sync
+	s.id = 10 //responder. Initiator is id 11.
+
+	tests := []struct {
+		respPVec, initPVec interfaces.PrefixGenVector
+		genDiffIn          genRangeVector
+		genDiffWant        genRangeVector
+	}{
+		{ // responder and initiator are at identical vectors.
+			respPVec:  interfaces.PrefixGenVector{10: 1, 11: 10, 12: 20, 13: 2},
+			initPVec:  interfaces.PrefixGenVector{10: 1, 11: 10, 12: 20, 13: 2},
+			genDiffIn: make(genRangeVector),
+		},
+		{ // responder and initiator are at identical vectors.
+			respPVec:  interfaces.PrefixGenVector{10: 0},
+			initPVec:  interfaces.PrefixGenVector{10: 0},
+			genDiffIn: make(genRangeVector),
+		},
+		{ // responder has no updates.
+			respPVec:  interfaces.PrefixGenVector{10: 0},
+			initPVec:  interfaces.PrefixGenVector{10: 5, 11: 10, 12: 20, 13: 8},
+			genDiffIn: make(genRangeVector),
+		},
+		{ // responder and initiator have no updates.
+			respPVec:  interfaces.PrefixGenVector{10: 0},
+			initPVec:  interfaces.PrefixGenVector{11: 0},
+			genDiffIn: make(genRangeVector),
+		},
+		{ // responder is staler than initiator.
+			respPVec:  interfaces.PrefixGenVector{10: 1, 11: 10, 12: 20, 13: 2},
+			initPVec:  interfaces.PrefixGenVector{10: 1, 11: 10, 12: 20, 13: 8, 14: 5},
+			genDiffIn: make(genRangeVector),
+		},
+		{ // responder is more up-to-date than initiator for local updates.
+			respPVec:    interfaces.PrefixGenVector{10: 5, 11: 10, 12: 20, 13: 2},
+			initPVec:    interfaces.PrefixGenVector{10: 1, 11: 10, 12: 20, 13: 2},
+			genDiffIn:   make(genRangeVector),
+			genDiffWant: genRangeVector{10: &genRange{min: 2, max: 5}},
+		},
+		{ // responder is fresher than initiator for local updates and one device.
+			respPVec:  interfaces.PrefixGenVector{10: 5, 11: 10, 12: 22, 13: 2},
+			initPVec:  interfaces.PrefixGenVector{10: 1, 11: 10, 12: 20, 13: 2, 14: 40},
+			genDiffIn: make(genRangeVector),
+			genDiffWant: genRangeVector{
+				10: &genRange{min: 2, max: 5},
+				12: &genRange{min: 21, max: 22},
+			},
+		},
+		{ // responder is fresher than initiator in all but one device.
+			respPVec:  interfaces.PrefixGenVector{10: 1, 11: 2, 12: 3, 13: 4},
+			initPVec:  interfaces.PrefixGenVector{10: 0, 11: 2, 12: 0},
+			genDiffIn: make(genRangeVector),
+			genDiffWant: genRangeVector{
+				10: &genRange{min: 1, max: 1},
+				12: &genRange{min: 1, max: 3},
+				13: &genRange{min: 1, max: 4},
+			},
+		},
+		{ // initiator has no updates.
+			respPVec:  interfaces.PrefixGenVector{10: 1, 11: 2, 12: 3, 13: 4},
+			initPVec:  interfaces.PrefixGenVector{},
+			genDiffIn: make(genRangeVector),
+			genDiffWant: genRangeVector{
+				10: &genRange{min: 1, max: 1},
+				11: &genRange{min: 1, max: 2},
+				12: &genRange{min: 1, max: 3},
+				13: &genRange{min: 1, max: 4},
+			},
+		},
+		{ // initiator has no updates, pre-existing diff.
+			respPVec: interfaces.PrefixGenVector{10: 1, 11: 2, 12: 3, 13: 4},
+			initPVec: interfaces.PrefixGenVector{13: 1},
+			genDiffIn: genRangeVector{
+				10: &genRange{min: 5, max: 20},
+				13: &genRange{min: 1, max: 3},
+			},
+			genDiffWant: genRangeVector{
+				10: &genRange{min: 1, max: 20},
+				11: &genRange{min: 1, max: 2},
+				12: &genRange{min: 1, max: 3},
+				13: &genRange{min: 1, max: 4},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		want := test.genDiffWant
+		got := test.genDiffIn
+		rSt := newResponderState(nil, nil, s, interfaces.DeltaReq{}, "fakeInitiator")
+		rSt.diff = got
+		rSt.diffPrefixGenVectors(test.respPVec, test.initPVec)
+		checkEqualDevRanges(t, got, want)
+	}
+}
+
+// TestSendDeltas tests the computation of the delta bound (computeDeltaBound)
+// and if the log records on the wire are correctly ordered (phases 2 and 3 of
+// SendDeltas).
+func TestSendDeltas(t *testing.T) {
+	appName := "mockapp"
+	dbName := "mockdb"
+
+	tests := []struct {
+		respVec, initVec, outVec interfaces.GenVector
+		respGen                  uint64
+		genDiff                  genRangeVector
+		keyPfxs                  []string
+	}{
+		{ // Identical prefixes, local and remote updates.
+			respVec: interfaces.GenVector{
+				"foo":    interfaces.PrefixGenVector{12: 8},
+				"foobar": interfaces.PrefixGenVector{12: 10},
+			},
+			initVec: interfaces.GenVector{
+				"foo":    interfaces.PrefixGenVector{11: 5},
+				"foobar": interfaces.PrefixGenVector{11: 5},
+			},
+			respGen: 5,
+			outVec: interfaces.GenVector{
+				"foo":    interfaces.PrefixGenVector{10: 5, 12: 8},
+				"foobar": interfaces.PrefixGenVector{10: 5, 12: 10},
+			},
+			genDiff: genRangeVector{
+				10: &genRange{min: 1, max: 5},
+				12: &genRange{min: 1, max: 10},
+			},
+			keyPfxs: []string{"baz", "wombat", "f", "foo", "foobar", ""},
+		},
+		{ // Identical prefixes, local and remote updates.
+			respVec: interfaces.GenVector{
+				"bar":    interfaces.PrefixGenVector{12: 20},
+				"foo":    interfaces.PrefixGenVector{12: 8},
+				"foobar": interfaces.PrefixGenVector{12: 10},
+			},
+			initVec: interfaces.GenVector{
+				"foo":    interfaces.PrefixGenVector{11: 5},
+				"foobar": interfaces.PrefixGenVector{11: 5, 12: 10},
+				"bar":    interfaces.PrefixGenVector{10: 5, 11: 5, 12: 5},
+			},
+			respGen: 5,
+			outVec: interfaces.GenVector{
+				"foo":    interfaces.PrefixGenVector{10: 5, 12: 8},
+				"foobar": interfaces.PrefixGenVector{10: 5, 12: 10},
+				"bar":    interfaces.PrefixGenVector{10: 5, 12: 20},
+			},
+			genDiff: genRangeVector{
+				10: &genRange{min: 1, max: 5},
+				12: &genRange{min: 1, max: 20},
+			},
+			keyPfxs: []string{"baz", "wombat", "f", "foo", "foobar", "bar", "barbaz", ""},
+		},
+		{ // Non-identical prefixes, local only updates.
+			initVec: interfaces.GenVector{
+				"foo":    interfaces.PrefixGenVector{11: 5},
+				"foobar": interfaces.PrefixGenVector{11: 5},
+			},
+			respGen: 5,
+			outVec: interfaces.GenVector{
+				"foo": interfaces.PrefixGenVector{10: 5},
+			},
+			genDiff: genRangeVector{
+				10: &genRange{min: 1, max: 5},
+			},
+			keyPfxs: []string{"baz", "wombat", "f", "foo", "foobar", "", "fo", "fooxyz"},
+		},
+		{ // Non-identical prefixes, local and remote updates.
+			respVec: interfaces.GenVector{
+				"f":      interfaces.PrefixGenVector{12: 5, 13: 5},
+				"foo":    interfaces.PrefixGenVector{12: 10, 13: 10},
+				"foobar": interfaces.PrefixGenVector{12: 20, 13: 20},
+			},
+			initVec: interfaces.GenVector{
+				"foo": interfaces.PrefixGenVector{11: 5, 12: 1},
+			},
+			respGen: 5,
+			outVec: interfaces.GenVector{
+				"foo":    interfaces.PrefixGenVector{10: 5, 12: 10, 13: 10},
+				"foobar": interfaces.PrefixGenVector{10: 5, 12: 20, 13: 20},
+			},
+			genDiff: genRangeVector{
+				10: &genRange{min: 1, max: 5},
+				12: &genRange{min: 2, max: 20},
+				13: &genRange{min: 1, max: 20},
+			},
+			keyPfxs: []string{"baz", "wombat", "f", "foo", "foobar", "", "fo", "fooxyz"},
+		},
+		{ // Non-identical prefixes, local and remote updates.
+			respVec: interfaces.GenVector{
+				"foobar": interfaces.PrefixGenVector{12: 20, 13: 20},
+			},
+			initVec: interfaces.GenVector{
+				"foo": interfaces.PrefixGenVector{11: 5, 12: 1},
+			},
+			respGen: 5,
+			outVec: interfaces.GenVector{
+				"foo":    interfaces.PrefixGenVector{10: 5},
+				"foobar": interfaces.PrefixGenVector{10: 5, 12: 20, 13: 20},
+			},
+			genDiff: genRangeVector{
+				10: &genRange{min: 1, max: 5},
+				12: &genRange{min: 2, max: 20},
+				13: &genRange{min: 1, max: 20},
+			},
+			keyPfxs: []string{"baz", "wombat", "f", "foo", "foobar", "", "fo", "fooxyz"},
+		},
+		{ // Non-identical prefixes, local and remote updates.
+			respVec: interfaces.GenVector{
+				"f": interfaces.PrefixGenVector{12: 20, 13: 20},
+			},
+			initVec: interfaces.GenVector{
+				"foo": interfaces.PrefixGenVector{11: 5, 12: 1},
+			},
+			respGen: 5,
+			outVec: interfaces.GenVector{
+				"foo": interfaces.PrefixGenVector{10: 5, 12: 20, 13: 20},
+			},
+			genDiff: genRangeVector{
+				10: &genRange{min: 1, max: 5},
+				12: &genRange{min: 2, max: 20},
+				13: &genRange{min: 1, max: 20},
+			},
+			keyPfxs: []string{"baz", "wombat", "f", "foo", "foobar", "", "fo", "fooxyz"},
+		},
+		{ // Non-identical interleaving prefixes.
+			respVec: interfaces.GenVector{
+				"f":      interfaces.PrefixGenVector{12: 20, 13: 10},
+				"foo":    interfaces.PrefixGenVector{12: 30, 13: 20},
+				"foobar": interfaces.PrefixGenVector{12: 40, 13: 30},
+			},
+			initVec: interfaces.GenVector{
+				"fo":        interfaces.PrefixGenVector{11: 5, 12: 1},
+				"foob":      interfaces.PrefixGenVector{11: 5, 12: 10},
+				"foobarxyz": interfaces.PrefixGenVector{11: 5, 12: 20},
+			},
+			respGen: 5,
+			outVec: interfaces.GenVector{
+				"fo":     interfaces.PrefixGenVector{10: 5, 12: 20, 13: 10},
+				"foo":    interfaces.PrefixGenVector{10: 5, 12: 30, 13: 20},
+				"foobar": interfaces.PrefixGenVector{10: 5, 12: 40, 13: 30},
+			},
+			genDiff: genRangeVector{
+				10: &genRange{min: 1, max: 5},
+				12: &genRange{min: 2, max: 40},
+				13: &genRange{min: 1, max: 30},
+			},
+			keyPfxs: []string{"baz", "wombat", "f", "foo", "foobar", "", "fo", "foob", "foobarxyz", "fooxyz"},
+		},
+		{ // Non-identical interleaving prefixes.
+			respVec: interfaces.GenVector{
+				"fo":        interfaces.PrefixGenVector{12: 20, 13: 10},
+				"foob":      interfaces.PrefixGenVector{12: 30, 13: 20},
+				"foobarxyz": interfaces.PrefixGenVector{12: 40, 13: 30},
+			},
+			initVec: interfaces.GenVector{
+				"f":      interfaces.PrefixGenVector{11: 5, 12: 1},
+				"foo":    interfaces.PrefixGenVector{11: 5, 12: 10},
+				"foobar": interfaces.PrefixGenVector{11: 5, 12: 20},
+			},
+			respGen: 5,
+			outVec: interfaces.GenVector{
+				"f":         interfaces.PrefixGenVector{10: 5},
+				"fo":        interfaces.PrefixGenVector{10: 5, 12: 20, 13: 10},
+				"foob":      interfaces.PrefixGenVector{10: 5, 12: 30, 13: 20},
+				"foobarxyz": interfaces.PrefixGenVector{10: 5, 12: 40, 13: 30},
+			},
+			genDiff: genRangeVector{
+				10: &genRange{min: 1, max: 5},
+				12: &genRange{min: 2, max: 40},
+				13: &genRange{min: 1, max: 30},
+			},
+			keyPfxs: []string{"baz", "wombat", "f", "foo", "foobar", "", "fo", "foob", "foobarxyz", "fooxyz"},
+		},
+		{ // Non-identical sibling prefixes.
+			respVec: interfaces.GenVector{
+				"foo":       interfaces.PrefixGenVector{12: 20, 13: 10},
+				"foobarabc": interfaces.PrefixGenVector{12: 40, 13: 30},
+				"foobarxyz": interfaces.PrefixGenVector{12: 30, 13: 20},
+			},
+			initVec: interfaces.GenVector{
+				"foo": interfaces.PrefixGenVector{11: 5, 12: 1},
+			},
+			respGen: 5,
+			outVec: interfaces.GenVector{
+				"foo":       interfaces.PrefixGenVector{10: 5, 12: 20, 13: 10},
+				"foobarabc": interfaces.PrefixGenVector{10: 5, 12: 40, 13: 30},
+				"foobarxyz": interfaces.PrefixGenVector{10: 5, 12: 30, 13: 20},
+			},
+			genDiff: genRangeVector{
+				10: &genRange{min: 1, max: 5},
+				12: &genRange{min: 2, max: 40},
+				13: &genRange{min: 1, max: 30},
+			},
+			keyPfxs: []string{"baz", "wombat", "f", "foo", "foobar", "", "foobarabc", "foobarxyz", "foobar123", "fooxyz"},
+		},
+		{ // Non-identical prefixes, local and remote updates.
+			respVec: interfaces.GenVector{
+				"barbaz": interfaces.PrefixGenVector{12: 18},
+				"f":      interfaces.PrefixGenVector{12: 30, 13: 5},
+				"foobar": interfaces.PrefixGenVector{12: 30, 13: 8},
+			},
+			initVec: interfaces.GenVector{
+				"foo":    interfaces.PrefixGenVector{11: 5, 12: 5},
+				"foobar": interfaces.PrefixGenVector{11: 5, 12: 5},
+				"bar":    interfaces.PrefixGenVector{10: 5, 11: 5, 12: 5},
+			},
+			respGen: 5,
+			outVec: interfaces.GenVector{
+				"foo":    interfaces.PrefixGenVector{10: 5, 12: 30, 13: 5},
+				"foobar": interfaces.PrefixGenVector{10: 5, 12: 30, 13: 8},
+				"bar":    interfaces.PrefixGenVector{10: 5},
+				"barbaz": interfaces.PrefixGenVector{10: 5, 12: 18},
+			},
+			genDiff: genRangeVector{
+				10: &genRange{min: 1, max: 5},
+				12: &genRange{min: 6, max: 30},
+				13: &genRange{min: 1, max: 8},
+			},
+			keyPfxs: []string{"baz", "wombat", "f", "foo", "foobar", "bar", "barbaz", ""},
+		},
+	}
+
+	for i, test := range tests {
+		svc := createService(t)
+		s := svc.sync
+		s.id = 10 //responder.
+
+		wantDiff, wantVec := test.genDiff, test.outVec
+		s.syncState[appDbName(appName, dbName)] = &dbSyncStateInMem{gen: test.respGen, checkptGen: test.respGen, genvec: test.respVec}
+
+		req := interfaces.DeltaReq{AppName: appName, DbName: dbName, InitVec: test.initVec}
+		rSt := newResponderState(nil, nil, s, req, "fakeInitiator")
+
+		rSt.computeDeltaBound(nil)
+		if rSt.errState != nil || !reflect.DeepEqual(rSt.outVec, wantVec) {
+			t.Fatalf("computeDeltaBound failed (I: %v), (R: %v, %v), got %v, want %v err %v", test.initVec, test.respGen, test.respVec, rSt.outVec, wantVec, rSt.errState)
+		}
+		checkEqualDevRanges(t, rSt.diff, wantDiff)
+
+		////////////////////////////////////////
+		// Test sending deltas.
+
+		// Insert some log records to bootstrap testing below.
+		tRng := rand.New(rand.NewSource(int64(i)))
+		var wantRecs []*localLogRec
+		st := svc.St()
+		tx := st.NewTransaction()
+		objKeyPfxs := test.keyPfxs
+		j := 0
+		for id, r := range wantDiff {
+			pos := uint64(tRng.Intn(50) + 100*j)
+			for k := r.min; k <= r.max; k++ {
+				opfx := objKeyPfxs[tRng.Intn(len(objKeyPfxs))]
+				// Create holes in the log records.
+				if opfx == "" {
+					continue
+				}
+				okey := makeRowKey(fmt.Sprintf("%s~%x", opfx, tRng.Int()))
+				vers := fmt.Sprintf("%x", tRng.Int())
+				rec := &localLogRec{
+					Metadata: interfaces.LogRecMetadata{Id: id, Gen: k, ObjId: okey, CurVers: vers, UpdTime: time.Now().UTC()},
+					Pos:      pos + k,
+				}
+				if err := putLogRec(nil, tx, rec); err != nil {
+					t.Fatalf("putLogRec(%d:%d) failed rec %v err %v", id, k, rec, err)
+				}
+				value := fmt.Sprintf("value_%s", okey)
+				if err := watchable.PutAtVersion(nil, tx, []byte(okey), []byte(value), []byte(vers)); err != nil {
+					t.Fatalf("PutAtVersion(%d:%d) failed rec %v value %s: err %v", id, k, rec, value, err)
+				}
+
+				initPfxs := extractAndSortPrefixes(test.initVec)
+				if !filterLogRec(rec, test.initVec, initPfxs) {
+					wantRecs = append(wantRecs, rec)
+				}
+			}
+			j++
+		}
+		if err := tx.Commit(); err != nil {
+			t.Fatalf("cannot commit putting log rec, err %v", err)
+		}
+
+		d := &dummyResponder{}
+		rSt.call = d
+		rSt.st, rSt.errState = rSt.sync.getDbStore(nil, nil, rSt.req.AppName, rSt.req.DbName)
+		if rSt.errState != nil {
+			t.Fatalf("filterAndSendDeltas failed to get store handle for app/db %v %v", rSt.req.AppName, rSt.req.DbName)
+		}
+		err := rSt.filterAndSendDeltas(nil)
+		if err != nil {
+			t.Fatalf("filterAndSendDeltas failed (I: %v), (R: %v, %v) err %v", test.initVec, test.respGen, test.respVec, err)
+		}
+		d.diffLogRecs(t, wantRecs, wantVec)
+
+		destroyService(t, svc)
+	}
+}
+
+//////////////////////////////
+// Helpers
+
+type dummyResponder struct {
+	start, finish int
+	gotRecs       []*localLogRec
+	outVec        interfaces.GenVector
+}
+
+func (d *dummyResponder) RecvStream() interface {
+	Advance() bool
+	Value() interfaces.DeltaReq
+	Err() error
+} {
+	return d
+}
+
+func (d *dummyResponder) Advance() bool {
+	return false
+}
+
+func (d *dummyResponder) Value() interfaces.DeltaReq {
+	return interfaces.DeltaReq{}
+}
+
+func (d *dummyResponder) Err() error { return nil }
+
+func (d *dummyResponder) SendStream() interface {
+	Send(item interfaces.DeltaResp) error
+} {
+	return d
+}
+
+func (d *dummyResponder) Send(item interfaces.DeltaResp) error {
+	switch v := item.(type) {
+	case interfaces.DeltaRespStart:
+		d.start++
+	case interfaces.DeltaRespFinish:
+		d.finish++
+	case interfaces.DeltaRespRespVec:
+		d.outVec = v.Value
+	case interfaces.DeltaRespRec:
+		d.gotRecs = append(d.gotRecs, &localLogRec{Metadata: v.Value.Metadata})
+	}
+	return nil
+}
+
+func (d *dummyResponder) Security() security.Call {
+	return nil
+}
+
+func (d *dummyResponder) Suffix() string {
+	return ""
+}
+
+func (d *dummyResponder) LocalEndpoint() naming.Endpoint {
+	return nil
+}
+
+func (d *dummyResponder) RemoteEndpoint() naming.Endpoint {
+	return nil
+}
+
+func (d *dummyResponder) GrantedBlessings() security.Blessings {
+	return security.Blessings{}
+}
+
+func (d *dummyResponder) Server() rpc.Server {
+	return nil
+}
+
+func (d *dummyResponder) diffLogRecs(t *testing.T, wantRecs []*localLogRec, wantVec interfaces.GenVector) {
+	if d.start != 1 || d.finish != 1 {
+		t.Fatalf("diffLogRecs incorrect start/finish records (%v, %v)", d.start, d.finish)
+	}
+	if len(d.gotRecs) != len(wantRecs) {
+		t.Fatalf("diffLogRecs failed, gotLen %v, wantLen %v\n", len(d.gotRecs), len(wantRecs))
+	}
+	for i, rec := range d.gotRecs {
+		if !reflect.DeepEqual(rec.Metadata, wantRecs[i].Metadata) {
+			t.Fatalf("diffLogRecs failed, i %v, got %v, want %v\n", i, rec.Metadata, wantRecs[i].Metadata)
+		}
+	}
+	if !reflect.DeepEqual(d.outVec, wantVec) {
+		t.Fatalf("diffLogRecs failed genvector, got %v, want %v\n", d.outVec, wantVec)
+	}
+}
+
+func checkEqualDevRanges(t *testing.T, s1, s2 genRangeVector) {
+	if len(s1) != len(s2) {
+		t.Fatalf("len(s1): %v != len(s2): %v", len(s1), len(s2))
+	}
+	for d1, r1 := range s1 {
+		if r2, ok := s2[d1]; !ok || !reflect.DeepEqual(r1, r2) {
+			t.Fatalf("Dev %v: r1 %v != r2 %v", d1, r1, r2)
+		}
+	}
+}
diff --git a/services/syncbase/vsync/sync.go b/services/syncbase/vsync/sync.go
new file mode 100644
index 0000000..fd65c66
--- /dev/null
+++ b/services/syncbase/vsync/sync.go
@@ -0,0 +1,197 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Package vsync provides sync functionality for Syncbase. Sync
+// service serves incoming GetDeltas requests and contacts other peers
+// to get deltas from them. When it receives a GetDeltas request, the
+// incoming generation vector is diffed with the local generation
+// vector, and missing generations are sent back. When it receives log
+// records in response to a GetDeltas request, it replays those log
+// records to get in sync with the sender.
+import (
+	"fmt"
+	"math/rand"
+	"path"
+	"sync"
+	"time"
+
+	"v.io/syncbase/v23/services/syncbase/nosql"
+	blob "v.io/syncbase/x/ref/services/syncbase/localblobstore"
+	fsblob "v.io/syncbase/x/ref/services/syncbase/localblobstore/fs_cablobstore"
+	"v.io/syncbase/x/ref/services/syncbase/server/interfaces"
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/context"
+	"v.io/v23/rpc"
+	"v.io/v23/verror"
+)
+
+// syncService contains the metadata for the sync module.
+type syncService struct {
+	// TODO(hpucha): see if "v.io/v23/uniqueid" is a better fit. It is 128 bits.
+	id     uint64 // globally unique id for this instance of Syncbase.
+	name   string // name derived from the global id.
+	sv     interfaces.Service
+	server rpc.Server
+
+	// High-level lock to serialize the watcher and the initiator. This lock is
+	// needed to handle the following cases: (a) When the initiator is
+	// cutting a local generation, it waits for the watcher to commit the
+	// latest local changes before including them in the checkpoint. (b)
+	// When the initiator is receiving updates, it reads the latest head of
+	// an object as per the DAG state in order to construct the in-memory
+	// graft map used for conflict detection. At the same time, if a watcher
+	// is processing local updates, it may move the object head. Hence the
+	// initiator and watcher contend on the DAG head of an object. Instead
+	// of retrying a transaction which causes the entire delta to be
+	// replayed, we use pessimistic locking to serialize the initiator and
+	// the watcher.
+	//
+	// TODO(hpucha): This is a temporary hack.
+	thLock sync.RWMutex
+
+	// State to coordinate shutdown of spawned goroutines.
+	pending sync.WaitGroup
+	closed  chan struct{}
+
+	// TODO(hpucha): Other global names to advertise to enable Syncbase
+	// discovery. For example, every Syncbase must be reachable under
+	// <mttable>/<syncbaseid> for p2p sync. This is the name advertised
+	// during SyncGroup join. In addition, a Syncbase might also be
+	// accepting "publish SyncGroup requests", and might use a more
+	// human-readable name such as <mttable>/<idp>/<sgserver>. All these
+	// names must be advertised in the appropriate mount tables.
+
+	// In-memory sync membership info aggregated across databases.
+	allMembers     *memberView
+	allMembersLock sync.RWMutex
+
+	// In-memory sync state per Database. This state is populated at
+	// startup, and periodically persisted by the initiator.
+	syncState     map[string]*dbSyncStateInMem
+	syncStateLock sync.Mutex // lock to protect access to the sync state.
+
+	// In-memory tracking of batches during their construction.
+	// The sync Initiator and Watcher build batches incrementally here
+	// and then persist them in DAG batch entries.  The mutex guards
+	// access to the batch set.
+	batchesLock sync.Mutex
+	batches     batchSet
+
+	// Metadata related to blob handling.
+	bst           blob.BlobStore                 // local blob store associated with this Syncbase.
+	blobDirectory map[nosql.BlobRef]*blobLocInfo // directory structure containing blob location information.
+	blobDirLock   sync.RWMutex                   // lock to synchronize access to the blob directory information.
+
+}
+
+// syncDatabase contains the metadata for syncing a database. This struct is
+// used as a receiver to hand off the app-initiated SyncGroup calls that arrive
+// against a nosql.Database to the sync module.
+type syncDatabase struct {
+	db   interfaces.Database
+	sync interfaces.SyncServerMethods
+}
+
+var (
+	rng     = rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
+	rngLock sync.Mutex
+	_       interfaces.SyncServerMethods = (*syncService)(nil)
+)
+
+// rand64 generates an unsigned 64-bit pseudo-random number.
+func rand64() uint64 {
+	rngLock.Lock()
+	defer rngLock.Unlock()
+	return (uint64(rng.Int63()) << 1) | uint64(rng.Int63n(2))
+}
+
+// randIntn mimics rand.Intn (generates a non-negative pseudo-random number in [0,n)).
+func randIntn(n int) int {
+	rngLock.Lock()
+	defer rngLock.Unlock()
+	return rng.Intn(n)
+}
+
+// New creates a new sync module.
+//
+// Concurrency: sync initializes two goroutines at startup: a "watcher" and an
+// "initiator". The "watcher" thread is responsible for watching the store for
+// changes to its objects. The "initiator" thread is responsible for
+// periodically contacting peers to fetch changes from them. In addition, the
+// sync module responds to incoming RPCs from remote sync modules.
+func New(ctx *context.T, call rpc.ServerCall, sv interfaces.Service, server rpc.Server, rootDir string) (*syncService, error) {
+	s := &syncService{
+		sv:      sv,
+		server:  server,
+		batches: make(batchSet),
+	}
+
+	data := &syncData{}
+	if err := store.RunInTransaction(sv.St(), func(tx store.Transaction) error {
+		if err := util.Get(ctx, sv.St(), s.stKey(), data); err != nil {
+			if verror.ErrorID(err) != verror.ErrNoExist.ID {
+				return err
+			}
+			// First invocation of vsync.New().
+			// TODO(sadovsky): Maybe move guid generation and storage to serviceData.
+			data.Id = rand64()
+			return util.Put(ctx, tx, s.stKey(), data)
+		}
+		return nil
+	}); err != nil {
+		return nil, err
+	}
+
+	// data.Id is now guaranteed to be initialized.
+	s.id = data.Id
+	s.name = syncbaseIdToName(s.id)
+
+	// Initialize in-memory state for the sync module before starting any threads.
+	if err := s.initSync(ctx); err != nil {
+		return nil, verror.New(verror.ErrInternal, ctx, err)
+	}
+
+	// Open a blob store.
+	var err error
+	s.bst, err = fsblob.Create(ctx, path.Join(rootDir, "blobs"))
+	if err != nil {
+		return nil, err
+	}
+	s.blobDirectory = make(map[nosql.BlobRef]*blobLocInfo)
+
+	// Channel to propagate close event to all threads.
+	s.closed = make(chan struct{})
+	s.pending.Add(2)
+
+	// Start watcher thread to watch for updates to local store.
+	go s.watchStore(ctx)
+
+	// Start initiator thread to periodically get deltas from peers.
+	go s.syncer(ctx)
+
+	return s, nil
+}
+
+// Close cleans up sync state.
+// TODO(hpucha): Hook it up to server shutdown of syncbased.
+func (s *syncService) Close() {
+	s.bst.Close()
+	close(s.closed)
+	s.pending.Wait()
+}
+
+func syncbaseIdToName(id uint64) string {
+	return fmt.Sprintf("%x", id)
+}
+
+func NewSyncDatabase(db interfaces.Database) *syncDatabase {
+	return &syncDatabase{db: db, sync: db.App().Service().Sync()}
+}
+
+func (s *syncService) stKey() string {
+	return util.SyncPrefix
+}
diff --git a/services/syncbase/vsync/sync_state.go b/services/syncbase/vsync/sync_state.go
new file mode 100644
index 0000000..8195559
--- /dev/null
+++ b/services/syncbase/vsync/sync_state.go
@@ -0,0 +1,349 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// New log records are created when objects in the local store are created,
+// updated or deleted. Local log records are also replayed to keep the
+// per-object dags consistent with the local store state. Sync module assigns
+// each log record created within a Database a unique sequence number, called
+// the generation number. Locally on each device, the position of each log
+// record is also recorded relative to other local and remote log records.
+//
+// When a device receives a request to send log records, it first computes the
+// missing generations between itself and the incoming request on a per-prefix
+// basis. It then sends all the log records belonging to the missing generations
+// in the order they occur locally (using the local log position). A device that
+// receives log records over the network replays all the records received from
+// another device in a single batch. Each replayed log record adds a new version
+// to the dag of the object contained in the log record. At the end of replaying
+// all the log records, conflict detection and resolution is carried out for all
+// the objects learned during this iteration. Conflict detection and resolution
+// is carried out after a batch of log records are replayed, instead of
+// incrementally after each record is replayed, to avoid repeating conflict
+// resolution already performed by other devices.
+//
+// Sync module tracks the current generation number and the current local log
+// position for each Database. In addition, it also tracks the current
+// generation vector for a Database. Log records are indexed such that they can
+// be selectively retrieved from the store for any missing generation from any
+// device.
+
+import (
+	"fmt"
+	"strconv"
+
+	"v.io/syncbase/x/ref/services/syncbase/server/interfaces"
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/context"
+	"v.io/v23/verror"
+)
+
+// dbSyncStateInMem represents the in-memory sync state of a Database.
+type dbSyncStateInMem struct {
+	gen uint64
+	pos uint64
+
+	checkptGen uint64
+	genvec     interfaces.GenVector // Note: Generation vector contains state from remote devices only.
+}
+
+// initSync initializes the sync module during startup. It scans all the
+// databases across all apps to initialize the following:
+// a) in-memory sync state of a Database consisting of the current generation
+//    number, log position and generation vector.
+// b) watcher map of prefixes currently being synced.
+// c) republish names in mount tables for all syncgroups.
+//
+// TODO(hpucha): This is incomplete. Flesh this out further.
+func (s *syncService) initSync(ctx *context.T) error {
+	s.syncStateLock.Lock()
+	defer s.syncStateLock.Unlock()
+
+	var errFinal error
+	s.syncState = make(map[string]*dbSyncStateInMem)
+
+	s.forEachDatabaseStore(ctx, func(appName, dbName string, st store.Store) bool {
+		// Scan the SyncGroups, skipping those not yet being watched.
+		forEachSyncGroup(st, func(sg *interfaces.SyncGroup) bool {
+			// TODO(rdaoud): only use SyncGroups that have been
+			// marked as "watchable" by the sync watcher thread.
+			// This is to handle the case of a SyncGroup being
+			// created but Syncbase restarting before the watcher
+			// processed the SyncGroupOp entry in the watch queue.
+			// It should not be syncing that SyncGroup's data after
+			// restart, but wait until the watcher processes the
+			// entry as would have happened without a restart.
+			for _, prefix := range sg.Spec.Prefixes {
+				incrWatchPrefix(appName, dbName, prefix)
+			}
+			return false
+		})
+
+		if false {
+			// Fetch the sync state.
+			ds, err := getDbSyncState(ctx, st)
+			if err != nil && verror.ErrorID(err) != verror.ErrNoExist.ID {
+				errFinal = err
+				return false
+			}
+			var scanStart, scanLimit []byte
+			// Figure out what to scan among local log records.
+			if verror.ErrorID(err) == verror.ErrNoExist.ID {
+				scanStart, scanLimit = util.ScanPrefixArgs(logRecsPerDeviceScanPrefix(s.id), "")
+			} else {
+				scanStart, scanLimit = util.ScanPrefixArgs(logRecKey(s.id, ds.Gen), "")
+			}
+			var maxpos uint64
+			var dbName string
+			// Scan local log records to find the most recent one.
+			st.Scan(scanStart, scanLimit)
+			// Scan remote log records using the persisted GenVector.
+			s.syncState[dbName] = &dbSyncStateInMem{pos: maxpos + 1}
+		}
+
+		return false
+	})
+
+	return errFinal
+}
+
+// reserveGenAndPosInDbLog reserves a chunk of generation numbers and log
+// positions in a Database's log. Used when local updates result in log
+// entries.
+func (s *syncService) reserveGenAndPosInDbLog(ctx *context.T, appName, dbName string, count uint64) (uint64, uint64) {
+	return s.reserveGenAndPosInternal(appName, dbName, count, count)
+}
+
+// reservePosInDbLog reserves a chunk of log positions in a Database's log. Used
+// when remote log records are received.
+func (s *syncService) reservePosInDbLog(ctx *context.T, appName, dbName string, count uint64) uint64 {
+	_, pos := s.reserveGenAndPosInternal(appName, dbName, 0, count)
+	return pos
+}
+
+func (s *syncService) reserveGenAndPosInternal(appName, dbName string, genCount, posCount uint64) (uint64, uint64) {
+	s.syncStateLock.Lock()
+	defer s.syncStateLock.Unlock()
+
+	name := appDbName(appName, dbName)
+	ds, ok := s.syncState[name]
+	if !ok {
+		ds = &dbSyncStateInMem{gen: 1}
+		s.syncState[name] = ds
+	}
+
+	gen := ds.gen
+	pos := ds.pos
+
+	ds.gen += genCount
+	ds.pos += posCount
+
+	return gen, pos
+}
+
+// checkptLocalGen freezes the local generation number for the responder's use.
+func (s *syncService) checkptLocalGen(ctx *context.T, appName, dbName string) error {
+	s.syncStateLock.Lock()
+	defer s.syncStateLock.Unlock()
+
+	name := appDbName(appName, dbName)
+	ds, ok := s.syncState[name]
+	if !ok {
+		return verror.New(verror.ErrInternal, ctx, "db state not found", name)
+	}
+
+	// The frozen generation is the last generation number used, i.e. one
+	// below the next available one to use.
+	ds.checkptGen = ds.gen - 1
+	return nil
+}
+
+// initDbSyncStateInMem initializes the in memory sync state of the Database if needed.
+func (s *syncService) initDbSyncStateInMem(ctx *context.T, appName, dbName string) {
+	s.syncStateLock.Lock()
+	defer s.syncStateLock.Unlock()
+
+	name := appDbName(appName, dbName)
+	if s.syncState[name] == nil {
+		s.syncState[name] = &dbSyncStateInMem{gen: 1}
+	}
+}
+
+// copyDbSyncStateInMem returns a copy of the current in memory sync state of the Database.
+func (s *syncService) copyDbSyncStateInMem(ctx *context.T, appName, dbName string) (*dbSyncStateInMem, error) {
+	s.syncStateLock.Lock()
+	defer s.syncStateLock.Unlock()
+
+	name := appDbName(appName, dbName)
+	ds, ok := s.syncState[name]
+	if !ok {
+		return nil, verror.New(verror.ErrInternal, ctx, "db state not found", name)
+	}
+
+	dsCopy := &dbSyncStateInMem{
+		gen:        ds.gen,
+		pos:        ds.pos,
+		checkptGen: ds.checkptGen,
+	}
+
+	dsCopy.genvec = copyGenVec(ds.genvec)
+
+	return dsCopy, nil
+}
+
+// copyDbGenInfo returns a copy of the current generation information of the Database.
+func (s *syncService) copyDbGenInfo(ctx *context.T, appName, dbName string) (interfaces.GenVector, uint64, error) {
+	s.syncStateLock.Lock()
+	defer s.syncStateLock.Unlock()
+
+	name := appDbName(appName, dbName)
+	ds, ok := s.syncState[name]
+	if !ok {
+		return nil, 0, verror.New(verror.ErrInternal, ctx, "db state not found", name)
+	}
+
+	genvec := copyGenVec(ds.genvec)
+
+	// Add local generation information to the genvec.
+	for _, gv := range genvec {
+		gv[s.id] = ds.checkptGen
+	}
+
+	return genvec, ds.checkptGen, nil
+}
+
+// putDbGenInfoRemote puts the current remote generation information of the Database.
+func (s *syncService) putDbGenInfoRemote(ctx *context.T, appName, dbName string, genvec interfaces.GenVector) error {
+	s.syncStateLock.Lock()
+	defer s.syncStateLock.Unlock()
+
+	name := appDbName(appName, dbName)
+	ds, ok := s.syncState[name]
+	if !ok {
+		return verror.New(verror.ErrInternal, ctx, "db state not found", name)
+	}
+
+	ds.genvec = copyGenVec(genvec)
+
+	return nil
+}
+
+// appDbName combines the app and db names to return a globally unique name for
+// a Database.  This relies on the fact that the app name is globally unique and
+// the db name is unique within the scope of the app.
+func appDbName(appName, dbName string) string {
+	return util.JoinKeyParts(appName, dbName)
+}
+
+// splitAppDbName is the inverse of appDbName and returns app and db name from a
+// globally unique name for a Database.
+func splitAppDbName(ctx *context.T, name string) (string, string, error) {
+	parts := util.SplitKeyParts(name)
+	if len(parts) != 2 {
+		return "", "", verror.New(verror.ErrInternal, ctx, "invalid appDbName", name)
+	}
+	return parts[0], parts[1], nil
+}
+
+func copyGenVec(in interfaces.GenVector) interfaces.GenVector {
+	genvec := make(interfaces.GenVector)
+	for p, inpgv := range in {
+		pgv := make(interfaces.PrefixGenVector)
+		for id, gen := range inpgv {
+			pgv[id] = gen
+		}
+		genvec[p] = pgv
+	}
+	return genvec
+}
+
+////////////////////////////////////////////////////////////
+// Low-level utility functions to access sync state.
+
+// dbSyncStateKey returns the key used to access the sync state of a Database.
+func dbSyncStateKey() string {
+	return util.JoinKeyParts(util.SyncPrefix, dbssPrefix)
+}
+
+// putDbSyncState persists the sync state object for a given Database.
+func putDbSyncState(ctx *context.T, tx store.Transaction, ds *dbSyncState) error {
+	return util.Put(ctx, tx, dbSyncStateKey(), ds)
+}
+
+// getDbSyncState retrieves the sync state object for a given Database.
+func getDbSyncState(ctx *context.T, st store.StoreReader) (*dbSyncState, error) {
+	var ds dbSyncState
+	if err := util.Get(ctx, st, dbSyncStateKey(), &ds); err != nil {
+		return nil, err
+	}
+	return &ds, nil
+}
+
+////////////////////////////////////////////////////////////
+// Low-level utility functions to access log records.
+
+// logRecsPerDeviceScanPrefix returns the prefix used to scan log records for a particular device.
+func logRecsPerDeviceScanPrefix(id uint64) string {
+	return util.JoinKeyParts(util.SyncPrefix, logPrefix, fmt.Sprintf("%x", id))
+}
+
+// logRecKey returns the key used to access a specific log record.
+func logRecKey(id, gen uint64) string {
+	return util.JoinKeyParts(util.SyncPrefix, logPrefix, fmt.Sprintf("%d", id), fmt.Sprintf("%016x", gen))
+}
+
+// splitLogRecKey is the inverse of logRecKey and returns device id and generation number.
+func splitLogRecKey(ctx *context.T, key string) (uint64, uint64, error) {
+	parts := util.SplitKeyParts(key)
+	verr := verror.New(verror.ErrInternal, ctx, "invalid logreckey", key)
+	if len(parts) != 4 {
+		return 0, 0, verr
+	}
+	if parts[0] != util.SyncPrefix || parts[1] != logPrefix {
+		return 0, 0, verr
+	}
+	id, err := strconv.ParseUint(parts[2], 10, 64)
+	if err != nil {
+		return 0, 0, verr
+	}
+	gen, err := strconv.ParseUint(parts[3], 16, 64)
+	if err != nil {
+		return 0, 0, verr
+	}
+	return id, gen, nil
+}
+
+// hasLogRec returns true if the log record for (devid, gen) exists.
+func hasLogRec(st store.StoreReader, id, gen uint64) (bool, error) {
+	// TODO(hpucha): optimize to avoid the unneeded fetch/decode of the data.
+	var rec localLogRec
+	if err := util.Get(nil, st, logRecKey(id, gen), &rec); err != nil {
+		if verror.ErrorID(err) == verror.ErrNoExist.ID {
+			err = nil
+		}
+		return false, err
+	}
+	return true, nil
+}
+
+// putLogRec stores the log record.
+func putLogRec(ctx *context.T, tx store.Transaction, rec *localLogRec) error {
+	return util.Put(ctx, tx, logRecKey(rec.Metadata.Id, rec.Metadata.Gen), rec)
+}
+
+// getLogRec retrieves the log record for a given (devid, gen).
+func getLogRec(ctx *context.T, st store.StoreReader, id, gen uint64) (*localLogRec, error) {
+	var rec localLogRec
+	if err := util.Get(ctx, st, logRecKey(id, gen), &rec); err != nil {
+		return nil, err
+	}
+	return &rec, nil
+}
+
+// delLogRec deletes the log record for a given (devid, gen).
+func delLogRec(ctx *context.T, tx store.Transaction, id, gen uint64) error {
+	return util.Delete(ctx, tx, logRecKey(id, gen))
+}
diff --git a/services/syncbase/vsync/sync_state_test.go b/services/syncbase/vsync/sync_state_test.go
new file mode 100644
index 0000000..7e9302b
--- /dev/null
+++ b/services/syncbase/vsync/sync_state_test.go
@@ -0,0 +1,174 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+import (
+	"reflect"
+	"testing"
+	"time"
+
+	"v.io/syncbase/x/ref/services/syncbase/server/interfaces"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	_ "v.io/x/ref/runtime/factories/generic"
+)
+
+// Tests for sync state management and storage in Syncbase.
+
+// TestReserveGenAndPos tests reserving generation numbers and log positions in a
+// Database log.
+func TestReserveGenAndPos(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	s := svc.sync
+
+	var wantGen, wantPos uint64 = 1, 0
+	for i := 0; i < 5; i++ {
+		gotGen, gotPos := s.reserveGenAndPosInternal("mockapp", "mockdb", 5, 10)
+		if gotGen != wantGen || gotPos != wantPos {
+			t.Fatalf("reserveGenAndPosInternal failed, gotGen %v wantGen %v, gotPos %v wantPos %v", gotGen, wantGen, gotPos, wantPos)
+		}
+		wantGen += 5
+		wantPos += 10
+
+		name := appDbName("mockapp", "mockdb")
+		if s.syncState[name].gen != wantGen || s.syncState[name].pos != wantPos {
+			t.Fatalf("reserveGenAndPosInternal failed, gotGen %v wantGen %v, gotPos %v wantPos %v", s.syncState[name].gen, wantGen, s.syncState[name].pos, wantPos)
+		}
+	}
+}
+
+// TestPutGetDbSyncState tests setting and getting sync metadata.
+func TestPutGetDbSyncState(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+
+	checkDbSyncState(t, st, false, nil)
+
+	gv := interfaces.GenVector{
+		"mocktbl/foo": interfaces.PrefixGenVector{
+			1: 2, 3: 4, 5: 6,
+		},
+	}
+
+	tx := st.NewTransaction()
+	wantSt := &dbSyncState{Gen: 40, GenVec: gv}
+	if err := putDbSyncState(nil, tx, wantSt); err != nil {
+		t.Fatalf("putDbSyncState failed, err %v", err)
+	}
+	if err := tx.Commit(); err != nil {
+		t.Fatalf("cannot commit putting db sync state, err %v", err)
+	}
+
+	checkDbSyncState(t, st, true, wantSt)
+}
+
+// TestPutGetDelLogRec tests setting, getting, and deleting a log record.
+func TestPutGetDelLogRec(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+
+	var id uint64 = 10
+	var gen uint64 = 100
+
+	checkLogRec(t, st, id, gen, false, nil)
+
+	tx := st.NewTransaction()
+	wantRec := &localLogRec{
+		Metadata: interfaces.LogRecMetadata{
+			Id:         id,
+			Gen:        gen,
+			RecType:    interfaces.NodeRec,
+			ObjId:      "foo",
+			CurVers:    "3",
+			Parents:    []string{"1", "2"},
+			UpdTime:    time.Now().UTC(),
+			Delete:     false,
+			BatchId:    10000,
+			BatchCount: 1,
+		},
+		Pos: 10,
+	}
+	if err := putLogRec(nil, tx, wantRec); err != nil {
+		t.Fatalf("putLogRec(%d:%d) failed err %v", id, gen, err)
+	}
+	if err := tx.Commit(); err != nil {
+		t.Fatalf("cannot commit putting log rec, err %v", err)
+	}
+
+	checkLogRec(t, st, id, gen, true, wantRec)
+
+	tx = st.NewTransaction()
+	if err := delLogRec(nil, tx, id, gen); err != nil {
+		t.Fatalf("delLogRec(%d:%d) failed err %v", id, gen, err)
+	}
+	if err := tx.Commit(); err != nil {
+		t.Fatalf("cannot commit deleting log rec, err %v", err)
+	}
+
+	checkLogRec(t, st, id, gen, false, nil)
+}
+
+func TestLogRecKeyUtils(t *testing.T) {
+	invalid := []string{"$sync:aa:bb", "log:aa:bb", "$sync:log:aa:xx", "$sync:log:x:bb"}
+
+	for _, k := range invalid {
+		if _, _, err := splitLogRecKey(nil, k); err == nil {
+			t.Fatalf("splitting log rec key didn't fail %q", k)
+		}
+	}
+
+	valid := []struct {
+		id  uint64
+		gen uint64
+	}{
+		{10, 20},
+		{190, 540},
+		{9999, 999999},
+	}
+
+	for _, v := range valid {
+		gotId, gotGen, err := splitLogRecKey(nil, logRecKey(v.id, v.gen))
+		if gotId != v.id || gotGen != v.gen || err != nil {
+			t.Fatalf("failed key conversion id got %v want %v, gen got %v want %v, err %v", gotId, v.id, gotGen, v.gen, err)
+		}
+	}
+}
+
+//////////////////////////////
+// Helpers
+
+// TODO(hpucha): Look into using v.io/syncbase/v23/syncbase/testutil.Fatalf()
+// for getting the stack trace. Right now cannot import the package due to a
+// cycle.
+
+func checkDbSyncState(t *testing.T, st store.StoreReader, exists bool, wantSt *dbSyncState) {
+	gotSt, err := getDbSyncState(nil, st)
+
+	if (!exists && err == nil) || (exists && err != nil) {
+		t.Fatalf("getDbSyncState failed, exists %v err %v", exists, err)
+	}
+
+	if !reflect.DeepEqual(gotSt, wantSt) {
+		t.Fatalf("getDbSyncState() failed, got %v, want %v", gotSt, wantSt)
+	}
+}
+
+func checkLogRec(t *testing.T, st store.StoreReader, id, gen uint64, exists bool, wantRec *localLogRec) {
+	gotRec, err := getLogRec(nil, st, id, gen)
+
+	if (!exists && err == nil) || (exists && err != nil) {
+		t.Fatalf("getLogRec(%d:%d) failed, exists %v err %v", id, gen, exists, err)
+	}
+
+	if !reflect.DeepEqual(gotRec, wantRec) {
+		t.Fatalf("getLogRec(%d:%d) failed, got %v, want %v", id, gen, gotRec, wantRec)
+	}
+
+	if ok, err := hasLogRec(st, id, gen); err != nil || ok != exists {
+		t.Fatalf("hasLogRec(%d:%d) failed, want %v", id, gen, exists)
+	}
+}
diff --git a/services/syncbase/vsync/syncgroup.go b/services/syncbase/vsync/syncgroup.go
new file mode 100644
index 0000000..905a319
--- /dev/null
+++ b/services/syncbase/vsync/syncgroup.go
@@ -0,0 +1,895 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// SyncGroup management and storage in Syncbase.  Handles the lifecycle
+// of SyncGroups (create, join, leave, etc.) and their persistence as
+// sync metadata in the application databases.  Provides helper functions
+// to the higher levels of sync (Initiator, Watcher) to get membership
+// information and map key/value changes to their matching SyncGroups.
+
+// TODO(hpucha): Add high level commentary about the logic behind create/join
+// etc.
+
+import (
+	"fmt"
+	"strings"
+	"time"
+
+	wire "v.io/syncbase/v23/services/syncbase/nosql"
+	"v.io/syncbase/x/ref/services/syncbase/server/interfaces"
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/syncbase/x/ref/services/syncbase/server/watchable"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/context"
+	"v.io/v23/naming"
+	"v.io/v23/rpc"
+	"v.io/v23/security"
+	"v.io/v23/security/access"
+	"v.io/v23/verror"
+	"v.io/v23/vom"
+	"v.io/x/lib/vlog"
+)
+
+var (
+	// memberViewTTL is the shelf-life of the aggregate view of SyncGroup members.
+	memberViewTTL = 2 * time.Second
+)
+
+////////////////////////////////////////////////////////////
+// SyncGroup management internal to Syncbase.
+
+// memberView holds an aggregated view of all SyncGroup members across
+// databases. The view is not coherent, it gets refreshed according to a
+// configured TTL and not (coherently) when SyncGroup membership is updated in
+// the various databases. It is needed by the sync Initiator, which must select
+// a peer to contact from a global view of all SyncGroup members gathered from
+// all databases. This is why a slightly stale view is acceptable.
+// The members are identified by their Vanadium names (map keys).
+type memberView struct {
+	expiration time.Time
+	members    map[string]*memberInfo
+}
+
+// memberInfo holds the member metadata for each SyncGroup this member belongs
+// to within each App/Database (i.e. global database name).  It's a mapping of
+// global DB names to sets of SyncGroup member information.
+type memberInfo struct {
+	db2sg map[string]sgMemberInfo
+}
+
+// sgMemberInfo maps SyncGroups to their member metadata.
+type sgMemberInfo map[interfaces.GroupId]wire.SyncGroupMemberInfo
+
+// newSyncGroupVersion generates a random SyncGroup version ("etag").
+func newSyncGroupVersion() string {
+	return fmt.Sprintf("%x", rand64())
+}
+
+// newSyncGroupId generates a random SyncGroup ID.
+func newSyncGroupId() interfaces.GroupId {
+	id := interfaces.NoGroupId
+	for id == interfaces.NoGroupId {
+		id = interfaces.GroupId(rand64())
+	}
+	return id
+}
+
+// verifySyncGroup verifies if a SyncGroup struct is well-formed.
+func verifySyncGroup(ctx *context.T, sg *interfaces.SyncGroup) error {
+	if sg == nil {
+		return verror.New(verror.ErrBadArg, ctx, "group information not specified")
+	}
+	if sg.Name == "" {
+		return verror.New(verror.ErrBadArg, ctx, "group name not specified")
+	}
+	if sg.AppName == "" {
+		return verror.New(verror.ErrBadArg, ctx, "app name not specified")
+	}
+	if sg.DbName == "" {
+		return verror.New(verror.ErrBadArg, ctx, "db name not specified")
+	}
+	if sg.Creator == "" {
+		return verror.New(verror.ErrBadArg, ctx, "creator id not specified")
+	}
+	if sg.Id == interfaces.NoGroupId {
+		return verror.New(verror.ErrBadArg, ctx, "group id not specified")
+	}
+	if sg.SpecVersion == "" {
+		return verror.New(verror.ErrBadArg, ctx, "group version not specified")
+	}
+	if len(sg.Joiners) == 0 {
+		return verror.New(verror.ErrBadArg, ctx, "group has no joiners")
+	}
+	if len(sg.Spec.Prefixes) == 0 {
+		return verror.New(verror.ErrBadArg, ctx, "group has no prefixes specified")
+	}
+	return nil
+}
+
+// addSyncGroup adds a new SyncGroup given its information.
+func addSyncGroup(ctx *context.T, tx store.Transaction, sg *interfaces.SyncGroup) error {
+	// Verify SyncGroup before storing it since it may have been received
+	// from a remote peer.
+	if err := verifySyncGroup(ctx, sg); err != nil {
+		return err
+	}
+
+	if ok, err := hasSGDataEntry(tx, sg.Id); err != nil {
+		return err
+	} else if ok {
+		return verror.New(verror.ErrExist, ctx, "group id already exists")
+	}
+	if ok, err := hasSGNameEntry(tx, sg.Name); err != nil {
+		return err
+	} else if ok {
+		return verror.New(verror.ErrExist, ctx, "group name already exists")
+	}
+
+	// Add the group name and data entries.
+	if err := setSGNameEntry(ctx, tx, sg.Name, sg.Id); err != nil {
+		return err
+	}
+	if err := setSGDataEntry(ctx, tx, sg.Id, sg); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// getSyncGroupId retrieves the SyncGroup ID given its name.
+func getSyncGroupId(ctx *context.T, st store.StoreReader, name string) (interfaces.GroupId, error) {
+	return getSGNameEntry(ctx, st, name)
+}
+
+// getSyncGroupName retrieves the SyncGroup name given its ID.
+func getSyncGroupName(ctx *context.T, st store.StoreReader, gid interfaces.GroupId) (string, error) {
+	sg, err := getSyncGroupById(ctx, st, gid)
+	if err != nil {
+		return "", err
+	}
+	return sg.Name, nil
+}
+
+// getSyncGroupById retrieves the SyncGroup given its ID.
+func getSyncGroupById(ctx *context.T, st store.StoreReader, gid interfaces.GroupId) (*interfaces.SyncGroup, error) {
+	return getSGDataEntry(ctx, st, gid)
+}
+
+// getSyncGroupByName retrieves the SyncGroup given its name.
+func getSyncGroupByName(ctx *context.T, st store.StoreReader, name string) (*interfaces.SyncGroup, error) {
+	gid, err := getSyncGroupId(ctx, st, name)
+	if err != nil {
+		return nil, err
+	}
+	return getSyncGroupById(ctx, st, gid)
+}
+
+// delSyncGroupById deletes the SyncGroup given its ID.
+func delSyncGroupById(ctx *context.T, tx store.Transaction, gid interfaces.GroupId) error {
+	sg, err := getSyncGroupById(ctx, tx, gid)
+	if err != nil {
+		return err
+	}
+	if err = delSGNameEntry(ctx, tx, sg.Name); err != nil {
+		return err
+	}
+	return delSGDataEntry(ctx, tx, sg.Id)
+}
+
+// delSyncGroupByName deletes the SyncGroup given its name.
+func delSyncGroupByName(ctx *context.T, tx store.Transaction, name string) error {
+	gid, err := getSyncGroupId(ctx, tx, name)
+	if err != nil {
+		return err
+	}
+	return delSyncGroupById(ctx, tx, gid)
+}
+
+// refreshMembersIfExpired updates the aggregate view of SyncGroup members
+// across databases if the view has expired.
+// TODO(rdaoud): track dirty apps/dbs since the last refresh and incrementally
+// update the membership view for them instead of always scanning all of them.
+func (s *syncService) refreshMembersIfExpired(ctx *context.T) {
+	view := s.allMembers
+	if view == nil {
+		// The empty expiration time in Go is before "now" and treated as expired
+		// below.
+		view = &memberView{expiration: time.Time{}, members: nil}
+		s.allMembers = view
+	}
+
+	if time.Now().Before(view.expiration) {
+		return
+	}
+
+	// Create a new aggregate view of SyncGroup members across all app databases.
+	newMembers := make(map[string]*memberInfo)
+
+	s.forEachDatabaseStore(ctx, func(appName, dbName string, st store.Store) bool {
+		// For each database, fetch its SyncGroup data entries by scanning their
+		// prefix range.  Use a database snapshot for the scan.
+		sn := st.NewSnapshot()
+		defer sn.Abort()
+		name := appDbName(appName, dbName)
+
+		forEachSyncGroup(sn, func(sg *interfaces.SyncGroup) bool {
+			// Add all members of this SyncGroup to the membership view.
+			// A member's info is different across SyncGroups, so gather all of them.
+			for member, info := range sg.Joiners {
+				if _, ok := newMembers[member]; !ok {
+					newMembers[member] = &memberInfo{db2sg: make(map[string]sgMemberInfo)}
+				}
+				if _, ok := newMembers[member].db2sg[name]; !ok {
+					newMembers[member].db2sg[name] = make(sgMemberInfo)
+				}
+				newMembers[member].db2sg[name][sg.Id] = info
+			}
+			return false
+		})
+		return false
+	})
+
+	view.members = newMembers
+	view.expiration = time.Now().Add(memberViewTTL)
+}
+
+// forEachSyncGroup iterates over all SyncGroups in the Database and invokes
+// the callback function on each one.  The callback returns a "done" flag to
+// make forEachSyncGroup() stop the iteration earlier; otherwise the function
+// loops across all SyncGroups in the Database.
+func forEachSyncGroup(st store.StoreReader, callback func(*interfaces.SyncGroup) bool) {
+	scanStart, scanLimit := util.ScanPrefixArgs(sgDataKeyScanPrefix, "")
+	stream := st.Scan(scanStart, scanLimit)
+	for stream.Advance() {
+		var sg interfaces.SyncGroup
+		if vom.Decode(stream.Value(nil), &sg) != nil {
+			vlog.Errorf("sync: forEachSyncGroup: invalid SyncGroup value for key %s", string(stream.Key(nil)))
+			continue
+		}
+
+		if callback(&sg) {
+			break // done, early exit
+		}
+	}
+
+	if err := stream.Err(); err != nil {
+		vlog.Errorf("sync: forEachSyncGroup: scan stream error: %v", err)
+	}
+}
+
+// getMembers returns all SyncGroup members and the count of SyncGroups each one
+// joined.
+func (s *syncService) getMembers(ctx *context.T) map[string]uint32 {
+	s.allMembersLock.Lock()
+	defer s.allMembersLock.Unlock()
+
+	s.refreshMembersIfExpired(ctx)
+
+	members := make(map[string]uint32)
+	for member, info := range s.allMembers.members {
+		count := 0
+		for _, sgmi := range info.db2sg {
+			count += len(sgmi)
+		}
+		members[member] = uint32(count)
+	}
+
+	return members
+}
+
+// copyMemberInfo returns a copy of the info for the requested peer.
+func (s *syncService) copyMemberInfo(ctx *context.T, member string) *memberInfo {
+	s.allMembersLock.RLock()
+	defer s.allMembersLock.RUnlock()
+
+	info, ok := s.allMembers.members[member]
+	if !ok {
+		return nil
+	}
+
+	// Make a copy.
+	infoCopy := &memberInfo{make(map[string]sgMemberInfo)}
+	for gdbName, sgInfo := range info.db2sg {
+		infoCopy.db2sg[gdbName] = make(sgMemberInfo)
+		for gid, mi := range sgInfo {
+			infoCopy.db2sg[gdbName][gid] = mi
+		}
+	}
+
+	return infoCopy
+}
+
+// Low-level utility functions to access DB entries without tracking their
+// relationships.
+// Use the functions above to manipulate SyncGroups.
+
+var (
+	// sgDataKeyScanPrefix is the prefix used to scan SyncGroup data entries.
+	sgDataKeyScanPrefix = util.JoinKeyParts(util.SyncPrefix, sgPrefix, "d")
+
+	// sgNameKeyScanPrefix is the prefix used to scan SyncGroup name entries.
+	sgNameKeyScanPrefix = util.JoinKeyParts(util.SyncPrefix, sgPrefix, "n")
+)
+
+// sgDataKey returns the key used to access the SyncGroup data entry.
+func sgDataKey(gid interfaces.GroupId) string {
+	return util.JoinKeyParts(util.SyncPrefix, sgPrefix, "d", fmt.Sprintf("%d", gid))
+}
+
+// sgNameKey returns the key used to access the SyncGroup name entry.
+func sgNameKey(name string) string {
+	return util.JoinKeyParts(util.SyncPrefix, sgPrefix, "n", name)
+}
+
+// splitSgNameKey is the inverse of sgNameKey and returns the SyncGroup name.
+func splitSgNameKey(ctx *context.T, key string) (string, error) {
+	prefix := util.JoinKeyParts(util.SyncPrefix, sgPrefix, "n", "")
+
+	// Note that the actual SyncGroup name may contain ":" as a separator.
+	if !strings.HasPrefix(key, prefix) {
+		return "", verror.New(verror.ErrInternal, ctx, "invalid sgNamekey", key)
+	}
+	return strings.TrimPrefix(key, prefix), nil
+}
+
+// hasSGDataEntry returns true if the SyncGroup data entry exists.
+func hasSGDataEntry(sntx store.SnapshotOrTransaction, gid interfaces.GroupId) (bool, error) {
+	// TODO(rdaoud): optimize to avoid the unneeded fetch/decode of the data.
+	var sg interfaces.SyncGroup
+	if err := util.Get(nil, sntx, sgDataKey(gid), &sg); err != nil {
+		if verror.ErrorID(err) == verror.ErrNoExist.ID {
+			err = nil
+		}
+		return false, err
+	}
+	return true, nil
+}
+
+// hasSGNameEntry returns true if the SyncGroup name entry exists.
+func hasSGNameEntry(sntx store.SnapshotOrTransaction, name string) (bool, error) {
+	// TODO(rdaoud): optimize to avoid the unneeded fetch/decode of the data.
+	var gid interfaces.GroupId
+	if err := util.Get(nil, sntx, sgNameKey(name), &gid); err != nil {
+		if verror.ErrorID(err) == verror.ErrNoExist.ID {
+			err = nil
+		}
+		return false, err
+	}
+	return true, nil
+}
+
+// setSGDataEntry stores the SyncGroup data entry.
+func setSGDataEntry(ctx *context.T, tx store.Transaction, gid interfaces.GroupId, sg *interfaces.SyncGroup) error {
+	return util.Put(ctx, tx, sgDataKey(gid), sg)
+}
+
+// setSGNameEntry stores the SyncGroup name entry.
+func setSGNameEntry(ctx *context.T, tx store.Transaction, name string, gid interfaces.GroupId) error {
+	return util.Put(ctx, tx, sgNameKey(name), gid)
+}
+
+// getSGDataEntry retrieves the SyncGroup data for a given group ID.
+func getSGDataEntry(ctx *context.T, st store.StoreReader, gid interfaces.GroupId) (*interfaces.SyncGroup, error) {
+	var sg interfaces.SyncGroup
+	if err := util.Get(ctx, st, sgDataKey(gid), &sg); err != nil {
+		return nil, err
+	}
+	return &sg, nil
+}
+
+// getSGNameEntry retrieves the SyncGroup name to ID mapping.
+func getSGNameEntry(ctx *context.T, st store.StoreReader, name string) (interfaces.GroupId, error) {
+	var gid interfaces.GroupId
+	if err := util.Get(ctx, st, sgNameKey(name), &gid); err != nil {
+		return gid, err
+	}
+	return gid, nil
+}
+
+// delSGDataEntry deletes the SyncGroup data entry.
+func delSGDataEntry(ctx *context.T, tx store.Transaction, gid interfaces.GroupId) error {
+	return util.Delete(ctx, tx, sgDataKey(gid))
+}
+
+// delSGNameEntry deletes the SyncGroup name to ID mapping.
+func delSGNameEntry(ctx *context.T, tx store.Transaction, name string) error {
+	return util.Delete(ctx, tx, sgNameKey(name))
+}
+
+////////////////////////////////////////////////////////////
+// SyncGroup methods between Client and Syncbase.
+
+// TODO(hpucha): Pass blessings along.
+func (sd *syncDatabase) CreateSyncGroup(ctx *context.T, call rpc.ServerCall, sgName string, spec wire.SyncGroupSpec, myInfo wire.SyncGroupMemberInfo) error {
+	vlog.VI(2).Infof("sync: CreateSyncGroup: begin: %s", sgName)
+	defer vlog.VI(2).Infof("sync: CreateSyncGroup: end: %s", sgName)
+
+	err := store.RunInTransaction(sd.db.St(), func(tx store.Transaction) error {
+		// Check permissions on Database.
+		if err := sd.db.CheckPermsInternal(ctx, call, tx); err != nil {
+			return err
+		}
+
+		// TODO(hpucha): Check prefix ACLs on all SG prefixes.
+		// This may need another method on util.Database interface.
+
+		// TODO(hpucha): Do some SG ACL checking. Check creator
+		// has Admin privilege.
+
+		// Get this Syncbase's sync module handle.
+		ss := sd.sync.(*syncService)
+
+		// Instantiate sg. Add self as joiner.
+		sg := &interfaces.SyncGroup{
+			Id:          newSyncGroupId(),
+			Name:        sgName,
+			SpecVersion: newSyncGroupVersion(),
+			Spec:        spec,
+			Creator:     ss.name,
+			AppName:     sd.db.App().Name(),
+			DbName:      sd.db.Name(),
+			Status:      interfaces.SyncGroupStatusPublishPending,
+			Joiners:     map[string]wire.SyncGroupMemberInfo{ss.name: myInfo},
+		}
+
+		if err := addSyncGroup(ctx, tx, sg); err != nil {
+			return err
+		}
+
+		// TODO(hpucha): Bootstrap DAG/Genvector etc for syncing the SG metadata.
+
+		// Take a snapshot of the data to bootstrap the SyncGroup.
+		return sd.bootstrapSyncGroup(ctx, tx, spec.Prefixes)
+	})
+
+	if err != nil {
+		return err
+	}
+
+	// Local SG create succeeded. Publish the SG at the chosen server.
+	sd.publishSyncGroup(ctx, call, sgName)
+
+	// Publish at the chosen mount table and in the neighborhood.
+	sd.publishInMountTables(ctx, call, spec)
+
+	return nil
+}
+
+// TODO(hpucha): Pass blessings along.
+func (sd *syncDatabase) JoinSyncGroup(ctx *context.T, call rpc.ServerCall, sgName string, myInfo wire.SyncGroupMemberInfo) (wire.SyncGroupSpec, error) {
+	vlog.VI(2).Infof("sync: JoinSyncGroup: begin: %s", sgName)
+	defer vlog.VI(2).Infof("sync: JoinSyncGroup: end: %s", sgName)
+
+	var sgErr error
+	var sg *interfaces.SyncGroup
+	nullSpec := wire.SyncGroupSpec{}
+
+	err := store.RunInTransaction(sd.db.St(), func(tx store.Transaction) error {
+		// Check permissions on Database.
+		if err := sd.db.CheckPermsInternal(ctx, call, tx); err != nil {
+			return err
+		}
+
+		// Check if SyncGroup already exists.
+		sg, sgErr = getSyncGroupByName(ctx, tx, sgName)
+		if sgErr != nil {
+			return sgErr
+		}
+
+		// SyncGroup already exists. Possibilities include created
+		// locally, already joined locally or published at the device as
+		// a result of SyncGroup creation on a different device.
+		//
+		// TODO(hpucha): Handle the above cases. If the SG was published
+		// locally, but not joined, we need to bootstrap the DAG and
+		// watcher. If multiple joins are done locally, we may want to
+		// ref count the SG state and track the leaves accordingly. So
+		// we may need to add some local state for each SyncGroup.
+
+		// Check SG ACL.
+		return authorize(ctx, call.Security(), sg)
+	})
+
+	// The presented blessing is allowed to make this Syncbase instance join
+	// the specified SyncGroup, but this Syncbase instance has in fact
+	// already joined the SyncGroup. Join is idempotent, so we simply return
+	// the spec to indicate success.
+	if err == nil {
+		return sg.Spec, nil
+	}
+
+	// Join is not allowed (possibilities include Database permissions check
+	// failed, SG ACL check failed or error during fetching SG information).
+	if verror.ErrorID(sgErr) != verror.ErrNoExist.ID {
+		return nullSpec, err
+	}
+
+	// Brand new join.
+
+	// Get this Syncbase's sync module handle.
+	ss := sd.sync.(*syncService)
+
+	// Contact a SyncGroup Admin to join the SyncGroup.
+	sg = &interfaces.SyncGroup{}
+	*sg, err = sd.joinSyncGroupAtAdmin(ctx, call, sgName, ss.name, myInfo)
+	if err != nil {
+		return nullSpec, err
+	}
+
+	// Verify that the app/db combination is valid for this SyncGroup.
+	if sg.AppName != sd.db.App().Name() || sg.DbName != sd.db.Name() {
+		return nullSpec, verror.New(verror.ErrBadArg, ctx, "bad app/db with syncgroup")
+	}
+
+	err = store.RunInTransaction(sd.db.St(), func(tx store.Transaction) error {
+
+		// TODO(hpucha): Bootstrap DAG/Genvector etc for syncing the SG metadata.
+
+		// TODO(hpucha): Get SG Deltas from Admin device.
+
+		if err := addSyncGroup(ctx, tx, sg); err != nil {
+			return err
+		}
+
+		// Take a snapshot of the data to bootstrap the SyncGroup.
+		return sd.bootstrapSyncGroup(ctx, tx, sg.Spec.Prefixes)
+	})
+
+	if err != nil {
+		return nullSpec, err
+	}
+
+	// Publish at the chosen mount table and in the neighborhood.
+	sd.publishInMountTables(ctx, call, sg.Spec)
+
+	return sg.Spec, nil
+}
+
+func (sd *syncDatabase) GetSyncGroupNames(ctx *context.T, call rpc.ServerCall) ([]string, error) {
+	var sgNames []string
+
+	vlog.VI(2).Infof("sync: GetSyncGroupNames: begin")
+	defer vlog.VI(2).Infof("sync: GetSyncGroupNames: end: %v", sgNames)
+
+	sn := sd.db.St().NewSnapshot()
+	defer sn.Abort()
+
+	// Check permissions on Database.
+	if err := sd.db.CheckPermsInternal(ctx, call, sn); err != nil {
+		return nil, err
+	}
+
+	// Scan all the SyncGroup names found in the Database.
+	scanStart, scanLimit := util.ScanPrefixArgs(sgNameKeyScanPrefix, "")
+	stream := sn.Scan(scanStart, scanLimit)
+	var key []byte
+	for stream.Advance() {
+		sgName, err := splitSgNameKey(ctx, string(stream.Key(key)))
+		if err != nil {
+			return nil, err
+		}
+		sgNames = append(sgNames, sgName)
+	}
+
+	if err := stream.Err(); err != nil {
+		return nil, err
+	}
+
+	return sgNames, nil
+}
+
+func (sd *syncDatabase) GetSyncGroupSpec(ctx *context.T, call rpc.ServerCall, sgName string) (wire.SyncGroupSpec, string, error) {
+	var spec wire.SyncGroupSpec
+
+	vlog.VI(2).Infof("sync: GetSyncGroupSpec: begin %s", sgName)
+	defer vlog.VI(2).Infof("sync: GetSyncGroupSpec: end: %s spec %v", sgName, spec)
+
+	sn := sd.db.St().NewSnapshot()
+	defer sn.Abort()
+
+	// Check permissions on Database.
+	if err := sd.db.CheckPermsInternal(ctx, call, sn); err != nil {
+		return spec, "", err
+	}
+
+	// Get the SyncGroup information.
+	sg, err := getSyncGroupByName(ctx, sn, sgName)
+	if err != nil {
+		return spec, "", err
+	}
+	// TODO(hpucha): Check SyncGroup ACL.
+
+	spec = sg.Spec
+	return spec, sg.SpecVersion, nil
+}
+
+func (sd *syncDatabase) GetSyncGroupMembers(ctx *context.T, call rpc.ServerCall, sgName string) (map[string]wire.SyncGroupMemberInfo, error) {
+	var members map[string]wire.SyncGroupMemberInfo
+
+	vlog.VI(2).Infof("sync: GetSyncGroupMembers: begin %s", sgName)
+	defer vlog.VI(2).Infof("sync: GetSyncGroupMembers: end: %s members %v", sgName, members)
+
+	sn := sd.db.St().NewSnapshot()
+	defer sn.Abort()
+
+	// Check permissions on Database.
+	if err := sd.db.CheckPermsInternal(ctx, call, sn); err != nil {
+		return members, err
+	}
+
+	// Get the SyncGroup information.
+	sg, err := getSyncGroupByName(ctx, sn, sgName)
+	if err != nil {
+		return members, err
+	}
+
+	// TODO(hpucha): Check SyncGroup ACL.
+
+	members = sg.Joiners
+	return members, nil
+}
+
+// TODO(hpucha): Enable syncing syncgroup metadata.
+func (sd *syncDatabase) SetSyncGroupSpec(ctx *context.T, call rpc.ServerCall, sgName string, spec wire.SyncGroupSpec, version string) error {
+	vlog.VI(2).Infof("sync: SetSyncGroupSpec: begin %s %v %s", sgName, spec, version)
+	defer vlog.VI(2).Infof("sync: SetSyncGroupSpec: end: %s", sgName)
+
+	err := store.RunInTransaction(sd.db.St(), func(tx store.Transaction) error {
+		// Check permissions on Database.
+		if err := sd.db.CheckPermsInternal(ctx, call, tx); err != nil {
+			return err
+		}
+
+		sg, err := getSyncGroupByName(ctx, tx, sgName)
+		if err != nil {
+			return err
+		}
+
+		// TODO(hpucha): Check SyncGroup ACL. Perform version checking.
+
+		sg.Spec = spec
+		return setSGDataEntry(ctx, tx, sg.Id, sg)
+	})
+	return err
+}
+
+//////////////////////////////
+// Helper functions
+
+// TODO(hpucha): Call this periodically until we are able to contact the remote peer.
+func (sd *syncDatabase) publishSyncGroup(ctx *context.T, call rpc.ServerCall, sgName string) error {
+	sg, err := getSyncGroupByName(ctx, sd.db.St(), sgName)
+	if err != nil {
+		return err
+	}
+
+	if sg.Status != interfaces.SyncGroupStatusPublishPending {
+		return nil
+	}
+
+	c := interfaces.SyncClient(sgName)
+	err = c.PublishSyncGroup(ctx, *sg)
+
+	// Publish failed temporarily. Retry later.
+	// TODO(hpucha): Is there an RPC error that we can check here?
+	if err != nil && verror.ErrorID(err) != verror.ErrExist.ID {
+		return err
+	}
+
+	// Publish succeeded.
+	if err == nil {
+		// TODO(hpucha): Get SG Deltas from publisher. Obtaining the
+		// new version from the publisher prevents SG conflicts.
+		return err
+	}
+
+	// Publish rejected. Persist that to avoid retrying in the
+	// future and to remember the split universe scenario.
+	err = store.RunInTransaction(sd.db.St(), func(tx store.Transaction) error {
+		// Ensure SG still exists.
+		sg, err := getSyncGroupByName(ctx, tx, sgName)
+		if err != nil {
+			return err
+		}
+
+		sg.Status = interfaces.SyncGroupStatusPublishRejected
+		return setSGDataEntry(ctx, tx, sg.Id, sg)
+	})
+	return err
+}
+
+// bootstrapSyncGroup inserts into the transaction log a SyncGroup operation and
+// a set of Snapshot operations to notify the sync watcher about the SyncGroup
+// prefixes to start accepting and the initial state of existing store keys that
+// match these prefixes (both data and permission keys).
+// TODO(rdaoud): this operation scans the managed keys of the database and can
+// be time consuming.  Consider doing it asynchronously and letting the server
+// reply to the client earlier.  However it must happen within the scope of this
+// transaction (and its snapshot view).
+func (sd *syncDatabase) bootstrapSyncGroup(ctx *context.T, tx store.Transaction, prefixes []string) error {
+	if len(prefixes) == 0 {
+		return verror.New(verror.ErrInternal, ctx, "no prefixes specified")
+	}
+
+	// Get the store options to retrieve the list of managed key prefixes.
+	opts, err := watchable.GetOptions(sd.db.St())
+	if err != nil {
+		return err
+	}
+	if len(opts.ManagedPrefixes) == 0 {
+		return verror.New(verror.ErrInternal, ctx, "store has no managed prefixes")
+	}
+
+	// Notify the watcher of the SyncGroup prefixes to start accepting.
+	if err := watchable.AddSyncGroupOp(ctx, tx, prefixes, false); err != nil {
+		return err
+	}
+
+	// Loop over the store managed key prefixes (e.g. data and permissions).
+	// For each one, scan the ranges of the given SyncGroup prefixes.  For
+	// each matching key, insert a snapshot operation in the log.  Scanning
+	// is done over the version entries to retrieve the matching keys and
+	// their version numbers (the key values).  Remove the version prefix
+	// from the key used in the snapshot operation.
+	// TODO(rdaoud): for SyncGroup prefixes, there should be a separation
+	// between their representation at the client (a list of (db, prefix)
+	// tuples) and internally as strings that match the store's key format.
+	for _, mp := range opts.ManagedPrefixes {
+		for _, p := range prefixes {
+			start, limit := util.ScanPrefixArgs(util.JoinKeyParts(util.VersionPrefix, mp), p)
+			stream := tx.Scan(start, limit)
+			for stream.Advance() {
+				k, v := stream.Key(nil), stream.Value(nil)
+				parts := util.SplitKeyParts(string(k))
+				if len(parts) < 2 {
+					vlog.Fatalf("sync: bootstrapSyncGroup: invalid version key %s", string(k))
+
+				}
+				key := []byte(util.JoinKeyParts(parts[1:]...))
+				if err := watchable.AddSyncSnapshotOp(ctx, tx, key, v); err != nil {
+					return err
+				}
+
+			}
+			if err := stream.Err(); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+func (sd *syncDatabase) publishInMountTables(ctx *context.T, call rpc.ServerCall, spec wire.SyncGroupSpec) error {
+	// Get this Syncbase's sync module handle.
+	ss := sd.sync.(*syncService)
+
+	for _, mt := range spec.MountTables {
+		name := naming.Join(mt, ss.name)
+		// TODO(hpucha): Is this add idempotent? Appears to be from code.
+		// Confirm that it is ok to use absolute names here.
+		if err := ss.server.AddName(name); err != nil {
+			return err
+		}
+	}
+
+	// TODO(hpucha): Do we have to publish in neighborhood explicitly?
+
+	return nil
+}
+
+func (sd *syncDatabase) joinSyncGroupAtAdmin(ctx *context.T, call rpc.ServerCall, sgName, name string, myInfo wire.SyncGroupMemberInfo) (interfaces.SyncGroup, error) {
+	c := interfaces.SyncClient(sgName)
+	return c.JoinSyncGroupAtAdmin(ctx, sgName, name, myInfo)
+
+	// TODO(hpucha): Try to join using an Admin on neighborhood if the publisher is not reachable.
+}
+
+func authorize(ctx *context.T, call security.Call, sg *interfaces.SyncGroup) error {
+	auth := access.TypicalTagTypePermissionsAuthorizer(sg.Spec.Perms)
+	if err := auth.Authorize(ctx, call); err != nil {
+		return verror.New(verror.ErrNoAccess, ctx, err)
+	}
+	return nil
+}
+
+////////////////////////////////////////////////////////////
+// Methods for SyncGroup create/join between Syncbases.
+
+func (s *syncService) PublishSyncGroup(ctx *context.T, call rpc.ServerCall, sg interfaces.SyncGroup) error {
+	st, err := s.getDbStore(ctx, call, sg.AppName, sg.DbName)
+	if err != nil {
+		return err
+	}
+
+	err = store.RunInTransaction(st, func(tx store.Transaction) error {
+		localSG, err := getSyncGroupByName(ctx, tx, sg.Name)
+
+		if err != nil && verror.ErrorID(err) != verror.ErrNoExist.ID {
+			return err
+		}
+
+		// SG name already claimed.
+		if err == nil && localSG.Id != sg.Id {
+			return verror.New(verror.ErrExist, ctx, sg.Name)
+		}
+
+		// TODO(hpucha): Bootstrap DAG/Genvector etc for syncing the SG
+		// metadata if needed.
+		//
+		// TODO(hpucha): Catch up on SG versions so far.
+
+		// SG already published. Update if needed.
+		if err == nil && localSG.Id == sg.Id {
+			if localSG.Status == interfaces.SyncGroupStatusPublishPending {
+				localSG.Status = interfaces.SyncGroupStatusRunning
+				return setSGDataEntry(ctx, tx, localSG.Id, localSG)
+			}
+			return nil
+		}
+
+		// Publish the SyncGroup.
+
+		// TODO(hpucha): Use some ACL check to allow/deny publishing.
+		// TODO(hpucha): Ensure node is on Admin ACL.
+
+		// TODO(hpucha): Default priority?
+		sg.Joiners[s.name] = wire.SyncGroupMemberInfo{}
+		sg.Status = interfaces.SyncGroupStatusRunning
+		return addSyncGroup(ctx, tx, &sg)
+	})
+
+	return err
+}
+
+func (s *syncService) JoinSyncGroupAtAdmin(ctx *context.T, call rpc.ServerCall, sgName, joinerName string, joinerInfo wire.SyncGroupMemberInfo) (interfaces.SyncGroup, error) {
+	var dbSt store.Store
+	var gid interfaces.GroupId
+	var err error
+
+	// Find the database store for this SyncGroup.
+	//
+	// TODO(hpucha): At a high level, we have yet to decide if the SG name
+	// is stand-alone or is derived from the app/db namespace, based on the
+	// feedback from app developers (see discussion in SyncGroup API
+	// doc). If we decide to keep the SG name as stand-alone, this scan can
+	// be optimized by a lazy cache of sgname to <app, db> info.
+	s.forEachDatabaseStore(ctx, func(appName, dbName string, st store.Store) bool {
+		if gid, err = getSyncGroupId(ctx, st, sgName); err == nil {
+			// Found the SyncGroup being looked for.
+			dbSt = st
+			return true
+		}
+		return false
+	})
+
+	// SyncGroup not found.
+	if err != nil {
+		return interfaces.SyncGroup{}, verror.New(verror.ErrNoExist, ctx, "SyncGroup not found", sgName)
+	}
+
+	var sg *interfaces.SyncGroup
+	err = store.RunInTransaction(dbSt, func(tx store.Transaction) error {
+		var err error
+		sg, err = getSyncGroupById(ctx, tx, gid)
+		if err != nil {
+			return err
+		}
+
+		// Check SG ACL.
+		if err := authorize(ctx, call.Security(), sg); err != nil {
+			return err
+		}
+
+		// Add to joiner list.
+		sg.Joiners[joinerName] = joinerInfo
+		return setSGDataEntry(ctx, tx, sg.Id, sg)
+	})
+
+	if err != nil {
+		return interfaces.SyncGroup{}, err
+	}
+	return *sg, nil
+}
diff --git a/services/syncbase/vsync/syncgroup_test.go b/services/syncbase/vsync/syncgroup_test.go
new file mode 100644
index 0000000..6c87ee7
--- /dev/null
+++ b/services/syncbase/vsync/syncgroup_test.go
@@ -0,0 +1,493 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Tests for SyncGroup management and storage in Syncbase.
+
+import (
+	"reflect"
+	"testing"
+	"time"
+
+	"v.io/syncbase/v23/services/syncbase/nosql"
+	"v.io/syncbase/x/ref/services/syncbase/server/interfaces"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	_ "v.io/x/ref/runtime/factories/generic"
+)
+
+// checkSGStats verifies SyncGroup stats.
+func checkSGStats(t *testing.T, svc *mockService, which string, numSG, numMembers int) {
+	memberViewTTL = 0 // Always recompute the SyncGroup membership view.
+	svc.sync.refreshMembersIfExpired(nil)
+
+	view := svc.sync.allMembers
+	if num := len(view.members); num != numMembers {
+		t.Errorf("num-members (%s): got %v instead of %v", which, num, numMembers)
+	}
+
+	sgids := make(map[interfaces.GroupId]bool)
+	for _, info := range view.members {
+		for _, sgmi := range info.db2sg {
+			for gid := range sgmi {
+				sgids[gid] = true
+			}
+		}
+	}
+
+	if num := len(sgids); num != numSG {
+		t.Errorf("num-syncgroups (%s): got %v instead of %v", which, num, numSG)
+	}
+}
+
+// TestAddSyncGroup tests adding SyncGroups.
+func TestAddSyncGroup(t *testing.T) {
+	// Set a large value to prevent the initiator from running. Since this
+	// test adds a fake SyncGroup, if the initiator runs, it will attempt
+	// to initiate using this fake and partial SyncGroup data.
+	peerSyncInterval = 1 * time.Hour
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+
+	checkSGStats(t, svc, "add-1", 0, 0)
+
+	// Add a SyncGroup.
+
+	sgName := "foobar"
+	sgId := interfaces.GroupId(1234)
+
+	sg := &interfaces.SyncGroup{
+		Name:        sgName,
+		Id:          sgId,
+		AppName:     "mockApp",
+		DbName:      "mockDB",
+		Creator:     "mockCreator",
+		SpecVersion: "etag-0",
+		Spec: nosql.SyncGroupSpec{
+			Prefixes: []string{"foo", "bar"},
+		},
+		Joiners: map[string]nosql.SyncGroupMemberInfo{
+			"phone":  nosql.SyncGroupMemberInfo{SyncPriority: 10},
+			"tablet": nosql.SyncGroupMemberInfo{SyncPriority: 25},
+			"cloud":  nosql.SyncGroupMemberInfo{SyncPriority: 1},
+		},
+	}
+
+	tx := st.NewTransaction()
+	if err := addSyncGroup(nil, tx, sg); err != nil {
+		t.Errorf("cannot add SyncGroup ID %d: %v", sg.Id, err)
+	}
+	if err := tx.Commit(); err != nil {
+		t.Errorf("cannot commit adding SyncGroup ID %d: %v", sg.Id, err)
+	}
+
+	// Verify SyncGroup ID, name, and data.
+
+	if id, err := getSyncGroupId(nil, st, sgName); err != nil || id != sgId {
+		t.Errorf("cannot get ID of SyncGroup %s: got %d instead of %d; err: %v", sgName, id, sgId, err)
+	}
+	if name, err := getSyncGroupName(nil, st, sgId); err != nil || name != sgName {
+		t.Errorf("cannot get name of SyncGroup %d: got %s instead of %s; err: %v",
+			sgId, name, sgName, err)
+	}
+
+	sgOut, err := getSyncGroupById(nil, st, sgId)
+	if err != nil {
+		t.Errorf("cannot get SyncGroup by ID %d: %v", sgId, err)
+	}
+	if !reflect.DeepEqual(sgOut, sg) {
+		t.Errorf("invalid SyncGroup data for group ID %d: got %v instead of %v", sgId, sgOut, sg)
+	}
+
+	sgOut, err = getSyncGroupByName(nil, st, sgName)
+	if err != nil {
+		t.Errorf("cannot get SyncGroup by Name %s: %v", sgName, err)
+	}
+	if !reflect.DeepEqual(sgOut, sg) {
+		t.Errorf("invalid SyncGroup data for group name %s: got %v instead of %v", sgName, sgOut, sg)
+	}
+
+	// Verify membership data.
+
+	expMembers := map[string]uint32{"phone": 1, "tablet": 1, "cloud": 1}
+
+	members := svc.sync.getMembers(nil)
+	if !reflect.DeepEqual(members, expMembers) {
+		t.Errorf("invalid SyncGroup members: got %v instead of %v", members, expMembers)
+	}
+
+	view := svc.sync.allMembers
+	for mm := range members {
+		mi := view.members[mm]
+		if mi == nil {
+			t.Errorf("cannot get info for SyncGroup member %s", mm)
+		}
+		if len(mi.db2sg) != 1 {
+			t.Errorf("invalid info for SyncGroup member %s: %v", mm, mi)
+		}
+		var sgmi sgMemberInfo
+		for _, v := range mi.db2sg {
+			sgmi = v
+			break
+		}
+		if len(sgmi) != 1 {
+			t.Errorf("invalid member info for SyncGroup member %s: %v", mm, sgmi)
+		}
+		expJoinerInfo := sg.Joiners[mm]
+		joinerInfo := sgmi[sgId]
+		if !reflect.DeepEqual(joinerInfo, expJoinerInfo) {
+			t.Errorf("invalid Info for SyncGroup member %s in group ID %d: got %v instead of %v",
+				mm, sgId, joinerInfo, expJoinerInfo)
+		}
+	}
+
+	checkSGStats(t, svc, "add-2", 1, 3)
+
+	// Adding a SyncGroup for a pre-existing group ID or name should fail.
+
+	sg.Name = "another-name"
+
+	tx = st.NewTransaction()
+	if err = addSyncGroup(nil, tx, sg); err == nil {
+		t.Errorf("re-adding SyncGroup %d did not fail", sgId)
+	}
+	tx.Abort()
+
+	sg.Name = sgName
+	sg.Id = interfaces.GroupId(5555)
+
+	tx = st.NewTransaction()
+	if err = addSyncGroup(nil, tx, sg); err == nil {
+		t.Errorf("adding SyncGroup %s with a different ID did not fail", sgName)
+	}
+	tx.Abort()
+
+	checkSGStats(t, svc, "add-3", 1, 3)
+
+	// Fetch a non-existing SyncGroup by ID or name should fail.
+
+	badName := "not-available"
+	badId := interfaces.GroupId(999)
+	if id, err := getSyncGroupId(nil, st, badName); err == nil {
+		t.Errorf("found non-existing SyncGroup %s: got ID %d", badName, id)
+	}
+	if name, err := getSyncGroupName(nil, st, badId); err == nil {
+		t.Errorf("found non-existing SyncGroup %d: got name %s", badId, name)
+	}
+	if sg, err := getSyncGroupByName(nil, st, badName); err == nil {
+		t.Errorf("found non-existing SyncGroup %s: got %v", badName, sg)
+	}
+	if sg, err := getSyncGroupById(nil, st, badId); err == nil {
+		t.Errorf("found non-existing SyncGroup %d: got %v", badId, sg)
+	}
+}
+
+// TestInvalidAddSyncGroup tests adding SyncGroups.
+func TestInvalidAddSyncGroup(t *testing.T) {
+	// Set a large value to prevent the threads from firing.
+	peerSyncInterval = 1 * time.Hour
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+
+	checkBadAddSyncGroup := func(t *testing.T, st store.Store, sg *interfaces.SyncGroup, msg string) {
+		tx := st.NewTransaction()
+		if err := addSyncGroup(nil, tx, sg); err == nil {
+			t.Errorf("checkBadAddSyncGroup: adding bad SyncGroup (%s) did not fail", msg)
+		}
+		tx.Abort()
+	}
+
+	checkBadAddSyncGroup(t, st, nil, "nil SG")
+
+	sg := &interfaces.SyncGroup{Id: 1234}
+	checkBadAddSyncGroup(t, st, sg, "SG w/o name")
+
+	sg = &interfaces.SyncGroup{Name: "foobar"}
+	checkBadAddSyncGroup(t, st, sg, "SG w/o Id")
+
+	sg.Id = 1234
+	checkBadAddSyncGroup(t, st, sg, "SG w/o Version")
+
+	sg.SpecVersion = "v1"
+	checkBadAddSyncGroup(t, st, sg, "SG w/o Joiners")
+
+	sg.Joiners = map[string]nosql.SyncGroupMemberInfo{
+		"phone": nosql.SyncGroupMemberInfo{SyncPriority: 10},
+	}
+	checkBadAddSyncGroup(t, st, sg, "SG w/o Prefixes")
+}
+
+// TestDeleteSyncGroup tests deleting a SyncGroup.
+func TestDeleteSyncGroup(t *testing.T) {
+	// Set a large value to prevent the threads from firing.
+	peerSyncInterval = 1 * time.Hour
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+
+	sgName := "foobar"
+	sgId := interfaces.GroupId(1234)
+
+	// Delete non-existing SyncGroups.
+
+	tx := st.NewTransaction()
+	if err := delSyncGroupById(nil, tx, sgId); err == nil {
+		t.Errorf("deleting a non-existing SyncGroup ID did not fail")
+	}
+	if err := delSyncGroupByName(nil, tx, sgName); err == nil {
+		t.Errorf("deleting a non-existing SyncGroup name did not fail")
+	}
+	tx.Abort()
+
+	checkSGStats(t, svc, "del-1", 0, 0)
+
+	// Create the SyncGroup to delete later.
+
+	sg := &interfaces.SyncGroup{
+		Name:        sgName,
+		Id:          sgId,
+		AppName:     "mockApp",
+		DbName:      "mockDB",
+		Creator:     "mockCreator",
+		SpecVersion: "etag-0",
+		Spec: nosql.SyncGroupSpec{
+			Prefixes: []string{"foo", "bar"},
+		},
+		Joiners: map[string]nosql.SyncGroupMemberInfo{
+			"phone":  nosql.SyncGroupMemberInfo{SyncPriority: 10},
+			"tablet": nosql.SyncGroupMemberInfo{SyncPriority: 25},
+			"cloud":  nosql.SyncGroupMemberInfo{SyncPriority: 1},
+		},
+	}
+
+	tx = st.NewTransaction()
+	if err := addSyncGroup(nil, tx, sg); err != nil {
+		t.Errorf("creating SyncGroup ID %d failed: %v", sgId, err)
+	}
+	if err := tx.Commit(); err != nil {
+		t.Errorf("cannot commit adding SyncGroup ID %d: %v", sgId, err)
+	}
+
+	checkSGStats(t, svc, "del-2", 1, 3)
+
+	// Delete it by ID.
+
+	tx = st.NewTransaction()
+	if err := delSyncGroupById(nil, tx, sgId); err != nil {
+		t.Errorf("deleting SyncGroup ID %d failed: %v", sgId, err)
+	}
+	if err := tx.Commit(); err != nil {
+		t.Errorf("cannot commit deleting SyncGroup ID %d: %v", sgId, err)
+	}
+
+	checkSGStats(t, svc, "del-3", 0, 0)
+
+	// Create it again then delete it by name.
+
+	tx = st.NewTransaction()
+	if err := addSyncGroup(nil, tx, sg); err != nil {
+		t.Errorf("creating SyncGroup ID %d after delete failed: %v", sgId, err)
+	}
+	if err := tx.Commit(); err != nil {
+		t.Errorf("cannot commit adding SyncGroup ID %d after delete: %v", sgId, err)
+	}
+
+	checkSGStats(t, svc, "del-4", 1, 3)
+
+	tx = st.NewTransaction()
+	if err := delSyncGroupByName(nil, tx, sgName); err != nil {
+		t.Errorf("deleting SyncGroup name %s failed: %v", sgName, err)
+	}
+	if err := tx.Commit(); err != nil {
+		t.Errorf("cannot commit deleting SyncGroup name %s: %v", sgName, err)
+	}
+
+	checkSGStats(t, svc, "del-5", 0, 0)
+}
+
+// TestMultiSyncGroups tests creating multiple SyncGroups.
+func TestMultiSyncGroups(t *testing.T) {
+	// Set a large value to prevent the threads from firing.
+	peerSyncInterval = 1 * time.Hour
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+
+	sgName1, sgName2 := "foo", "bar"
+	sgId1, sgId2 := interfaces.GroupId(1234), interfaces.GroupId(8888)
+
+	// Add two SyncGroups.
+
+	sg1 := &interfaces.SyncGroup{
+		Name:        sgName1,
+		Id:          sgId1,
+		AppName:     "mockApp",
+		DbName:      "mockDB",
+		Creator:     "mockCreator",
+		SpecVersion: "etag-1",
+		Spec: nosql.SyncGroupSpec{
+			Prefixes: []string{"foo"},
+		},
+		Joiners: map[string]nosql.SyncGroupMemberInfo{
+			"phone":  nosql.SyncGroupMemberInfo{SyncPriority: 10},
+			"tablet": nosql.SyncGroupMemberInfo{SyncPriority: 25},
+			"cloud":  nosql.SyncGroupMemberInfo{SyncPriority: 1},
+		},
+	}
+	sg2 := &interfaces.SyncGroup{
+		Name:        sgName2,
+		Id:          sgId2,
+		AppName:     "mockApp",
+		DbName:      "mockDB",
+		Creator:     "mockCreator",
+		SpecVersion: "etag-2",
+		Spec: nosql.SyncGroupSpec{
+			Prefixes: []string{"bar"},
+		},
+		Joiners: map[string]nosql.SyncGroupMemberInfo{
+			"tablet": nosql.SyncGroupMemberInfo{SyncPriority: 111},
+			"door":   nosql.SyncGroupMemberInfo{SyncPriority: 33},
+			"lamp":   nosql.SyncGroupMemberInfo{SyncPriority: 9},
+		},
+	}
+
+	tx := st.NewTransaction()
+	if err := addSyncGroup(nil, tx, sg1); err != nil {
+		t.Errorf("creating SyncGroup ID %d failed: %v", sgId1, err)
+	}
+	if err := tx.Commit(); err != nil {
+		t.Errorf("cannot commit adding SyncGroup ID %d: %v", sgId1, err)
+	}
+
+	checkSGStats(t, svc, "multi-1", 1, 3)
+
+	tx = st.NewTransaction()
+	if err := addSyncGroup(nil, tx, sg2); err != nil {
+		t.Errorf("creating SyncGroup ID %d failed: %v", sgId2, err)
+	}
+	if err := tx.Commit(); err != nil {
+		t.Errorf("cannot commit adding SyncGroup ID %d: %v", sgId2, err)
+	}
+
+	checkSGStats(t, svc, "multi-2", 2, 5)
+
+	// Verify membership data.
+
+	expMembers := map[string]uint32{"phone": 1, "tablet": 2, "cloud": 1, "door": 1, "lamp": 1}
+
+	members := svc.sync.getMembers(nil)
+	if !reflect.DeepEqual(members, expMembers) {
+		t.Errorf("invalid SyncGroup members: got %v instead of %v", members, expMembers)
+	}
+
+	expMemberInfo := map[string]*memberInfo{
+		"phone": &memberInfo{
+			db2sg: map[string]sgMemberInfo{
+				"mockapp:mockdb": sgMemberInfo{
+					sgId1: sg1.Joiners["phone"],
+				},
+			},
+		},
+		"tablet": &memberInfo{
+			db2sg: map[string]sgMemberInfo{
+				"mockapp:mockdb": sgMemberInfo{
+					sgId1: sg1.Joiners["tablet"],
+					sgId2: sg2.Joiners["tablet"],
+				},
+			},
+		},
+		"cloud": &memberInfo{
+			db2sg: map[string]sgMemberInfo{
+				"mockapp:mockdb": sgMemberInfo{
+					sgId1: sg1.Joiners["cloud"],
+				},
+			},
+		},
+		"door": &memberInfo{
+			db2sg: map[string]sgMemberInfo{
+				"mockapp:mockdb": sgMemberInfo{
+					sgId2: sg2.Joiners["door"],
+				},
+			},
+		},
+		"lamp": &memberInfo{
+			db2sg: map[string]sgMemberInfo{
+				"mockapp:mockdb": sgMemberInfo{
+					sgId2: sg2.Joiners["lamp"],
+				},
+			},
+		},
+	}
+
+	view := svc.sync.allMembers
+	for mm := range members {
+		mi := view.members[mm]
+		if mi == nil {
+			t.Errorf("cannot get info for SyncGroup member %s", mm)
+		}
+		expInfo := expMemberInfo[mm]
+		if !reflect.DeepEqual(mi, expInfo) {
+			t.Errorf("invalid Info for SyncGroup member %s: got %v instead of %v", mm, mi, expInfo)
+		}
+	}
+
+	// Delete the 1st SyncGroup.
+
+	tx = st.NewTransaction()
+	if err := delSyncGroupById(nil, tx, sgId1); err != nil {
+		t.Errorf("deleting SyncGroup ID %d failed: %v", sgId1, err)
+	}
+	if err := tx.Commit(); err != nil {
+		t.Errorf("cannot commit deleting SyncGroup ID %d: %v", sgId1, err)
+	}
+
+	checkSGStats(t, svc, "multi-3", 1, 3)
+
+	// Verify SyncGroup membership data.
+
+	expMembers = map[string]uint32{"tablet": 1, "door": 1, "lamp": 1}
+
+	members = svc.sync.getMembers(nil)
+	if !reflect.DeepEqual(members, expMembers) {
+		t.Errorf("invalid SyncGroup members: got %v instead of %v", members, expMembers)
+	}
+
+	expMemberInfo = map[string]*memberInfo{
+		"tablet": &memberInfo{
+			db2sg: map[string]sgMemberInfo{
+				"mockapp:mockdb": sgMemberInfo{
+					sgId2: sg2.Joiners["tablet"],
+				},
+			},
+		},
+		"door": &memberInfo{
+			db2sg: map[string]sgMemberInfo{
+				"mockapp:mockdb": sgMemberInfo{
+					sgId2: sg2.Joiners["door"],
+				},
+			},
+		},
+		"lamp": &memberInfo{
+			db2sg: map[string]sgMemberInfo{
+				"mockapp:mockdb": sgMemberInfo{
+					sgId2: sg2.Joiners["lamp"],
+				},
+			},
+		},
+	}
+
+	view = svc.sync.allMembers
+	for mm := range members {
+		mi := view.members[mm]
+		if mi == nil {
+			t.Errorf("cannot get info for SyncGroup member %s", mm)
+		}
+		expInfo := expMemberInfo[mm]
+		if !reflect.DeepEqual(mi, expInfo) {
+			t.Errorf("invalid Info for SyncGroup member %s: got %v instead of %v", mm, mi, expInfo)
+		}
+	}
+}
diff --git a/services/syncbase/vsync/test_util.go b/services/syncbase/vsync/test_util.go
new file mode 100644
index 0000000..8328fda
--- /dev/null
+++ b/services/syncbase/vsync/test_util.go
@@ -0,0 +1,154 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Utilities for testing sync.
+
+import (
+	"fmt"
+	"os"
+	"path"
+	"testing"
+	"time"
+
+	wire "v.io/syncbase/v23/services/syncbase/nosql"
+	"v.io/syncbase/x/ref/services/syncbase/clock"
+	"v.io/syncbase/x/ref/services/syncbase/server/interfaces"
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/syncbase/x/ref/services/syncbase/server/watchable"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/context"
+	"v.io/v23/rpc"
+	"v.io/v23/security/access"
+	"v.io/v23/verror"
+	"v.io/x/ref/test"
+)
+
+// mockService emulates a Syncbase service that includes store and sync.
+// It is used to access a mock application.
+type mockService struct {
+	engine   string
+	dir      string
+	st       store.Store
+	sync     *syncService
+	shutdown func()
+}
+
+func (s *mockService) St() store.Store {
+	return s.st
+}
+
+func (s *mockService) Sync() interfaces.SyncServerMethods {
+	return s.sync
+}
+
+func (s *mockService) App(ctx *context.T, call rpc.ServerCall, appName string) (interfaces.App, error) {
+	return &mockApp{st: s.st}, nil
+}
+
+func (s *mockService) AppNames(ctx *context.T, call rpc.ServerCall) ([]string, error) {
+	return []string{"mockapp"}, nil
+}
+
+// mockApp emulates a Syncbase App.  It is used to access a mock database.
+type mockApp struct {
+	st store.Store
+}
+
+func (a *mockApp) NoSQLDatabase(ctx *context.T, call rpc.ServerCall, dbName string) (interfaces.Database, error) {
+	return &mockDatabase{st: a.st}, nil
+}
+
+func (a *mockApp) NoSQLDatabaseNames(ctx *context.T, call rpc.ServerCall) ([]string, error) {
+	return []string{"mockdb"}, nil
+}
+
+func (a *mockApp) CreateNoSQLDatabase(ctx *context.T, call rpc.ServerCall, dbName string, perms access.Permissions, metadata *wire.SchemaMetadata) error {
+	return verror.NewErrNotImplemented(ctx)
+}
+
+func (a *mockApp) DeleteNoSQLDatabase(ctx *context.T, call rpc.ServerCall, dbName string) error {
+	return verror.NewErrNotImplemented(ctx)
+}
+
+func (a *mockApp) SetDatabasePerms(ctx *context.T, call rpc.ServerCall, dbName string, perms access.Permissions, version string) error {
+	return verror.NewErrNotImplemented(ctx)
+}
+
+func (a *mockApp) Service() interfaces.Service {
+	return nil
+}
+
+func (a *mockApp) Name() string {
+	return "mockapp"
+}
+
+// mockDatabase emulates a Syncbase Database.  It is used to test sync functionality.
+type mockDatabase struct {
+	st store.Store
+}
+
+func (d *mockDatabase) St() store.Store {
+	return d.st
+}
+
+func (d *mockDatabase) CheckPermsInternal(ctx *context.T, call rpc.ServerCall, st store.StoreReader) error {
+	return verror.NewErrNotImplemented(ctx)
+}
+
+func (d *mockDatabase) SetPermsInternal(ctx *context.T, call rpc.ServerCall, perms access.Permissions, version string) error {
+	return verror.NewErrNotImplemented(ctx)
+}
+
+func (d *mockDatabase) Name() string {
+	return "mockdb"
+}
+
+func (d *mockDatabase) App() interfaces.App {
+	return nil
+}
+
+// createService creates a mock Syncbase service used for testing sync functionality.
+func createService(t *testing.T) *mockService {
+	ctx, shutdown := test.V23Init()
+	engine := "leveldb"
+	opts := util.OpenOptions{CreateIfMissing: true, ErrorIfExists: false}
+	dir := fmt.Sprintf("%s/vsync_test_%d_%d", os.TempDir(), os.Getpid(), time.Now().UnixNano())
+
+	st, err := util.OpenStore(engine, path.Join(dir, engine), opts)
+	if err != nil {
+		t.Fatalf("cannot create store %s (%s): %v", engine, dir, err)
+	}
+	vclock := clock.NewVClock(st)
+	st, err = watchable.Wrap(st, vclock, &watchable.Options{
+		ManagedPrefixes: []string{util.RowPrefix, util.PermsPrefix},
+	})
+
+	s := &mockService{
+		st:       st,
+		engine:   engine,
+		dir:      dir,
+		shutdown: shutdown,
+	}
+	if s.sync, err = New(ctx, nil, s, nil, dir); err != nil {
+		util.DestroyStore(engine, dir)
+		t.Fatalf("cannot create sync service: %v", err)
+	}
+	return s
+}
+
+// destroyService cleans up the mock Syncbase service.
+func destroyService(t *testing.T, s *mockService) {
+	defer s.shutdown()
+	defer s.sync.Close()
+	if err := util.DestroyStore(s.engine, s.dir); err != nil {
+		t.Fatalf("cannot destroy store %s (%s): %v", s.engine, s.dir, err)
+	}
+}
+
+// makeRowKey returns the database row key for a given application key.
+func makeRowKey(key string) string {
+	return util.JoinKeyParts(util.RowPrefix, key)
+}
diff --git a/services/syncbase/vsync/testdata/local-init-00.log.sync b/services/syncbase/vsync/testdata/local-init-00.log.sync
new file mode 100644
index 0000000..7435348
--- /dev/null
+++ b/services/syncbase/vsync/testdata/local-init-00.log.sync
@@ -0,0 +1,6 @@
+# Create an object locally and update it twice (linked-list).
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addl|foo1|1|||$sync:log:10:1|0|1|false
+addl|foo1|2|1||$sync:log:10:2|0|1|false
+addl|foo1|3|2||$sync:log:10:3|0|1|false
diff --git a/services/syncbase/vsync/testdata/local-init-01.sync b/services/syncbase/vsync/testdata/local-init-01.sync
new file mode 100644
index 0000000..86e24de
--- /dev/null
+++ b/services/syncbase/vsync/testdata/local-init-01.sync
@@ -0,0 +1,12 @@
+# Create an object DAG locally with branches and resolved conflicts.
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addl|1234|1|||logrec-00|0|1|false
+addl|1234|2|1||logrec-01|0|1|false
+addl|1234|3|2||logrec-02|0|1|false
+addl|1234|4|2||logrec-03|0|1|false
+addl|1234|5|3|4|logrec-04|0|1|false
+addl|1234|6|5||logrec-05|0|1|false
+addl|1234|7|2||logrec-06|0|1|false
+addl|1234|8|6|7|logrec-07|0|1|false
+addl|1234|9|8||logrec-08|0|1|false
diff --git a/services/syncbase/vsync/testdata/local-init-02.sync b/services/syncbase/vsync/testdata/local-init-02.sync
new file mode 100644
index 0000000..cb60a79
--- /dev/null
+++ b/services/syncbase/vsync/testdata/local-init-02.sync
@@ -0,0 +1,10 @@
+# Create DAGs for 3 objects locally.
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addl|1234|1|||logrec-a-01|0|1|false
+addl|1234|2|1||logrec-a-02|0|1|false
+
+addl|6789|1|||logrec-b-01|0|1|false
+addl|6789|2|1||logrec-b-02|0|1|false
+
+addl|2222|1|||logrec-c-01|0|1|false
diff --git a/services/syncbase/vsync/testdata/local-init-03.sync b/services/syncbase/vsync/testdata/local-init-03.sync
new file mode 100644
index 0000000..202a752
--- /dev/null
+++ b/services/syncbase/vsync/testdata/local-init-03.sync
@@ -0,0 +1,10 @@
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addl|1234|1|||logrec-01|0|1|false
+addl|1234|2|1||logrec-02|0|1|false
+addl|1234|3|1||logrec-03|0|1|false
+addl|1234|4|2||logrec-04|0|1|false
+addl|1234|5|2||logrec-05|0|1|true
+addl|1234|6|4|5|logrec-06|0|1|false
+addl|1234|7|3|5|logrec-07|0|1|false
+addl|1234|8|6|7|logrec-08|0|1|false
diff --git a/services/syncbase/vsync/testdata/local-resolve-00.sync b/services/syncbase/vsync/testdata/local-resolve-00.sync
new file mode 100644
index 0000000..1666cf0
--- /dev/null
+++ b/services/syncbase/vsync/testdata/local-resolve-00.sync
@@ -0,0 +1,4 @@
+# Create an object locally and update it twice (linked-list).
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addl|foo1|7|3|6|logrec-06|0|1|false
diff --git a/services/syncbase/vsync/testdata/remote-conf-00.log.sync b/services/syncbase/vsync/testdata/remote-conf-00.log.sync
new file mode 100644
index 0000000..060cf0c
--- /dev/null
+++ b/services/syncbase/vsync/testdata/remote-conf-00.log.sync
@@ -0,0 +1,8 @@
+# Update an object remotely three times triggering one conflict after
+# it was created locally up to v3 (i.e. assume the remote sync received
+# it from the local sync at v2, then updated separately).
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|foo1|4|2||$sync:log:11:1|0|1|false
+addr|foo1|5|4||$sync:log:11:2|0|1|false
+addr|foo1|6|5||$sync:log:11:3|0|1|false
diff --git a/services/syncbase/vsync/testdata/remote-conf-01.log.sync b/services/syncbase/vsync/testdata/remote-conf-01.log.sync
new file mode 100644
index 0000000..2053157
--- /dev/null
+++ b/services/syncbase/vsync/testdata/remote-conf-01.log.sync
@@ -0,0 +1,10 @@
+# Update an object remotely three times triggering a conflict with
+# 2 graft points: v1 and v4.  This assumes that the remote sync got
+# v1, made its own conflicting v4 that it resolved into v5 (against v2)
+# then made a v6 change.  When the local sync gets back this info it
+# sees 2 graft points: v1-v4 and v2-v5.
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|foo1|4|1||$sync:log:12:1|0|1|false
+addr|foo1|5|2|4|$sync:log:11:1|0|1|false
+addr|foo1|6|5||$sync:log:11:2|0|1|false
diff --git a/services/syncbase/vsync/testdata/remote-conf-03.log.sync b/services/syncbase/vsync/testdata/remote-conf-03.log.sync
new file mode 100644
index 0000000..673405e
--- /dev/null
+++ b/services/syncbase/vsync/testdata/remote-conf-03.log.sync
@@ -0,0 +1,6 @@
+# Create the same object remotely from scratch and update it twice (linked-list).
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|foo1|4|||$sync:log:11:1|0|1|false
+addr|foo1|5|4||$sync:log:11:2|0|1|false
+addr|foo1|6|5||$sync:log:11:3|0|1|false
diff --git a/services/syncbase/vsync/testdata/remote-conf-link.log.sync b/services/syncbase/vsync/testdata/remote-conf-link.log.sync
new file mode 100644
index 0000000..bdf0331
--- /dev/null
+++ b/services/syncbase/vsync/testdata/remote-conf-link.log.sync
@@ -0,0 +1,5 @@
+# Update an object remotely, detect conflict, and bless the local version.
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|foo1|4|1||$sync:log:11:1|0|1|false
+linkr|foo1|4|2||$sync:log:11:2
diff --git a/services/syncbase/vsync/testdata/remote-init-00.log.sync b/services/syncbase/vsync/testdata/remote-init-00.log.sync
new file mode 100644
index 0000000..2546c47
--- /dev/null
+++ b/services/syncbase/vsync/testdata/remote-init-00.log.sync
@@ -0,0 +1,7 @@
+# Create an object remotely and update it twice (linked-list).
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|foo1|1|||$sync:log:11:1|0|1|false
+addr|foo1|2|1||$sync:log:11:2|0|1|false
+addr|foo1|3|2||$sync:log:11:3|0|1|false
+genvec|foo1|10:0,11:3|bar|11:0
\ No newline at end of file
diff --git a/services/syncbase/vsync/testdata/remote-noconf-00.log.sync b/services/syncbase/vsync/testdata/remote-noconf-00.log.sync
new file mode 100644
index 0000000..6adf5dd
--- /dev/null
+++ b/services/syncbase/vsync/testdata/remote-noconf-00.log.sync
@@ -0,0 +1,9 @@
+# Update an object remotely three times without triggering a conflict
+# after it was created locally up to v3 (i.e. assume the remote sync
+# received it from the local sync first, then updated it).
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|foo1|4|3||$sync:log:11:1|0|1|false
+addr|foo1|5|4||$sync:log:11:2|0|1|false
+addr|foo1|6|5||$sync:log:11:3|0|1|false
+genvec|foo1|10:0,11:3|bar|11:0
\ No newline at end of file
diff --git a/services/syncbase/vsync/testdata/remote-noconf-link-00.log.sync b/services/syncbase/vsync/testdata/remote-noconf-link-00.log.sync
new file mode 100644
index 0000000..a06bec5
--- /dev/null
+++ b/services/syncbase/vsync/testdata/remote-noconf-link-00.log.sync
@@ -0,0 +1,5 @@
+# Update an object remotely, detect conflict, and bless the remote version.
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|foo1|4|1||$sync:log:11:1|0|1|false
+linkr|foo1|2|4||$sync:log:11:2
diff --git a/services/syncbase/vsync/testdata/remote-noconf-link-01.log.sync b/services/syncbase/vsync/testdata/remote-noconf-link-01.log.sync
new file mode 100644
index 0000000..1271e23
--- /dev/null
+++ b/services/syncbase/vsync/testdata/remote-noconf-link-01.log.sync
@@ -0,0 +1,5 @@
+# Update an object remotely, detect conflict, and bless the local version.
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|foo1|4|1||$sync:log:11:1|0|1|false
+linkr|foo1|4|3||$sync:log:11:2
diff --git a/services/syncbase/vsync/testdata/remote-noconf-link-02.log.sync b/services/syncbase/vsync/testdata/remote-noconf-link-02.log.sync
new file mode 100644
index 0000000..890d2bc
--- /dev/null
+++ b/services/syncbase/vsync/testdata/remote-noconf-link-02.log.sync
@@ -0,0 +1,6 @@
+# Update an object remotely, detect conflict, and bless the remote version, and continue updating.
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+addr|foo1|4|1||$sync:log:11:1|0|1|false
+linkr|foo1|3|4||$sync:log:11:2
+addr|foo1|5|3||$sync:log:11:3|0|1|false
diff --git a/services/syncbase/vsync/testdata/remote-noconf-link-repeat.log.sync b/services/syncbase/vsync/testdata/remote-noconf-link-repeat.log.sync
new file mode 100644
index 0000000..31e85a9
--- /dev/null
+++ b/services/syncbase/vsync/testdata/remote-noconf-link-repeat.log.sync
@@ -0,0 +1,4 @@
+# Resolve the same conflict on two different devices.
+# The format is: <cmd>|<objid>|<version>|<parent1>|<parent2>|<logrec>|<txid>|<txcount>|<deleted>
+
+linkr|foo1|3|4||$sync:log:12:1
\ No newline at end of file
diff --git a/services/syncbase/vsync/types.vdl b/services/syncbase/vsync/types.vdl
new file mode 100644
index 0000000..f44468e
--- /dev/null
+++ b/services/syncbase/vsync/types.vdl
@@ -0,0 +1,37 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+import (
+	"v.io/syncbase/x/ref/services/syncbase/server/interfaces"
+)
+
+// Key prefixes for sync data structures. All these prefixes are prepended with
+// util.SyncPrefix.
+const (
+	logPrefix  = "log"
+	dbssPrefix = "dbss"
+	dagPrefix  = "dag"
+	sgPrefix   = "sg"
+)
+
+// syncData represents the persistent state of the sync module.
+type syncData struct {
+	Id uint64
+}
+
+// dbSyncState represents the persistent sync state of a Database.
+type dbSyncState struct {
+	Gen        uint64               // local generation number incremented on every local update.
+	CheckptGen uint64               // local generation number advertised to remote peers (used by the responder).
+	GenVec     interfaces.GenVector // generation vector capturing the locally-known generations of remote peers.
+}
+
+// localLogRec represents the persistent local state of a log record. Metadata
+// is synced across peers, while pos is local-only.
+type localLogRec struct {
+	Metadata interfaces.LogRecMetadata
+	Pos      uint64 // position in the Database log.
+}
diff --git a/services/syncbase/vsync/types.vdl.go b/services/syncbase/vsync/types.vdl.go
new file mode 100644
index 0000000..e9b1d7d
--- /dev/null
+++ b/services/syncbase/vsync/types.vdl.go
@@ -0,0 +1,64 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file was auto-generated by the vanadium vdl tool.
+// Source: types.vdl
+
+package vsync
+
+import (
+	// VDL system imports
+	"v.io/v23/vdl"
+
+	// VDL user imports
+	"v.io/syncbase/x/ref/services/syncbase/server/interfaces"
+)
+
+// syncData represents the persistent state of the sync module.
+type syncData struct {
+	Id uint64
+}
+
+func (syncData) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/vsync.syncData"`
+}) {
+}
+
+// dbSyncState represents the persistent sync state of a Database.
+type dbSyncState struct {
+	Gen        uint64               // local generation number incremented on every local update.
+	CheckptGen uint64               // local generation number advertised to remote peers (used by the responder).
+	GenVec     interfaces.GenVector // generation vector capturing the locally-known generations of remote peers.
+}
+
+func (dbSyncState) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/vsync.dbSyncState"`
+}) {
+}
+
+// localLogRec represents the persistent local state of a log record. Metadata
+// is synced across peers, while pos is local-only.
+type localLogRec struct {
+	Metadata interfaces.LogRecMetadata
+	Pos      uint64 // position in the Database log.
+}
+
+func (localLogRec) __VDLReflect(struct {
+	Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/vsync.localLogRec"`
+}) {
+}
+
+func init() {
+	vdl.Register((*syncData)(nil))
+	vdl.Register((*dbSyncState)(nil))
+	vdl.Register((*localLogRec)(nil))
+}
+
+const logPrefix = "log"
+
+const dbssPrefix = "dbss"
+
+const dagPrefix = "dag"
+
+const sgPrefix = "sg"
diff --git a/services/syncbase/vsync/util.go b/services/syncbase/vsync/util.go
new file mode 100644
index 0000000..2f15cf8
--- /dev/null
+++ b/services/syncbase/vsync/util.go
@@ -0,0 +1,98 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Sync utility functions
+
+import (
+	"time"
+
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/context"
+	"v.io/v23/rpc"
+	"v.io/x/lib/vlog"
+)
+
+const (
+	nanoPerSec = int64(1000000000)
+)
+
+// forEachDatabaseStore iterates over all Databases in all Apps within the
+// service and invokes the callback function on each database. The callback
+// returns a "done" flag to make forEachDatabaseStore() stop the iteration
+// earlier; otherwise the function loops across all databases of all apps.
+func (s *syncService) forEachDatabaseStore(ctx *context.T, callback func(string, string, store.Store) bool) {
+	// Get the apps and iterate over them.
+	// TODO(rdaoud): use a "privileged call" parameter instead of nil (here and
+	// elsewhere).
+	appNames, err := s.sv.AppNames(ctx, nil)
+	if err != nil {
+		vlog.Errorf("sync: forEachDatabaseStore: cannot get all app names: %v", err)
+		return
+	}
+
+	for _, a := range appNames {
+		// For each app, get its databases and iterate over them.
+		app, err := s.sv.App(ctx, nil, a)
+		if err != nil {
+			vlog.Errorf("sync: forEachDatabaseStore: cannot get app %s: %v", a, err)
+			continue
+		}
+		dbNames, err := app.NoSQLDatabaseNames(ctx, nil)
+		if err != nil {
+			vlog.Errorf("sync: forEachDatabaseStore: cannot get all db names for app %s: %v", a, err)
+			continue
+		}
+
+		for _, d := range dbNames {
+			// For each database, get its Store and invoke the callback.
+			db, err := app.NoSQLDatabase(ctx, nil, d)
+			if err != nil {
+				vlog.Errorf("sync: forEachDatabaseStore: cannot get db %s for app %s: %v", d, a, err)
+				continue
+			}
+
+			if callback(a, d, db.St()) {
+				return // done, early exit
+			}
+		}
+	}
+}
+
+// getDbStore gets the store handle to the database.
+func (s *syncService) getDbStore(ctx *context.T, call rpc.ServerCall, appName, dbName string) (store.Store, error) {
+	app, err := s.sv.App(ctx, call, appName)
+	if err != nil {
+		return nil, err
+	}
+	db, err := app.NoSQLDatabase(ctx, call, dbName)
+	if err != nil {
+		return nil, err
+	}
+	return db.St(), nil
+}
+
+// unixNanoToTime converts a Unix timestamp in nanoseconds to a Time object.
+func unixNanoToTime(timestamp int64) time.Time {
+	if timestamp < 0 {
+		vlog.Fatalf("sync: unixNanoToTime: invalid timestamp %d", timestamp)
+	}
+	return time.Unix(timestamp/nanoPerSec, timestamp%nanoPerSec)
+}
+
+// extractAppKey extracts the app key from the key sent over the wire between
+// two Syncbases. The on-wire key starts with one of the store's reserved
+// prefixes for managed namespaces (e.g. $row, $perms). This function removes
+// that prefix and returns the application component of the key. This is done
+// typically before comparing keys with the SyncGroup prefixes which are defined
+// by the application.
+func extractAppKey(key string) string {
+	parts := util.SplitKeyParts(key)
+	if len(parts) < 2 {
+		vlog.Fatalf("sync: extractAppKey: invalid entry key %s", key)
+	}
+	return util.JoinKeyParts(parts[1:]...)
+}
diff --git a/services/syncbase/vsync/watcher.go b/services/syncbase/vsync/watcher.go
new file mode 100644
index 0000000..ae69dc6
--- /dev/null
+++ b/services/syncbase/vsync/watcher.go
@@ -0,0 +1,436 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Syncbase Watcher is a goroutine that listens to local Database updates from
+// applications and modifies sync metadata (e.g. DAG and local log records).
+// The coupling between Syncbase storage and sync is loose, via asynchronous
+// listening by the Watcher, to unblock the application operations as soon as
+// possible, and offload the sync metadata update to the Watcher.  When the
+// application mutates objects in a Database, additional entries are written
+// to a log queue, persisted in the same Database.  This queue is read by the
+// sync Watcher to learn of the changes.
+
+import (
+	"strings"
+	"time"
+
+	"v.io/syncbase/x/ref/services/syncbase/server/interfaces"
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/syncbase/x/ref/services/syncbase/server/watchable"
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/context"
+	"v.io/v23/services/watch"
+	"v.io/v23/verror"
+	"v.io/x/lib/vlog"
+)
+
+var (
+	// watchPollInterval is the duration between consecutive watch polling
+	// events across all app databases.  Every watch event loops across all
+	// app databases and fetches from each one at most one batch update
+	// (transaction) to process.
+	// TODO(rdaoud): add a channel between store and watch to get change
+	// notifications instead of using a polling solution.
+	watchPollInterval = 100 * time.Millisecond
+
+	// watchPrefixes is an in-memory cache of SyncGroup prefixes for each
+	// app database.  It is filled at startup from persisted SyncGroup data
+	// and updated at runtime when SyncGroups are joined or left.  It is
+	// not guarded by a mutex because only the watcher goroutine uses it
+	// beyond the startup phase (before any sync goroutines are started).
+	// The map keys are the appdb names (globally unique).
+	watchPrefixes = make(map[string]sgPrefixes)
+)
+
+// sgPrefixes tracks SyncGroup prefixes being synced in a database and their
+// counts.
+type sgPrefixes map[string]uint32
+
+// watchStore processes updates obtained by watching the store.  This is the
+// sync watcher goroutine that learns about store updates asynchronously by
+// reading log records that track object mutation histories in each database.
+// For each batch mutation, the watcher updates the sync DAG and log records.
+// When an application makes a single non-transactional put, it is represented
+// as a batch of one log record. Thus the watcher only deals with batches.
+func (s *syncService) watchStore(ctx *context.T) {
+	defer s.pending.Done()
+
+	ticker := time.NewTicker(watchPollInterval)
+	defer ticker.Stop()
+
+	ctx, cancel := context.WithCancel(ctx)
+	defer cancel()
+
+	for {
+		select {
+		case <-s.closed:
+			vlog.VI(1).Info("sync: watchStore: channel closed, stop watching and exit")
+			return
+
+		case <-ticker.C:
+			s.processStoreUpdates(ctx)
+		}
+	}
+}
+
+// processStoreUpdates fetches updates from all databases and processes them.
+// To maintain fairness among databases, it processes one batch update from
+// each database, in a round-robin manner, until there are no further updates
+// from any database.
+func (s *syncService) processStoreUpdates(ctx *context.T) {
+	for {
+		total, active := 0, 0
+		s.forEachDatabaseStore(ctx, func(appName, dbName string, st store.Store) bool {
+			if s.processDatabase(ctx, appName, dbName, st) {
+				active++
+			}
+			total++
+			return false
+		})
+
+		vlog.VI(2).Infof("sync: processStoreUpdates: %d/%d databases had updates", active, total)
+		if active == 0 {
+			break
+		}
+	}
+}
+
+// processDatabase fetches from the given database at most one new batch update
+// (transaction) and processes it.  The one-batch limit prevents one database
+// from starving others.  A batch is stored as a contiguous set of log records
+// ending with one record having the "continued" flag set to false.  The call
+// returns true if a new batch update was processed.
+func (s *syncService) processDatabase(ctx *context.T, appName, dbName string, st store.Store) bool {
+	s.thLock.Lock()
+	defer s.thLock.Unlock()
+
+	vlog.VI(2).Infof("sync: processDatabase: begin: %s, %s", appName, dbName)
+	defer vlog.VI(2).Infof("sync: processDatabase: end: %s, %s", appName, dbName)
+
+	resMark, err := getResMark(ctx, st)
+	if err != nil {
+		if verror.ErrorID(err) != verror.ErrNoExist.ID {
+			vlog.Errorf("sync: processDatabase: %s, %s: cannot get resMark: %v", appName, dbName, err)
+			return false
+		}
+		resMark = watchable.MakeResumeMarker(0)
+	}
+
+	// Initialize Database sync state if needed.
+	s.initDbSyncStateInMem(ctx, appName, dbName)
+
+	// Get a batch of watch log entries, if any, after this resume marker.
+	logs, nextResmark, err := watchable.ReadBatchFromLog(st, resMark)
+	if err != nil {
+		vlog.Fatalf("sync: processDatabase: %s, %s: cannot get watch log batch: %v", appName, dbName, verror.DebugString(err))
+	}
+	if logs != nil {
+		s.processWatchLogBatch(ctx, appName, dbName, st, logs, nextResmark)
+		return true
+	}
+	return false
+}
+
+// processWatchLogBatch parses the given batch of watch log records, updates the
+// watchable SyncGroup prefixes, uses the prefixes to filter the batch to the
+// subset of syncable records, and transactionally applies these updates to the
+// sync metadata (DAG & log records) and updates the watch resume marker.
+func (s *syncService) processWatchLogBatch(ctx *context.T, appName, dbName string, st store.Store, logs []*watchable.LogEntry, resMark watch.ResumeMarker) {
+	if len(logs) == 0 {
+		return
+	}
+
+	// If the first log entry is a SyncGroup prefix operation, then this is
+	// a SyncGroup snapshot and not an application batch.  In this case,
+	// handle the SyncGroup prefix changes by updating the watch prefixes
+	// and exclude the first entry from the batch.  Also inform the batch
+	// processing below to not assign it a batch ID in the DAG.
+	appBatch := true
+	if processSyncGroupLogRecord(appName, dbName, logs[0]) {
+		appBatch = false
+		logs = logs[1:]
+	}
+
+	// Filter out the log entries for keys not part of any SyncGroup.
+	// Ignore as well log entries made by sync (echo suppression).
+	totalCount := uint64(len(logs))
+	appdb := appDbName(appName, dbName)
+
+	i := 0
+	for _, entry := range logs {
+		if !entry.FromSync && syncable(appdb, entry) {
+			logs[i] = entry
+			i++
+		}
+	}
+	logs = logs[:i]
+	vlog.VI(3).Infof("sync: processWatchLogBatch: %s, %s: sg snap %t, syncable %d, total %d",
+		appName, dbName, !appBatch, len(logs), totalCount)
+
+	// Transactional processing of the batch: convert these syncable log
+	// records to a batch of sync log records, filling their parent versions
+	// from the DAG head nodes.
+	err := store.RunInTransaction(st, func(tx store.Transaction) error {
+		batch := make([]*localLogRec, 0, len(logs))
+		for _, entry := range logs {
+			if rec, err := convertLogRecord(ctx, tx, entry); err != nil {
+				return err
+			} else if rec != nil {
+				batch = append(batch, rec)
+			}
+		}
+
+		if err := s.processBatch(ctx, appName, dbName, batch, appBatch, totalCount, tx); err != nil {
+			return err
+		}
+		return setResMark(ctx, tx, resMark)
+	})
+
+	if err != nil {
+		// TODO(rdaoud): don't crash, quarantine this app database.
+		vlog.Fatalf("sync: processWatchLogBatch:: %s, %s: watcher cannot process batch: %v", appName, dbName, err)
+	}
+}
+
+// processBatch applies a single batch of changes (object mutations) received
+// from watching a particular Database.
+func (s *syncService) processBatch(ctx *context.T, appName, dbName string, batch []*localLogRec, appBatch bool, totalCount uint64, tx store.Transaction) error {
+	count := uint64(len(batch))
+	if count == 0 {
+		return nil
+	}
+
+	// If an application batch has more than one mutation, start a batch for it.
+	batchId := NoBatchId
+	if appBatch && totalCount > 1 {
+		batchId = s.startBatch(ctx, tx, batchId)
+		if batchId == NoBatchId {
+			return verror.New(verror.ErrInternal, ctx, "failed to generate batch ID")
+		}
+	}
+
+	gen, pos := s.reserveGenAndPosInDbLog(ctx, appName, dbName, count)
+
+	vlog.VI(3).Infof("sync: processBatch: %s, %s: len %d, total %d, btid %x, gen %d, pos %d",
+		appName, dbName, count, totalCount, batchId, gen, pos)
+
+	for _, rec := range batch {
+		// Update the log record. Portions of the record Metadata must
+		// already be filled.
+		rec.Metadata.Id = s.id
+		rec.Metadata.Gen = gen
+		rec.Metadata.RecType = interfaces.NodeRec
+
+		rec.Metadata.BatchId = batchId
+		rec.Metadata.BatchCount = totalCount
+
+		rec.Pos = pos
+
+		gen++
+		pos++
+
+		if err := s.processLocalLogRec(ctx, tx, rec); err != nil {
+			return verror.New(verror.ErrInternal, ctx, err)
+		}
+	}
+
+	// End the batch if any.
+	if batchId != NoBatchId {
+		if err := s.endBatch(ctx, tx, batchId, totalCount); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// processLocalLogRec processes a local log record by adding to the Database and
+// suitably updating the DAG metadata.
+func (s *syncService) processLocalLogRec(ctx *context.T, tx store.Transaction, rec *localLogRec) error {
+	// Insert the new log record into the log.
+	if err := putLogRec(ctx, tx, rec); err != nil {
+		return err
+	}
+
+	m := rec.Metadata
+	logKey := logRecKey(m.Id, m.Gen)
+
+	// Insert the new log record into dag.
+	if err := s.addNode(ctx, tx, m.ObjId, m.CurVers, logKey, m.Delete, m.Parents, m.BatchId, nil); err != nil {
+		return err
+	}
+
+	// Move the head.
+	return moveHead(ctx, tx, m.ObjId, m.CurVers)
+}
+
+// incrWatchPrefix increments (or sets) a SyncGroup prefix for an app database
+// in the watch prefix cache.
+func incrWatchPrefix(appName, dbName, prefix string) {
+	name := appDbName(appName, dbName)
+	if pfxs := watchPrefixes[name]; pfxs != nil {
+		pfxs[prefix]++ // it auto-initializes a non-existent prefix
+	} else {
+		watchPrefixes[name] = sgPrefixes{prefix: 1}
+	}
+}
+
+// decrWatchPrefix decrements (or unsets) a SyncGroup prefix for an app database
+// in the watch prefix cache.
+func decrWatchPrefix(appName, dbName, prefix string) {
+	name := appDbName(appName, dbName)
+	if pfxs := watchPrefixes[name]; pfxs != nil {
+		if pfxs[prefix] > 1 {
+			pfxs[prefix]--
+		} else if len(pfxs) > 1 {
+			delete(pfxs, prefix)
+		} else {
+			delete(watchPrefixes, name)
+		}
+	}
+}
+
+// convertLogRecord converts a store log entry to a sync log record.  It fills
+// the previous object version (parent) by fetching its current DAG head if it
+// has one.  For a delete, it generates a new object version because the store
+// does not version a deletion.
+// TODO(rdaoud): change Syncbase to store and version a deleted object to
+// simplify the store-to-sync interaction.  A deleted key would still have a
+// version and its value entry would encode the "deleted" flag, either in the
+// key or probably in a value wrapper that would contain other metadata.
+func convertLogRecord(ctx *context.T, tx store.Transaction, logEnt *watchable.LogEntry) (*localLogRec, error) {
+	var rec *localLogRec
+	timestamp := logEnt.CommitTimestamp
+
+	switch op := logEnt.Op.(type) {
+	case watchable.OpGet:
+		// TODO(rdaoud): save read-set in sync.
+
+	case watchable.OpScan:
+		// TODO(rdaoud): save scan-set in sync.
+
+	case watchable.OpPut:
+		rec = newLocalLogRec(ctx, tx, op.Value.Key, op.Value.Version, false, timestamp)
+
+	case watchable.OpSyncSnapshot:
+		// Create records for object versions not already in the DAG.
+		// Duplicates can appear here in cases of nested SyncGroups or
+		// peer SyncGroups.
+		if ok, err := hasNode(ctx, tx, string(op.Value.Key), string(op.Value.Version)); err != nil {
+			return nil, err
+		} else if !ok {
+			rec = newLocalLogRec(ctx, tx, op.Value.Key, op.Value.Version, false, timestamp)
+		}
+
+	case watchable.OpDelete:
+		rec = newLocalLogRec(ctx, tx, op.Value.Key, watchable.NewVersion(), true, timestamp)
+
+	case watchable.OpSyncGroup:
+		vlog.Errorf("sync: convertLogRecord: watch LogEntry for SyncGroup should not be converted: %v", logEnt)
+		return nil, verror.New(verror.ErrInternal, ctx, "cannot convert a watch log OpSyncGroup entry")
+
+	default:
+		vlog.Errorf("sync: convertLogRecord: invalid watch LogEntry: %v", logEnt)
+		return nil, verror.New(verror.ErrInternal, ctx, "cannot convert unknown watch log entry")
+	}
+
+	return rec, nil
+}
+
+// newLocalLogRec creates a local sync log record given its information: key,
+// version, deletion flag, and timestamp.  It retrieves the current DAG head
+// for the key (if one exists) to use as its parent (previous) version.
+func newLocalLogRec(ctx *context.T, tx store.Transaction, key, version []byte, deleted bool, timestamp int64) *localLogRec {
+	rec := localLogRec{}
+	oid := string(key)
+
+	rec.Metadata.ObjId = oid
+	rec.Metadata.CurVers = string(version)
+	rec.Metadata.Delete = deleted
+	if head, err := getHead(ctx, tx, oid); err == nil {
+		rec.Metadata.Parents = []string{head}
+	} else if deleted || (verror.ErrorID(err) != verror.ErrNoExist.ID) {
+		vlog.Fatalf("sync: newLocalLogRec: cannot getHead to convert log record for %s: %v", oid, err)
+	}
+	rec.Metadata.UpdTime = unixNanoToTime(timestamp)
+	return &rec
+}
+
+// processSyncGroupLogRecord checks if the log entry is a SyncGroup update and,
+// if it is, updates the watch prefixes for the app database and returns true.
+// Otherwise it returns false with no other changes.
+func processSyncGroupLogRecord(appName, dbName string, logEnt *watchable.LogEntry) bool {
+	switch op := logEnt.Op.(type) {
+	case watchable.OpSyncGroup:
+		remove := op.Value.Remove
+		for _, prefix := range op.Value.Prefixes {
+			if remove {
+				decrWatchPrefix(appName, dbName, prefix)
+			} else {
+				incrWatchPrefix(appName, dbName, prefix)
+			}
+		}
+		vlog.VI(3).Infof("sync: processSyncGroupLogRecord: %s, %s: remove %t, prefixes: %q",
+			appName, dbName, remove, op.Value.Prefixes)
+		return true
+
+	default:
+		return false
+	}
+}
+
+// syncable returns true if the given log entry falls within the scope of a
+// SyncGroup prefix for the given app database, and thus should be synced.
+// It is used to pre-filter the batch of log entries before sync processing.
+func syncable(appdb string, logEnt *watchable.LogEntry) bool {
+	var key string
+	switch op := logEnt.Op.(type) {
+	case watchable.OpPut:
+		key = string(op.Value.Key)
+	case watchable.OpDelete:
+		key = string(op.Value.Key)
+	case watchable.OpSyncSnapshot:
+		key = string(op.Value.Key)
+	default:
+		return false
+	}
+
+	// The key starts with one of the store's reserved prefixes for managed
+	// namespaced (e.g. $row or $perm).  Remove that prefix before comparing
+	// it with the SyncGroup prefixes which are defined by the application.
+	parts := util.SplitKeyParts(key)
+	if len(parts) < 2 {
+		vlog.Fatalf("sync: syncable: %s: invalid entry key %s: %v", appdb, key, logEnt)
+	}
+	key = util.JoinKeyParts(parts[1:]...)
+
+	for prefix := range watchPrefixes[appdb] {
+		if strings.HasPrefix(key, prefix) {
+			return true
+		}
+	}
+	return false
+}
+
+// resMarkKey returns the key used to access the watcher resume marker.
+func resMarkKey() string {
+	return util.JoinKeyParts(util.SyncPrefix, "w", "rm")
+}
+
+// setResMark stores the watcher resume marker for a database.
+func setResMark(ctx *context.T, tx store.Transaction, resMark watch.ResumeMarker) error {
+	return util.Put(ctx, tx, resMarkKey(), resMark)
+}
+
+// getResMark retrieves the watcher resume marker for a database.
+func getResMark(ctx *context.T, st store.StoreReader) (watch.ResumeMarker, error) {
+	var resMark watch.ResumeMarker
+	key := resMarkKey()
+	if err := util.Get(ctx, st, key, &resMark); err != nil {
+		return nil, err
+	}
+	return resMark, nil
+}
diff --git a/services/syncbase/vsync/watcher_test.go b/services/syncbase/vsync/watcher_test.go
new file mode 100644
index 0000000..be77b94
--- /dev/null
+++ b/services/syncbase/vsync/watcher_test.go
@@ -0,0 +1,317 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Tests for the sync watcher in Syncbase.
+
+import (
+	"bytes"
+	"reflect"
+	"testing"
+	"time"
+
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+	"v.io/syncbase/x/ref/services/syncbase/server/watchable"
+	"v.io/v23/vom"
+	_ "v.io/x/ref/runtime/factories/generic"
+)
+
+// TestSetResmark tests setting and getting a resume marker.
+func TestSetResmark(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+
+	resmark, err := getResMark(nil, st)
+	if err == nil || resmark != nil {
+		t.Errorf("found non-existent resume marker: %s, %v", resmark, err)
+	}
+
+	wantResmark := watchable.MakeResumeMarker(1234567890)
+	tx := st.NewTransaction()
+	if err := setResMark(nil, tx, wantResmark); err != nil {
+		t.Errorf("cannot set resume marker: %v", err)
+	}
+	tx.Commit()
+
+	resmark, err = getResMark(nil, st)
+	if err != nil {
+		t.Errorf("cannot get new resume marker: %v", err)
+	}
+	if !bytes.Equal(resmark, wantResmark) {
+		t.Errorf("invalid new resume: got %s instead of %s", resmark, wantResmark)
+	}
+}
+
+// TestWatchPrefixes tests setting and updating the watch prefixes map.
+func TestWatchPrefixes(t *testing.T) {
+	watchPollInterval = time.Millisecond
+	svc := createService(t)
+	defer destroyService(t, svc)
+
+	if len(watchPrefixes) != 0 {
+		t.Errorf("watch prefixes not empty: %v", watchPrefixes)
+	}
+
+	watchPrefixOps := []struct {
+		appName, dbName, key string
+		incr                 bool
+	}{
+		{"app1", "db1", "foo", true},
+		{"app1", "db1", "bar", true},
+		{"app2", "db1", "xyz", true},
+		{"app3", "db1", "haha", true},
+		{"app1", "db1", "foo", true},
+		{"app1", "db1", "foo", true},
+		{"app1", "db1", "foo", false},
+		{"app2", "db1", "ttt", true},
+		{"app2", "db1", "ttt", true},
+		{"app2", "db1", "ttt", false},
+		{"app2", "db1", "ttt", false},
+		{"app2", "db2", "qwerty", true},
+		{"app3", "db1", "haha", true},
+		{"app2", "db2", "qwerty", false},
+		{"app3", "db1", "haha", false},
+	}
+
+	for _, op := range watchPrefixOps {
+		if op.incr {
+			incrWatchPrefix(op.appName, op.dbName, op.key)
+		} else {
+			decrWatchPrefix(op.appName, op.dbName, op.key)
+		}
+	}
+
+	expPrefixes := map[string]sgPrefixes{
+		"app1:db1": sgPrefixes{"foo": 2, "bar": 1},
+		"app2:db1": sgPrefixes{"xyz": 1},
+		"app3:db1": sgPrefixes{"haha": 1},
+	}
+	if !reflect.DeepEqual(watchPrefixes, expPrefixes) {
+		t.Errorf("invalid watch prefixes: got %v instead of %v", watchPrefixes, expPrefixes)
+	}
+
+	checkSyncableTests := []struct {
+		appName, dbName, key string
+		result               bool
+	}{
+		{"app1", "db1", "foo", true},
+		{"app1", "db1", "foobar", true},
+		{"app1", "db1", "bar", true},
+		{"app1", "db1", "bar123", true},
+		{"app1", "db1", "f", false},
+		{"app1", "db1", "ba", false},
+		{"app1", "db1", "xyz", false},
+		{"app1", "db555", "foo", false},
+		{"app555", "db1", "foo", false},
+		{"app2", "db1", "xyz123", true},
+		{"app2", "db1", "ttt123", false},
+		{"app2", "db2", "qwerty", false},
+		{"app3", "db1", "hahahoho", true},
+		{"app3", "db1", "hoho", false},
+		{"app3", "db1", "h", false},
+	}
+
+	for _, test := range checkSyncableTests {
+		log := &watchable.LogEntry{
+			Op: watchable.OpPut{
+				Value: watchable.PutOp{Key: []byte(makeRowKey(test.key))},
+			},
+		}
+		res := syncable(appDbName(test.appName, test.dbName), log)
+		if res != test.result {
+			t.Errorf("checkSyncable: invalid output: %s, %s, %s: got %t instead of %t",
+				test.appName, test.dbName, test.key, res, test.result)
+		}
+	}
+}
+
+// newLog creates a Put or Delete watch log entry.
+func newLog(key, version string, delete bool) *watchable.LogEntry {
+	k, v := []byte(key), []byte(version)
+	log := &watchable.LogEntry{}
+	if delete {
+		log.Op = watchable.OpDelete{watchable.DeleteOp{Key: k}}
+	} else {
+		log.Op = watchable.OpPut{watchable.PutOp{Key: k, Version: v}}
+	}
+	return log
+}
+
+// newSGLog creates a SyncGroup watch log entry.
+func newSGLog(prefixes []string, remove bool) *watchable.LogEntry {
+	return &watchable.LogEntry{
+		Op: watchable.OpSyncGroup{
+			Value: watchable.SyncGroupOp{Prefixes: prefixes, Remove: remove},
+		},
+	}
+}
+
+// TestProcessWatchLogBatch tests the processing of a batch of log records.
+func TestProcessWatchLogBatch(t *testing.T) {
+	svc := createService(t)
+	defer destroyService(t, svc)
+	st := svc.St()
+	s := svc.sync
+
+	app, db := "mockapp", "mockdb"
+	fooKey := makeRowKey("foo")
+	barKey := makeRowKey("bar")
+	fooxyzKey := makeRowKey("fooxyz")
+
+	// Empty logs does not fail.
+	s.processWatchLogBatch(nil, app, db, st, nil, nil)
+
+	// Non-syncable logs.
+	batch := []*watchable.LogEntry{
+		newLog(fooKey, "123", false),
+		newLog(barKey, "555", false),
+	}
+
+	resmark := watchable.MakeResumeMarker(1234)
+	s.processWatchLogBatch(nil, app, db, st, batch, resmark)
+
+	if res, err := getResMark(nil, st); err != nil && !bytes.Equal(res, resmark) {
+		t.Errorf("invalid resmark batch processing: got %s instead of %s", res, resmark)
+	}
+	if ok, err := hasNode(nil, st, fooKey, "123"); err != nil || ok {
+		t.Error("hasNode() found DAG entry for non-syncable log on foo")
+	}
+	if ok, err := hasNode(nil, st, barKey, "555"); err != nil || ok {
+		t.Error("hasNode() found DAG entry for non-syncable log on bar")
+	}
+
+	// Partially syncable logs.
+	batch = []*watchable.LogEntry{
+		newSGLog([]string{"f", "x"}, false),
+		newLog(fooKey, "333", false),
+		newLog(fooxyzKey, "444", false),
+		newLog(barKey, "222", false),
+	}
+
+	resmark = watchable.MakeResumeMarker(3456)
+	s.processWatchLogBatch(nil, app, db, st, batch, resmark)
+
+	if res, err := getResMark(nil, st); err != nil && !bytes.Equal(res, resmark) {
+		t.Errorf("invalid resmark batch processing: got %s instead of %s", res, resmark)
+	}
+	if head, err := getHead(nil, st, fooKey); err != nil && head != "333" {
+		t.Errorf("getHead() did not find foo: %s, %v", head, err)
+	}
+	node, err := getNode(nil, st, fooKey, "333")
+	if err != nil {
+		t.Errorf("getNode() did not find foo: %v", err)
+	}
+	if node.Level != 0 || node.Parents != nil || node.Logrec == "" || node.BatchId != NoBatchId {
+		t.Errorf("invalid DAG node for foo: %v", node)
+	}
+	node2, err := getNode(nil, st, fooxyzKey, "444")
+	if err != nil {
+		t.Errorf("getNode() did not find fooxyz: %v", err)
+	}
+	if node2.Level != 0 || node2.Parents != nil || node2.Logrec == "" || node2.BatchId != NoBatchId {
+		t.Errorf("invalid DAG node for fooxyz: %v", node2)
+	}
+	if ok, err := hasNode(nil, st, barKey, "222"); err != nil || ok {
+		t.Error("hasNode() found DAG entry for non-syncable log on bar")
+	}
+
+	// More partially syncable logs updating existing ones.
+	batch = []*watchable.LogEntry{
+		newLog(fooKey, "1", false),
+		newLog(fooxyzKey, "", true),
+		newLog(barKey, "7", false),
+	}
+
+	resmark = watchable.MakeResumeMarker(7890)
+	s.processWatchLogBatch(nil, app, db, st, batch, resmark)
+
+	if res, err := getResMark(nil, st); err != nil && !bytes.Equal(res, resmark) {
+		t.Errorf("invalid resmark batch processing: got %s instead of %s", res, resmark)
+	}
+	if head, err := getHead(nil, st, fooKey); err != nil && head != "1" {
+		t.Errorf("getHead() did not find foo: %s, %v", head, err)
+	}
+	node, err = getNode(nil, st, fooKey, "1")
+	if err != nil {
+		t.Errorf("getNode() did not find foo: %v", err)
+	}
+	expParents := []string{"333"}
+	if node.Level != 1 || !reflect.DeepEqual(node.Parents, expParents) ||
+		node.Logrec == "" || node.BatchId == NoBatchId {
+		t.Errorf("invalid DAG node for foo: %v", node)
+	}
+	head2, err := getHead(nil, st, fooxyzKey)
+	if err != nil {
+		t.Errorf("getHead() did not find fooxyz: %v", err)
+	}
+	node2, err = getNode(nil, st, fooxyzKey, head2)
+	if err != nil {
+		t.Errorf("getNode() did not find fooxyz: %v", err)
+	}
+	expParents = []string{"444"}
+	if node2.Level != 1 || !reflect.DeepEqual(node2.Parents, expParents) ||
+		node2.Logrec == "" || node2.BatchId == NoBatchId {
+		t.Errorf("invalid DAG node for fooxyz: %v", node2)
+	}
+	if ok, err := hasNode(nil, st, barKey, "7"); err != nil || ok {
+		t.Error("hasNode() found DAG entry for non-syncable log on bar")
+	}
+
+	// Back to non-syncable logs (remove "f" prefix).
+	batch = []*watchable.LogEntry{
+		newSGLog([]string{"f"}, true),
+		newLog(fooKey, "99", false),
+		newLog(fooxyzKey, "888", true),
+		newLog(barKey, "007", false),
+	}
+
+	resmark = watchable.MakeResumeMarker(20212223)
+	s.processWatchLogBatch(nil, app, db, st, batch, resmark)
+
+	if res, err := getResMark(nil, st); err != nil && !bytes.Equal(res, resmark) {
+		t.Errorf("invalid resmark batch processing: got %s instead of %s", res, resmark)
+	}
+	// No changes to "foo".
+	if head, err := getHead(nil, st, fooKey); err != nil && head != "333" {
+		t.Errorf("getHead() did not find foo: %s, %v", head, err)
+	}
+	if node, err := getNode(nil, st, fooKey, "99"); err == nil {
+		t.Errorf("getNode() should not have found foo @ 99: %v", node)
+	}
+	if node, err := getNode(nil, st, fooxyzKey, "888"); err == nil {
+		t.Errorf("getNode() should not have found fooxyz @ 888: %v", node)
+	}
+	if ok, err := hasNode(nil, st, barKey, "007"); err != nil || ok {
+		t.Error("hasNode() found DAG entry for non-syncable log on bar")
+	}
+
+	// Scan the batch records and verify that there is only 1 DAG batch
+	// stored, with a total count of 3 and a map of 2 syncable entries.
+	// This is because the 1st batch, while containing syncable keys, is a
+	// SyncGroup snapshot that does not get assigned a batch ID.  The 2nd
+	// batch is an application batch with 3 keys of which 2 are syncable.
+	// The 3rd batch is also a SyncGroup snapshot.
+	count := 0
+	start, limit := util.ScanPrefixArgs(util.JoinKeyParts(util.SyncPrefix, "dag", "b"), "")
+	stream := st.Scan(start, limit)
+	for stream.Advance() {
+		count++
+		key := string(stream.Key(nil))
+		var info batchInfo
+		if err := vom.Decode(stream.Value(nil), &info); err != nil {
+			t.Errorf("cannot decode batch %s: %v", key, err)
+		}
+		if info.Count != 3 {
+			t.Errorf("wrong total count in batch %s: got %d instead of 3", key, info.Count)
+		}
+		if n := len(info.Objects); n != 2 {
+			t.Errorf("wrong object count in batch %s: got %d instead of 2", key, n)
+		}
+	}
+	if count != 1 {
+		t.Errorf("wrong count of batches: got %d instead of 2", count)
+	}
+}