SDK Realtime & Channels New
The realtime module lets you subscribe to database changes, broadcast messages between clients, track who's online, and build full chat systems with persistent channels β all through a single WebSocket connection managed by the SDK.
Database Changes
Get notified instantly when rows are inserted, updated, or deleted:
const channel = dypai.realtime.channel('orders-updates')
channel
.on('postgres_changes', { event: '*', table: 'orders' }, (payload) => {
console.log('Event:', payload.eventType) // INSERT, UPDATE, or DELETE
console.log('New data:', payload.new)
console.log('Old data:', payload.old)
})
.subscribe()
Filter by event type
// Only new orders
channel.on('postgres_changes', { event: 'INSERT', table: 'orders' }, callback)
// Only status changes
channel.on('postgres_changes', {
event: 'UPDATE',
table: 'orders',
filter: 'status=eq.completed'
}, callback)
Broadcast
Send ephemeral messages between connected clients β no database involved:
const channel = dypai.realtime.channel('room-1')
// Listen
channel.on('broadcast', { event: 'typing' }, (payload) => {
console.log(`${payload.user} is typing...`)
})
channel.subscribe()
// Send
channel.send('typing', { user: 'John' })
Presence
Track who's online in real time:
const channel = dypai.realtime.channel('room-1')
channel.on('presence', { event: 'sync' }, () => {
const users = channel.presenceState()
console.log('Online:', Object.keys(users))
})
channel.subscribe()
// Announce yourself
channel.track({ user_id: '123', name: 'John' })
React Hooks
useRealtime β Database changes
import { useRealtime } from '@dypai-ai/client-sdk/react'
function OrderList() {
const { data: orders, refetch } = useEndpoint('list_orders')
useRealtime('orders', {
onInsert: () => refetch(),
onUpdate: () => refetch(),
onDelete: () => refetch(),
})
return <ul>{orders?.map(o => <li key={o.id}>{o.status}</li>)}</ul>
}
| Parameter | Type | Description |
|---|---|---|
tablerequired | string | Table name to subscribe to |
event | '*' | 'INSERT' | 'UPDATE' | 'DELETE'= '*' | Event type filter |
filter | object | Filter by field values, e.g. { status: 'active' } |
onInsert(record) | function | Called when a row is inserted |
onUpdate(record, old) | function | Called when a row is updated |
onDelete(old) | function | Called when a row is deleted |
onChange(change) | function | Called on any change |
enabled | boolean= true | Enable/disable the subscription |
Returns: changes, isConnected, status, error
useChannel β Broadcast & Presence
import { useChannel } from '@dypai-ai/client-sdk/react'
function Editor() {
const { presences, broadcast, memberCount } = useChannel('doc:abc', {
presence: { name: 'Alice' },
onBroadcast: (msg) => {
if (msg.event === 'cursor') updateCursor(msg.sender, msg.payload)
},
})
return <span>{memberCount} online</span>
}
| Parameter | Type | Description |
|---|---|---|
channelrequired | string | Channel name (any string) |
presence | boolean | object | Track presence. Pass object with custom data |
onBroadcast(msg) | function | Called on broadcast messages |
onJoin(userId, data) | function | Called when someone joins |
onLeave(userId, data) | function | Called when someone leaves |
enabled | boolean= true | Enable/disable |
Returns: broadcast(event, payload?), presences, isConnected, memberCount
useChannelMessages β Persistent Chat
Full messaging with database storage and real-time delivery:
import { useChannelMessages } from '@dypai-ai/client-sdk/react'
function TeamChat({ channelId }) {
const { messages, send, isLoading, isConnected } = useChannelMessages(channelId)
return (
<div>
{messages.map(m => (
<div key={m.id}>
<b>{m.sender_email}</b>: {m.content}
</div>
))}
<input onKeyDown={e => {
if (e.key === 'Enter') { send(e.target.value); e.target.value = '' }
}} />
</div>
)
}
| Parameter | Type | Description |
|---|---|---|
channelIdrequired | string | Channel ID (e.g. 'ch_abc123') |
loadHistory | boolean= true | Load message history on mount |
limit | number= 50 | Max messages to load |
Returns: messages, send(content, metadata?), isLoading, isConnected, error
useChannels β Channel Management
import { useChannels } from '@dypai-ai/client-sdk/react'
function ChannelList() {
const { channels, createChannel } = useChannels()
return (
<div>
{channels.map(ch => <div key={ch.id}>{ch.name} ({ch.member_count})</div>)}
<button onClick={() => createChannel('General', [], 'public')}>New</button>
</div>
)
}
| Parameter | Type | Description |
|---|---|---|
channels | Channel[] | User's channels + public channels |
createChannel(name, members?, type?) | async | Create a channel. Type: 'group', 'dm', or 'public' |
isLoading | boolean | Loading state |
refetch() | async | Reload channel list |
Channel Types
| Type | Description | Who can join | Auto-dedup |
|---|---|---|---|
group | Private group chat | Invited members only | No |
dm | Direct message (1-to-1) | The 2 participants | Yes β won't create duplicates |
public | Open channel | Anyone (auto-joins on message) | No |
// DM β calling twice with same users returns the existing channel
const dm = await createChannel('Alice β Bob', ['bob-user-id'], 'dm')
Member Roles
| Action | admin | member |
|---|---|---|
| Send/read messages | β | β |
| Edit/delete own messages | β | β |
| Delete others' messages | β | β |
| Add/remove members | β | β |
| Change roles | β | β |
The channel creator gets the admin role automatically.
Channels REST API
Built-in endpoints at /api/v0/channels β no workflow setup needed:
| Method | Path | Description |
|---|---|---|
POST | /channels | Create channel |
GET | /channels | List channels |
GET | /channels/:id | Channel details + members |
PATCH | /channels/:id | Update name |
DELETE | /channels/:id | Delete channel |
GET | /channels/users | List project users |
POST | /channels/:id/messages | Send message |
GET | /channels/:id/messages | History (?limit=50&before=timestamp) |
PATCH | /channels/:id/messages/:msgId | Edit message |
DELETE | /channels/:id/messages/:msgId | Soft delete |
GET | /channels/:id/unread | Unread count |
POST | /channels/:id/read | Mark as read |
POST | /channels/:id/join | Join public channel |
POST | /channels/:id/leave | Leave channel |
POST | /channels/:id/members | Add members |
DELETE | /channels/:id/members/:userId | Remove member |
PATCH | /channels/:id/members/:userId | Change role |
GET | /channels/:id/members | List members |
Connection Behavior
- Single WebSocket β all hooks share one connection. No duplicates.
- Auto-reconnect β exponential backoff (1s β 2s β 5s β 10s β 30s).
- Heartbeat β ping every 25s to keep the connection alive.
- Auto-replay β subscriptions and channel joins are replayed on reconnect.
- Multi-tab β each tab has its own connection. Presence tracks each separately.
Complete Example β Team Chat
import {
DypaiProvider, useAuth, useChannelMessages, useChannels, useChannel
} from '@dypai-ai/client-sdk/react'
import { dypai } from './lib/dypai'
function App() {
return (
<DypaiProvider client={dypai}>
<Chat />
</DypaiProvider>
)
}
function Chat() {
const { user } = useAuth()
const myName = user?.email?.split('@')[0] || 'anonymous'
const { channels, createChannel } = useChannels()
const [activeId, setActiveId] = useState(channels[0]?.id)
return (
<div className="flex">
<aside className="w-56">
{channels.map(ch => (
<button key={ch.id} onClick={() => setActiveId(ch.id)}>
# {ch.name}
</button>
))}
<button onClick={() => createChannel('New Channel')}>+</button>
</aside>
{activeId && <ChatRoom channelId={activeId} myName={myName} />}
</div>
)
}
function ChatRoom({ channelId, myName }) {
const { messages, send, isLoading, isConnected } = useChannelMessages(channelId)
const { presences } = useChannel(`channel:${channelId}`, {
presence: { name: myName },
})
// Typing indicator
const [typingUsers, setTypingUsers] = useState(new Set())
const { broadcast } = useChannel(`channel:${channelId}:typing`, {
onBroadcast: (msg) => {
if (msg.event === 'typing' && msg.payload.name !== myName) {
setTypingUsers(prev => new Set(prev).add(msg.payload.name))
setTimeout(() => setTypingUsers(prev => {
const next = new Set(prev); next.delete(msg.payload.name); return next
}), 2000)
}
},
})
const [input, setInput] = useState('')
return (
<div className="flex-1 flex flex-col">
<div className="p-2 border-b flex justify-between">
<span>{isConnected ? 'β Live' : 'β Offline'}</span>
<span>{Object.values(presences).map(p => p.data?.name).join(', ')}</span>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-2">
{isLoading && <p>Loading...</p>}
{messages.map(m => (
<div key={m.id}>
<b>{m.sender_email}</b>: {m.content}
</div>
))}
{typingUsers.size > 0 && (
<p className="text-sm italic">{[...typingUsers].join(', ')} typing...</p>
)}
</div>
<div className="p-3 border-t">
<input
value={input}
onChange={e => { setInput(e.target.value); broadcast('typing', { name: myName }) }}
onKeyDown={e => { if (e.key === 'Enter') { send(input); setInput('') } }}
placeholder="Type a message..."
className="w-full border rounded px-3 py-2"
/>
</div>
</div>
)
}
Cleanup
// Unsubscribe from a specific channel
channel.unsubscribe()
// Remove all subscriptions
dypai.realtime.removeAllChannels()