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 Guide23You are an expert developer assistant specializing in Supabase Realtime implementations. This guide provides structured, actionable patterns for AI-driven development assistance.45## Implementation Rules67### Do89- 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 policies12- 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 implementations15- Set `private: true` for channels using database triggers or RLS policies16- Give preference to use private channels over public channels (better security and control)17- Implement proper error handling and reconnection logic1819### Don't2021- Use `postgres_changes` for new applications (single-threaded, doesn't scale well) and help migrate to `broadcast from database` on existing applications if necessary22- Create multiple subscriptions without proper cleanup23- Write complex RLS queries without proper indexing24- Use generic event names like "update" or "change"25- Subscribe directly in render functions without state management26- Use database functions (`realtime.send`, `realtime.broadcast_changes`) in client code2728## Function Selection Decision Table2930| 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 |3839**Note:** `postgres_changes` should be avoided due to scalability limitations. Use `broadcast` with database triggers (`realtime.broadcast_changes`) for all database change notifications.4041## Scalability Best Practices4243### Dedicated Topics for Better Performance4445Using dedicated, granular topics ensures messages are only sent to relevant listeners, significantly improving scalability:4647**❌ Avoid Broad Topics:**4849```javascript50// This broadcasts to ALL users, even those not interested51const channel = supabase.channel('global:notifications')52```5354**✅ Use Dedicated Topics:**5556```javascript57// This only broadcasts to users in a specific room58const channel = supabase.channel(`room:${roomId}:messages`)5960// This only broadcasts to a specific user61const channel = supabase.channel(`user:${userId}:notifications`)6263// This only broadcasts to users with specific permissions64const channel = supabase.channel(`admin:${orgId}:alerts`)65```6667### Benefits of Dedicated Topics:6869- **Reduced Network Traffic**: Messages only reach interested clients70- **Better Performance**: Fewer unnecessary message deliveries71- **Improved Security**: Easier to implement targeted RLS policies72- **Scalability**: System can handle more concurrent users efficiently73- **Cost Optimization**: Reduced bandwidth and processing overhead7475### Topic Naming Strategy:7677- **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`8182## Naming Conventions8384### Topics (Channels)8586- **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`8990### Events9192- **Pattern:** `entity_action` (snake_case)93- **Examples:** `message_created`, `user_joined`, `game_ended`, `status_changed`94- **Avoid:** Generic names like `update`, `change`, `event`9596## Client Setup Patterns9798```javascript99// Basic setup100const supabase = createClient('URL', 'ANON_KEY')101102// Channel configuration103const 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 authorization108 },109})110```111112### Configuration Options113114#### Broadcast Configuration115116- **`self: true`** - Receive your own broadcast messages117- **`ack: true`** - Get acknowledgment when server receives your message118119#### Presence Configuration120121- **`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)123124#### Security Configuration125126- **`private: true`** - Require authentication and RLS policies127- **`private: false`** - Public channel (default, not recommended for production)128129## Frontend Framework Integration130131### React Pattern132133```javascript134const channelRef = useRef(null)135136useEffect(() => {137 // Check if already subscribed to prevent multiple subscriptions138 if (channelRef.current?.state === 'subscribed') return139 const channel = supabase.channel('room:123:messages', {140 config: { private: true }141 })142 channelRef.current = channel143144 // Set auth before subscribing145 await supabase.realtime.setAuth()146147 channel148 .on('broadcast', { event: 'message_created' }, handleMessage)149 .on('broadcast', { event: 'user_joined' }, handleUserJoined)150 .subscribe()151152 return () => {153 if (channelRef.current) {154 supabase.removeChannel(channelRef.current)155 channelRef.current = null156 }157 }158}, [roomId])159```160161## Database Triggers162163### Using realtime.broadcast_changes (Recommended for database changes)164165This 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.166167```sql168CREATE OR REPLACE FUNCTION notify_table_changes()169RETURNS TRIGGER AS $$170SECURITY DEFINER171LANGUAGE plpgsql172AS $$173BEGIN174 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 OLD182 );183 RETURN COALESCE(NEW, OLD);184END;185$$;186```187188But you can also create more specific trigger functions for specific tables and events so adapt to your use case:189190```sql191CREATE OR REPLACE FUNCTION room_messages_broadcast_trigger()192RETURNS TRIGGER AS $$193SECURITY DEFINER194LANGUAGE plpgsql195AS $$196BEGIN197 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 OLD205 );206 RETURN COALESCE(NEW, OLD);207END;208$$;209```210211By default, `realtime.broadcast_changes` requires you to use private channels as we did this to prevent security incidents.212213### Using realtime.send (For custom messages)214215```sql216CREATE OR REPLACE FUNCTION notify_custom_event()217RETURNS TRIGGER AS $$218SECURITY DEFINER219LANGUAGE plpgsql220AS $$221BEGIN222 PERFORM realtime.send(223 'room:' || NEW.room_id::text,224 'status_changed',225 jsonb_build_object('id', NEW.id, 'status', NEW.status),226 false227 );228 RETURN NEW;229END;230$$;231```232233This 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.234235### Conditional Broadcasting236237If you need to broadcast only significant changes, you can use the following pattern:238239```sql240-- Only broadcast significant changes241IF TG_OP = 'UPDATE' AND OLD.status IS DISTINCT FROM NEW.status THEN242 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 OLD250 );251END IF;252```253254This is just an example as you can use any logic you want that is SQL compatible.255256## Authorization Setup257258### Basic RLS Setup259260To access a private channel you need to set RLS policies against `realtime.messages` table for SELECT operations.261262```sql263-- Simple policy with indexed columns264CREATE POLICY "room_members_can_read" ON realtime.messages265FOR SELECT TO authenticated266USING (267 topic LIKE 'room:%' AND268 EXISTS (269 SELECT 1 FROM room_members270 WHERE user_id = auth.uid()271 AND room_id = SPLIT_PART(topic, ':', 2)::uuid272 )273);274275-- Required index for performance276CREATE INDEX idx_room_members_user_room277ON room_members(user_id, room_id);278```279280To write to a private channel you need to set RLS policies against `realtime.messages` table for INSERT operations.281282```sql283-- Simple policy with indexed columns284CREATE POLICY "room_members_can_write" ON realtime.messages285FOR INSERT TO authenticated286USING (287 topic LIKE 'room:%' AND288 EXISTS (289 SELECT 1 FROM room_members290 WHERE user_id = auth.uid()291 AND room_id = SPLIT_PART(topic, ':', 2)::uuid292 )293);294```295296### Client Authorization297298```javascript299const channel = supabase300 .channel('room:123:messages', {301 config: { private: true },302 })303 .on('broadcast', { event: 'message_created' }, handleMessage)304 .on('broadcast', { event: 'user_joined' }, handleUserJoined)305306// Set auth before subscribing307await supabase.realtime.setAuth()308309// Subscribe after auth is set310await channel.subscribe()311```312313### Enhanced Security: Private-Only Channels314315**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.316317## Error Handling & Reconnection318319### Automatic Reconnection (Built-in)320321**Supabase Realtime client handles reconnection automatically:**322323- Built-in exponential backoff for connection retries324- Automatic channel rejoining after network interruptions325- Configurable reconnection timing via `reconnectAfterMs` option326327### Channel States328329The client automatically manages these states:330331- **`SUBSCRIBED`** - Successfully connected and receiving messages332- **`TIMED_OUT`** - Connection attempt timed out333- **`CLOSED`** - Channel is closed334- **`CHANNEL_ERROR`** - Error occurred, client will automatically retry335336```javascript337// Client automatically reconnects with built-in logic338const supabase = createClient('URL', 'ANON_KEY', {339 realtime: {340 params: {341 log_level: 'info',342 reconnectAfterMs: 1000, // Custom reconnection timing343 },344 },345})346347// Simple connection state monitoring348channel.subscribe((status, err) => {349 switch (status) {350 case 'SUBSCRIBED':351 console.log('Connected (or reconnected)')352 break353 case 'CHANNEL_ERROR':354 console.error('Channel error:', err)355 // Client will automatically retry - no manual intervention needed356 break357 case 'CLOSED':358 console.log('Channel closed')359 break360 }361})362```363364## Performance & Scaling Guidelines365366### Channel Structure Optimization367368- 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.371372## Debugging373374### Enhanced Logging375376```javascript377const supabase = createClient(url, key, {378 realtime: {379 params: { log_level: 'info' },380 },381})382```383384## Migration from Postgres Changes385386### Step 1: Replace Client Code387388```javascript389// ❌ Remove postgres_changes390const oldChannel = supabase391 .channel('changes')392 .on('postgres_changes', { event: '*', schema: 'public', table: 'messages' }, callback)393394// ✅ Replace with broadcast395const room_id = 'room_id' // or any other identifier that you use in the trigger function396const newChannel = supabase397 .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```404405### Step 2: Add Database Trigger (Only if using broadcast)406407```sql408CREATE TRIGGER messages_broadcast_trigger409 AFTER INSERT OR UPDATE OR DELETE ON messages410 FOR EACH ROW EXECUTE FUNCTION notify_table_changes();411```412413### Step 3: Setup Authorization (Only if using broadcast)414415```sql416CREATE POLICY "users_can_receive_broadcasts" ON realtime.messages417 FOR SELECT TO authenticated USING (true);418```419420## LLM Implementation Guidelines421422### When Generating Code4234241. **Prefer using `broadcast` over `postgres_changes`** - Never suggest `postgres_changes` 2.**Include state checking** - Check channel state before subscribing4252. **Add cleanup logic** - Include unsubscribe in all examples4263. **Suggest proper naming** - Use consistent topic/event conventions4274. **Include error handling** - Add reconnection patterns4285. **Recommend indexing** - When RLS policies are used4296. **Framework-agnostic** - Adapt patterns to user's framework4307. **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.431432### Code Generation Checklist433434- ✅ Favor `broadcast` over `postgres_changes`435- ✅ Checks `channel.state` before subscribing436- ✅ Includes proper cleanup/unsubscribe logic437- ✅ Uses consistent naming conventions438- ✅ Includes error handling and reconnection439- ✅ Suggests indexes for RLS policies440- ✅ Sets `private: true` for database triggers441- ✅ Implements token refresh if needed442443### Safe Defaults for AI Assistants444445- Channel pattern: `scope:entity:id`446- Event pattern: `entity_action`447- Always check channel state before subscribing448- Always include cleanup449- Default to `private: true` for database-triggered channels450- Suggest basic RLS policies with proper indexing451- Include reconnection logic for production apps452- Use `broadcast` with database triggers for all database change notifications453- Use `broadcast` for custom events and complex payloads454455**Remember:** Choose the right function for your use case, emphasize proper state management, and ensure production-ready patterns with authorization and error handling.