mirror of
https://github.com/supabase/agent-skills.git
synced 2026-03-27 10:09:26 +08:00
feat: realtime agent references
This commit is contained in:
@@ -24,10 +24,15 @@ supabase/
|
|||||||
| Priority | Category | Impact | Prefix |
|
| Priority | Category | Impact | Prefix |
|
||||||
|----------|----------|--------|--------|
|
|----------|----------|--------|--------|
|
||||||
| 1 | Row Level Security | CRITICAL | `rls-` |
|
| 1 | Row Level Security | CRITICAL | `rls-` |
|
||||||
|
| 1 | Channel Setup | HIGH | `setup-` |
|
||||||
| 2 | Connection Pooling | CRITICAL | `conn-` |
|
| 2 | Connection Pooling | CRITICAL | `conn-` |
|
||||||
|
| 2 | Broadcast Messaging | CRITICAL | `broadcast-` |
|
||||||
| 3 | Schema Design | HIGH | `schema-` |
|
| 3 | Schema Design | HIGH | `schema-` |
|
||||||
|
| 3 | Presence Tracking | MEDIUM | `presence-` |
|
||||||
| 4 | Migrations | HIGH | `migrations-` |
|
| 4 | Migrations | HIGH | `migrations-` |
|
||||||
|
| 4 | Postgres Changes | MEDIUM | `postgres-` |
|
||||||
| 5 | Performance | CRITICAL | `perf-` |
|
| 5 | Performance | CRITICAL | `perf-` |
|
||||||
|
| 5 | Implementation Patterns | CRITICAL | `patterns-` |
|
||||||
| 6 | Security | CRITICAL | `security-` |
|
| 6 | Security | CRITICAL | `security-` |
|
||||||
|
|
||||||
Reference files are named `{prefix}-{topic}.md` (e.g., `query-missing-indexes.md`).
|
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-functions.md`
|
||||||
- `references/db/security-service-role.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*
|
||||||
@@ -71,9 +71,14 @@ Reference the appropriate resource file based on the user's needs:
|
|||||||
|
|
||||||
### Realtime
|
### Realtime
|
||||||
|
|
||||||
| Area | Resource | When to Use |
|
| Area | Resource | When to Use |
|
||||||
| -------- | ------------------------ | -------------------------------------------- |
|
| ---------------- | ----------------------------------- | --------------------------------------------------- |
|
||||||
| Realtime | `references/realtime.md` | Real-time subscriptions, presence, broadcast |
|
| 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
|
### Client Libraries & CLI
|
||||||
|
|
||||||
|
|||||||
31
skills/supabase/references/realtime/_sections.md
Normal file
31
skills/supabase/references/realtime/_sections.md
Normal 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.
|
||||||
98
skills/supabase/references/realtime/broadcast-basics.md
Normal file
98
skills/supabase/references/realtime/broadcast-basics.md
Normal 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)
|
||||||
96
skills/supabase/references/realtime/broadcast-database.md
Normal file
96
skills/supabase/references/realtime/broadcast-database.md
Normal 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)
|
||||||
94
skills/supabase/references/realtime/patterns-cleanup.md
Normal file
94
skills/supabase/references/realtime/patterns-cleanup.md
Normal 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)
|
||||||
98
skills/supabase/references/realtime/patterns-errors.md
Normal file
98
skills/supabase/references/realtime/patterns-errors.md
Normal 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)
|
||||||
99
skills/supabase/references/realtime/postgres-changes.md
Normal file
99
skills/supabase/references/realtime/postgres-changes.md
Normal 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)
|
||||||
87
skills/supabase/references/realtime/presence-tracking.md
Normal file
87
skills/supabase/references/realtime/presence-tracking.md
Normal 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)
|
||||||
83
skills/supabase/references/realtime/setup-auth.md
Normal file
83
skills/supabase/references/realtime/setup-auth.md
Normal 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)
|
||||||
70
skills/supabase/references/realtime/setup-channels.md
Normal file
70
skills/supabase/references/realtime/setup-channels.md
Normal 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)
|
||||||
Reference in New Issue
Block a user