BricqsBricqs
Documentation

Live Updates (Streaming)

The headless SDK can hold an open connection to Bricqs and receive progression events (points, badges, tiers, streaks) the moment they happen, so your UI updates without polling. This page explains exactly what the stream does, how it recovers from failures, and where you are still responsible.

What it is, and what it is not

  • The stream is a one-way, per-participant feed of progression events over Server-Sent Events (SSE). It tells your UI “this participant just earned something.”
  • It is not a real-time leaderboard feed and not a transport for sending data to Bricqs. You still write events with the events API and read state with the data hooks.
  • It is delivered under GET /api/v1/gamify/stream/{participant_id} and consumed in React with useBricqsStream (or the lower-level BricqsStream class in @bricqs/sdk-core).

Authentication

The stream is authenticated with a participant token only (never an API key), sent as a bearer header. The token must be for the same participant as the path, and it must carry the read:state scope (the mint default includes it). Two auth failures are terminal:

  • STREAM_PARTICIPANT_MISMATCH (403) — the token’s participant does not match the path participant.
  • STREAM_SCOPE_MISSING (403) — the token lacks read:state.

Because participant tokens are short-lived (default 5 minutes, clamped to 1–60), the SDK is built to re-mint mid-stream. You supply a getToken function; see refresh behavior below. See Authentication for how to mint tokens on your server.

Using it in React

import { useBricqsStream, usePoints, useTier } from "@bricqs/sdk-react";

function LiveWallet() {
  const wallet = usePoints();
  const tierState = useTier();

  useBricqsStream({
    onEvent: (evt) => {
      // Fires for every frame, including control frames
      // (stream.hello.v1 / stream.goodbye.v1).
      if (evt.event === "points.awarded.v1") wallet.refresh();
      if (evt.event === "tier.changed.v1") tierState.refresh();
    },
    onError: (err) => {
      // Terminal failure (e.g. 403, or retries exhausted).
      // The stream has stopped; fall back to polling (see below).
      console.warn("stream stopped:", err.message);
    },
  });

  return <WalletUI points={wallet.data} tier={tierState.data} />;
}

A common and robust pattern is to treat the stream as a refresh trigger: when a relevant event arrives, call the data hook’s refresh() to re-read authoritative state, rather than trusting the event payload to be the whole picture.

How reconnect, refresh, and replay work

SituationWhat the SDK doesWhat you should do
Token expired (401)Re-mints once via your getToken(true) and retries immediately (no backoff).Make getToken return a fresh token; nothing else.
Forbidden (403)Terminal. Fires onError and stops. No retry.Fix scope/participant mismatch; fall back to polling.
Network drop / 5xxExponential backoff (1s→30s), up to maxRetries (default 5), then terminal.Usually nothing; raise maxRetries for long-lived tabs.
Reconnect after any blipSends Last-Event-ID to replay events missed during the gap.Keep handlers idempotent (a replayed event may repeat).
No data for a whileServer sends a heartbeat every 30s; client treats 60s of silence as dead and reconnects.Nothing.
Idle too longAfter 1 hour idle the server sends stream.goodbye.v1 and closes; the client reconnects.Nothing.

A connection is only considered open once the server’s stream.hello.v1 frame arrives (that is when onOpen fires). A 200 response that never yields a hello is not yet “connected.” The hello frame’s id is not a replay cursor — only real event ids advance Last-Event-ID.

There is no automatic polling fallback

This is the most important thing to design for. When the stream fails terminally (a 403, or retries exhausted), the SDK fires onError and stops. It does not silently switch to polling. If your UI must stay fresh even when streaming is unavailable, you own that fallback:

function useLiveOrPolled(refreshers: Array<() => void>) {
  const [streaming, setStreaming] = useState(true);

  useBricqsStream({
    onOpen: () => setStreaming(true),
    onError: () => setStreaming(false), // stream gave up
    onEvent: (evt) => { /* refresh on relevant events */ },
  });

  // Manual fallback: poll while the stream is down.
  useEffect(() => {
    if (streaming) return;
    const id = setInterval(() => refreshers.forEach((r) => r()), 15000);
    return () => clearInterval(id);
  }, [streaming, refreshers]);
}

For most apps, polling every 15–30s while the stream is down is plenty. Progression reads are already eventually consistent (some caches are up to ~30s stale), so a short poll interval matches the platform’s real freshness. See Known Limitations & Guarantees.

When to use streaming (and when not to)

  • Use it for live wallets, badge/tier celebration moments, and any screen where a participant expects an instant reaction to their own action.
  • Skip it for screens that only render on load, batch dashboards, or server-side rendering — a single read on mount is simpler and cheaper.
  • Do not use it for leaderboards of many participants; general leaderboards are recomputed on a cache window, not streamed. The stream is per-participant progression only.

Next steps