# How to Set Up Real-Time UI Synchronization in Agent-Native Using the useDbSync Hook

> Learn to set up real-time UI synchronization in Agent-Native with the useDbSync hook. Automatically sync your React UI with database changes via SSE or polling.

- Repository: [Builder.io/agent-native](https://github.com/BuilderIO/agent-native)
- Tags: how-to-guide
- Published: 2026-07-02

---

**Agent-Native provides a built-in `useDbSync` hook that automatically wires your React UI to the framework's real-time change-event stream, invalidating React Query caches whenever the SQL database updates via Server-Sent Events (SSE) or polling.**

The `useDbSync` hook in the BuilderIO/agent-native repository eliminates manual refresh logic by subscribing to database change events. When any write occurs—whether from an agent action, user input, or external script—the server emits a **SyncEvent** that triggers automatic UI updates. This guide explains the hook's architecture and implementation based on the source code in [`packages/core/src/client/use-db-sync.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/use-db-sync.ts).

## Architecture of Real-Time Synchronization

### Transport Layer

At the heart of the system is a singleton **`SyncTransport`** (defined in [`packages/core/src/client/use-db-sync.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/use-db-sync.ts) lines [22-30](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/use-db-sync.ts#L22-L30)) that manages a single SSE connection and poll loop per `(pollUrl, sseUrl)` pair. All `useDbSync` instances share this transport, guaranteeing **only one network connection per browser tab** regardless of how many components use the hook.

The transport aggregates subscriber preferences such as `interval`, `fallbackInterval`, and `pauseWhenHidden` (see lines [35-48](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/use-db-sync.ts#L35-L48)). When the transport receives a batch of events via SSE or poll, it calls each subscriber’s `onEvents` callback (lines [23-27](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/use-db-sync.ts#L23-L27)).

### Version Cursor and Event Ordering

Each subscriber maintains its own **version cursor** (lines [28-33](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/use-db-sync.ts#L28-L33)). Because the server writes a monotonically increasing `version` on each database change, every poll request includes a `?since=` parameter (lines [39-42](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/use-db-sync.ts#L39-L42)). The transport updates its internal cursor via `applyVersion` (lines [26-33](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/use-db-sync.ts#L26-L33)) and only forwards events newer than a subscriber’s cursor, guaranteeing **event ordering and exactly-once UI updates**.

### Cache Invalidation Strategy

Inside `useDbSync`, fresh events are mapped to per-source change counters via **`bumpChangeVersion`** (lines [54-58](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/use-db-sync.ts#L54-L58)). React Query queries that embed those counters in their `queryKey` automatically refetch. For backward compatibility, the hook also invalidates a broader set of framework-owned query keys including `"action"`, `"extension"`, and `"app-state"` (lines [59-88](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/use-db-sync.ts#L59-L88)).

### Lifecycle Management

When a component mounts, `useEffect` registers the subscriber with `transport.add(id, …)` (lines [29-36](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/use-db-sync.ts#L29-L36)). On unmount, it removes the subscriber and releases the transport if empty (lines [37-46](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/use-db-sync.ts#L37-L46)).

## Configuration Options

The hook accepts several parameters to control synchronization behavior:

- **`pollUrl`** – Defaults to `/_agent-native/poll`
- **`sseUrl`** – Defaults to `/_agent-native/events` (set to `false` to disable SSE)
- **`interval`** / **`fallbackInterval`** – Control polling cadence in milliseconds
- **`pauseWhenHidden`** – Avoids network traffic when the tab is backgrounded (see [`packages/core/src/client/route-state.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/route-state.ts))
- **`ignoreSource`** – Lets a tab ignore its own writes to prevent echo updates
- **`onEvent`** – Optional callback for custom event handling

## Implementation Examples

### Root-Level Setup

Initialize `useDbSync` once at the top level of your application to establish the global sync connection:

```tsx
// src/app/root.tsx
import { useDbSync } from "@agent-native/core/client";
import { useQueryClient } from "@tanstack/react-query";

export default function RootApp() {
  const queryClient = useQueryClient();

  useDbSync({
    queryClient,
    pollUrl: "/_agent-native/poll",
    sseUrl: "/_agent-native/events",
    interval: 2000,
    fallbackInterval: 15000,
    pauseWhenHidden: true,
    ignoreSource: "my-tab-id",
    onEvent: (e) => console.log("Sync event:", e),
  });

  return (
    <AppLayout>
      <MainContent />
    </AppLayout>
  );
}

```

### Component-Level Integration

Use **`useChangeVersion`** (from [`packages/core/src/client/use-change-version.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/use-change-version.ts)) to attach source-specific versions to your query keys:

```tsx
import { useQuery } from "@tanstack/react-query";
import { useChangeVersion } from "@agent-native/core/client";

function TodoList() {
  const version = useChangeVersion("todos");
  const { data } = useQuery(
    ["todos", version],
    () => fetch("/api/todos").then((r) => r.json()),
    { staleTime: 30_000 }
  );

  return <ul>{data?.map((t) => <li key={t.id}>{t.title}</li>)}</ul>;
}

```

### Poll-Only Mode

To disable SSE and rely solely on polling:

```tsx
useDbSync({ sseUrl: false, interval: 5000 });

```

### Preventing Echo Updates

Generate a unique tab identifier to prevent the UI from re-fetching data that originated from the same tab:

```tsx
const tabId = useMemo(() => crypto.randomUUID(), []);
useDbSync({ ignoreSource: tabId });

// When mutating, include the source identifier
await mutate({ ...payload, requestSource: tabId });

```

## Summary

- **`useDbSync`** establishes a single shared transport connection per browser tab via the `SyncTransport` singleton.
- The hook subscribes to `/_agent-native/events` via SSE, falling back to `/_agent-native/poll` when necessary.
- **Version cursors** ensure exactly-once delivery of change events, preventing stale updates from triggering redundant refetches.
- Cache invalidation occurs automatically via `bumpChangeVersion` and React Query key embedding.
- Configuration options include `pauseWhenHidden`, `ignoreSource`, and custom intervals for bandwidth optimization.

## Frequently Asked Questions

### What transport protocol does useDbSync use?

The hook uses **Server-Sent Events (SSE)** on the `/_agent-native/events` endpoint by default, with automatic fallback to HTTP polling on `/_agent-native/poll` if SSE is unavailable or explicitly disabled. This dual-transport approach ensures real-time updates across diverse network environments.

### How does useDbSync prevent duplicate UI updates?

The implementation relies on **version cursors** maintained per subscriber. As implemented in [`packages/core/src/client/use-db-sync.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/use-db-sync.ts) lines [28-33](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/use-db-sync.ts#L28-L33), each subscriber tracks the last seen event version and ignores any events with versions less than or equal to its cursor. The transport layer also uses `applyVersion` (lines [26-33](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/use-db-sync.ts#L26-L33)) to manage the global `?since=` parameter for poll requests.

### Can I use useDbSync without Server-Sent Events?

Yes. Set `sseUrl: false` in the hook options to disable SSE entirely. The transport will then use HTTP polling exclusively at the interval specified by the `interval` parameter (defaulting to 2000ms). This configuration is useful in environments with strict proxy rules or when working with older browsers that lack SSE support.

### How do I prevent my own mutations from triggering refetches?

Pass a unique `ignoreSource` identifier to `useDbSync`, then include that same identifier as `requestSource` in your mutation payloads. According to the [`route-state.ts`](https://github.com/BuilderIO/agent-native/blob/main/route-state.ts) implementation, events matching the ignored source are filtered out before reaching the cache invalidation logic. Generate the tab ID using `crypto.randomUUID()` or similar, and provide it via React Context or a global store to both the sync hook and your mutation functions.