SyncStore Protocol

A language-agnostic protocol for building offline-first applications

SyncStore is an open protocol specification for client-first, offline-capable datastores. It provides a standardized way to synchronize collected key-value pairs holding JSON data between clients and servers, with optional graph-like relations between records.

Why SyncStore?

Offline-First by Design

Build applications that work seamlessly without connectivity. Changes sync automatically when back online.

Platform Agnostic

Implement in any language or framework. The protocol speaks JSON, MsgPack, or Protobuf over HTTP.

Conflict-Aware

The inbox/outbox pattern with explicit outcomes (accepted/rejected) gives you full control over conflict resolution.

Flexible Data Model

Collections, indexes, and optional graph relations let you model complex data without sacrificing sync simplicity.

How It Works

1

Outbox — Clients queue local changes (set, patch, delete) with a unique reference ID.

2

Sync — Changes are pushed to the server via POST /sync/outbox.

3

Inbox — Clients pull confirmed changes via GET /sync/inbox, learning what was accepted or rejected.

4

Checkpoint — Periodic snapshots allow clients to bootstrap without replaying full history.

Concepts

Collections

A collection is a named container for key-value pairs. Keys are strings and are automatically sorted in lexicographical order. Each key maps to a JSON value. Collections provide the primary way to organize and query data in SyncStore.

Indexes

Indexes allow you to filter and re-order a subset of items in a collection using arbitrary string values as ordering keys. An item can belong to multiple indexes, each with its own ordering key. When querying with an index, only items in that index are returned, sorted lexicographically by their ordering key.

Relations

Relations create graph-like connections between records across collections using a triple-based model: subject → predicate → object. The subject is the item the relation belongs to, the predicate (type) describes the relationship, and the object references another item in any collection.

Tags

Tags provide row-level access control by attaching string labels to key-value entries. Each authenticated client has a set of allowed tags, and can only read or sync entries where at least one tag matches. Tags are assigned during the approval/commit process, enabling fine-grained data partitioning—from simple user isolation to complex multi-tier architectures like CQRS event sourcing across services.

Operations

Data Operations

Operation Description
set Replace the entire value at a key in a collection
patch Apply a JSON Patch (RFC 6902) to an existing value
delete Remove a key from a collection

Endpoints

Sync

Method Endpoint Description
GET /v1/:db/sync/inbox Pull confirmed changes from server
POST /v1/:db/sync/outbox Push local changes to server
[
  // Data operations
  { "ref": "uuid-1", "type": "set", "collection": "tasks", "key": "task_1", "data": { ... }, "tags": ["user:123"], "timestamp": "..." },
  { "ref": "uuid-2", "type": "patch", "collection": "tasks", "key": "task_1", "data": [{ "op": "replace", ... }], "timestamp": "..." },
  { "ref": "uuid-3", "type": "delete", "collection": "tasks", "key": "task_1", "timestamp": "..." },

  // Index operations
  { "ref": "uuid-4", "type": "index_set", "collection": "tasks", "key": "task_1", "index": "by_due", "value": "2024-06-15", "timestamp": "..." },
  { "ref": "uuid-5", "type": "index_delete", "collection": "tasks", "key": "task_1", "index": "by_due", "timestamp": "..." },

  // Relation operations
  { "ref": "uuid-6", "type": "relation_set", "collection": "tasks", "key": "task_1", "relation": "rel_1", "relationType": "assigned_to", "object": { "collection": "users", "key": "user_1" }, "timestamp": "..." },
  { "ref": "uuid-7", "type": "relation_delete", "collection": "tasks", "key": "task_1", "relation": "rel_1", "timestamp": "..." },

  // Tag operations
  { "ref": "uuid-8", "type": "tag_add", "collection": "tasks", "key": "task_1", "tag": "project:abc", "timestamp": "..." },
  { "ref": "uuid-9", "type": "tag_remove", "collection": "tasks", "key": "task_1", "tag": "project:abc", "timestamp": "..." }
]
GET /v1/:db/sync/checkpoint Retrieve a snapshot for bootstrapping

Data

Method Endpoint Description
GET /v1/:db/data List all collections
GET /v1/:db/data/:collection List items in a collection (with optional index query)
GET /v1/:db/data/:collection/:key Read a single item
PUT /v1/:db/data/:collection/:key Create or replace an item
{
  "name": "My Task",
  "completed": false,
  "dueDate": "2024-06-15"
}
PATCH /v1/:db/data/:collection/:key Apply JSON Patch (RFC 6902) to an item
[
  { "op": "replace", "path": "/completed", "value": true },
  { "op": "add", "path": "/completedAt", "value": "2024-06-10T14:30:00Z" }
]
DELETE /v1/:db/data/:collection/:key Remove an item

Indexes

Indexes allow you to assign an arbitrary ordering key to items in a collection. When querying with an index, results are sorted lexicographically by this key, enabling filtering and custom ordering. Only items with an index entry are returned.

Method Endpoint Description
GET /v1/:db/data/:collection/:key/indexes List all index entries for an item
PUT /v1/:db/data/:collection/:key/indexes/:name Set an item's entry in an index
{
  "value": "2024-06-10T14:30:00Z"
}

Query with: GET /v1/:db/data/tasks?index=completed&dir=asc

DELETE /v1/:db/data/:collection/:key/indexes/:name Remove an item from an index

Relations

Method Endpoint Description
GET /v1/:db/data/:collection/:key/relations List all relations for an item
PUT /v1/:db/data/:collection/:key/relations/:id Create or update a relation
{
  "type": "belongs_to",
  "object": {
    "collection": "users",
    "key": "user_123"
  }
}
DELETE /v1/:db/data/:collection/:key/relations/:id Remove a relation

Tags

