Skip to main content

ADR 0014 -- Client SDK Architecture

Status: Accepted Context date: 2026-07

Context

The platform needs a first-party JavaScript/TypeScript SDK so that tenant developers can integrate EthioConnect into their applications without hand-coding HTTP requests, WebSocket frame parsing, or signaling state machines. The SDK must serve two distinct runtime environments (Node.js backends and browsers) with fundamentally different authentication models, security postures, and dependency constraints.

Key tensions driving the design:

  1. Trust boundary. Server-side code holds the tenant API key and can perform privileged operations (token minting, billing queries). Browser-side code holds only a short-lived user JWT and must never have access to the API key. Shipping both in a single package creates a risk surface where backend-only code is bundled into the browser.
  2. Bundle size. Frontend applications are sensitive to bundle size. A monolithic SDK that includes server-only HTTP retry logic, analytics resources, and billing resources would bloat browser bundles even when those features are unused.
  3. Type safety. Both SDKs and the signaling protocol share a large surface of request/response types. Duplicating them across packages leads to drift; a single source of truth is needed.
  4. Media layer coupling. The platform uses LiveKit as the SFU. The SDK must integrate with LiveKit without hard-coupling to it, so that (a) LiveKit can be upgraded independently, (b) the SDK works for signaling-only use cases without pulling in the LiveKit client, and (c) tenants can use the full LiveKit API when the SDK abstraction is insufficient.
  5. Code generation vs. hand-authored. OpenAPI code generators produce boilerplate-heavy clients with limited ergonomics. The API surface is small enough (five resource namespaces, roughly 20 methods) that a hand-authored client delivers a better developer experience with typed resource facades.

Decision

1. Three npm packages in a single workspace

The SDK is split into three packages in an npm workspaces monorepo at sdks/js/:

  • @comm-baas/types -- Pure TypeScript type definitions derived from the API contracts. Zero runtime code. Consumed as a dependency by both SDKs.
  • @comm-baas/server-sdk -- Node.js client authenticated with a tenant API key (ApiKey header). Provides typed resource facades for auth, rooms, presence, analytics, and billing. Includes automatic retry with exponential backoff for 429 and 5xx responses.
  • @comm-baas/client-sdk -- Browser client authenticated with a short-lived JWT (Bearer header). Provides authenticated HTTP access, a WebSocket signaling connection with session resume, and a typed event emitter.

This separation enforces the trust boundary at the package level: a frontend bundler that imports only @comm-baas/client-sdk never includes server-only code. Tree-shaking is improved because each package exports only what is relevant to its runtime.

2. Hand-authored rather than OpenAPI-generated

The SDK is hand-authored with typed resource facades (client.rooms.create(), client.auth.mintToken()) rather than generated from an OpenAPI specification. The reasons:

  • The API surface is small (approximately 20 methods across 5 resources) and stable. Code generation overhead is not justified.
  • Hand-authored facades provide better IDE ergonomics: inline JSDoc, grouped by resource namespace, and natural method signatures.
  • Error handling is customized with a typed error hierarchy (AuthenticationError, RateLimitError, etc.) that maps HTTP status codes to specific exception classes -- something generators typically do not provide.
  • The signaling layer (WebSocket state machine, heartbeat, resume) has no OpenAPI representation and must be hand-authored regardless.

If the API surface grows significantly, a hybrid approach (generated HTTP layer with hand-authored facades on top) can be adopted without breaking the public API.

3. LiveKit as an optional peer dependency

The @comm-baas/client-sdk declares livekit-client as an optional peer dependency (peerDependencies with optional: true in peerDependenciesMeta). This means:

  • The SDK works without livekit-client for signaling-only use cases (roster tracking, event streaming, moderation).
  • When installed, tenants can use the LiveKit token from the join response to create a livekit-client Room directly (the "escape hatch"), giving full access to the LiveKit media API.
  • LiveKit major version upgrades do not require an SDK release as long as the token format remains compatible. The ^2.0.0 peer range allows minor and patch updates.
  • The SDK never wraps or re-exports LiveKit APIs. It provides the credentials; the tenant owns the media layer integration.

4. Lockstep versioning

All three packages share the same version number and are published together. A change to @comm-baas/types that adds a new request field bumps the version of all three packages simultaneously. This eliminates version matrix incompatibilities: if @comm-baas/server-sdk@0.5.0 and @comm-baas/client-sdk@0.5.0 are installed, the shared types are guaranteed to be compatible.

The workspace uses "workspace:*" dependency specifiers which resolve to the current local version during development and are rewritten to exact versions at publish time.

5. Signaling session resume design

The SignalingConnection class implements a state machine for WebSocket session resume:

disconnected --> connecting --> connected
^ |
| (ws close)
| |
+-- (max retries) -- resuming -+
|
(resumed msg)
|
v
connected

On unexpected WebSocket close, the client:

  1. Checks whether it has a session_id from a prior hello message.
  2. If yes, enters resuming state and reconnects with exponential backoff (1s base, up to 3 attempts).
  3. Sends a resume frame with session_id and last_seq (the sequence number of the last received room event).
  4. The server replays missed events, then sends a resumed message confirming the session is restored.
  5. If all retry attempts fail, the connection transitions to disconnected.

This design ensures that short network interruptions (mobile network handoffs, laptop sleep/wake) do not cause the client to lose room state or miss events. The last_seq mechanism guarantees exactly-once event delivery without requiring the client to maintain a local event log.

Intentional disconnects (disconnect() calls) and normal WebSocket closures (code 1000) bypass the resume logic entirely.

6. QoS telemetry is opt-in

Quality of Service telemetry (packet loss, jitter, RTT) is reported only when the application explicitly calls the QoS ingest endpoint. The SDK does not automatically collect or transmit telemetry. This is a deliberate choice:

  • Respects tenant control over what data leaves the browser.
  • Avoids background network traffic that tenants have not opted into.
  • Simplifies privacy compliance -- tenants decide what metrics to collect and can apply their own consent flows before reporting.

The QoSSample and QoSIngestRequest types in @comm-baas/types define the reporting contract.

Consequences

  • Positive: Clean trust boundary between server and client code. Frontend bundles contain only browser-relevant code. Typed error hierarchy provides consistent error handling across both SDKs. Session resume makes the signaling layer resilient to transient network failures.
  • Positive: LiveKit as a peer dependency keeps the SDK decoupled from the media layer. Tenants can upgrade LiveKit independently and use its full API surface when needed.
  • Positive: Lockstep versioning eliminates type compatibility concerns across packages.
  • Negative: Three packages require coordinated publishing. Mitigated by the npm workspaces setup and lockstep version policy.
  • Negative: Hand-authored client requires manual updates when API endpoints change. Mitigated by the small, stable API surface and the types package providing compile-time validation against the contracts.
  • Negative: No automatic telemetry means tenants must opt in explicitly, which reduces default observability. Mitigated by documentation and examples showing how to wire up QoS reporting.