CtrlK
BlogDocsLog inGet started
Tessl Logo

real-time-sync

Decide whether a page needs external changes before refresh, then use the opt-in shared SSE and polling transport safely.

57

Quality

72%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Passed

No findings from the security scan

Fix and improve this skill with Tessl

tessl review fix ./.agents/skills/real-time-sync/SKILL.md
SKILL.md
Quality
Evals
Security

Real-Time Sync

Decision rule

Opt in only when both are true:

  1. The data can change without the current user acting.
  2. The user needs to see that change before refreshing or navigating.

A local agent edit does not meet this rule. The chat run stream already invalidates action queries and source counters after each side-effecting tool and again at run end, without opening a background poll.

Opt in

  • Slides deck editor: another collaborator can edit the open deck.
  • Mail inbox: new mail can arrive while the inbox is open.
  • A watched job: a background job changes progress while the user watches it.
  • Cross-tab state: state must converge across this user's open tabs without a new action.

Keep it off

  • Public, anonymous, marketing, docs, and SSR pages.
  • Settings, forms, and read-mostly lists or dashboards. Refresh them on focus or navigation instead.
  • Single-user agent edits. The chat run stream handles those.
  • Pages where an update is useful eventually but not before the next refresh.

Cost and behavior

useDbSync() has no background transport unless the page opts in with a non-empty reason. One transport is shared per tab. A visible opted-in tab polls at 1 minute, then 2, then 5 minutes while idle; user activity or a local mutation resets that sequence. Hidden tabs pause by default. During an active agent run, opted-in sync can poll at its configured interval; local tool completion and run-end invalidation do not depend on that poll.

On a long-lived host, same-process changes stream over /_agent-native/events; polling is the cross-process fallback. On a production serverless host, the local events endpoint refuses the long-lived stream and /_agent-native/poll carries remote changes. The paid Hosted Realtime Sync Gateway is not required for this pattern.

While a collab doc shows another person present, the transport polls every 2.5 s instead (acquireCollabPollBoost(), held by the collab client, not by pages). It is a no-op whenever a stream is connected, lapses after 3 minutes without input or remote events, and never applies to lone tabs.

Use the hook

Declare the reason beside the route gate so reviewers can see why the page pays for remote sync:

useDbSync({
  queryClient,
  realtime: isPrivateDeckEditorPath(location.pathname)
    ? { reason: "other collaborators can edit this deck while it is open" }
    : undefined,
  pauseWhenHidden: true,
});

The reason is required by the TypeScript API. guard:realtime-opt-in also requires a named private or authenticated pathname predicate, and rejects public/docs/SSR files and known anonymous routes. A reviewed exception must put this pragma on the opt-in or the line immediately above it:

// guard:allow-realtime-opt-in — short reason

An opted-in page only hears about an action when its change event reaches the current user. By default an action event reaches the actor alone, so a collaborator's comment, save, or agent edit never arrives. Declare changeResource: (input, result) => ({ resourceType, resourceId }) on the mutating action and the event also reaches everyone who can read that resource. Name it from input when the call carries the resource id (Content's documentChangeResource, Slides' comment actions) and from result when the call is keyed by a child id (Design's designChangeResource for delete-file); return null for a call that changed nothing. Do not publish a parallel per-template event for the same purpose.

Do not start subscribeSyncEvents() or an EventSource in a feature to bypass the decision. subscribeSyncEvents() is a lower-level transport subscription used by the existing Yjs collaboration client and narrow framework plumbing. Keep Yjs collaborative editing on its existing channel.

Query freshness

Prefer useActionQuery() for action-backed data. Mutating actions refresh local action observers; the chat run stream also invalidates them for agent tool side effects. Raw queries should include the relevant source counters:

const versions = useChangeVersions(["dashboards", "action"]);
useQuery({
  queryKey: ["dashboard", id, versions],
  queryFn: () => fetchDashboard(id),
  placeholderData: (previous) => previous,
});

That covers local chat-run edits. Remote edits reach the counter through useDbSync() only on an opted-in page. Use useReconciledState when a form or inline editor copies a query value into local state so incoming data does not replace active typing.

URL commands (__set_url__, set-url, and set-search-params) and refresh-screen also flow through local chat events. The sidebar listens for screen refresh only while its panel is open or a chat run is active; public docs can disable that boundary with screenRefreshEnabled={false}.

Source counters

On local tool completion, useDbSync() advances the action counter and any other raw-query source counters currently observed by the page. Generic tool completion events do not identify their data domain, so keep raw-query source lists narrow. Remote sync events advance their specific source counters.

SourceChanged by
actionA successful mutating action or local chat-run side-effect completion
app-stateWrites to application_state, including URL commands
settingsWrites to settings
dashboards, analyses, extensionsDomain-specific mutations that emit those sources
collabYjs collaborative document updates
screen-refreshThe explicit refresh-screen agent tool

Use useChangeVersions() when one query depends on more than one source.

Avoid

  • Do not create manual polling loops or a second EventSource.
  • Do not enable background sync for a whole app root when only one private route needs remote updates.
  • Do not assume a successful local action is a reason for a background subscriber; use local mutation invalidation and the chat run stream.
  • Do not blanket-invalidate template queries when a source-versioned query or action-backed query can target the refreshed data.
Repository
BuilderIO/agent-native
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.