Method Endpoint Description
GET /v1/:db/data/:collection/:key/tags List all tags for an item
PUT /v1/:db/data/:collection/:key/tags/:tag Add a tag to an item
DELETE /v1/:db/data/:collection/:key/tags/:tag Remove a tag from an item

Batch

Method Endpoint Description
POST /v1/:db/batch Execute multiple data operations atomically
[
  {
    "method": "PUT",
    "path": "/data/tasks/task_1",
    "body": { "name": "Task 1" }
  },
  {
    "method": "PUT",
    "path": "/data/tasks/task_2",
    "body": { "name": "Task 2" }
  },
  {
    "method": "DELETE",
    "path": "/data/tasks/task_3"
  }
]

Supported Formats

SyncStore is transport-format agnostic. Any format that can be encoded to and decoded from JSON without loss is valid. We recommend implementors support JSON and MessagePack at minimum.

Content-Type Status Description
application/json Recommended Standard JSON encoding, universally supported
application/msgpack Recommended Binary format, more compact and faster to parse
application/protobuf+json Optional Protocol Buffers with JSON mapping

Clients specify their preferred format via the Content-Type and Accept headers.

Recommended Limits

These limits are recommendations to keep the protocol lightweight and performant. Implementors may choose to adjust them based on their use case.

Resource Limit
Item keys 128 bytes
Index names 128 bytes
Index ordering keys 128 bytes
Relation IDs 128 bytes
Tags 128 bytes
Item values 1 MB (uncompressed JSON)

Example Architectures

SyncStore's protocol enables a variety of architectural patterns, from simple client-server setups to complex multi-tier systems with CQRS event sourcing. Tags provide row-level access control at each tier.

Direct Client

Simplest setup: clients read and write directly to SyncStore. Ideal for trusted environments or personal apps where all writes are automatically accepted.

%%{init: {'theme': 'base', 'themeVariables': { 'edgeLabelBackground':'#292524' }, 'flowchart': { 'htmlLabels': true }}}%%
flowchart LR
    C1[["🖥️ Client
───
Local DB"]] SS1[["📦 SyncStore
───
Full DB"]] C1 -.->|"Reads / Writes"| SS1 C1 -.->|"Syncs"| SS1 %% Styling style C1 fill:#162456,stroke:#2b7fff,color:#eff6ff style SS1 fill:#032e15,stroke:#00c951,color:#f0fdf4 linkStyle default stroke:#a6a09b
* Writes are automatically accepted by SyncStore

Proxy Validation

A stateless proxy validates, modifies, or tags writes before forwarding to SyncStore. The proxy can enforce business rules, assign user-specific tags, and reject invalid operations. Ideal for edge deployment.

%%{init: {'theme': 'base', 'themeVariables': { 'edgeLabelBackground':'#292524' }, 'flowchart': { 'htmlLabels': true }}}%%
flowchart LR
    C2[["🖥️ Client
───
Local DB"]] P2[["🔀 Proxy
───
Local Cache"]] SS2[["📦 SyncStore
───
Full DB"]] C2 -.->|"Reads / Writes"| P2 P2 -.->|"Reads / Writes"| SS2 P2 -.->|"Syncs"| C2 %% Styling style C2 fill:#162456,stroke:#2b7fff,color:#eff6ff style SS2 fill:#032e15,stroke:#00c951,color:#f0fdf4 style P2 fill:#1e1a4d,stroke:#615fff,color:#eef2ff linkStyle default stroke:#a6a09b
* Writes are proxied through a service that determines whether the write is allowed and correct. Writes can be rejected, modified or tagged at this stage to allow for complex and multi-tenanted systems

** The Proxy is stateless, and therefore can be run entirely on the edge

Microservices with CQRS

Full event-sourcing architecture: a gateway routes writes to domain-specific microservices, each maintaining its own local view of data. SyncStore acts as the central event log, with each tier syncing only the data it's tagged to access.

%%{init: {'theme': 'base', 'themeVariables': { 'edgeLabelBackground':'#292524' }, 'flowchart': { 'htmlLabels': true }}}%%
flowchart LR
    C3[["🖥️ Client
───
Local DB"]] G3[["🚪 Gateway
───
Local Cache"]] subgraph Services[" "] M3a[["⚙️ Microservice
───
Local DB"]] M3b[["⚙️ Microservice
───
Local DB"]] M3a ~~~ M3b end P3[["🔀 Proxy
───
Local Cache"]] SS3[["📦 SyncStore
───
Full DB"]] C3 -.->|"Reads / Writes"| G3 G3 -.->|"Reads / Writes"| M3a G3 -.->|"Reads / Writes"| M3b M3a -.->|"Reads / Writes"| P3 M3b -.->|"Reads / Writes"| P3 P3 -.->|"Reads / Writes"| SS3 SS3 -.->|"Syncs"| P3 P3 -.->|"Syncs"| M3a P3 -.->|"Syncs"| M3b %% Styling style C3 fill:#162456,stroke:#2b7fff,color:#eff6ff style SS3 fill:#032e15,stroke:#00c951,color:#f0fdf4 style P3 fill:#1e1a4d,stroke:#615fff,color:#eef2ff style G3 fill:#1e1a4d,stroke:#615fff,color:#eef2ff style M3a fill:#461901,stroke:#fd9a00,color:#fffbeb style M3b fill:#461901,stroke:#fd9a00,color:#fffbeb style Services fill:transparent,stroke:transparent linkStyle default stroke:#a6a09b
* Writes are routed through a Gateway that determines which service receives which type of write, which then each in turn can reject, modify or tag writes before they hit the Proxy, which does the same and sends them to SyncStore. Each microservice can have a full copy of its own DB (tags it can view) synced to SyncStore, just like any other client

** The Gateway and Proxy are stateless, and therefore can be run entirely on the edge