Offline & Local-First Sync
RebaseJS natively supports a lightweight, scalable local-first synchronization strategy through the Webhook Sync Adapter (@rebase-js/sync).
Instead of bundling heavy client-side database engines, RebaseJS leverages the browser's native IndexedDB (idb) to achieve 0ms UI responsiveness. All local mutations are queued in an internal _outbox and coalesced via background operation compaction before being batched and delivered as a single JSON payload to your secure webhook endpoint.
Architecture: The Outbox Pattern
┌─────────────────────────┐
│ React UI (0ms) │
└────────────┬────────────┘
│ 1. Local Mutation (insert/update/delete)
▼
┌─────────────────────────┐
│ IndexedDB (Local DB) │
├─────────────────────────┤
│ • collection stores │ <-- UI re-renders instantly from local read replica
│ • _outbox store │ <-- Mutation queued with timestamp
└────────────┬────────────┘
│ 2. Background Loop (syncIntervalMs)
▼
┌─────────────────────────┐
│ Operation Compaction │ <-- Coalesces redundant edits (e.g. insert + update -> insert)
└────────────┬────────────┘
│ 3. POST /webhook (Batched JSON payload)
▼
┌─────────────────────────┐
│ Server Authority │ <-- Acknowledges payload (HTTP 200 OK) -> local _outbox cleared
└─────────────────────────┘1. Zero-Latency Updates
When you trigger a mutation (e.g., calling insert or update), the record is written to local storage and the UI updates instantaneously. Concurrently, a sync task is pushed to the hidden _outbox store inside IndexedDB.
2. Background Operation Compaction
To minimize network overhead and ensure your server never receives redundant intermediate states, RebaseJS compacts pending operations before sending them:
- An insert followed by an update coalesces into a single insert containing the final merged data.
- Multiple updates to the same record collapse into a single update payload.
- An insert followed by a delete cancels out entirely, never hitting the network.
3. Server Delivery & Resilience
A background loop sweeps the outbox at your configured syncIntervalMs and pushes the payload to your webhook. If the network is offline or your backend returns a 4xx/5xx error, RebaseJS retains the operations locally and automatically retries using exponential backoff. Once the server returns an HTTP 200 OK, the processed events are deleted from the local outbox.
Setup: <WebhookSyncProvider>
Wrap your data layer in the provider to initialize the background engine. You can pass standard authentication headers to secure your endpoint.
// app/providers.tsx
import { WebhookSyncProvider } from "@rebase-js/sync/adapters/webhook";
import { schema } from "./models";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WebhookSyncProvider
schema={schema}
webhookUrl="https://api.my-app.com/webhooks/rebase-sync"
headers={{
Authorization: `Bearer ${process.env.REBASE_PUBLIC_SYNC_TOKEN ?? ""}`,
}}
batchSize={50}
syncIntervalMs={5000}
>
{children}
</WebhookSyncProvider>
);
}Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
schema | TypedSchema | Required | The fully typed schema aggregate created via defineSchema() |
webhookUrl | string | Required | The absolute destination URL for batched POST payloads |
headers | object | {} | Custom HTTP headers (e.g., Authorization) attached to outgoing requests |
batchSize | number | 50 | Maximum number of outbox events processed per network request |
syncIntervalMs | number | 5000 | Background polling loop interval in milliseconds |
Querying Local Data
Use the useWebhookQuery hook to stream live, offline-capable rows directly from your local IndexedDB replica. It automatically re-renders when mutations occur.
import { useWebhookQuery } from "@rebase-js/sync/adapters/webhook";
export function TodoList() {
const { rows: todos, loading, error } = useWebhookQuery("todos", {
limit: 20,
// Optional index filtering:
// index: "status",
// equals: "active"
});
if (loading) return <p>Loading offline storage...</p>;
if (error) return <p>Storage error: {error.message}</p>;
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}Writing Data Locally
Use the useWebhookMutation hook to safely dispatch changes. The record IDs are automatically generated via crypto.randomUUID() if omitted.
import { useWebhookMutation } from "@rebase-js/sync/adapters/webhook";
export function AddTodo() {
const { insert } = useWebhookMutation("todos");
const handleAdd = async () => {
// 0ms UI update + automatic outbox queuing
await insert({
title: "Ship RebaseJS App",
done: false,
});
};
return <button onClick={handleAdd}>Add Task</button>;
}Webhook Payload Format
Your backend API route will receive batched requests adhering to the following JSON schema:
{
"batchId": "msg_1778745132_x9a8b7c6d",
"operations": [
{
"action": "insert",
"collection": "todos",
"recordId": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d",
"record": {
"id": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d",
"title": "Ship RebaseJS App",
"done": false
}
},
{
"action": "update",
"collection": "users",
"recordId": "u987654321",
"changes": {
"status": "online"
}
}
]
}Your server should apply these operations in transactional batches and return an HTTP 200 OK to signal success. If any operation fails validation, return a 4xx status to instruct the framework's background worker to hold the queue and try again later.