Skip to content

Cloud Relay: Session architecture rework for multi-user scalability #856

@accius

Description

@accius

Background

The cloud relay feature allows users to control their home rig-bridge from a remote browser (e.g. openhamclock.com) by proxying state and commands through the OHC server. It works for single-user self-hosted setups but has architectural issues that prevent it from scaling to multi-user deployments.

PR #852 (direct SSE mode) is the right long-term direction for self-hosted/LAN users — it eliminates server round-trips entirely. But cloud relay remains necessary for users who access openhamclock.com from outside their home network, especially on browsers with mixed-content restrictions (Safari, Chrome on HTTPS).

This issue tracks the session management and scalability rework needed to make cloud relay production-ready for multi-user environments.


Problems to solve

1. Session ID is not consistently paired between browser and rig-bridge

The session ID lives in two places that aren't automatically linked:

  • Rig-bridge config file (cloud-relay.js reads cfg.session) — set manually by the user
  • Browser Settings panel (cloudRelaySession in SettingsPanel.jsx) — typed in manually by the user

There's no automatic pairing. The user must copy the same session ID into both places. If they mismatch, data goes to one session but the browser listens on another. The /api/rig-bridge/relay/credentials endpoint can generate a session ID, but nothing ensures the browser and rig-bridge actually use the same one.

2. Per-plugin data is not scoped to sessions consistently

All plugin data (decodes, APRS packets, rig state) flows through a single session. But there's no clear contract about which data is session-private vs. shared:

  • Rig freq/mode/PTT — clearly session-private (each user has their own rig)
  • WSJT-X decodes — session-private (from that user's WSJT-X instance)
  • APRS RF packets — could be shared (local RF data from a club station TNC) or private (user's personal iGate)
  • QSOs — session-private

Currently everything is bundled into one session blob. There's no mechanism for a user to share their APRS TNC data publicly while keeping rig control private.

3. Server memory scales linearly with connected users

Each cloud relay session stores in a server-side Map:

  • Rig state object
  • Up to 500 WSJT-X decodes
  • APRS packet buffer
  • Command queue
  • SSE client connections
  • Long-poll command waiters

With the current cap of MAX_RELAY_SESSIONS = 50 and RELAY_SESSION_TTL = 1 hour, this is bounded but still means potentially 50 × (500 decodes + APRS buffers + SSE connections) in memory. For openhamclock.com with many simultaneous users, this could become a performance concern.

4. No session lifecycle management

  • Sessions are created implicitly on first contact (no explicit "connect" step)
  • The only cleanup is a 1-hour TTL with 5-minute sweep interval
  • No way for a user to see active sessions, force-disconnect, or manage their session from the browser
  • If a user's rig-bridge disconnects, stale sessions linger for up to an hour serving stale data

5. Credential endpoint exposes relay key to browser

GET /api/rig-bridge/relay/credentials returns relayKey (the RIG_BRIDGE_RELAY_KEY env var) to the browser. This is the same key rig-bridge uses to authenticate state pushes. Any browser user could extract it and push fake rig state to any session.


Proposed phased approach

Phase 1 — Automatic session pairing

Goal: Eliminate manual session ID copy-paste. Browser and rig-bridge should auto-pair.

  • Generate session ID server-side when a user enables cloud relay in Settings
  • Store session ID in browser localStorage (persist across refreshes)
  • Provide a one-time pairing code or QR code the user enters in rig-bridge setup UI (port 5555)
  • Rig-bridge exchanges the pairing code for the actual session ID + relay credentials
  • Display connection status in both browser Settings and rig-bridge UI

Phase 2 — Separate auth for push vs. consume

Goal: Rig-bridge can push data, browser can consume data, but browser can't impersonate rig-bridge.

  • Split RIG_BRIDGE_RELAY_KEY into two tokens:
    • Push token — only rig-bridge has this, used for POST /relay/state
    • Session token — browser gets this, used for SSE stream + command POST
  • Remove relay key from /relay/credentials response
  • Commands from browser are authenticated by session token, not relay key

Phase 3 — Data scoping (private vs. shared)

Goal: Let users choose which plugin data is session-private vs. publicly visible.

  • Add per-plugin visibility flags to session config: { aprs: 'public', wsjtx: 'private', rig: 'private' }
  • Public data (e.g. APRS RF from a club TNC) gets merged into the global APRS station pool
  • Private data (rig state, decodes) only accessible to the owning session
  • Default: everything private (current behavior, safe default)

Phase 4 — Session lifecycle and memory optimization

Goal: Reduce server memory footprint, add explicit session management.

  • Add explicit connect/disconnect lifecycle (not just implicit creation)
  • Reduce decode buffer per session (500 → 100, or use a ring buffer with age-based eviction)
  • Add session dashboard for admins (active sessions, memory usage, last activity)
  • Consider moving session storage to Redis for horizontal scaling (future, if needed)
  • Add configurable MAX_RELAY_SESSIONS via env var for different deployment sizes

References

cc @ceotjoe

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

Status

In Progress

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions