Appearance
Raw Channels & Connection Info
Beyond the shared data structures, the P2P module gives you direct access to peer-to-peer messaging and connection state.
Raw Data Channels
Send any data directly to peers without CRDT overhead. Useful for custom sync protocols, ephemeral events, or binary data.
Broadcast to All Peers
typescript
const p2p = await client.p2p;
// Send to every connected peer
p2p.send({ type: 'cursor-highlight', x: 100, y: 200, color: '#ff0' });
p2p.send('simple string message');Send to Specific Peer
typescript
p2p.sendTo('user-123', { type: 'private-ping', timestamp: Date.now() });Receive Messages
typescript
p2p.on('message', (msg) => {
// msg.data — the parsed message (JSON parsed automatically, or raw string)
// msg.fromUserId — who sent it
console.log(`${msg.fromUserId}: ${msg.data}`);
});| Method | Description |
|---|---|
send(data) | Broadcast to all connected peers |
sendTo(userId, data) | Send to a specific peer |
on('message', handler) | Listen for incoming messages |
Serialization
Objects are automatically JSON-serialized when sending and JSON-parsed when receiving. Strings are sent as-is.
Use Cases
- Custom cursor protocols (higher frequency than Pulse's built-in cursors)
- Ephemeral reactions / effects
- Lightweight game state
- Direct user-to-user communication
Connection Info
Peer List
typescript
const peers = p2p.peers; // Map<string, PeerState>
for (const [userId, state] of peers) {
console.log(userId, state.state, state.dataChannelOpen);
}Each PeerState has:
| Field | Type | Description |
|---|---|---|
userId | string | The peer's user ID |
state | 'connecting' | 'connected' | 'disconnected' | WebRTC connection state |
dataChannelOpen | boolean | Whether the data channel is ready for messages |
Current Transport
typescript
console.log(p2p.transport); // 'p2p' or 'websocket''p2p'— Data flows directly between browsers via WebRTC'websocket'— Fallback mode: data relayed through the Pulse server
Events
typescript
// Peer connected via WebRTC
p2p.on('peer:connected', (userId) => {
console.log(`${userId} connected via P2P`);
});
// Peer disconnected
p2p.on('peer:disconnected', (userId) => {
console.log(`${userId} disconnected`);
});
// Transport changed (e.g., fell back from P2P to WebSocket)
p2p.on('transport:change', (transport) => {
console.log(`Now using: ${transport}`);
});| Event | Payload | When |
|---|---|---|
peer:connected | userId: string | WebRTC connection established with a peer |
peer:disconnected | userId: string | Peer's WebRTC connection closed |
transport:change | 'p2p' | 'websocket' | Sync transport switched |
Mesh Limit & Fallback
P2P uses a full-mesh topology — every peer connects to every other peer. This works well for small groups (2-6 users) but doesn't scale to large rooms.
When the peer count exceeds the mesh limit (configurable per environment, default 6):
- New peers skip WebRTC and use the WebSocket fallback
- CRDT updates flow through the Pulse server instead of peer-to-peer
- Shared data structures work identically — your code doesn't change
- The
transport:changeevent fires with'websocket'
typescript
p2p.on('transport:change', (transport) => {
if (transport === 'websocket') {
console.log('Room is large — using server relay');
}
});ICE Servers (STUN/TURN)
WebRTC needs STUN/TURN servers to establish connections through NATs and firewalls.
Default
By default, Pulse uses Google's public STUN server (stun:stun.l.google.com:19302). This works for most cases but doesn't include a TURN relay.
Custom STUN/TURN
You can configure a TURN server in the Admin Panel → Environment Settings → TURN Server. When configured, the SDK automatically fetches time-limited credentials.
Or pass ICE servers manually:
typescript
const client = new PulseClient({
apiKey: 'pk_...',
token: userToken,
room: 'my-room',
endpoint: 'wss://pulse.example.com',
p2p: true,
iceServers: [
{ urls: 'stun:stun.example.com:3478' },
{ urls: 'turn:turn.example.com:3478', username: '...', credential: '...' },
],
});TURN Credentials API
If your environment has a TURN server configured, you can also fetch credentials via the REST API:
bash
curl https://pulse.example.com/api/v1/turn-credentials \
-H "Authorization: Bearer sk_..."Response:
json
{
"username": "1712000000:env-id:turn",
"credential": "base64-hmac",
"ttl": 86400,
"iceServers": [
{ "urls": "stun:turn.example.com:3478" },
{ "urls": "turn:turn.example.com:3478", "username": "...", "credential": "..." }
]
}Cleanup
The P2P module is cleaned up automatically when you call client.destroy(). If you need manual cleanup:
typescript
p2p.destroy();This:
- Closes all WebRTC peer connections
- Persists final CRDT state to the server
- Cleans up Yjs document and observers
- Removes all event listeners