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.
Build applications that work seamlessly without connectivity. Changes sync automatically when back online.
Implement in any language or framework. The protocol speaks JSON, MsgPack, or Protobuf over HTTP.
The inbox/outbox pattern with explicit outcomes (accepted/rejected) gives you full control over conflict resolution.
Collections, indexes, and optional graph relations let you model complex data without sacrificing sync simplicity.
Outbox — Clients queue local changes (set, patch, delete) with a unique reference ID.
Sync —
Changes are pushed to the server via
POST /sync/outbox.
Inbox —
Clients pull confirmed changes via
GET /sync/inbox, learning what was accepted or rejected.
Checkpoint — Periodic snapshots allow clients to bootstrap without replaying full history.
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 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 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 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.
| 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 |
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/:db/sync/inbox | Pull confirmed changes from server |
| POST | /v1/:db/sync/outbox | Push local changes to server |
|
||
| GET | /v1/:db/sync/checkpoint | Retrieve a snapshot for bootstrapping |
| 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 |
|
||
| PATCH | /v1/:db/data/:collection/:key | Apply JSON Patch (RFC 6902) to an item |
|
||
| DELETE | /v1/:db/data/:collection/:key | Remove an item |
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 |
Query with:
|
||
| DELETE | /v1/:db/data/:collection/:key/indexes/:name | Remove an item from an index |
| 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 |
|
||
| DELETE | /v1/:db/data/:collection/:key/relations/:id | Remove a relation |
| 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 |
| Method | Endpoint | Description |
|---|---|---|
| POST | /v1/:db/batch | Execute multiple data operations atomically |
|
||
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.
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) |
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.
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
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
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