⚠️ RebaseJS is under active development and not yet production-ready. APIs may change before v1.0. Feel free to explore, contribute, or star the repo.
Skip to content

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

text
┌─────────────────────────┐
│     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.

tsx
// 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

OptionTypeDefaultDescription
schemaTypedSchemaRequiredThe fully typed schema aggregate created via defineSchema()
webhookUrlstringRequiredThe absolute destination URL for batched POST payloads
headersobject{}Custom HTTP headers (e.g., Authorization) attached to outgoing requests
batchSizenumber50Maximum number of outbox events processed per network request
syncIntervalMsnumber5000Background 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.

tsx
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.

tsx
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:

json
{
  "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.

Released under the MIT License.