Skip to main content
Most admin and customer screens can stay current without polling. Voyant ships a provider-agnostic realtime layer that pushes live updates to channels, and a React layer that turns those pushes into query invalidations. The platform never couples to a single transport vendor, and the whole feature is optional: a deployment with no provider configured stays inert. The backend lives in @voyant-travel/realtime. The React hooks live in @voyant-travel/realtime-react. This page covers both, plus how they relate to the managed Voyant realtime service.

The core idea: hints, not records

The realtime layer does not stream changed records over channels. It streams invalidation hints, small { event, entity, id } envelopes that say “this thing changed, refetch it.” The React layer reacts by invalidating matching React Query keys, which then refetch over the existing authenticated HTTP path. This design has three consequences worth understanding:
  • HTTP stays the source of truth. A channel never carries entity data, so it never leaks state through channel capabilities.
  • At-most-once delivery is acceptable. A missed hint self-heals on the next refetch or staleTime tick.
  • The transaction that emitted the event is never blocked by a publish.
If a channel ever needs at-least-once delivery, that is a durable channel-push pattern (write an intent row, drain it from a workflow), not this module. The realtime module ships the at-most-once tier by design.

The provider interface

The transport is injected through RealtimeProvider. Voyant is one implementation; any pub/sub backend (a hosted vendor, a self-hosted WebSocket or SSE service) can satisfy the same interface.
mintClientToken is the abstraction line. Every vendor mints tokens differently, but the token-mint route never knows the vendor. The package ships two built-in providers: To bring your own backend, implement RealtimeProvider and pass it through. No framework change is required.

Wiring the module

@voyant-travel/realtime is part of the standard product graph. It owns no schema, is stateless, and contributes its runtime ports through the runtime entry in its voyant.package.v1 manifest, so the token-mint route and the deferred event bridge are resolved into the application rather than assembled by hand. What you do configure is the provider and the bridge routes that fan domain events out to channels:
The token route is served on both surfaces, at POST /v1/admin/realtime/token and POST /v1/public/realtime/token. With no provider configured, the token route returns 503 and no bridge subscribers register.

The event bridge

bridgeRoutes is a declarative table from event name to channels and an optional hint. At bootstrap, the module registers one deferred event subscriber per route. Because the subscribers are deferred (inline: false), they run after the HTTP response via the runtime scheduler and never block the emitting transaction. Publish failures are swallowed and routed to onPublishError, because a dropped hint is self-healing. The module also exports the lower-level pieces: createRealtimeService, resolveRealtimeCapabilities, and the route helpers (createRealtimeRoutes, buildRealtimeRouteRuntime) for that path.

Channel conventions

Channels and the capabilities that grant them follow a stable convention.

The operator pattern on the client

@voyant-travel/realtime-react makes existing screens live without rewriting their data layer. The transport is injected as a RealtimeConnector, so the React layer stays vendor-agnostic too. Wrap the app once. createRealtimeChannelConnector adapts the Voyant RealtimeChannel into a connector, but any vendor works by implementing RealtimeConnector directly.
Then make a screen live with useLiveQueries. It subscribes to channels and translates each invalidation hint into queryClient.invalidateQueries calls. The operator pattern is exactly this: subscribe to the admin channel and map each hint’s entity to a query-key root.
Keep staleTime: 60_000 as a floor. A missed hint self-heals on the next stale tick, so a slow polling fallback remains a safe net under the hint-driven path. For finer control, the package also exports useChannel(channel, options) to subscribe to a single channel with auto token-mint, reconnect, and sinceId resume, and usePresence(channel, profile) for member lists (“Ana is viewing this booking”). Custom hint-to-key mapping is available through resolveInvalidationKeys and the HintToQueryKeys type from @voyant-travel/realtime-react/query-keys.

Framework realtime versus Cloud realtime

These are two layers, and they cooperate.
  • Framework realtime (@voyant-travel/realtime and @voyant-travel/realtime-react) is the deployment-side contract: the provider interface, the event bridge, the token-mint route, and the React hooks. It is vendor-agnostic.
  • Cloud realtime is the managed pub/sub transport: channels, presence, history, and RealtimeChannel. It is one implementation behind the provider interface, wired in through createVoyantCloudRealtimeProvider.
If you self-host a different backend, you keep the platform layer and swap the provider. If you use Voyant, the two snap together with no glue beyond the provider factory.

Next steps

Events

The fire-and-forget event bus the realtime bridge subscribes to.

Cloud realtime

The managed pub/sub transport behind the Voyant provider.

Caching

The HTTP path that hints invalidate, and its staleness model.

API routes

Where the token-mint route mounts and how surfaces are split.