Real Time Block

Complete real-time communication for any application
The Real Time Block provides everything you need for instant, bidirectional communication. Build chat applications, video conferencing, live collaboration, and real-time dashboards with WebSocket connections, WebRTC support, presence tracking, and message history.
Features
| Feature | Description |
|---|---|
| WebSocket Connections | Persistent bidirectional connections with auto-reconnect |
| Channels & Rooms | Organize communication with public, private, and presence channels |
| Presence Tracking | Know who is online with custom status states |
| WebRTC Support | Peer-to-peer video, audio, and screen sharing |
| Message History | Persist and retrieve past messages with pagination |
| Broadcast Events | Send messages to thousands of subscribers instantly |
| Typing Indicators | Show real-time typing status to other users |
| Read Receipts | Track message delivery and read status |
API Endpoint
| Service | URL |
|---|---|
| Real Time | https://conversations.api.us.23blocks.com |
Environment Routing: Use the same URL for all environments. Your API key determines the environment:
pk_test_*/sk_test_*→ Routes to Stagingpk_live_*/sk_live_*→ Routes to Production
Quick Start
Installation
npm install @23blocks/sdk
Initialize the Client
import { create23BlocksClient } from '@23blocks/sdk';
const client = create23BlocksClient({
urls: { realtime: 'https://conversations.api.us.23blocks.com' },
apiKey: 'your-api-key', // Use pk_test_* for staging, pk_live_* for production
});
Basic Usage
// Connect to real-time service
const realtime = await client.realtime.connect({
token: 'user-auth-token'
});
// Subscribe to a channel
const channel = realtime.subscribe('chat:room-1');
// Listen for messages
channel.on('message', (message) => {
console.log('New message:', message);
});
// Listen for presence changes
channel.on('presence', (users) => {
console.log('Online users:', users);
});
// Send a message
channel.send({
type: 'message',
text: 'Hello, everyone!',
user_id: 'user_123'
});
// Track typing status
channel.typing.start();
// ... after user stops typing
channel.typing.stop();
API Reference
Base URL
wss://realtime.23blocks.com
https://api.23blocks.com/v1/realtime
Authentication
WebSocket connections require a connection token:
// Get connection token
const { token } = await client.realtime.getToken({
user_id: 'user_123',
channels: ['chat:*', 'notifications:user_123']
});
// Connect with token
const realtime = await client.realtime.connect({ token });
REST API calls require standard headers:
X-App-Id: your-app-id
X-Api-Key: your-api-key
Authorization: Bearer <user-token>
Connection API
Manage WebSocket connections with automatic reconnection and state handling.
Connect
Establish a WebSocket connection.
const realtime = await client.realtime.connect({
token: 'connection-token',
options: {
autoReconnect: true,
reconnectDelay: 1000,
maxReconnectAttempts: 10,
heartbeatInterval: 30000
}
});
Connection Events:
realtime.on('connected', () => {
console.log('Connected to real-time service');
});
realtime.on('disconnected', (reason) => {
console.log('Disconnected:', reason);
});
realtime.on('reconnecting', (attempt) => {
console.log('Reconnecting, attempt:', attempt);
});
realtime.on('error', (error) => {
console.error('Connection error:', error);
});
Disconnect
Close the WebSocket connection.
realtime.disconnect();
Connection State
Check connection status.
const state = realtime.getState();
// 'connecting' | 'connected' | 'disconnecting' | 'disconnected'
const isConnected = realtime.isConnected();
Channels API
Organize communication with channels. Channels can be public, private, or presence-enabled.
Channel Types
| Type | Prefix | Description |
|---|---|---|
| Public | public: | Anyone can subscribe and send messages |
| Private | private: | Requires authorization to subscribe |
| Presence | presence: | Includes presence tracking for subscribers |
Subscribe to Channel
// Subscribe to a public channel
const channel = realtime.subscribe('public:news');
// Subscribe to a private channel
const privateChannel = realtime.subscribe('private:team-123');
// Subscribe to a presence channel
const presenceChannel = realtime.subscribe('presence:room-1');
Channel Events
// Message received
channel.on('message', (message) => {
console.log('Message:', message);
});
// Custom event
channel.on('custom-event', (data) => {
console.log('Custom event:', data);
});
// Presence updates (presence channels only)
channel.on('presence:join', (user) => {
console.log('User joined:', user);
});
channel.on('presence:leave', (user) => {
console.log('User left:', user);
});
channel.on('presence:update', (user) => {
console.log('User updated:', user);
});
Send Message
channel.send({
type: 'message',
data: {
text: 'Hello, world!',
attachments: []
}
});
Send Custom Event
channel.trigger('cursor-move', {
x: 100,
y: 200,
user_id: 'user_123'
});
Unsubscribe
channel.unsubscribe();
Presence API
Track user presence and custom status in real-time.
Get Presence Members
const members = await channel.presence.getMembers();
// Returns array of currently present users
console.log(members);
// [
// { user_id: 'user_1', status: 'online', data: { name: 'Alice' } },
// { user_id: 'user_2', status: 'away', data: { name: 'Bob' } }
// ]
Track Presence
// Join with custom data
channel.presence.enter({
name: 'Alice',
avatar: 'https://example.com/alice.jpg',
status: 'online'
});
// Update presence data
channel.presence.update({
status: 'busy',
statusMessage: 'In a meeting'
});
// Leave presence
channel.presence.leave();
Presence Events
channel.on('presence:join', (member) => {
console.log(`${member.data.name} joined`);
});
channel.on('presence:leave', (member) => {
console.log(`${member.data.name} left`);
});
channel.on('presence:update', (member) => {
console.log(`${member.data.name} is now ${member.data.status}`);
});
channel.on('presence:sync', (members) => {
console.log('Full member list:', members);
});
Typing Indicators API
Show real-time typing status to other users.
Start Typing
channel.typing.start({
user_id: 'user_123',
user_name: 'Alice'
});
Stop Typing
channel.typing.stop();
Listen for Typing
channel.on('typing:start', (user) => {
console.log(`${user.user_name} is typing...`);
});
channel.on('typing:stop', (user) => {
console.log(`${user.user_name} stopped typing`);
});
Message History API
Persist and retrieve past messages.
Get Message History
GET /v1/realtime/channels/:channel_id/messages
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
limit | integer | Max messages to return (default: 50, max: 100) |
before | string | Get messages before this message ID |
after | string | Get messages after this message ID |
order | string | Sort order: asc or desc (default: desc) |
Response:
{
"data": [
{
"id": "msg_abc123",
"type": "messages",
"attributes": {
"channel_id": "chat:room-1",
"user_id": "user_123",
"event": "message",
"data": {
"text": "Hello, everyone!",
"attachments": []
},
"created_at": "2024-01-15T10:30:00Z"
}
}
],
"meta": {
"has_more": true,
"oldest_id": "msg_xyz789",
"newest_id": "msg_abc123"
}
}
SDK Usage
// Get latest messages
const messages = await channel.history.fetch({
limit: 50
});
// Get older messages (pagination)
const olderMessages = await channel.history.fetch({
limit: 50,
before: 'msg_xyz789'
});
// Search messages
const searchResults = await client.realtime.messages.search({
channel_id: 'chat:room-1',
query: 'meeting',
limit: 20
});
Read Receipts API
Track message delivery and read status.
Mark as Read
POST /v1/realtime/channels/:channel_id/read
Request Body:
{
"data": {
"type": "read_receipt",
"attributes": {
"message_id": "msg_abc123",
"user_id": "user_456"
}
}
}
SDK Usage
// Mark messages as read
await channel.messages.markAsRead('msg_abc123');
// Get read status
const readStatus = await channel.messages.getReadStatus('msg_abc123');
// { read_by: ['user_123', 'user_456'], read_count: 2 }
Listen for Read Events
channel.on('message:read', (data) => {
console.log(`Message ${data.message_id} read by ${data.user_id}`);
});
WebRTC API
Peer-to-peer video, audio, and screen sharing.
Create a Room
POST /v1/realtime/webrtc/rooms
Request Body:
{
"data": {
"type": "webrtc_rooms",
"attributes": {
"name": "Team Meeting",
"max_participants": 10,
"recording_enabled": true,
"settings": {
"video": {
"enabled": true,
"quality": "hd"
},
"audio": {
"enabled": true,
"noise_suppression": true
},
"screen_sharing": {
"enabled": true
}
}
}
}
}
Response:
{
"data": {
"id": "room_abc123",
"type": "webrtc_rooms",
"attributes": {
"name": "Team Meeting",
"join_url": "https://meet.23blocks.com/room_abc123",
"token": "eyJ...",
"max_participants": 10,
"recording_enabled": true,
"settings": {...},
"created_at": "2024-01-15T10:30:00Z"
}
}
}
Join a Room
const room = await client.realtime.webrtc.join({
room_id: 'room_abc123',
token: 'room-token',
audio: true,
video: true
});
// Listen for participants
room.on('participant:joined', (participant) => {
console.log('Participant joined:', participant);
// Add their video stream to UI
addVideoElement(participant.stream);
});
room.on('participant:left', (participant) => {
console.log('Participant left:', participant);
removeVideoElement(participant.id);
});
Media Controls
// Toggle video
room.video.toggle();
room.video.enable();
room.video.disable();
// Toggle audio
room.audio.toggle();
room.audio.mute();
room.audio.unmute();
// Share screen
await room.screen.share();
room.screen.stop();
Leave Room
room.leave();
Broadcast API
Send messages from your server to all subscribers.
Publish to Channel
POST /v1/realtime/publish
Request Body:
{
"data": {
"type": "broadcast",
"attributes": {
"channel": "notifications:all",
"event": "announcement",
"data": {
"title": "System Maintenance",
"message": "Scheduled maintenance in 1 hour",
"type": "warning"
}
}
}
}
Batch Publish
POST /v1/realtime/publish/batch
Request Body:
{
"data": {
"type": "batch_broadcast",
"attributes": {
"messages": [
{
"channel": "notifications:user_123",
"event": "notification",
"data": { "message": "You have a new message" }
},
{
"channel": "notifications:user_456",
"event": "notification",
"data": { "message": "Your order shipped" }
}
]
}
}
}
Webhooks
Receive real-time events on your server.
Supported Events
| Event | Description |
|---|---|
channel.subscribed | User subscribed to a channel |
channel.unsubscribed | User unsubscribed from a channel |
message.sent | Message sent to a channel |
presence.join | User joined presence channel |
presence.leave | User left presence channel |
presence.update | User updated presence data |
webrtc.room.created | WebRTC room created |
webrtc.participant.joined | Participant joined WebRTC room |
webrtc.participant.left | Participant left WebRTC room |
webrtc.recording.ready | Room recording is ready |
Webhook Payload
{
"id": "evt_abc123",
"type": "message.sent",
"created_at": "2024-01-15T10:30:00Z",
"data": {
"channel_id": "chat:room-1",
"user_id": "user_123",
"message": {
"id": "msg_xyz789",
"text": "Hello, world!",
"created_at": "2024-01-15T10:30:00Z"
}
}
}
Configure Webhooks
POST /v1/realtime/webhooks
Request Body:
{
"data": {
"type": "webhooks",
"attributes": {
"url": "https://api.yourapp.com/webhooks/realtime",
"events": ["message.sent", "presence.join", "presence.leave"],
"secret": "whsec_...",
"active": true
}
}
}
Channel Authorization
Control access to private and presence channels.
Authorization Endpoint
When a user tries to subscribe to a private or presence channel, your server must authorize the request.
POST /your-app/auth/realtime
Your server receives:
{
"socket_id": "socket_abc123",
"channel_name": "private:team-123",
"user_id": "user_123"
}
Your server responds:
{
"auth": "app_id:signature",
"channel_data": {
"user_id": "user_123",
"user_info": {
"name": "Alice",
"avatar": "https://example.com/alice.jpg"
}
}
}
SDK Configuration
const realtime = await client.realtime.connect({
token: 'connection-token',
authEndpoint: 'https://api.yourapp.com/auth/realtime',
authHeaders: {
'Authorization': 'Bearer user-session-token'
}
});
Error Handling
Error Response Format
{
"errors": [
{
"status": "401",
"code": "unauthorized_channel",
"title": "Unauthorized",
"detail": "You are not authorized to subscribe to this channel"
}
]
}
Common Error Codes
| Status | Code | Description |
|---|---|---|
| 400 | invalid_channel | Invalid channel name format |
| 401 | invalid_token | Connection token is invalid or expired |
| 401 | unauthorized_channel | Not authorized to access this channel |
| 403 | channel_limit_exceeded | Maximum channel subscriptions reached |
| 404 | channel_not_found | Channel does not exist |
| 429 | rate_limit_exceeded | Too many messages |
| 500 | connection_failed | Failed to establish connection |
SDK Examples
TypeScript/JavaScript
import { create23BlocksClient } from '@23blocks/sdk';
const client = create23BlocksClient({
urls: { realtime: 'https://conversations.api.us.23blocks.com' },
appId: process.env.APP_ID,
apiKey: process.env.API_KEY,
});
// Initialize real-time connection
async function initRealtime(userId: string) {
// Get connection token
const { token } = await client.realtime.getToken({
user_id: userId,
channels: ['chat:*', `notifications:${userId}`]
});
// Connect
const realtime = await client.realtime.connect({ token });
// Handle connection events
realtime.on('connected', () => {
console.log('Connected!');
});
realtime.on('error', (error) => {
console.error('Connection error:', error);
});
return realtime;
}
// Chat room example
async function joinChatRoom(realtime: any, roomId: string, userId: string) {
const channel = realtime.subscribe(`presence:chat:${roomId}`);
// Enter presence
channel.presence.enter({
user_id: userId,
name: 'Alice',
avatar: 'https://example.com/alice.jpg'
});
// Listen for messages
channel.on('message', (message) => {
displayMessage(message);
});
// Listen for presence
channel.on('presence:join', (member) => {
addUserToList(member);
});
channel.on('presence:leave', (member) => {
removeUserFromList(member);
});
// Listen for typing
channel.on('typing:start', (user) => {
showTypingIndicator(user);
});
channel.on('typing:stop', (user) => {
hideTypingIndicator(user);
});
return channel;
}
// Send message
function sendMessage(channel: any, text: string) {
channel.send({
type: 'message',
data: {
text,
timestamp: new Date().toISOString()
}
});
}
React Chat Component
import React, { useEffect, useState, useRef } from 'react';
import { create23BlocksClient, RealtimeChannel } from '@23blocks/sdk';
interface Message {
id: string;
user_id: string;
user_name: string;
text: string;
created_at: string;
}
interface User {
user_id: string;
name: string;
avatar: string;
}
const client = create23BlocksClient({
urls: { realtime: 'https://conversations.api.us.23blocks.com' },
appId: process.env.REACT_APP_APP_ID!,
apiKey: process.env.REACT_APP_API_KEY!,
});
export function ChatRoom({ roomId, currentUser }: { roomId: string; currentUser: User }) {
const [messages, setMessages] = useState<Message[]>([]);
const [members, setMembers] = useState<User[]>([]);
const [typingUsers, setTypingUsers] = useState<string[]>([]);
const [inputValue, setInputValue] = useState('');
const [isConnected, setIsConnected] = useState(false);
const channelRef = useRef<RealtimeChannel | null>(null);
const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
let realtime: any;
async function connect() {
// Get token and connect
const { token } = await client.realtime.getToken({
user_id: currentUser.user_id,
channels: [`presence:chat:${roomId}`]
});
realtime = await client.realtime.connect({ token });
realtime.on('connected', () => {
setIsConnected(true);
});
// Subscribe to chat room
const channel = realtime.subscribe(`presence:chat:${roomId}`);
channelRef.current = channel;
// Enter presence
channel.presence.enter({
user_id: currentUser.user_id,
name: currentUser.name,
avatar: currentUser.avatar
});
// Get presence members
const currentMembers = await channel.presence.getMembers();
setMembers(currentMembers.map((m: any) => m.data));
// Load message history
const history = await channel.history.fetch({ limit: 50 });
setMessages(history.reverse());
// Listen for new messages
channel.on('message', (message: any) => {
setMessages(prev => [...prev, message.data]);
});
// Listen for presence changes
channel.on('presence:join', (member: any) => {
setMembers(prev => [...prev, member.data]);
});
channel.on('presence:leave', (member: any) => {
setMembers(prev => prev.filter(m => m.user_id !== member.data.user_id));
});
// Listen for typing indicators
channel.on('typing:start', (user: any) => {
if (user.user_id !== currentUser.user_id) {
setTypingUsers(prev => [...new Set([...prev, user.user_name])]);
}
});
channel.on('typing:stop', (user: any) => {
setTypingUsers(prev => prev.filter(name => name !== user.user_name));
});
}
connect();
return () => {
if (channelRef.current) {
channelRef.current.presence.leave();
channelRef.current.unsubscribe();
}
if (realtime) {
realtime.disconnect();
}
};
}, [roomId, currentUser]);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
// Send typing indicator
if (channelRef.current) {
channelRef.current.typing.start({
user_id: currentUser.user_id,
user_name: currentUser.name
});
// Clear previous timeout
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
// Stop typing after 2 seconds of inactivity
typingTimeoutRef.current = setTimeout(() => {
channelRef.current?.typing.stop();
}, 2000);
}
};
const sendMessage = () => {
if (!inputValue.trim() || !channelRef.current) return;
channelRef.current.send({
type: 'message',
data: {
id: `msg_${Date.now()}`,
user_id: currentUser.user_id,
user_name: currentUser.name,
text: inputValue,
created_at: new Date().toISOString()
}
});
channelRef.current.typing.stop();
setInputValue('');
};
return (
<div className="chat-room">
<div className="chat-header">
<span className={`status ${isConnected ? 'online' : 'offline'}`} />
<span>{members.length} online</span>
</div>
<div className="members-list">
{members.map(member => (
<div key={member.user_id} className="member">
<img src={member.avatar} alt={member.name} />
<span>{member.name}</span>
</div>
))}
</div>
<div className="messages">
{messages.map(message => (
<div
key={message.id}
className={`message ${message.user_id === currentUser.user_id ? 'own' : ''}`}
>
<strong>{message.user_name}</strong>
<p>{message.text}</p>
<small>{new Date(message.created_at).toLocaleTimeString()}</small>
</div>
))}
</div>
{typingUsers.length > 0 && (
<div className="typing-indicator">
{typingUsers.join(', ')} {typingUsers.length === 1 ? 'is' : 'are'} typing...
</div>
)}
<div className="input-area">
<input
type="text"
value={inputValue}
onChange={handleInputChange}
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
placeholder="Type a message..."
/>
<button onClick={sendMessage}>Send</button>
</div>
</div>
);
}
React Video Call Component
import React, { useEffect, useRef, useState } from 'react';
import { create23BlocksClient } from '@23blocks/sdk';
interface Participant {
id: string;
user_id: string;
name: string;
stream: MediaStream;
audio: boolean;
video: boolean;
}
const client = create23BlocksClient({
urls: { realtime: 'https://conversations.api.us.23blocks.com' },
appId: process.env.REACT_APP_APP_ID!,
apiKey: process.env.REACT_APP_API_KEY!,
});
export function VideoRoom({ roomId, currentUser }: { roomId: string; currentUser: any }) {
const [participants, setParticipants] = useState<Participant[]>([]);
const [isAudioEnabled, setIsAudioEnabled] = useState(true);
const [isVideoEnabled, setIsVideoEnabled] = useState(true);
const [isScreenSharing, setIsScreenSharing] = useState(false);
const localVideoRef = useRef<HTMLVideoElement>(null);
const roomRef = useRef<any>(null);
useEffect(() => {
async function joinRoom() {
// Join WebRTC room
const room = await client.realtime.webrtc.join({
room_id: roomId,
audio: true,
video: true,
user_data: {
user_id: currentUser.user_id,
name: currentUser.name
}
});
roomRef.current = room;
// Set local video
if (localVideoRef.current && room.localStream) {
localVideoRef.current.srcObject = room.localStream;
}
// Handle participant events
room.on('participant:joined', (participant: Participant) => {
setParticipants(prev => [...prev, participant]);
});
room.on('participant:left', (participant: Participant) => {
setParticipants(prev => prev.filter(p => p.id !== participant.id));
});
room.on('participant:stream', (participant: Participant) => {
setParticipants(prev =>
prev.map(p => p.id === participant.id ? { ...p, stream: participant.stream } : p)
);
});
room.on('participant:audio', (participant: Participant) => {
setParticipants(prev =>
prev.map(p => p.id === participant.id ? { ...p, audio: participant.audio } : p)
);
});
room.on('participant:video', (participant: Participant) => {
setParticipants(prev =>
prev.map(p => p.id === participant.id ? { ...p, video: participant.video } : p)
);
});
}
joinRoom();
return () => {
if (roomRef.current) {
roomRef.current.leave();
}
};
}, [roomId, currentUser]);
const toggleAudio = () => {
if (roomRef.current) {
roomRef.current.audio.toggle();
setIsAudioEnabled(!isAudioEnabled);
}
};
const toggleVideo = () => {
if (roomRef.current) {
roomRef.current.video.toggle();
setIsVideoEnabled(!isVideoEnabled);
}
};
const toggleScreenShare = async () => {
if (roomRef.current) {
if (isScreenSharing) {
roomRef.current.screen.stop();
} else {
await roomRef.current.screen.share();
}
setIsScreenSharing(!isScreenSharing);
}
};
const leaveRoom = () => {
if (roomRef.current) {
roomRef.current.leave();
}
};
return (
<div className="video-room">
<div className="video-grid">
{/* Local video */}
<div className="video-tile local">
<video
ref={localVideoRef}
autoPlay
muted
playsInline
/>
<span className="name">{currentUser.name} (You)</span>
</div>
{/* Remote participants */}
{participants.map(participant => (
<ParticipantVideo
key={participant.id}
participant={participant}
/>
))}
</div>
<div className="controls">
<button
onClick={toggleAudio}
className={isAudioEnabled ? 'active' : 'muted'}
>
{isAudioEnabled ? 'Mute' : 'Unmute'}
</button>
<button
onClick={toggleVideo}
className={isVideoEnabled ? 'active' : 'disabled'}
>
{isVideoEnabled ? 'Stop Video' : 'Start Video'}
</button>
<button onClick={toggleScreenShare}>
{isScreenSharing ? 'Stop Share' : 'Share Screen'}
</button>
<button onClick={leaveRoom} className="leave">
Leave
</button>
</div>
</div>
);
}
function ParticipantVideo({ participant }: { participant: Participant }) {
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
if (videoRef.current && participant.stream) {
videoRef.current.srcObject = participant.stream;
}
}, [participant.stream]);
return (
<div className="video-tile">
<video
ref={videoRef}
autoPlay
playsInline
/>
<span className="name">{participant.name}</span>
{!participant.audio && <span className="muted-icon">🔇</span>}
</div>
);
}
Rate Limits
| Plan | Connections | Messages/sec | Channels | History Retention |
|---|---|---|---|---|
| Free | 100 | 50 | 10 | 24 hours |
| Starter | 5,000 | 500 | 100 | 7 days |
| Pro | 50,000 | 5,000 | 1,000 | 30 days |
| Enterprise | Unlimited | Custom | Unlimited | Custom |
Best Practices
Optimize Connection Handling
// Use a singleton for real-time connection
class RealtimeService {
private static instance: any = null;
static async getInstance(userId: string) {
if (!this.instance) {
const { token } = await client.realtime.getToken({ user_id: userId });
this.instance = await client.realtime.connect({ token });
// Handle reconnection
this.instance.on('disconnected', async () => {
// Token might be expired, get new one
const { token: newToken } = await client.realtime.getToken({ user_id: userId });
await this.instance.reconnect({ token: newToken });
});
}
return this.instance;
}
}
Handle Offline Messages
// Queue messages when offline
const messageQueue: any[] = [];
realtime.on('disconnected', () => {
// Switch to queueing mode
});
realtime.on('connected', async () => {
// Send queued messages
for (const msg of messageQueue) {
await channel.send(msg);
}
messageQueue.length = 0;
});
Implement Typing Indicators Efficiently
// Debounce typing indicators
let typingTimeout: NodeJS.Timeout | null = null;
function handleTyping() {
if (!typingTimeout) {
channel.typing.start();
}
if (typingTimeout) {
clearTimeout(typingTimeout);
}
typingTimeout = setTimeout(() => {
channel.typing.stop();
typingTimeout = null;
}, 2000);
}
Changelog
v2.1.0
- Added WebRTC screen sharing
- Improved reconnection logic
- Added message search API
v2.0.0
- Complete API redesign
- Added WebRTC support
- Added typing indicators and read receipts
- Enhanced presence tracking
v1.0.0
- Initial release with WebSocket channels and basic presence