---
title: "Show new hosted data in the browser"
description: "Use a short-lived ticket, apply events before ACK, and resync through HTTP."
canonical_url: "https://gscdump.com/gscdump-sdk/guides/build-integrations/realtime"
last_updated: "2026-10-03T07:15:32.046Z"
---

# Show new hosted data in the browser

Realtime tells your app that hosted data changed. Read the current data through HTTP after a change.

::pre{tabindex="0" svg="<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 426.561 430" width="426.561" height="430" style="--bg:var(--ui-bg-muted);--fg:var(--ui-text-highlighted);--accent:var(--ui-link)">
<style>
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;display=swap');
  text { font-family: 'Inter', system-ui, sans-serif; }
  svg {
    /* Derived from --bg and --fg (overridable via --line, --accent, etc.) */
    --_text:          var(--fg);
    --_text-sec:      var(--muted, color-mix(in srgb, var(--fg) 60%, var(--bg)));
    --_text-muted:    var(--muted, color-mix(in srgb, var(--fg) 40%, var(--bg)));
    --_text-faint:    color-mix(in srgb, var(--fg) 25%, var(--bg));
    --_line:          var(--line, color-mix(in srgb, var(--fg) 50%, var(--bg)));
    --_arrow:         var(--accent, color-mix(in srgb, var(--fg) 85%, var(--bg)));
    --_node-fill:     var(--surface, color-mix(in srgb, var(--fg) 3%, var(--bg)));
    --_node-stroke:   var(--border, color-mix(in srgb, var(--fg) 20%, var(--bg)));
    --_group-fill:    var(--bg);
    --_group-hdr:     color-mix(in srgb, var(--fg) 5%, var(--bg));
    --_inner-stroke:  color-mix(in srgb, var(--fg) 12%, var(--bg));
    --_key-badge:     color-mix(in srgb, var(--fg) 10%, var(--bg));
  }
</style>
<defs>
  <marker id="seq-arrow" markerWidth="8" markerHeight="5" refX="8" refY="2.5" orient="auto-start-reverse">
    <polygon points="0 0, 8 2.5, 0 5" fill="var(--_arrow)" />
  </marker>
  <marker id="seq-arrow-open" markerWidth="8" markerHeight="5" refX="8" refY="2.5" orient="auto-start-reverse">
    <polyline points="0 0, 8 2.5, 0 5" fill="none" stroke="var(--_arrow)" stroke-width="1" />
  </marker>
</defs>
<line class="lifeline" data-actor="UI" x1="72.91" y1="70" x2="72.91" y2="400" stroke="var(--_line)" stroke-width="0.75" stroke-dasharray="6 4" />
<line class="lifeline" data-actor="Server" x1="212.91" y1="70" x2="212.91" y2="400" stroke="var(--_line)" stroke-width="0.75" stroke-dasharray="6 4" />
<line class="lifeline" data-actor="API" x1="352.90999999999997" y1="70" x2="352.90999999999997" y2="400" stroke="var(--_line)" stroke-width="0.75" stroke-dasharray="6 4" />
<g class="message" data-from="UI" data-to="Server" data-label="Request ticket" data-line-style="solid" data-arrow-head="filled" data-self="false">
  <line x1="72.91" y1="90" x2="212.91" y2="90" stroke="var(--_line)" stroke-width="1" marker-end="url(#seq-arrow)" />
  <text x="142.91" y="80" font-size="11" text-anchor="middle" font-weight="400" fill="var(--_text-muted)" dy="3.8499999999999996">Request ticket</text>
</g>
<g class="message" data-from="Server" data-to="API" data-label="createRealtimeTicket" data-line-style="solid" data-arrow-head="filled" data-self="false">
  <line x1="212.91" y1="130" x2="352.90999999999997" y2="130" stroke="var(--_line)" stroke-width="1" marker-end="url(#seq-arrow)" />
  <text x="282.90999999999997" y="120" font-size="11" text-anchor="middle" font-weight="400" fill="var(--_text-muted)" dy="3.8499999999999996">createRealtimeTicket</text>
</g>
<g class="message" data-from="API" data-to="UI" data-label="Single-use ticket" data-line-style="dashed" data-arrow-head="filled" data-self="false">
  <line x1="352.90999999999997" y1="170" x2="72.91" y2="170" stroke="var(--_line)" stroke-width="1" stroke-dasharray="6 4" marker-end="url(#seq-arrow)" />
  <text x="212.90999999999997" y="160" font-size="11" text-anchor="middle" font-weight="400" fill="var(--_text-muted)" dy="3.8499999999999996">Single-use ticket</text>
</g>
<g class="message" data-from="API" data-to="UI" data-label="Durable event" data-line-style="dashed" data-arrow-head="filled" data-self="false">
  <line x1="352.90999999999997" y1="210" x2="72.91" y2="210" stroke="var(--_line)" stroke-width="1" stroke-dasharray="6 4" marker-end="url(#seq-arrow)" />
  <text x="212.90999999999997" y="200" font-size="11" text-anchor="middle" font-weight="400" fill="var(--_text-muted)" dy="3.8499999999999996">Durable event</text>
</g>
<g class="message" data-from="UI" data-to="UI" data-label="Apply cache change" data-line-style="solid" data-arrow-head="filled" data-self="true">
  <polyline points="72.91,250 102.91,250 102.91,270 72.91,270" fill="none" stroke="var(--_line)" stroke-width="1" marker-end="url(#seq-arrow)" />
  <text x="110.91" y="260" font-size="11" text-anchor="start" font-weight="400" fill="var(--_text-muted)" dy="3.8499999999999996">Apply cache change</text>
</g>
<g class="message" data-from="UI" data-to="API" data-label="ACK and saved cursor" data-line-style="solid" data-arrow-head="filled" data-self="false">
  <line x1="72.91" y1="320" x2="352.90999999999997" y2="320" stroke="var(--_line)" stroke-width="1" marker-end="url(#seq-arrow)" />
  <text x="212.90999999999997" y="310" font-size="11" text-anchor="middle" font-weight="400" fill="var(--_text-muted)" dy="3.8499999999999996">ACK and saved cursor</text>
</g>
<g class="message" data-from="UI" data-to="Server" data-label="HTTP resync after a gap" data-line-style="solid" data-arrow-head="filled" data-self="false">
  <line x1="72.91" y1="360" x2="212.91" y2="360" stroke="var(--_line)" stroke-width="1" marker-end="url(#seq-arrow)" />
  <text x="142.91" y="350" font-size="11" text-anchor="middle" font-weight="400" fill="var(--_text-muted)" dy="3.8499999999999996">HTTP resync after a gap</text>
</g>
<g class="actor" data-id="UI" data-label="Browser" data-type="participant">
  <rect x="30" y="30" width="85.82" height="40" rx="4" ry="4" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="1" />
  <text x="72.91" y="50" font-size="13" text-anchor="middle" font-weight="500" fill="var(--_text)" dy="4.55">Browser</text>
</g>
<g class="actor" data-id="Server" data-label="Your server" data-type="participant">
  <rect x="159.25549999999998" y="30" width="107.30900000000001" height="40" rx="4" ry="4" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="1" />
  <text x="212.91" y="50" font-size="13" text-anchor="middle" font-weight="500" fill="var(--_text)" dy="4.55">Your server</text>
</g>
<g class="actor" data-id="API" data-label="gscdump" data-type="participant">
  <rect x="309.25899999999996" y="30" width="87.30199999999999" height="40" rx="4" ry="4" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="1" />
  <text x="352.90999999999997" y="50" font-size="13" text-anchor="middle" font-weight="500" fill="var(--_text)" dy="4.55">gscdump</text>
</g>
</svg>"}
```mermaid
sequenceDiagram
  participant UI as Browser
  participant Server as Your server
  participant API as gscdump
  UI->>Server: Request ticket
  Server->>API: createRealtimeTicket
  API-->>UI: Single-use ticket
  API-->>UI: Durable event
  UI->>UI: Apply cache change
  UI->>API: ACK and saved cursor
  UI->>Server: HTTP resync after a gap
```
::

## Before you start

Use the [hosted first-result guide](/gscdump-sdk/guides/start/hosted-first-result) to set up a server client. Keep its API key on the server. Register the browser's exact Origin for ticket creation. A ticket expires and can be used once.

## Issue a browser ticket on the server

Your same-origin endpoint calls `createRealtimeTicket({ body: { origin } })` with the browser's registered Origin.

The ticket opens the API key's principal stream. That stream can include several Sites. Give the ticket only to a signed-in user allowed to receive all of them. A check for one Site is insufficient.

Return the ticket response. Keep the API key on the server.

## Connect the browser

::pre{tabindex="0"}
```ts
import { createGscdumpRealtimeV1Client } from '@gscdump/sdk/v1'

const realtime = createGscdumpRealtimeV1Client({
  ticketProvider: async () => {
    const response = await fetch('/api/gscdump/realtime/v1/ticket', {
      method: 'POST',
    })
    if (!response.ok)
      throw new Error('Ticket request failed')
    return response.json()
  },
  applyEvent: async (event) => {
    await invalidateAffectedData(event.changes)
  },
  resync: async () => {
    await reloadCurrentData()
  },
})

await realtime.start()
```
::

`invalidateAffectedData` and `reloadCurrentData` are application functions. The first must finish before the SDK advances its cursor or sends an ACK. The second must fetch authoritative state over HTTP when replay cannot safely catch up.

## Save the cursor

Pass a durable `cursorStore` if the app must resume across page loads. Its `load` and `save` methods must persist the cursor after the change is applied. Without one, the SDK manages only the current session.

## Resync after a gap

If replay is unsafe, clear affected cached state and query [hosted analytics](/gscdump-sdk/guides/read-data/hosted-analytics) again. A notification is not the new analytics payload. The SDK owns reconnect, replay, ACK, and ticket rotation; your callbacks own cache correctness.

## Check reconnect behavior

Drop the connection in development. Confirm that the ticket endpoint issues a new ticket, the app either replays or resyncs, and the UI returns to a current HTTP result. See the [realtime reference](/gscdump-sdk/api/realtime) and [Errors and retry](/gscdump-sdk/guides/operate/errors-and-retry).

## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
