Getting Started

AI Prompt: Supabase Realtime AI Assistant Guide


How to use#

Copy the prompt to a file in your repo.

Use the "include file" feature from your AI tool to include the prompt when chatting with your AI assistant. For example, with GitHub Copilot, use #<filename>, in Cursor, use @Files, and in Zed, use /file.

Prompt#

1
# Supabase Realtime AI Assistant Guide
2
3
You are an expert developer assistant specializing in Supabase Realtime implementations. This guide provides structured, actionable patterns for AI-driven development assistance.
4
5
## Implementation Rules
6
7
### Do
8
9
- Use `broadcast` for all realtime events (database changes via triggers, messaging, notifications, game state)
10
- Use `presence` sparingly for user state tracking (online status, user counters)
11
- Create indexes for all columns used in RLS policies
12
- Advice to use topic names that corrolate with concepts and tables: `scope:entity` (e.g., `room:123:messages`)
13
- Use snake_case for event names: `entity_action` (e.g., `message_created`)
14
- Include unsubscribe/cleanup logic in all implementations
15
- Set `private: true` for channels using database triggers or RLS policies
16
- Give preference to use private channels over public channels (better security and control)
17
- Implement proper error handling and reconnection logic
18
19
### Don't
20
21
- Use `postgres_changes` for new applications (single-threaded, doesn't scale well) and help migrate to `broadcast from database` on existing applications if necessary
22
- Create multiple subscriptions without proper cleanup
23
- Write complex RLS queries without proper indexing
24
- Use generic event names like "update" or "change"
25
- Subscribe directly in render functions without state management
26
- Use database functions (`realtime.send`, `realtime.broadcast_changes`) in client code
27
28
## Function Selection Decision Table
29
30
| Use Case | Recommended Function | Why Not postgres_changes |
31
| ----------------------------------- | ------------------------------------------------------ | ------------------------------------- |
32
| Custom payloads with business logic | `broadcast` | More flexible, better performance |
33
| Database change notifications | `broadcast` via database triggers | More scalable, customizable payloads |
34
| High-frequency updates | `broadcast` with minimal payload | Better throughput and control |
35
| User presence/status tracking | `presence` (sparingly) | Specialized for state synchronization |
36
| Simple table mirroring | `broadcast` via database triggers | More scalable, customizable payloads |
37
| Client to client communication | `broadcast` without triggers and using only websockets | More flexible, better performance |
38
39
**Note:** `postgres_changes` should be avoided due to scalability limitations. Use `broadcast` with database triggers (`realtime.broadcast_changes`) for all database change notifications.
40
41
## Scalability Best Practices
42
43
### Dedicated Topics for Better Performance
44
45
Using dedicated, granular topics ensures messages are only sent to relevant listeners, significantly improving scalability:
46
47
**❌ Avoid Broad Topics:**
48
49
```javascript
50
// This broadcasts to ALL users, even those not interested
51
const channel = supabase.channel('global:notifications')
52
```
53
54
**✅ Use Dedicated Topics:**
55
56
```javascript
57
// This only broadcasts to users in a specific room
58
const channel = supabase.channel(`room:${roomId}:messages`)
59
60
// This only broadcasts to a specific user
61
const channel = supabase.channel(`user:${userId}:notifications`)
62
63
// This only broadcasts to users with specific permissions
64
const channel = supabase.channel(`admin:${orgId}:alerts`)
65
```
66
67
### Benefits of Dedicated Topics:
68
69
- **Reduced Network Traffic**: Messages only reach interested clients
70
- **Better Performance**: Fewer unnecessary message deliveries
71
- **Improved Security**: Easier to implement targeted RLS policies
72
- **Scalability**: System can handle more concurrent users efficiently
73
- **Cost Optimization**: Reduced bandwidth and processing overhead
74
75
### Topic Naming Strategy:
76
77
- **One topic per room**: `room:123:messages`, `room:123:presence`
78
- **One topic per user**: `user:456:notifications`, `user:456:status`
79
- **One topic per organization**: `org:789:announcements`
80
- **One topic per feature**: `game:123:moves`, `game:123:chat`
81
82
## Naming Conventions
83
84
### Topics (Channels)
85
86
- **Pattern:** `scope:entity` or `scope:entity:id`
87
- **Examples:** `room:123:messages`, `game:456:moves`, `user:789:notifications`
88
- **Public channels:** `public:announcements`, `global:status`
89
90
### Events
91
92
- **Pattern:** `entity_action` (snake_case)
93
- **Examples:** `message_created`, `user_joined`, `game_ended`, `status_changed`
94
- **Avoid:** Generic names like `update`, `change`, `event`
95
96
## Client Setup Patterns
97
98
```javascript
99
// Basic setup
100
const supabase = createClient('URL', 'ANON_KEY')
101
102
// Channel configuration
103
const channel = supabase.channel('room:123:messages', {
104
config: {
105
broadcast: { self: true, ack: true },
106
presence: { key: 'user-session-id', enabled: true },
107
private: true, // Required for RLS authorization
108
},
109
})
110
```
111
112
### Configuration Options
113
114
#### Broadcast Configuration
115
116
- **`self: true`** - Receive your own broadcast messages
117
- **`ack: true`** - Get acknowledgment when server receives your message
118
119
#### Presence Configuration
120
121
- **`enabled: true`** - Enable presence tracking for this channel. This flag is set automatically by client library if `on('presence')` is set.
122
- **`key: string`** - Custom key to identify presence state (useful for user sessions)
123
124
#### Security Configuration
125
126
- **`private: true`** - Require authentication and RLS policies
127
- **`private: false`** - Public channel (default, not recommended for production)
128
129
## Frontend Framework Integration
130
131
### React Pattern
132
133
```javascript
134
const channelRef = useRef(null)
135
136
useEffect(() => {
137
// Check if already subscribed to prevent multiple subscriptions
138
if (channelRef.current?.state === 'subscribed') return
139
const channel = supabase.channel('room:123:messages', {
140
config: { private: true }
141
})
142
channelRef.current = channel
143
144
// Set auth before subscribing
145
await supabase.realtime.setAuth()
146
147
channel
148
.on('broadcast', { event: 'message_created' }, handleMessage)
149
.on('broadcast', { event: 'user_joined' }, handleUserJoined)
150
.subscribe()
151
152
return () => {
153
if (channelRef.current) {
154
supabase.removeChannel(channelRef.current)
155
channelRef.current = null
156
}
157
}
158
}, [roomId])
159
```
160
161
## Database Triggers
162
163
### Using realtime.broadcast_changes (Recommended for database changes)
164
165
This would be an example of catch all trigger function that would broadcast to topics starting with the table name and the id of the row.
166
167
```sql
168
CREATE OR REPLACE FUNCTION notify_table_changes()
169
RETURNS TRIGGER AS $$
170
SECURITY DEFINER
171
LANGUAGE plpgsql
172
AS $$
173
BEGIN
174
PERFORM realtime.broadcast_changes(
175
TG_TABLE_NAME ||':' || COALESCE(NEW.id, OLD.id)::text,
176
TG_OP,
177
TG_OP,
178
TG_TABLE_NAME,
179
TG_TABLE_SCHEMA,
180
NEW,
181
OLD
182
);
183
RETURN COALESCE(NEW, OLD);
184
END;
185
$$;
186
```
187
188
But you can also create more specific trigger functions for specific tables and events so adapt to your use case:
189
190
```sql
191
CREATE OR REPLACE FUNCTION room_messages_broadcast_trigger()
192
RETURNS TRIGGER AS $$
193
SECURITY DEFINER
194
LANGUAGE plpgsql
195
AS $$
196
BEGIN
197
PERFORM realtime.broadcast_changes(
198
'room:' || COALESCE(NEW.room_id, OLD.room_id)::text,
199
TG_OP,
200
TG_OP,
201
TG_TABLE_NAME,
202
TG_TABLE_SCHEMA,
203
NEW,
204
OLD
205
);
206
RETURN COALESCE(NEW, OLD);
207
END;
208
$$;
209
```
210
211
By default, `realtime.broadcast_changes` requires you to use private channels as we did this to prevent security incidents.
212
213
### Using realtime.send (For custom messages)
214
215
```sql
216
CREATE OR REPLACE FUNCTION notify_custom_event()
217
RETURNS TRIGGER AS $$
218
SECURITY DEFINER
219
LANGUAGE plpgsql
220
AS $$
221
BEGIN
222
PERFORM realtime.send(
223
'room:' || NEW.room_id::text,
224
'status_changed',
225
jsonb_build_object('id', NEW.id, 'status', NEW.status),
226
false
227
);
228
RETURN NEW;
229
END;
230
$$;
231
```
232
233
This allows us to broadcast to a specific room with any content that is not bound to a table or if you need to send data to public channels. It's also a good way to integrate with other services and extensions.
234
235
### Conditional Broadcasting
236
237
If you need to broadcast only significant changes, you can use the following pattern:
238
239
```sql
240
-- Only broadcast significant changes
241
IF TG_OP = 'UPDATE' AND OLD.status IS DISTINCT FROM NEW.status THEN
242
PERFORM realtime.broadcast_changes(
243
'room:' || NEW.room_id::text,
244
TG_OP,
245
TG_OP,
246
TG_TABLE_NAME,
247
TG_TABLE_SCHEMA,
248
NEW,
249
OLD
250
);
251
END IF;
252
```
253
254
This is just an example as you can use any logic you want that is SQL compatible.
255
256
## Authorization Setup
257
258
### Basic RLS Setup
259
260
To access a private channel you need to set RLS policies against `realtime.messages` table for SELECT operations.
261
262
```sql
263
-- Simple policy with indexed columns
264
CREATE POLICY "room_members_can_read" ON realtime.messages
265
FOR SELECT TO authenticated
266
USING (
267
topic LIKE 'room:%' AND
268
EXISTS (
269
SELECT 1 FROM room_members
270
WHERE user_id = auth.uid()
271
AND room_id = SPLIT_PART(topic, ':', 2)::uuid
272
)
273
);
274
275
-- Required index for performance
276
CREATE INDEX idx_room_members_user_room
277
ON room_members(user_id, room_id);
278
```
279
280
To write to a private channel you need to set RLS policies against `realtime.messages` table for INSERT operations.
281
282
```sql
283
-- Simple policy with indexed columns
284
CREATE POLICY "room_members_can_write" ON realtime.messages
285
FOR INSERT TO authenticated
286
USING (
287
topic LIKE 'room:%' AND
288
EXISTS (
289
SELECT 1 FROM room_members
290
WHERE user_id = auth.uid()
291
AND room_id = SPLIT_PART(topic, ':', 2)::uuid
292
)
293
);
294
```
295
296
### Client Authorization
297
298
```javascript
299
const channel = supabase
300
.channel('room:123:messages', {
301
config: { private: true },
302
})
303
.on('broadcast', { event: 'message_created' }, handleMessage)
304
.on('broadcast', { event: 'user_joined' }, handleUserJoined)
305
306
// Set auth before subscribing
307
await supabase.realtime.setAuth()
308
309
// Subscribe after auth is set
310
await channel.subscribe()
311
```
312
313
### Enhanced Security: Private-Only Channels
314
315
**Enable private-only channels** in Realtime Settings (Dashboard > Project Settings > Realtime Settings) to enforce authorization on all channels and prevent public channel access. This setting requires all clients to use `private: true` and proper authentication, providing additional security for production applications.
316
317
## Error Handling & Reconnection
318
319
### Automatic Reconnection (Built-in)
320
321
**Supabase Realtime client handles reconnection automatically:**
322
323
- Built-in exponential backoff for connection retries
324
- Automatic channel rejoining after network interruptions
325
- Configurable reconnection timing via `reconnectAfterMs` option
326
327
### Channel States
328
329
The client automatically manages these states:
330
331
- **`SUBSCRIBED`** - Successfully connected and receiving messages
332
- **`TIMED_OUT`** - Connection attempt timed out
333
- **`CLOSED`** - Channel is closed
334
- **`CHANNEL_ERROR`** - Error occurred, client will automatically retry
335
336
```javascript
337
// Client automatically reconnects with built-in logic
338
const supabase = createClient('URL', 'ANON_KEY', {
339
realtime: {
340
params: {
341
log_level: 'info',
342
reconnectAfterMs: 1000, // Custom reconnection timing
343
},
344
},
345
})
346
347
// Simple connection state monitoring
348
channel.subscribe((status, err) => {
349
switch (status) {
350
case 'SUBSCRIBED':
351
console.log('Connected (or reconnected)')
352
break
353
case 'CHANNEL_ERROR':
354
console.error('Channel error:', err)
355
// Client will automatically retry - no manual intervention needed
356
break
357
case 'CLOSED':
358
console.log('Channel closed')
359
break
360
}
361
})
362
```
363
364
## Performance & Scaling Guidelines
365
366
### Channel Structure Optimization
367
368
- Use one channel per logical scope (`room:123`, not `user:456:room:123`)
369
- Shard high-volume topics: `chat:shard:1`, `chat:shard:2`
370
- Ensure you have enough connections set in your pool, you can refer to [Realtime Settings](https://supabase.com/docs/guides/realtime/settings) and the option `Database connection pool size` to set it.
371
372
## Debugging
373
374
### Enhanced Logging
375
376
```javascript
377
const supabase = createClient(url, key, {
378
realtime: {
379
params: { log_level: 'info' },
380
},
381
})
382
```
383
384
## Migration from Postgres Changes
385
386
### Step 1: Replace Client Code
387
388
```javascript
389
// ❌ Remove postgres_changes
390
const oldChannel = supabase
391
.channel('changes')
392
.on('postgres_changes', { event: '*', schema: 'public', table: 'messages' }, callback)
393
394
// ✅ Replace with broadcast
395
const room_id = 'room_id' // or any other identifier that you use in the trigger function
396
const newChannel = supabase
397
.channel(`messages:${room_id}:changes`, {
398
config: { private: true },
399
})
400
.on('broadcast', { event: 'INSERT' }, callback)
401
.on('broadcast', { event: 'DELETE' }, callback)
402
.on('broadcast', { event: 'UPDATE' }, callback)
403
```
404
405
### Step 2: Add Database Trigger (Only if using broadcast)
406
407
```sql
408
CREATE TRIGGER messages_broadcast_trigger
409
AFTER INSERT OR UPDATE OR DELETE ON messages
410
FOR EACH ROW EXECUTE FUNCTION notify_table_changes();
411
```
412
413
### Step 3: Setup Authorization (Only if using broadcast)
414
415
```sql
416
CREATE POLICY "users_can_receive_broadcasts" ON realtime.messages
417
FOR SELECT TO authenticated USING (true);
418
```
419
420
## LLM Implementation Guidelines
421
422
### When Generating Code
423
424
1. **Prefer using `broadcast` over `postgres_changes`** - Never suggest `postgres_changes` 2.**Include state checking** - Check channel state before subscribing
425
2. **Add cleanup logic** - Include unsubscribe in all examples
426
3. **Suggest proper naming** - Use consistent topic/event conventions
427
4. **Include error handling** - Add reconnection patterns
428
5. **Recommend indexing** - When RLS policies are used
429
6. **Framework-agnostic** - Adapt patterns to user's framework
430
7. **Inform users to prefer the usage of private channels only** - users can refer to [Realtime Settings](https://supabase.com/docs/guides/realtime/settings) to enable it.
431
432
### Code Generation Checklist
433
434
- ✅ Favor `broadcast` over `postgres_changes`
435
- ✅ Checks `channel.state` before subscribing
436
- ✅ Includes proper cleanup/unsubscribe logic
437
- ✅ Uses consistent naming conventions
438
- ✅ Includes error handling and reconnection
439
- ✅ Suggests indexes for RLS policies
440
- ✅ Sets `private: true` for database triggers
441
- ✅ Implements token refresh if needed
442
443
### Safe Defaults for AI Assistants
444
445
- Channel pattern: `scope:entity:id`
446
- Event pattern: `entity_action`
447
- Always check channel state before subscribing
448
- Always include cleanup
449
- Default to `private: true` for database-triggered channels
450
- Suggest basic RLS policies with proper indexing
451
- Include reconnection logic for production apps
452
- Use `broadcast` with database triggers for all database change notifications
453
- Use `broadcast` for custom events and complex payloads
454
455
**Remember:** Choose the right function for your use case, emphasize proper state management, and ensure production-ready patterns with authorization and error handling.