Merge pull request #21 from supabase/feature/realtime-agent-reference

feat: realtime agent reference
This commit is contained in:
Greg Richardson
2026-02-06 15:19:41 -07:00
committed by GitHub
11 changed files with 825 additions and 10 deletions

View File

@@ -62,6 +62,17 @@ Reference files are named `{prefix}-{topic}.md` (e.g., `query-missing-indexes.md
- `references/db-security-functions.md`
- `references/db-security-service-role.md`
**Realtime** (`realtime-`):
- `references/realtime-broadcast-basics.md`
- `references/realtime-broadcast-database.md`
- `references/realtime-patterns-cleanup.md`
- `references/realtime-patterns-debugging.md`
- `references/realtime-patterns-errors.md`
- `references/realtime-postgres-changes.md`
- `references/realtime-presence-tracking.md`
- `references/realtime-setup-auth.md`
- `references/realtime-setup-channels.md`
---
*18 reference files across 11 categories*
*27 reference files across 11 categories*

View File

@@ -4,7 +4,7 @@ description: Guides and best practices for working with Supabase. Covers getting
license: MIT
metadata:
author: supabase
version: "1.0.0"
version: '1.0.0'
organization: Supabase
date: January 2026
abstract: Comprehensive Supabase development guide for building applications with Supabase services. Contains guides covering Auth, Database, Storage, Edge Functions, Realtime, client libraries, CLI, and tooling. Each reference includes setup instructions, code examples, common mistakes, and integration patterns.
@@ -34,7 +34,7 @@ Reference the appropriate resource file based on the user's needs:
### Database
| Area | Resource | When to Use |
| ------------------ | -------------------------------- | ---------------------------------------------- |
| ------------------ | ------------------------------- | ---------------------------------------------- |
| RLS Security | `references/db-rls-*.md` | Row Level Security policies, common mistakes |
| Connection Pooling | `references/db-conn-pooling.md` | Transaction vs Session mode, port 6543 vs 5432 |
| Schema Design | `references/db-schema-*.md` | auth.users FKs, timestamps, JSONB, extensions |
@@ -42,4 +42,14 @@ Reference the appropriate resource file based on the user's needs:
| Performance | `references/db-perf-*.md` | Indexes (BRIN, GIN), query optimization |
| Security | `references/db-security-*.md` | Service role key, security_definer functions |
### Realtime
| Area | Resource | When to Use |
| ---------------- | ------------------------------------ | ----------------------------------------------- |
| Channel Setup | `references/realtime-setup-*.md` | Creating channels, naming conventions, auth |
| Broadcast | `references/realtime-broadcast-*.md` | Client messaging, database-triggered broadcasts |
| Presence | `references/realtime-presence-*.md` | User online status, shared state tracking |
| Postgres Changes | `references/realtime-postgres-*.md` | Database change listeners (prefer Broadcast) |
| Patterns | `references/realtime-patterns-*.md` | Cleanup, error handling, React integration |
**CLI Usage:** Always use `npx supabase` instead of `supabase` for version consistency across team members.

View File

@@ -0,0 +1,94 @@
---
title: Send and Receive Broadcast Messages
impact: HIGH
impactDescription: Core pattern for real-time client-to-client messaging
tags: realtime, broadcast, send, receive, subscribe
---
## Send and Receive Broadcast Messages
Broadcast enables low-latency pub/sub messaging between clients. Prefer Broadcast over Postgres Changes for applications that require more concurrent connections.
## Subscribe to Broadcast Events
```javascript
const channel = supabase.channel('room:123:messages', {
config: { private: true },
})
channel
.on('broadcast', { event: 'message_created' }, (payload) => {
console.log('New message:', payload.payload)
})
.on('broadcast', { event: '*' }, (payload) => {
// Listen to all events on this channel
})
.subscribe((status) => {
if (status === 'SUBSCRIBED') {
console.log('Connected!')
}
})
```
## Send Messages
**After subscribing (WebSocket - lower latency):**
```javascript
channel.send({
type: 'broadcast',
event: 'message_created',
payload: { text: 'Hello!', user_id: '123' },
})
```
**Before subscribing or one-off (HTTP):**
```javascript
await channel.httpSend('message_created', { text: 'Hello!' })
```
## Receive Own Messages
By default, senders don't receive their own broadcasts.
**Incorrect:**
```javascript
// Won't receive own messages
const channel = supabase.channel('room:123')
```
**Correct:**
```javascript
// Enable self-receive when needed (e.g., optimistic UI confirmation)
const channel = supabase.channel('room:123', {
config: {
broadcast: { self: true },
},
})
```
## Get Server Acknowledgment
```javascript
const channel = supabase.channel('room:123', {
config: {
broadcast: { ack: true },
},
})
// Returns 'ok' when server confirms receipt
const status = await channel.send({
type: 'broadcast',
event: 'message_created',
payload: { text: 'Hello!' },
})
```
## Related
- [broadcast-database.md](broadcast-database.md)
- [patterns-cleanup.md](patterns-cleanup.md)
- [Docs](https://supabase.com/docs/guides/realtime/broadcast)

View File

@@ -0,0 +1,95 @@
---
title: Broadcast from Database Triggers
impact: CRITICAL
impactDescription: Scalable pattern for notifying clients of database changes
tags: realtime, broadcast, database, triggers, realtime.send, realtime.broadcast_changes
---
## Broadcast from Database Triggers
Use database triggers with `realtime.broadcast_changes()` instead of `postgres_changes` for better scalability. This avoids per-subscriber RLS checks.
## realtime.broadcast_changes()
Broadcasts database changes in a standard format.
```sql
create or replace function room_messages_broadcast()
returns trigger
security definer
language plpgsql
as $$
begin
perform realtime.broadcast_changes(
'room:' || coalesce(new.room_id, old.room_id)::text, -- topic
tg_op, -- event (INSERT/UPDATE/DELETE)
tg_op, -- operation
tg_table_name, -- table
tg_table_schema, -- schema
new, -- new record
old -- old record
);
return coalesce(new, old);
end;
$$;
create trigger messages_broadcast_trigger
after insert or update or delete on messages
for each row execute function room_messages_broadcast();
```
**Client subscription:**
```javascript
const channel = supabase
.channel('room:123', { config: { private: true } })
.on('broadcast', { event: 'INSERT' }, (payload) => console.log('Insert:', payload))
.on('broadcast', { event: 'UPDATE' }, (payload) => console.log('Update:', payload))
.on('broadcast', { event: 'DELETE' }, (payload) => console.log('Delete:', payload))
.subscribe()
```
## realtime.send()
Sends custom payloads without table binding.
```sql
select realtime.send(
jsonb_build_object('message', 'Custom notification'), -- payload
'notification_sent', -- event
'user:456:notifications', -- topic
true -- private (true = requires auth)
);
```
## Public vs Private Mismatch
**Incorrect:**
```sql
-- Database sends to public channel
select realtime.send('{}', 'event', 'topic', false); -- private = false
```
```javascript
// Client expects private channel - won't receive message
const channel = supabase.channel('topic', { config: { private: true } })
```
**Correct:**
```sql
-- Database sends to private channel
select realtime.send('{}', 'event', 'topic', true); -- private = true
```
```javascript
// Client matches
const channel = supabase.channel('topic', { config: { private: true } })
```
## Related
- [setup-auth.md](setup-auth.md)
- [broadcast-basics.md](broadcast-basics.md)
- [Docs](https://supabase.com/docs/guides/realtime/broadcast)

View File

@@ -0,0 +1,92 @@
---
title: Clean Up Channels to Prevent Memory Leaks
impact: CRITICAL
impactDescription: Prevents memory leaks and connection quota exhaustion
tags: realtime, cleanup, react, lifecycle, removeChannel
---
## Clean Up Channels to Prevent Memory Leaks
Always remove channels when components unmount or subscriptions are no longer needed.
## React Pattern
**Incorrect:**
```javascript
function ChatRoom({ roomId }) {
useEffect(() => {
const channel = supabase.channel(`room:${roomId}`)
channel.on('broadcast', { event: 'message' }, handleMessage).subscribe()
// Missing cleanup - channel persists after unmount
}, [roomId])
}
```
**Correct:**
```javascript
function ChatRoom({ roomId }) {
const channelRef = useRef(null)
useEffect(() => {
// Prevent duplicate subscriptions
if (channelRef.current?.state === 'subscribed') return
const channel = supabase.channel(`room:${roomId}:messages`, {
config: { private: true },
})
channelRef.current = channel
channel
.on('broadcast', { event: 'message_created' }, handleMessage)
.subscribe()
return () => {
if (channelRef.current) {
supabase.removeChannel(channelRef.current)
channelRef.current = null
}
}
}, [roomId])
}
```
## Channel Lifecycle Methods
```javascript
// Remove specific channel
supabase.removeChannel(channel)
// Remove all channels (e.g., on logout)
supabase.removeAllChannels()
// Get active channels
const channels = supabase.getChannels()
```
## Check Channel State Before Subscribing
```javascript
// Prevent duplicate subscriptions
if (channel.state === 'subscribed') {
return
}
channel.subscribe()
```
## Connection Quotas
| Plan | Max Connections | Channels per Connection |
|------|-----------------|------------------------|
| Free | 200 | 100 |
| Pro | 500 | 100 |
| Team | 10,000 | 100 |
Leaked channels count against quotas even when inactive.
For Pay as you go customers you can edit these limits on [Realtime Settings](https://supabase.com/dashboard/project/_/realtime/settings)
## Related
- [patterns-errors.md](patterns-errors.md)
- [setup-channels.md](setup-channels.md)
- [Docs](https://supabase.com/docs/guides/realtime/quotas)

View File

@@ -0,0 +1,78 @@
---
title: Debug Realtime Connections
impact: MEDIUM
impactDescription: Enables visibility into connection and message flow issues
tags: realtime, debugging, logging, troubleshooting
---
## Debug Realtime Connections
Use logging to diagnose connection issues, message flow, and performance problems.
## Client-Side Logging
**Incorrect:**
```javascript
// No logging - no visibility into issues
const supabase = createClient(url, key)
```
**Correct:**
Enable client-side logging with a custom logger function:
```javascript
const supabase = createClient(url, key, {
realtime: {
logger: (kind, msg, data) => {
console.log(`[${kind}] ${msg}`, data)
},
},
})
```
Log message types:
- `push` - Messages sent to server
- `receive` - Messages received from server
- `transport` - Connection events (connect, disconnect, heartbeat)
- `error` - Error events
- `worker` - Web Worker events
## Server-Side Log Level
Configure Realtime server log verbosity via client params:
```javascript
const supabase = createClient(url, key, {
realtime: {
params: {
log_level: 'info', // 'debug' | 'info' | 'warn' | 'error'
},
},
})
```
This affects the verbosity of logs from the Realtime server, not client-side logs.
## Filtering Logs for Debugging
Filter logs to focus on specific events:
```javascript
const supabase = createClient(url, key, {
realtime: {
logger: (kind, msg, data) => {
// Only log push/receive for subscription debugging
if (kind === 'push' || kind === 'receive') {
console.log(`[${kind}] ${msg}`, data)
}
},
},
})
```
## Related
- [patterns-errors.md](patterns-errors.md)
- [Docs](https://supabase.com/docs/guides/troubleshooting/realtime-debugging-with-logger)

View File

@@ -0,0 +1,97 @@
---
title: Handle Realtime Errors and Connection Issues
impact: HIGH
impactDescription: Enables graceful handling of connection failures
tags: realtime, errors, subscribe, status, reconnection
---
## Handle Realtime Errors and Connection Issues
Handle subscription status and errors to provide reliable user experiences.
## Subscription Status Handling
**Incorrect:**
```javascript
// Ignoring subscription status - no visibility into connection issues
channel.subscribe()
```
**Correct:**
```javascript
channel.subscribe((status, err) => {
switch (status) {
case 'SUBSCRIBED':
console.log('Connected!')
break
case 'CHANNEL_ERROR':
console.error('Channel error:', err)
// Client retries automatically
break
case 'TIMED_OUT':
console.error('Connection timed out')
break
case 'CLOSED':
console.log('Channel closed')
break
}
})
```
## Common Error Codes
| Error | Cause | Solution |
|-------|-------|----------|
| `too_many_connections` | Connection limit exceeded | Clean up unused channels, upgrade plan |
| `too_many_joins` | Channel join rate exceeded | Reduce join frequency |
| `ConnectionRateLimitReached` | Max connections reached | Upgrade plan |
| `DatabaseLackOfConnections` | No available DB connections | Increase compute size |
| `TenantNotFound` | Invalid project reference | Verify project URL |
## Automatic Reconnection
Supabase handles reconnection automatically with exponential backoff. No manual re-subscribe is needed.
## Client-Side Logging
Enable client-side logging to debug connection issues:
```javascript
const supabase = createClient(url, key, {
realtime: {
logger: (kind, msg, data) => {
console.log(`[${kind}] ${msg}`, data)
},
},
})
```
Log message types include `push`, `receive`, `transport`, `error`, and `worker`.
## Silent Disconnections in Background
WebSocket connections can disconnect when apps are backgrounded (mobile, inactive tabs). Supabase reconnects automatically. Re-track presence after reconnection if needed:
```javascript
channel.subscribe((status) => {
if (status === 'SUBSCRIBED') {
// Re-track presence after reconnection
channel.track({ user_id: userId, online_at: new Date().toISOString() })
}
})
```
## Authorization Errors
Private channel authorization fails when:
- User not authenticated
- Missing RLS policies on `realtime.messages`
- Token expired
## Related
- [patterns-cleanup.md](patterns-cleanup.md)
- [setup-auth.md](setup-auth.md)
- [Docs](https://supabase.com/docs/guides/realtime/troubleshooting)

View File

@@ -0,0 +1,99 @@
---
title: Listen to Database Changes with Postgres Changes
impact: MEDIUM
impactDescription: Simple database change listeners with scaling limitations
tags: realtime, postgres_changes, database, subscribe, publication
---
## Listen to Database Changes with Postgres Changes
Postgres Changes streams database changes via logical replication. Note: **Broadcast is recommended for applications that demand higher scalability**.
## When to Use Postgres Changes
- Quick prototyping and development
- Low user counts (< 100 concurrent subscribers per table)
- When simplicity is more important than scale
## Basic Setup
**1. Add table to publication:**
```sql
alter publication supabase_realtime add table messages;
```
**2. Subscribe to changes:**
```javascript
const channel = supabase
.channel('db-changes')
.on(
'postgres_changes',
{
event: 'INSERT', // 'INSERT' | 'UPDATE' | 'DELETE' | '*'
schema: 'public',
table: 'messages',
},
(payload) => console.log('New row:', payload.new)
)
.subscribe()
```
## Filter Syntax
```javascript
.on('postgres_changes', {
event: '*',
schema: 'public',
table: 'messages',
filter: 'room_id=eq.123', // Only changes where room_id = 123
}, callback)
```
| Filter | Example |
|--------|---------|
| `eq` | `id=eq.1` |
| `neq` | `status=neq.deleted` |
| `lt`, `lte` | `age=lt.65` |
| `gt`, `gte` | `quantity=gt.10` |
| `in` | `name=in.(red,blue,yellow)` (max 100 values) |
## Receive Old Records on UPDATE/DELETE
By default, only `new` records are sent.
**Incorrect:**
```sql
-- Only new record available in payload
alter publication supabase_realtime add table messages;
```
**Correct:**
```sql
-- Enable old record in payload
alter table messages replica identity full;
alter publication supabase_realtime add table messages;
```
## Scaling Limitation
Each change triggers RLS checks for every subscriber:
```text
100 subscribers = 100 database reads per change
```
For high-traffic tables, migrate to [broadcast-database.md](broadcast-database.md).
## DELETE Events Not Filterable
Filters don't work on DELETE events due to how Postgres logical replication works.
## Related
- [broadcast-database.md](broadcast-database.md)
- [patterns-cleanup.md](patterns-cleanup.md)
- [Docs](https://supabase.com/docs/guides/realtime/postgres-changes)

View File

@@ -0,0 +1,87 @@
---
title: Track User Presence and Online Status
impact: MEDIUM
impactDescription: Enables features like online indicators and typing status
tags: realtime, presence, track, online, state
---
## Track User Presence and Online Status
Presence synchronizes shared state between users. Use sparingly due to computational overhead.
## Track Presence
```javascript
const channel = supabase.channel('room:123', {
config: { private: true },
})
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
console.log('Online users:', Object.keys(state))
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('User joined:', key, newPresences)
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('User left:', key, leftPresences)
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({
user_id: 'user-123',
online_at: new Date().toISOString(),
})
}
})
```
## Get Current State
```javascript
const state = channel.presenceState()
// Returns: { "key1": [{ user_id: "123" }], "key2": [{ user_id: "456" }] }
```
## Stop Tracking
```javascript
await channel.untrack()
```
## Custom Presence Key
By default, presence uses a UUIDv1 key. Override for user-specific tracking.
**Incorrect:**
```javascript
// Each browser tab gets separate presence entry
const channel = supabase.channel('room:123')
```
**Correct:**
```javascript
// Same user shows once across tabs
const channel = supabase.channel('room:123', {
config: {
presence: { key: `user:${userId}` },
},
})
```
## Quotas
| Plan | Presence Messages/Second |
|------|-------------------------|
| Free | 20 |
| Pro | 50 |
| Team/Enterprise | 1,000 |
For Pay as you go customers you can edit these limits on [Realtime Settings](https://supabase.com/dashboard/project/_/realtime/settings)
## Related
- [setup-channels.md](setup-channels.md)
- [patterns-cleanup.md](patterns-cleanup.md)
- [Docs](https://supabase.com/docs/guides/realtime/presence)

View File

@@ -0,0 +1,82 @@
---
title: Configure Private Channels with Authentication
impact: CRITICAL
impactDescription: Prevents unauthorized access to real-time messages
tags: realtime, auth, private, rls, security, setAuth
---
## Configure Private Channels with Authentication
Always use private channels in production. Public channels allow any client to subscribe.
## Enable Private Channels
**Incorrect:**
```javascript
// Public channel - anyone can subscribe
const channel = supabase.channel('room:123:messages')
```
**Correct:**
```javascript
// Private channel requires authentication
const channel = supabase.channel('room:123:messages', {
config: { private: true },
})
```
## RLS Policies on realtime.messages
Private channels require RLS policies on the `realtime.messages` table.
**Read access (subscribe to channel):**
```sql
create policy "authenticated_users_can_receive"
on realtime.messages for select
to authenticated
using (true);
```
**Write access (send to channel):**
```sql
create policy "authenticated_users_can_send"
on realtime.messages for insert
to authenticated
with check (true);
```
**Topic-specific access:**
```sql
-- Only room members can receive messages
create policy "room_members_can_read"
on realtime.messages for select
to authenticated
using (
extension in ('broadcast', 'presence')
and exists (
select 1 from room_members
where user_id = (select auth.uid())
and room_id = split_part(realtime.topic(), ':', 2)::uuid
)
);
```
## Index RLS Policy Columns
Missing indexes slow channel joins significantly.
```sql
create index idx_room_members_user_room
on room_members(user_id, room_id);
```
## Related
- [setup-channels.md](setup-channels.md)
- [broadcast-database.md](broadcast-database.md)
- [Docs](https://supabase.com/docs/guides/realtime/authorization)

View File

@@ -0,0 +1,70 @@
---
title: Create and Configure Realtime Channels
impact: HIGH
impactDescription: Proper channel setup enables reliable real-time communication
tags: realtime, channels, configuration, topics, naming
---
## Create and Configure Realtime Channels
Channels are rooms where clients communicate. Use consistent naming and appropriate configuration.
## Topic Naming Convention
Use `scope:entity:id` format for predictable, filterable topics.
**Incorrect:**
```javascript
// Generic names make filtering impossible
const channel = supabase.channel('messages')
const channel = supabase.channel('room1')
```
**Correct:**
```javascript
// Structured naming enables topic-based RLS policies
const channel = supabase.channel('room:123:messages')
const channel = supabase.channel('user:456:notifications')
const channel = supabase.channel('game:789:moves')
```
## Channel Configuration Options
```javascript
const channel = supabase.channel('room:123:messages', {
config: {
private: true, // Require authentication (recommended)
broadcast: {
self: true, // Receive own messages
ack: true, // Get server acknowledgment
},
presence: {
key: 'user-session-id', // Custom presence key (default: UUIDv1)
},
},
})
```
## Event Naming
Use snake_case for event names.
**Incorrect:**
```javascript
channel.send({ type: 'broadcast', event: 'newMessage', payload: {} })
```
**Correct:**
```javascript
channel.send({ type: 'broadcast', event: 'message_created', payload: {} })
channel.send({ type: 'broadcast', event: 'user_joined', payload: {} })
```
## Related
- [setup-auth.md](setup-auth.md)
- [Docs](https://supabase.com/docs/guides/realtime/concepts)