Skip to content

Every Bridge app holds one live channel to the platform: a persistent realtime connection the SDK maintains for you. On connect (and on every reconnect) the server pushes a session.snapshot with everything your UI needs (branding, workspace, subscription, entitlements, user), and after that, targeted pushes keep it current: flag changes, plan changes, payment events, quota updates. No polling, no refresh.

The bridge object is the single object that exposes all of it, grouped by scope:

<script lang="ts">
  import { bridge } from '@nebulr-group/bridge-svelte';

  const branding = bridge.app.branding;
  const workspace = bridge.tenant.name;
  const subscription = bridge.tenant.subscription;
  const user = bridge.user;
</script>

{#if $branding}<img src={$branding.logo} alt={$branding.name} />{/if}
<p>Workspace: {$workspace}</p>
<p>Plan: {$subscription?.plan.name}</p>
<p>Signed in as {$user?.email}</p>

Every field is a Svelte readable store. They are null until the channel delivers the first snapshot; gate on null for skeletons, or fall back to defaults. The bridge object’s identity is stable; destructure and pass sub-references freely.

A note on naming: a workspace is called a tenant in the API, which is why the identifiers below say tenant.

PathTypeWhat it holds
bridge.app.brandingReadable<BrandingSnapshot | null>Whitelabel branding: logo, name, colors, font
bridge.app.plansfetched on first access and cachedFull plan catalog: await bridge.app.plans fetches it the first time you read it
bridge.tenant.id / .nameReadable<string | null>Current workspace identity
bridge.tenant.subscriptionReadable<SubscriptionSnapshot | null>Canonical plan + status + endsAt (see How billing works)
bridge.tenant.entitlementssnapshot store + can(key)Plan-granted capabilities, replaced live on change
bridge.userReadable<UserSnapshot | null>Authenticated user: id, email, role, tenantId
bridge.attributeswrite surfacePublish your own attributes into flag targeting (below)
bridge.eventsdispatcherSubscribe to every live channel event (below)

bridge.events.handle({...}) is the one API for reacting to channel events. Use it for side effects like analytics, audit logging, or alerting (UI state updates automatically through the stores above and the drop-in components):

import { bridge } from '@nebulr-group/bridge-svelte';

const unsubscribe = bridge.events.handle({
  'flag.updated':              (m) => console.log('flag changed:', m.flag.key),
  'subscription.plan_changed': (m) => analytics.track('plan_changed', m),
  // m.policy ('hard' | 'metered'); for metered also m.unitAmount / m.currency /
  // m.overageEstimate / m.overcap (TBP-275) for live in-app cost display.
  'quota.updated':             (m) => updateMeter(m.metric, m.remaining),
  'session.snapshot':          (m) => analytics.track('hydrated'),
  '*':                         (m) => debugLog(m.kind, m),
});

// later: one call removes every handler registered above
unsubscribe();

Event kinds:

  • Flags: flag.updated, flag.removed
  • Session: session.snapshot, user.state_changed
  • Subscription: subscription.plan_changed, subscription.created / updated / canceled / reactivated, subscription.trial_started / trial_ending_soon / trial_converted / trial_expired
  • Payments: payment.succeeded, payment.failed, dunning.entered / retry_scheduled / recovered / exhausted (dunning is the automated payment-retry process that follows a failed charge)
  • Quotas & entitlements: quota.updated, entitlements.changed

Semantics worth knowing:

  • Multiple handlers per kind: every registered handler fires; registering is additive across your app.
  • '*' is a fallback, not a firehose: it fires only for kinds that have no specific handler registered (so you never double-handle).
  • Errors are isolated: one throwing handler doesn’t block the others or break the dispatch loop.

bridge.attributes is the write surface for feeding your own data into feature-flag targeting. Keys you publish here are usable in flag rules immediately and win over Bridge-managed attributes on collision:

import { bridge } from '@nebulr-group/bridge-svelte';

// Static value
bridge.attributes.set('beta_cohort', true);

// Live-bound: the getter re-runs on every flag evaluation
bridge.attributes.bind('cart_size', () => cart.items.length);

// Bulk: one getter returning a whole map
bridge.attributes.bindMany(() => ({
  theme: currentTheme,
  locale: navigator.language,
}));

// Read the merged map / remove keys
bridge.attributes.get();
bridge.attributes.unset('beta_cohort');

The bridge: namespace is reserved for Bridge-managed attributes; writes to it are rejected with a console warning. Pass { observed: false } to set/bind/bindMany to keep a key out of attribute-discovery telemetry.

The live channel’s connection state is exposed as a store, from the package root or the flags entry point:

<script lang="ts">
  import { realtimeStatus } from '@nebulr-group/bridge-svelte';
</script>

{#if $realtimeStatus !== 'open'}
  <span class="badge">reconnecting…</span>
{/if}

While the channel is down, everything keeps working from the last known state: flags evaluate from cache, stores hold their last snapshot. On reconnect the server re-sends a full session.snapshot, so every scope updates atomically and nothing is missed.

realtimeStatus is just the state ('idle', 'connecting', 'open', 'closed', 'unauthorized', 'degraded'). realtimeStatusDetail adds why:

FieldMeaning
stateSame value as realtimeStatus
reasonMachine-readable reason when live updates are not working, e.g. expired
sideWhose move it is: app (your app’s session/token handling), config (your Bridge settings — apiBaseUrl, appId or the app’s configuration in Bridge — don’t match), bridge (nothing to change in your app) or network (transient; the client is retrying)
retryingtrue while the client keeps trying on its own
docsUrlTroubleshooting page for this reason
refSupport reference for this run of trouble

Every reason has an entry in Live Updates Troubleshooting: what it means, whose side it is on, and the exact fix. The console message, docsUrl and the dev badge all link straight to it.

<script lang="ts">
  import { realtimeStatusDetail } from '@nebulr-group/bridge-svelte';
</script>

{#if $realtimeStatusDetail.state === 'unauthorized'}
  <p>Live updates are off ({$realtimeStatusDetail.reason}). <a href={$realtimeStatusDetail.docsUrl}>Why?</a></p>
{/if}

For side effects (logging, alerting) subscribe instead; it returns an unsubscribe function:

import { onBridgeRealtimeStatus } from '@nebulr-group/bridge-svelte';

const stop = onBridgeRealtimeStatus((status) => {
  if (status.reason) console.warn('live updates:', status.reason, status.side);
});

Dev badge. In development builds, <BridgeBootstrap> mounts a small “Live updates off — why?” badge in the corner whenever live updates are refused (unauthorized), connected but receiving nothing (degraded), or still retrying after 30 seconds. It shows the reason, whose side it is, a docs link and the support reference, and can be dismissed until a different problem appears. It never renders in production builds. Turn it off in development too with devBadge: false in the config you pass to bridgeBootstrap. If you render your own bootstrap instead of <BridgeBootstrap>, the badge is exported as RealtimeDevBadge.

The bridge object and the original module-level stores (appConfigStore, subscriptionStore, profileStore, isAuthenticated, …) are both supported and fed by the same internal state. The bridge object is the newer, scoped way to read live platform state; the module-level stores remain the API for auth state and the classic checkout flow, covered in the Auth and How billing works guides. Use whichever fits; they don’t conflict.