---
title: "Real Time"
description: "TunnelHub broker and native WSS client transport for Ring Platform realtime"
locale: "en"
---
# Realtime Transport

`TunnelProvider` (in `app-client-shell.tsx`) owns the shared WSS/SSE connection and `channel-subscription-registry`. Tunnel consumers — including `CreditBalanceProvider` and `GlobalTunnelListeners` — must mount **inside** `TunnelProvider`. Feature hooks such as `useSync` and `useUnreadCount` should run **once** inside `NotificationProvider` — nav widgets consume `useNotificationContext()`, not duplicate `useUnreadCount()` calls.

Ring Platform delivers realtime updates through **Tunnel Protocol**:

- **TunnelHub** (`lib/tunnel/hub/`) — server-side broker; SSE, native WSS, poll queues, channel subscriptions, offline per-user queue
- **Custom server** (`server.ts`) — Next.js handler + `attachTunnelWss` when `RING_DEPLOY_TARGET` is `k8s` or `self-hosted`
- **TunnelTransport** — client-side WSS → SSE → long-polling fallback chain
- **TunnelProvider** — shared React connection with progressive connect after auth
- **Connect timing** — `lib/tunnel/tunnel-timing.ts` priority/deferred routes ([Tunnel protocol — timing](/docs/features/tunnel-protocol.md))
- **Discovery mutations** — `lib/discovery/sync-discovery.ts` publishes `opportunity:*` and `entity:*` on topic channels after CRUD ([Discovery mutation sync](/docs/architecture/discovery-mutation-sync.md))

When `publishToUserTunnel` runs with no live socket, `InMemoryTunnelHub` **queues** the message (publisher logs at `console.debug`, not an error). On connect:

- **SSE** — `app/api/tunnel/sse/route.ts` drains via `hub.drainUserQueueForSse(userId)`
- **Native WSS** — `lib/tunnel/native-ws/attach.ts` drains via `hub.drainUserQueue(userId)` immediately after `{ op: 'auth_ok' }` (parity with SSE)

Boot-race messages (e.g. device telemetry during auth-grace) are delivered on the **first** transport handshake, not the next page load.

  
- **[Tunnel protocol](/docs/features/tunnel-protocol.md)** — Routes, auth frames, priority timing, HTTP dedup hooks, and alpha limitations.

  
- **[Notifications feature](/docs/features/notifications.md)** — In-app alerts, preferences, and FCM push when the tab is closed.

  
- **[Hooks reference](/docs/development/code-structure.md#hooks-and-providers)** — Provider matrix and subscription dedup backlog.

## Request flow

```mermaid
sequenceDiagram
    participant Client as Web Client
    participant WSS as /api/tunnel/ws
    participant SSE as /api/tunnel/sse
    participant Hub as TunnelHub
    participant Service as Server Service
    participant DB as PostgreSQL

    alt k8s / self-hosted
        Client->>WSS: WS connect + auth frame
        WSS->>Hub: registerWsConnection(userId)
        WSS->>Hub: drainUserQueue on auth_ok
    else vercel / SSE fallback
        Client->>SSE: GET SSE (authenticated)
        SSE->>Hub: registerSseConnection(userId)
        SSE->>Hub: drainUserQueueForSse(userId)
    end

    Service->>DB: persist mutation
    Service->>Hub: publishToUser / publishToChannel
    Hub->>WSS: deliverWsToUser (when connected)
    Hub->>SSE: enqueue to SSE controller
    Client->>Client: useTunnelChannel / useSync updates UI

    Note over Client,DB: Fallback: long-polling via /api/tunnel/poll
```

## Connection timing

`TunnelProvider` defers connect briefly after auth so HTTP ingest (analytics device registration, initial REST) can complete first. **Priority routes** — including `/admin` and nested `/admin/*` — get faster auto-connect via `matchesRoutePrefix()` in `lib/tunnel/tunnel-timing.ts` (prefix match, not exact pathname string).

Full route tables, env overrides, and boot-race SSOT: [Tunnel protocol](/docs/features/tunnel-protocol.md).

## Where realtime is used

| Feature | Server publish | Client subscribe |
|---------|----------------|------------------|
| Notifications | `publishToUserTunnel` → `notifications:unread` / `notifications:inbox` | `NotificationProvider` → `useNotificationContext()` |
| Credit balance | `publishToUserTunnel` → `credit:balance` (via `creditBalanceService.publishBalanceUpdate`) | `CreditBalanceProvider` → `useCreditBalanceContext()` (bootstrap REST dedup + tunnel push; 60s poll only if tunnel down) |
| Wallet list / native balances | `publishWalletListUpdate` → `wallet:list` | `WalletListProvider` (`wallet-wrapper`) → `useTunnelChannel('wallet:list')` |
| Account status | `publishToUserTunnel` → `account:status` | `GlobalTunnelListeners` → `AccountStatusTunnelListener` |
| Vendor sidebar gate | — (HTTP only) | `useVendorStatus()` → `GET /api/vendor/status` (30s TTL, single-flight) |
| Chat | `publishToChannel` → `conversation:${id}` | `hooks/use-messaging.ts` → `useTunnelChannel` |
| Discovery | `syncDiscovery` → `opportunities` / `entities` | `use-realtime-opportunities.ts` / `use-realtime-entities.ts` |
| Matcher | `publishToChannel` → `matcher` | Admin/matcher UI |
| Collaboration (flagged) | `collab:${id}` binary CRDT frames | `useCollaboration` (requires WSS) |

See [Tunnel protocol](/docs/features/tunnel-protocol.md) for API routes, `RING_DEPLOY_TARGET`, channel patterns, and alpha limitations.
