feat: realtime agent references

This commit is contained in:
Pedro Rodrigues
2026-01-27 22:19:10 +00:00
parent f5587cb4cf
commit 3cfd5f9ed8
11 changed files with 788 additions and 4 deletions

View File

@@ -24,10 +24,15 @@ supabase/
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | Row Level Security | CRITICAL | `rls-` |
| 1 | Channel Setup | HIGH | `setup-` |
| 2 | Connection Pooling | CRITICAL | `conn-` |
| 2 | Broadcast Messaging | CRITICAL | `broadcast-` |
| 3 | Schema Design | HIGH | `schema-` |
| 3 | Presence Tracking | MEDIUM | `presence-` |
| 4 | Migrations | HIGH | `migrations-` |
| 4 | Postgres Changes | MEDIUM | `postgres-` |
| 5 | Performance | CRITICAL | `perf-` |
| 5 | Implementation Patterns | CRITICAL | `patterns-` |
| 6 | Security | CRITICAL | `security-` |
Reference files are named `{prefix}-{topic}.md` (e.g., `query-missing-indexes.md`).
@@ -64,6 +69,24 @@ Reference files are named `{prefix}-{topic}.md` (e.g., `query-missing-indexes.md
- `references/db/security-functions.md`
- `references/db/security-service-role.md`
**Broadcast Messaging** (`broadcast-`):
- `references/realtime/broadcast-basics.md`
- `references/realtime/broadcast-database.md`
**Implementation Patterns** (`patterns-`):
- `references/realtime/patterns-cleanup.md`
- `references/realtime/patterns-errors.md`
**Postgres Changes** (`postgres-`):
- `references/realtime/postgres-changes.md`
**Presence Tracking** (`presence-`):
- `references/realtime/presence-tracking.md`
**Channel Setup** (`setup-`):
- `references/realtime/setup-auth.md`
- `references/realtime/setup-channels.md`
---
*18 reference files across 6 categories*
*26 reference files across 11 categories*

View File

@@ -72,8 +72,13 @@ Reference the appropriate resource file based on the user's needs:
### Realtime
| Area | Resource | When to Use |
| -------- | ------------------------ | -------------------------------------------- |
| ---------------- | ----------------------------------- | --------------------------------------------------- |
| Realtime | `references/realtime.md` | Real-time subscriptions, presence, broadcast |
| 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 |
### Client Libraries & CLI

View File

@@ -0,0 +1,31 @@
# Section Definitions
Reference files are grouped by prefix. Claude loads specific files based on user
queries.
---
## 1. Channel Setup (setup)
**Impact:** HIGH
**Description:** Channel creation, naming conventions, configuration options, and authentication setup for private channels.
## 2. Broadcast Messaging (broadcast)
**Impact:** CRITICAL
**Description:** Sending and receiving real-time messages between clients, database-triggered broadcasts using `realtime.broadcast_changes()` and `realtime.send()`.
## 3. Presence Tracking (presence)
**Impact:** MEDIUM
**Description:** Tracking user online status, shared state synchronization, and presence lifecycle management.
## 4. Postgres Changes (postgres)
**Impact:** MEDIUM
**Description:** Database change listeners via logical replication. Note: Broadcast is recommended for new applications due to better scalability.
## 5. Implementation Patterns (patterns)
**Impact:** CRITICAL
**Description:** Channel cleanup, React integration patterns, error handling, and connection management best practices.

View File

@@ -0,0 +1,98 @@
---
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 production applications.
## 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
const { error } = await channel.send({
type: 'broadcast',
event: 'message_created',
payload: { 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,96 @@
---
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
await supabase.realtime.setAuth()
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,94 @@
---
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
supabase.realtime.setAuth()
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.
## 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,98 @@
---
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 with exponential backoff. Configure timing:
```javascript
const supabase = createClient(url, key, {
realtime: {
params: {
log_level: 'info', // 'debug' | 'info' | 'warn' | 'error'
},
},
})
```
## Silent Disconnections in Background
WebSocket connections can disconnect when apps are backgrounded (mobile, inactive tabs).
**Solution:** Monitor connection state and re-subscribe when needed:
```javascript
channel.subscribe((status) => {
if (status === 'SUBSCRIBED') {
// Re-track presence if needed
channel.track({ user_id: userId, online_at: new Date().toISOString() })
}
})
```
## Authorization Errors
Private channel authorization fails when:
- User not authenticated (`setAuth()` not called)
- Missing RLS policies on `realtime.messages`
- Token expired (refresh before expiry)
```javascript
// Refresh auth before token expires
await supabase.realtime.setAuth('fresh-jwt-token')
```
## 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 new applications** due to better 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 |
## 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,83 @@
---
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
await supabase.realtime.setAuth() // Must call before subscribing
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)