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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421---# Specify the following for Cursor rulesdescription: Coding rules for Supabase RealtimealwaysApply: false---# Supabase Realtime AI Assistant GuideYou are an expert developer assistant specializing in Supabase Realtime implementations. This guide provides structured, actionable patterns for AI-driven development assistance.## Implementation Rules### Do- Use `broadcast` for all realtime events (database changes via triggers, messaging, notifications, game state)- Use `presence` sparingly for user state tracking (online status, user counters)- Create indexes for all columns used in RLS policies- Advice to use topic names that corrolate with concepts and tables: `scope:entity` (e.g., `room:123:messages`)- Use snake_case for event names: `entity_action` (e.g., `message_created`)- Include unsubscribe/cleanup logic in all implementations- Set `private: true` for channels using database triggers or RLS policies- Give preference to use private channels over public channels (better security and control)- Implement proper error handling and reconnection logic### Don't- Use `postgres_changes` for new applications (single-threaded, doesn't scale well) and help migrate to `broadcast from database` on existing applications if necessary- Create multiple subscriptions without proper cleanup- Write complex RLS queries without proper indexing- Use generic event names like "update" or "change"- Subscribe directly in render functions without state management- Use database functions (`realtime.send`, `realtime.broadcast_changes`) in client code## Function Selection Decision Table| Use Case | Recommended Function | Why Not postgres_changes ||----------|---------------------|--------------------------|| Custom payloads with business logic | `broadcast` | More flexible, better performance || Database change notifications | `broadcast` via database triggers | More scalable, customizable payloads || High-frequency updates | `broadcast` with minimal payload | Better throughput and control || User presence/status tracking | `presence` (sparingly) | Specialized for state synchronization || Simple table mirroring | `broadcast` via database triggers | More scalable, customizable payloads || Client to client communication | `broadcast` without triggers and using only websockets | More flexible, better performance |**Note:** `postgres_changes` should be avoided due to scalability limitations. Use `broadcast` with database triggers (`realtime.broadcast_changes`) for all database change notifications.## Scalability Best Practices### Dedicated Topics for Better PerformanceUsing dedicated, granular topics ensures messages are only sent to relevant listeners, significantly improving scalability:**❌ Avoid Broad Topics:**```javascript// This broadcasts to ALL users, even those not interestedconst channel = supabase.channel('global:notifications')```**✅ Use Dedicated Topics:**```javascript// This only broadcasts to users in a specific roomconst channel = supabase.channel(`room:${roomId}:messages`)// This only broadcasts to a specific userconst channel = supabase.channel(`user:${userId}:notifications`)// This only broadcasts to users with specific permissionsconst channel = supabase.channel(`admin:${orgId}:alerts`)```### Benefits of Dedicated Topics:- **Reduced Network Traffic**: Messages only reach interested clients- **Better Performance**: Fewer unnecessary message deliveries- **Improved Security**: Easier to implement targeted RLS policies- **Scalability**: System can handle more concurrent users efficiently- **Cost Optimization**: Reduced bandwidth and processing overhead### Topic Naming Strategy:- **One topic per room**: `room:123:messages`, `room:123:presence`- **One topic per user**: `user:456:notifications`, `user:456:status`- **One topic per organization**: `org:789:announcements`- **One topic per feature**: `game:123:moves`, `game:123:chat`## Naming Conventions### Topics (Channels)- **Pattern:** `scope:entity` or `scope:entity:id`- **Examples:** `room:123:messages`, `game:456:moves`, `user:789:notifications`- **Public channels:** `public:announcements`, `global:status`### Events- **Pattern:** `entity_action` (snake_case)- **Examples:** `message_created`, `user_joined`, `game_ended`, `status_changed`- **Avoid:** Generic names like `update`, `change`, `event`## Client Setup Patterns```javascript// Basic setupconst supabase = createClient('URL', 'ANON_KEY')// Channel configurationconst channel = supabase.channel('room:123:messages', { config: { broadcast: { self: true, ack: true }, presence: { key: 'user-session-id', enabled: true }, private: true // Required for RLS authorization }})```### Configuration Options#### Broadcast Configuration- **`self: true`** - Receive your own broadcast messages- **`ack: true`** - Get acknowledgment when server receives your message#### Presence Configuration- **`enabled: true`** - Enable presence tracking for this channel. This flag is set automatically by client library if `on('presence')` is set.- **`key: string`** - Custom key to identify presence state (useful for user sessions)#### Security Configuration- **`private: true`** - Require authentication and RLS policies- **`private: false`** - Public channel (default, not recommended for production)## Frontend Framework Integration### React Pattern```javascriptconst channelRef = useRef(null)useEffect(() => { // Check if already subscribed to prevent multiple subscriptions if (channelRef.current?.state === 'subscribed') return const channel = supabase.channel('room:123:messages', { config: { private: true } }) channelRef.current = channel // Set auth before subscribing await supabase.realtime.setAuth() channel .on('broadcast', { event: 'message_created' }, handleMessage) .on('broadcast', { event: 'user_joined' }, handleUserJoined) .subscribe() return () => { if (channelRef.current) { supabase.removeChannel(channelRef.current) channelRef.current = null } }}, [roomId])```## Database Triggers### Using realtime.broadcast_changes (Recommended for database changes)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.```sqlCREATE OR REPLACE FUNCTION notify_table_changes()RETURNS TRIGGER AS $$SECURITY DEFINERLANGUAGE plpgsqlAS $$BEGIN PERFORM realtime.broadcast_changes( TG_TABLE_NAME ||':' || COALESCE(NEW.id, OLD.id)::text, TG_OP, TG_OP, TG_TABLE_NAME, TG_TABLE_SCHEMA, NEW, OLD ); RETURN COALESCE(NEW, OLD);END;$$;```But you can also create more specific trigger functions for specific tables and events so adapt to your use case:```sqlCREATE OR REPLACE FUNCTION room_messages_broadcast_trigger()RETURNS TRIGGER AS $$SECURITY DEFINERLANGUAGE plpgsqlAS $$BEGIN PERFORM realtime.broadcast_changes( 'room:' || COALESCE(NEW.room_id, OLD.room_id)::text, TG_OP, TG_OP, TG_TABLE_NAME, TG_TABLE_SCHEMA, NEW, OLD ); RETURN COALESCE(NEW, OLD);END;$$;```By default, `realtime.broadcast_changes` requires you to use private channels as we did this to prevent security incidents.### Using realtime.send (For custom messages)```sqlCREATE OR REPLACE FUNCTION notify_custom_event()RETURNS TRIGGER AS $$SECURITY DEFINERLANGUAGE plpgsqlAS $$BEGIN PERFORM realtime.send( 'room:' || NEW.room_id::text, 'status_changed', jsonb_build_object('id', NEW.id, 'status', NEW.status), false ); RETURN NEW;END;$$;```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.### Conditional BroadcastingIf you need to broadcast only significant changes, you can use the following pattern:```sql-- Only broadcast significant changesIF TG_OP = 'UPDATE' AND OLD.status IS DISTINCT FROM NEW.status THEN PERFORM realtime.broadcast_changes( 'room:' || NEW.room_id::text, TG_OP, TG_OP, TG_TABLE_NAME, TG_TABLE_SCHEMA, NEW, OLD );END IF;```This is just an example as you can use any logic you want that is SQL compatible.## Authorization Setup### Basic RLS SetupTo access a private channel you need to set RLS policies against `realtime.messages` table for SELECT operations.```sql-- Simple policy with indexed columnsCREATE POLICY "room_members_can_read" ON realtime.messagesFOR SELECT TO authenticatedUSING ( topic LIKE 'room:%' AND EXISTS ( SELECT 1 FROM room_members WHERE user_id = auth.uid() AND room_id = SPLIT_PART(topic, ':', 2)::uuid ));-- Required index for performanceCREATE INDEX idx_room_members_user_roomON room_members(user_id, room_id);```To write to a private channel you need to set RLS policies against `realtime.messages` table for INSERT operations.```sql-- Simple policy with indexed columnsCREATE POLICY "room_members_can_write" ON realtime.messagesFOR INSERT TO authenticatedUSING ( topic LIKE 'room:%' AND EXISTS ( SELECT 1 FROM room_members WHERE user_id = auth.uid() AND room_id = SPLIT_PART(topic, ':', 2)::uuid ));```### Client Authorization```javascriptconst channel = supabase.channel('room:123:messages', { config: { private: true }}) .on('broadcast', { event: 'message_created' }, handleMessage) .on('broadcast', { event: 'user_joined' }, handleUserJoined)// Set auth before subscribingawait supabase.realtime.setAuth()// Subscribe after auth is setawait channel.subscribe()```### Enhanced Security: Private-Only Channels**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.## Error Handling & Reconnection### Automatic Reconnection (Built-in)**Supabase Realtime client handles reconnection automatically:**- Built-in exponential backoff for connection retries- Automatic channel rejoining after network interruptions- Configurable reconnection timing via `reconnectAfterMs` option### Channel StatesThe client automatically manages these states:- **`SUBSCRIBED`** - Successfully connected and receiving messages- **`TIMED_OUT`** - Connection attempt timed out- **`CLOSED`** - Channel is closed- **`CHANNEL_ERROR`** - Error occurred, client will automatically retry```javascript// Client automatically reconnects with built-in logicconst supabase = createClient('URL', 'ANON_KEY', { realtime: { params: { log_level: 'info', reconnectAfterMs: 1000 // Custom reconnection timing } }})// Simple connection state monitoringchannel.subscribe((status, err) => { switch (status) { case 'SUBSCRIBED': console.log('Connected (or reconnected)') break case 'CHANNEL_ERROR': console.error('Channel error:', err) // Client will automatically retry - no manual intervention needed break case 'CLOSED': console.log('Channel closed') break }})```## Performance & Scaling Guidelines### Channel Structure Optimization- Use one channel per logical scope (`room:123`, not `user:456:room:123`)- Shard high-volume topics: `chat:shard:1`, `chat:shard:2`- 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.## Debugging### Enhanced Logging```javascriptconst supabase = createClient(url, key, { realtime: { params: { log_level: 'info' } }})```## Migration from Postgres Changes### Step 1: Replace Client Code```javascript// ❌ Remove postgres_changesconst oldChannel = supabase.channel('changes').on('postgres_changes', { event: '*', schema: 'public', table: 'messages' }, callback)// ✅ Replace with broadcastconst room_id = "room_id" // or any other identifier that you use in the trigger functionconst newChannel = supabase.channel(`messages:${room_id}:changes`, { config: { private: true }}).on('broadcast', { event: 'INSERT' }, callback).on('broadcast', { event: 'DELETE' }, callback).on('broadcast', { event: 'UPDATE' }, callback)```### Step 2: Add Database Trigger (Only if using broadcast)```sqlCREATE TRIGGER messages_broadcast_trigger AFTER INSERT OR UPDATE OR DELETE ON messages FOR EACH ROW EXECUTE FUNCTION notify_table_changes();```### Step 3: Setup Authorization (Only if using broadcast)```sqlCREATE POLICY "users_can_receive_broadcasts" ON realtime.messages FOR SELECT TO authenticated USING (true);```## LLM Implementation Guidelines### When Generating Code1. **Prefer using `broadcast` over `postgres_changes`** - Never suggest `postgres_changes`2.**Include state checking** - Check channel state before subscribing3. **Add cleanup logic** - Include unsubscribe in all examples4. **Suggest proper naming** - Use consistent topic/event conventions5. **Include error handling** - Add reconnection patterns6. **Recommend indexing** - When RLS policies are used7. **Framework-agnostic** - Adapt patterns to user's framework8. **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.### Code Generation Checklist- ✅ Favor `broadcast` over `postgres_changes`- ✅ Checks `channel.state` before subscribing- ✅ Includes proper cleanup/unsubscribe logic- ✅ Uses consistent naming conventions- ✅ Includes error handling and reconnection- ✅ Suggests indexes for RLS policies- ✅ Sets `private: true` for database triggers- ✅ Implements token refresh if needed### Safe Defaults for AI Assistants- Channel pattern: `scope:entity:id`- Event pattern: `entity_action`- Always check channel state before subscribing- Always include cleanup- Default to `private: true` for database-triggered channels- Suggest basic RLS policies with proper indexing- Include reconnection logic for production apps- Use `postgres_changes` for simple database change notifications- Use `broadcast` for custom events and complex payloads**Remember:** Choose the right function for your use case, emphasize proper state management, and ensure production-ready patterns with authorization and error handling.