---
title: "Signal SDK"
description: "WebRTC signalling over NoLag: peer discovery, offer/answer exchange, and ICE candidate relay."
---

# @nolag/signal

WebRTC signalling over NoLag: peer discovery, offer/answer exchange, and ICE candidate relay.

## Overview

WebRTC needs a signalling channel before a peer connection can exist. Two browsers cannot exchange session descriptions or ICE candidates over WebRTC itself, because that is the very thing they are negotiating. Something else has to carry those messages, and it has to know who is in the call.

`@nolag/signal` is that channel. It handles peer discovery through presence, routes offers, answers and ICE candidates to a specific peer, and tells you when someone joins or leaves. It does not touch `RTCPeerConnection`: the media is yours, this moves the negotiation.

### Key Features

- Peer discovery through room presence, so you know who to call
- Directed offer, answer and ICE candidate delivery
- Join and leave events, including hangup
- Arbitrary metadata per peer, for display names or capabilities
- Automatic reconnection with presence restored

## How It Works

`NoLagSignal` attaches to an injected `@nolag/js-sdk` client. Joining a room returns a `SignalRoom`, which subscribes to one topic and advertises the local peer through presence. Every message is addressed to a specific peer id, so a room with several participants does not broadcast negotiation traffic to everyone.

| Topic | Purpose | Replay |
| --- | --- | --- |
| `signaling` | Offers, answers, ICE candidates, hangup | None |

Signalling is deliberately ephemeral. A replayed offer is worse than no offer: by the time it arrives the peer connection it referred to is long gone.

## Installation

```bash [Terminal]
npm install @nolag/signal @nolag/js-sdk
```

One core NoLag client can back several wrapper SDKs at once, as long as each uses a distinct `appName`. Each wrapper attaches its handlers on construction and releases them with `detach()`, and never touches the socket itself. Your app owns `connect()` and `disconnect()`.

## Quick Start

```typescript [TypeScript]
import { NoLag } from '@nolag/js-sdk'
import { NoLagSignal } from '@nolag/signal'

const client = NoLag(async () => (await (await fetch('/api/nolag-token')).json()).token)

const signal = new NoLagSignal({
  client,
  metadata: { displayName: 'Alice' }
})

await client.connect()
await signal.ready()

const room = signal.joinRoom('standup')

// Someone new arrived: offer them a connection.
room.on('peerJoined', async (peer) => {
  const pc = createPeerConnection(peer.peerId)
  const offer = await pc.createOffer()
  await pc.setLocalDescription(offer)
  room.sendOffer(peer.peerId, offer)
})

// Everything addressed to us arrives here.
room.on('signal', async (message) => {
  const pc = peerConnectionFor(message.from)

  if (message.type === 'offer') {
    await pc.setRemoteDescription(message.payload)
    const answer = await pc.createAnswer()
    await pc.setLocalDescription(answer)
    room.sendAnswer(message.from, answer)
  }

  if (message.type === 'answer') {
    await pc.setRemoteDescription(message.payload)
  }

  if (message.type === 'ice') {
    await pc.addIceCandidate(message.payload)
  }
})

room.on('peerLeft', (peer) => teardown(peer.peerId))

// Trickle ICE as candidates are discovered.
pc.onicecandidate = ({ candidate }) => {
  if (candidate) room.sendIceCandidate(remotePeerId, candidate)
}

// Teardown
room.sendBye(remotePeerId)
signal.detach()
client.disconnect()
```

## API Reference

### NoLagSignal

The main class. Attaches to the injected core client and manages the room lifecycle.

#### Constructor Options

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `client` | `NoLagSocket` | required | The injected core client |
| `metadata` | `Record<string, unknown>` | none | Advertised with this peer's presence |
| `appName` | `string` | `"signal"` | App slug for the signalling app |
| `debug` | `boolean` | `false` | Verbose logging |

#### Methods

| Method | Returns | Description |
| --- | --- | --- |
| `ready()` | `Promise<void>` | Resolves once wrapper setup has completed |
| `joinRoom(name)` | `SignalRoom` | Join a signalling room |
| `leaveRoom(name)` | `void` | Leave a room |
| `getRooms()` | `SignalRoom[]` | All joined rooms |
| `getOnlinePeers()` | `Peer[]` | Peers visible across rooms |
| `detach()` | `void` | Release handlers and topics; never closes the socket |

### SignalRoom

| Method | Description |
| --- | --- |
| `sendOffer(toPeerId, offer)` | Send an `RTCSessionDescriptionInit` offer |
| `sendAnswer(toPeerId, answer)` | Send an answer |
| `sendIceCandidate(toPeerId, candidate)` | Send one ICE candidate |
| `sendBye(toPeerId)` | Tell a peer you are hanging up |
| `signal(toPeerId, type, payload)` | Send a custom signal type |
| `getPeers()` | Peers currently in this room |
| `getPeer(peerId)` | One peer, or `undefined` |

#### Events

| Event | Payload | Description |
| --- | --- | --- |
| `signal` | `SignalMessage` | A message addressed to you: `from`, `type`, `payload` |
| `peerJoined` | `Peer` | Someone joined the room |
| `peerLeft` | `Peer` | Someone left, or sent `bye` |

`@nolag/react-native` does not re-export `WebRTCManager`, and the core SDK's React Native build omits it, because its Node path bare-requires `wrtc` and Metro would fail the build. Signalling itself works fine there; the media side needs `react-native-webrtc`. See the [React Native SDK](/docs/sdks/react-native).
