Appearance
How It Works
Synced-Store enables real-time collaboration by keeping data synchronized across multiple clients and a server. Instead of waiting for server responses, your application works with a local copy of the data that stays in sync automatically. This creates instant, responsive user interfaces while maintaining eventual consistency across all connected users.
Design Goals
- Eventual consistency — All clients are guaranteed to converge to the same state
- Fast sync — Changes sync between clients quickly (<1 second)
- Turn-based game friendly — Works well for collaborative apps; FPS-style games are not the target use case
- No loading indicators — Operations feel instant even when offline (data syncs in background)
- Optimistic updates — Each action has an immediate local update, later replaced by the server result
- Offline support — Clients can go offline, make changes, come back online, and sync seamlessly
The Big Picture: Local-First Synchronization
Think of Synced-Store like a Git repository for your application data. Each client has a complete local copy that they can read and write to instantly. Changes are synchronized in the background.
How It Works: The Three-Part System
Synced-Store consists of three main components working together:
- Local State — A local copy of the server state
- Synchronization Engine — Handles push/pull with the server
- Server State — The canonical source of truth
Step 1: Instant Local Updates
On page load, you load a copy of the server state into your local store. When you perform an action that would modify the app's persistent state (like adding a todo item), the action is first applied immediately to your local store. These actions that modify persistent state are called mutations:
typescript
// This runs instantly - no network delay
const { confirmed } = await store.mutate.addTodo({
id: "unique-id-123",
text: "Buy groceries",
completed: false,
});
// UI updates immediately
// Optionally wait for server confirmation
await confirmed;Your UI updates right away because it's reading from the local store, not waiting for the server.
Step 2: Background Synchronization
While your UI is already updated, the system queues your mutation and sends it to the server in the background, batched with other recent mutations. The server runs the same mutation code against its canonical state, persists the result, and broadcasts the new state to all connected clients.
Step 3: Merge Server Updates
After the server runs the mutations, it "pokes" all clients to notify about the changed canonical state. A poke is a message sent over WebSocket that contains:
Poke Message
─────────────────────────────────────────────────
min_included_version: 104
max_included_version: 105
patches: [
{ op: "set", key: "todos/abc", value: {...} },
{ op: "del", key: "todos/xyz" }
]
client_id_to_last_mutation_id: {
"client-A": 3, // Client A's mutation #3 was processed
"client-B": 7 // Client B's mutation #7 was processed
}Each client handles a poke by:
- If the poke's
min_included_versionindicates the client has missing updates, the client performs a "pull" to fetch the missing patches - Applying the patches to local state
- Looking up its own ID in
client_id_to_last_mutation_idand removing all mutations up to that ID from its pending list - Re-running any remaining pending mutations on top of the updated state (rebase)
- UI automatically re-renders
Because clients always replace "local optimistic" updates with server results, the framework ensures that all clients have an eventually consistent view of the data.
Understanding Rebase
When your client receives server updates while you have pending local changes, the system performs a "rebase" to reapply the pending mutations on top of the client's copy of the server data.
Imagine this todo app scenario:
- You add "Buy milk" (pending locally)
- Someone else adds "Buy eggs" (comes from server)
- You add "Buy bread" (pending locally)
Your client integrates the server change while preserving your local changes:
typescript
// Step 1: Roll back to last known server state
// Local state: [] (empty, before any changes)
// Step 2: Apply server update
// Local state: ["Buy eggs"] (server change applied)
// Step 3: Replay your local mutations in order
// Local state: ["Buy eggs", "Buy milk", "Buy bread"]Data Visibility
Within a space, all data is visible to all connected clients by default. A space is an isolated data container identified by the store type ID and an instance ID.
Synced-Store also supports server-enforced access control with three visibility levels:
| Level | Key Prefix | Visible To |
|---|---|---|
| Shared | (none) | All users |
| Private | $$pu/{userId}/ | One user only |
| Server-only | $$so/ | No users (server only) |
All three levels can be updated atomically in the same mutation and are enforced by the server.
Next Steps
- Getting Started — Set up your first Synced-Store app
- Schema — Define your data model with Zod
- Mutators — Write shared mutation handlers
- Actions — Server-only operations