From 6855e027cdc54f9983425031a4776350f5c255e2 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Thu, 28 Aug 2025 08:16:20 -0300 Subject: [PATCH 1/2] docs(auth): add Flutter signInWithIdToken example for Facebook auth (#38265) * docs(auth): add Flutter signInWithIdToken example for Facebook auth Add documentation section showing how to integrate Facebook authentication in Flutter using the signInWithIdToken method with the Facebook SDK. Includes dependency setup, basic implementation, and error handling. * fix style * docs(dart): add Facebook signInWithIdToken example Add native Facebook sign-in example to the Dart client reference documentation. Includes integration with flutter_facebook_auth package and proper error handling for the signInWithIdToken method. * docs(dart): simplify provider parameter description Remove specific provider enumeration from parameter description to keep it more generic and maintainable. * Add signInWithIdToken to spelling allow list * Update auth-facebook.mdx Co-authored-by: Chris Chinchilla * Update apps/docs/content/guides/auth/social-login/auth-facebook.mdx Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Update apps/docs/content/guides/auth/social-login/auth-facebook.mdx Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --------- Co-authored-by: Chris Chinchilla Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../auth/social-login/auth-facebook.mdx | 49 +++++++++++++++++++ apps/docs/spec/supabase_dart_v2.yml | 34 ++++++++++++- supa-mdx-lint/Rule003Spelling.toml | 1 + 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/apps/docs/content/guides/auth/social-login/auth-facebook.mdx b/apps/docs/content/guides/auth/social-login/auth-facebook.mdx index f6a0d5538cb56..5412fefbec71b 100644 --- a/apps/docs/content/guides/auth/social-login/auth-facebook.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-facebook.mdx @@ -121,6 +121,55 @@ Future signInWithFacebook() async { } ``` +### Alternative: Using Facebook SDK with signInWithIdToken + +For more control over the Facebook authentication flow, you can use the Facebook SDK directly and then authenticate with Supabase using [`signInWithIdToken()`](/docs/reference/dart/auth-signinwithidtoken): + +First, add the Facebook SDK dependency to your `pubspec.yaml`: + +```yaml +dependencies: + flutter_facebook_auth: ^7.0.1 +``` + +Then implement the Facebook authentication: + +```dart +import 'package:flutter_facebook_auth/flutter_facebook_auth.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +Future signInWithFacebook() async { + try { + final LoginResult result = await FacebookAuth.instance.login( + permissions: ['public_profile', 'email'], + ); + + if (result.status == LoginStatus.success) { + final accessToken = result.accessToken!.tokenString; + + await Supabase.instance.client.auth.signInWithIdToken( + provider: OAuthProvider.facebook, + idToken: accessToken, + ); + + // Authentication successful + } else { + // Handle login cancellation or failure + throw Exception('Facebook login failed: ${result.status}'); + } + } catch (e) { + // Handle errors + throw Exception('Facebook authentication error: ${e.toString()}'); + } +} +``` + + + +Make sure to configure your Facebook app properly and add the required permissions in the Facebook Developer Console. The `signInWithIdToken` method requires the Facebook access token to be valid and properly scoped. + + + diff --git a/apps/docs/spec/supabase_dart_v2.yml b/apps/docs/spec/supabase_dart_v2.yml index be74eaaf1ee66..2a1dd1b2d4e1b 100644 --- a/apps/docs/spec/supabase_dart_v2.yml +++ b/apps/docs/spec/supabase_dart_v2.yml @@ -558,12 +558,12 @@ functions: - id: sign-in-with-id-token title: 'signInWithIdToken()' description: | - Allows you to perform native Google and Apple sign in by combining it with [google_sign_in](https://pub.dev/packages/google_sign_in) or [sign_in_with_apple](https://pub.dev/packages/sign_in_with_apple) packages. + Allows you to perform native Google, Apple, and Facebook sign in by combining it with [google_sign_in](https://pub.dev/packages/google_sign_in), [sign_in_with_apple](https://pub.dev/packages/sign_in_with_apple), or [flutter_facebook_auth](https://pub.dev/packages/flutter_facebook_auth) packages. params: - name: provider isOptional: false type: OAuthProvider - description: The provider to perform the sign in with. Currently, `OAuthProvider.google` and `OAuthProvider.apple` are supported. + description: The provider to perform the sign in with. - name: idToken isOptional: false type: String @@ -719,6 +719,36 @@ functions: nonce: rawNonce, ); ``` + - id: sign-in-with-facebook + name: Native Facebook Sign in + description: | + You can perform native Facebook sign in using [flutter_facebook_auth](https://pub.dev/packages/flutter_facebook_auth). + + First, set up your Facebook app in the [Facebook Developer Console](https://developers.facebook.com) and configure it in your Supabase dashboard under `Authentication -> Providers -> Facebook`. + code: | + ```dart + import 'package:flutter_facebook_auth/flutter_facebook_auth.dart'; + import 'package:supabase_flutter/supabase_flutter.dart'; + + Future signInWithFacebook() async { + final LoginResult result = await FacebookAuth.instance.login( + permissions: ['public_profile', 'email'], + ); + + if (result.status == LoginStatus.success) { + final accessToken = result.accessToken!.tokenString; + + final response = await supabase.auth.signInWithIdToken( + provider: OAuthProvider.facebook, + idToken: accessToken, + ); + } else { + throw const AuthException( + 'Facebook login failed: ${result.status}', + ); + } + } + ``` - id: sign-in-with-oauth title: 'signInWithOAuth()' description: | diff --git a/supa-mdx-lint/Rule003Spelling.toml b/supa-mdx-lint/Rule003Spelling.toml index c38cdf3637e41..6234bbea74abd 100644 --- a/supa-mdx-lint/Rule003Spelling.toml +++ b/supa-mdx-lint/Rule003Spelling.toml @@ -336,6 +336,7 @@ allow_list = [ "psql", "scrypt", "sessionStorage", + "signInWithIdToken", "stdin", "stdout", "[Ss]ubnet(s)?", From 7fb163daa97db9148664ff05945c9edd1ad186f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20Caba=C3=A7o?= Date: Thu, 28 Aug 2025 13:39:50 +0100 Subject: [PATCH 2/2] realtime: add ai rules (#38220) --------- Co-authored-by: Ivan Vasilov --- apps/ui-library/__registry__/index.tsx | 2 +- apps/ui-library/public/llms.txt | 4 +- apps/ui-library/public/r/ai-editor-rules.json | 6 + .../public/r/social-auth-nextjs.json | 2 +- .../public/r/social-auth-react-router.json | 2 +- .../public/r/social-auth-react.json | 2 +- .../public/r/social-auth-tanstack.json | 2 +- apps/ui-library/registry.json | 5 + .../ai-editor-rules/registry-item.json | 5 + .../default/ai-editor-rules/use-realtime.mdc | 421 ++++++++++++++++++ examples/prompts/use-realtime.md | 421 ++++++++++++++++++ 11 files changed, 866 insertions(+), 6 deletions(-) create mode 100644 apps/ui-library/registry/default/ai-editor-rules/use-realtime.mdc create mode 100644 examples/prompts/use-realtime.md diff --git a/apps/ui-library/__registry__/index.tsx b/apps/ui-library/__registry__/index.tsx index 003c426d55e83..fff2697c3a979 100644 --- a/apps/ui-library/__registry__/index.tsx +++ b/apps/ui-library/__registry__/index.tsx @@ -421,7 +421,7 @@ export const Index: Record = { registryDependencies: [], source: "", - files: ["registry/default/ai-editor-rules/create-db-functions.mdc","registry/default/ai-editor-rules/create-migration.mdc","registry/default/ai-editor-rules/create-rls-policies.mdc","registry/default/ai-editor-rules/postgres-sql-style-guide.mdc","registry/default/ai-editor-rules/writing-supabase-edge-functions.mdc"], + files: ["registry/default/ai-editor-rules/create-db-functions.mdc","registry/default/ai-editor-rules/create-migration.mdc","registry/default/ai-editor-rules/create-rls-policies.mdc","registry/default/ai-editor-rules/postgres-sql-style-guide.mdc","registry/default/ai-editor-rules/writing-supabase-edge-functions.mdc","registry/default/ai-editor-rules/use-realtime.mdc"], category: "undefined", subcategory: "undefined", chunks: [] diff --git a/apps/ui-library/public/llms.txt b/apps/ui-library/public/llms.txt index 137187575d0f5..b7779d597aff6 100644 --- a/apps/ui-library/public/llms.txt +++ b/apps/ui-library/public/llms.txt @@ -1,5 +1,5 @@ # Supabase UI Library -Last updated: 2025-07-13T02:57:35.186Z +Last updated: 2025-08-27T11:26:46.645Z ## Overview Library of components for your project. The components integrate with Supabase and are shadcn compatible. @@ -31,6 +31,8 @@ Library of components for your project. The components integrate with Supabase a - Real-time cursor sharing for collaborative applications - [Social Authentication](https://supabase.com/ui/docs/nextjs/social-auth) - Social authentication block for Next.js +- [Platform Kit](https://supabase.com/ui/docs/platform/platform-kit) + - The easiest way to build platforms on top of Supabase - [Supabase Client Libraries](https://supabase.com/ui/docs/react-router/client) - Supabase client for React Router - [Current User Avatar](https://supabase.com/ui/docs/react-router/current-user-avatar) diff --git a/apps/ui-library/public/r/ai-editor-rules.json b/apps/ui-library/public/r/ai-editor-rules.json index d2aef7edfca9f..7d59f8be2528f 100644 --- a/apps/ui-library/public/r/ai-editor-rules.json +++ b/apps/ui-library/public/r/ai-editor-rules.json @@ -36,6 +36,12 @@ "content": "---\ndescription: Coding rules for Supabase Edge Functions\nalwaysApply: false\n---\n\n# Writing Supabase Edge Functions\n\nYou're an expert in writing TypeScript and Deno JavaScript runtime. Generate **high-quality Supabase Edge Functions** that adhere to the following best practices:\n\n## Guidelines\n\n1. Try to use Web APIs and Deno’s core APIs instead of external dependencies (eg: use fetch instead of Axios, use WebSockets API instead of node-ws)\n2. If you are reusing utility methods between Edge Functions, add them to `supabase/functions/_shared` and import using a relative path. Do NOT have cross dependencies between Edge Functions.\n3. Do NOT use bare specifiers when importing dependecnies. If you need to use an external dependency, make sure it's prefixed with either `npm:` or `jsr:`. For example, `@supabase/supabase-js` should be written as `npm:@supabase/supabase-js`.\n4. For external imports, always define a version. For example, `npm:@express` should be written as `npm:express@4.18.2`.\n5. For external dependencies, importing via `npm:` and `jsr:` is preferred. Minimize the use of imports from @`deno.land/x` , `esm.sh` and @`unpkg.com` . If you have a package from one of those CDNs, you can replace the CDN hostname with `npm:` specifier.\n6. You can also use Node built-in APIs. You will need to import them using `node:` specifier. For example, to import Node process: `import process from \"node:process\". Use Node APIs when you find gaps in Deno APIs.\n7. Do NOT use `import { serve } from \"https://deno.land/std@0.168.0/http/server.ts\"`. Instead use the built-in `Deno.serve`.\n8. Following environment variables (ie. secrets) are pre-populated in both local and hosted Supabase environments. Users don't need to manually set them:\n - SUPABASE_URL\n - SUPABASE_PUBLISHABLE_OR_ANON_KEY\n - SUPABASE_SERVICE_ROLE_KEY\n - SUPABASE_DB_URL\n9. To set other environment variables (ie. secrets) users can put them in a env file and run the `supabase secrets set --env-file path/to/env-file`\n10. A single Edge Function can handle multiple routes. It is recommended to use a library like Express or Hono to handle the routes as it's easier for developer to understand and maintain. Each route must be prefixed with `/function-name` so they are routed correctly.\n11. File write operations are ONLY permitted on `/tmp` directory. You can use either Deno or Node File APIs.\n12. Use `EdgeRuntime.waitUntil(promise)` static method to run long-running tasks in the background without blocking response to a request. Do NOT assume it is available in the request / execution context.\n\n## Example Templates\n\n### Simple Hello World Function\n\n```tsx\ninterface reqPayload {\n name: string\n}\n\nconsole.info('server started')\n\nDeno.serve(async (req: Request) => {\n const { name }: reqPayload = await req.json()\n const data = {\n message: `Hello ${name} from foo!`,\n }\n\n return new Response(JSON.stringify(data), {\n headers: { 'Content-Type': 'application/json', Connection: 'keep-alive' },\n })\n})\n```\n\n### Example Function using Node built-in API\n\n```tsx\nimport { randomBytes } from 'node:crypto'\nimport { createServer } from 'node:http'\nimport process from 'node:process'\n\nconst generateRandomString = (length) => {\n const buffer = randomBytes(length)\n return buffer.toString('hex')\n}\n\nconst randomString = generateRandomString(10)\nconsole.log(randomString)\n\nconst server = createServer((req, res) => {\n const message = `Hello`\n res.end(message)\n})\n\nserver.listen(9999)\n```\n\n### Using npm packages in Functions\n\n```tsx\nimport express from 'npm:express@4.18.2'\n\nconst app = express()\n\napp.get(/(.*)/, (req, res) => {\n res.send('Welcome to Supabase')\n})\n\napp.listen(8000)\n```\n\n### Generate embeddings using built-in @Supabase.ai API\n\n```tsx\nconst model = new Supabase.ai.Session('gte-small')\n\nDeno.serve(async (req: Request) => {\n const params = new URL(req.url).searchParams\n const input = params.get('text')\n const output = await model.run(input, { mean_pool: true, normalize: true })\n return new Response(JSON.stringify(output), {\n headers: {\n 'Content-Type': 'application/json',\n Connection: 'keep-alive',\n },\n })\n})\n```\n", "type": "registry:file", "target": "~/.cursor/rules/writing-supabase-edge-functions.mdc" + }, + { + "path": "registry/default/ai-editor-rules/use-realtime.mdc", + "content": "---\n# Specify the following for Cursor rules\ndescription: Coding rules for Supabase Realtime\nalwaysApply: false\n---\n\n# Supabase Realtime AI Assistant Guide\n\nYou are an expert developer assistant specializing in Supabase Realtime implementations. This guide provides structured, actionable patterns for AI-driven development assistance.\n\n## Implementation Rules\n\n### Do\n- Use `broadcast` for all realtime events (database changes via triggers, messaging, notifications, game state)\n- Use `presence` sparingly for user state tracking (online status, user counters)\n- Create indexes for all columns used in RLS policies\n- Advice to use topic names that corrolate with concepts and tables: `scope:entity` (e.g., `room:123:messages`)\n- Use snake_case for event names: `entity_action` (e.g., `message_created`)\n- Include unsubscribe/cleanup logic in all implementations\n- Set `private: true` for channels using database triggers or RLS policies\n- Give preference to use private channels over public channels (better security and control)\n- Implement proper error handling and reconnection logic\n\n### Don't\n- Use `postgres_changes` for new applications (single-threaded, doesn't scale well) and help migrate to `broadcast from database` on existing applications if necessary\n- Create multiple subscriptions without proper cleanup\n- Write complex RLS queries without proper indexing\n- Use generic event names like \"update\" or \"change\"\n- Subscribe directly in render functions without state management\n- Use database functions (`realtime.send`, `realtime.broadcast_changes`) in client code\n\n## Function Selection Decision Table\n\n| Use Case | Recommended Function | Why Not postgres_changes |\n|----------|---------------------|--------------------------|\n| Custom payloads with business logic | `broadcast` | More flexible, better performance |\n| Database change notifications | `broadcast` via database triggers | More scalable, customizable payloads |\n| High-frequency updates | `broadcast` with minimal payload | Better throughput and control |\n| User presence/status tracking | `presence` (sparingly) | Specialized for state synchronization |\n| Simple table mirroring | `broadcast` via database triggers | More scalable, customizable payloads |\n| Client to client communication | `broadcast` without triggers and using only websockets | More flexible, better performance |\n\n**Note:** `postgres_changes` should be avoided due to scalability limitations. Use `broadcast` with database triggers (`realtime.broadcast_changes`) for all database change notifications.\n\n## Scalability Best Practices\n\n### Dedicated Topics for Better Performance\nUsing dedicated, granular topics ensures messages are only sent to relevant listeners, significantly improving scalability:\n\n**❌ Avoid Broad Topics:**\n```javascript\n// This broadcasts to ALL users, even those not interested\nconst channel = supabase.channel('global:notifications')\n```\n\n**✅ Use Dedicated Topics:**\n```javascript\n// This only broadcasts to users in a specific room\nconst channel = supabase.channel(`room:${roomId}:messages`)\n\n// This only broadcasts to a specific user\nconst channel = supabase.channel(`user:${userId}:notifications`)\n\n// This only broadcasts to users with specific permissions\nconst channel = supabase.channel(`admin:${orgId}:alerts`)\n```\n\n### Benefits of Dedicated Topics:\n- **Reduced Network Traffic**: Messages only reach interested clients\n- **Better Performance**: Fewer unnecessary message deliveries\n- **Improved Security**: Easier to implement targeted RLS policies\n- **Scalability**: System can handle more concurrent users efficiently\n- **Cost Optimization**: Reduced bandwidth and processing overhead\n\n### Topic Naming Strategy:\n- **One topic per room**: `room:123:messages`, `room:123:presence`\n- **One topic per user**: `user:456:notifications`, `user:456:status`\n- **One topic per organization**: `org:789:announcements`\n- **One topic per feature**: `game:123:moves`, `game:123:chat`\n\n## Naming Conventions\n\n### Topics (Channels)\n- **Pattern:** `scope:entity` or `scope:entity:id`\n- **Examples:** `room:123:messages`, `game:456:moves`, `user:789:notifications`\n- **Public channels:** `public:announcements`, `global:status`\n\n### Events\n- **Pattern:** `entity_action` (snake_case)\n- **Examples:** `message_created`, `user_joined`, `game_ended`, `status_changed`\n- **Avoid:** Generic names like `update`, `change`, `event`\n\n## Client Setup Patterns\n\n```javascript\n// Basic setup\nconst supabase = createClient('URL', 'ANON_KEY')\n\n// Channel configuration\nconst channel = supabase.channel('room:123:messages', {\n config: {\n broadcast: { self: true, ack: true },\n presence: { key: 'user-session-id', enabled: true },\n private: true // Required for RLS authorization\n }\n})\n```\n\n### Configuration Options\n\n#### Broadcast Configuration\n- **`self: true`** - Receive your own broadcast messages\n- **`ack: true`** - Get acknowledgment when server receives your message\n\n#### Presence Configuration\n- **`enabled: true`** - Enable presence tracking for this channel. This flag is set automatically by client library if `on('presence')` is set.\n- **`key: string`** - Custom key to identify presence state (useful for user sessions)\n\n#### Security Configuration\n- **`private: true`** - Require authentication and RLS policies\n- **`private: false`** - Public channel (default, not recommended for production)\n\n## Frontend Framework Integration\n\n### React Pattern\n```javascript\nconst channelRef = useRef(null)\n\nuseEffect(() => {\n // Check if already subscribed to prevent multiple subscriptions\n if (channelRef.current?.state === 'subscribed') return\n const channel = supabase.channel('room:123:messages', {\n config: { private: true }\n })\n channelRef.current = channel\n\n // Set auth before subscribing\n await supabase.realtime.setAuth()\n\n channel\n .on('broadcast', { event: 'message_created' }, handleMessage)\n .on('broadcast', { event: 'user_joined' }, handleUserJoined)\n .subscribe()\n\n return () => {\n if (channelRef.current) {\n supabase.removeChannel(channelRef.current)\n channelRef.current = null\n }\n }\n}, [roomId])\n```\n\n## Database Triggers\n\n### Using realtime.broadcast_changes (Recommended for database changes)\nThis 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.\n```sql\nCREATE OR REPLACE FUNCTION notify_table_changes()\nRETURNS TRIGGER AS $$\nSECURITY DEFINER\nLANGUAGE plpgsql\nAS $$\nBEGIN\n PERFORM realtime.broadcast_changes(\n TG_TABLE_NAME ||':' || COALESCE(NEW.id, OLD.id)::text,\n TG_OP,\n TG_OP,\n TG_TABLE_NAME,\n TG_TABLE_SCHEMA,\n NEW,\n OLD\n );\n RETURN COALESCE(NEW, OLD);\nEND;\n$$;\n```\nBut you can also create more specific trigger functions for specific tables and events so adapt to your use case:\n\n```sql\nCREATE OR REPLACE FUNCTION room_messages_broadcast_trigger()\nRETURNS TRIGGER AS $$\nSECURITY DEFINER\nLANGUAGE plpgsql\nAS $$\nBEGIN\n PERFORM realtime.broadcast_changes(\n 'room:' || COALESCE(NEW.room_id, OLD.room_id)::text,\n TG_OP,\n TG_OP,\n TG_TABLE_NAME,\n TG_TABLE_SCHEMA,\n NEW,\n OLD\n );\n RETURN COALESCE(NEW, OLD);\nEND;\n$$;\n```\n\nBy default, `realtime.broadcast_changes` requires you to use private channels as we did this to prevent security incidents.\n\n### Using realtime.send (For custom messages)\n```sql\nCREATE OR REPLACE FUNCTION notify_custom_event()\nRETURNS TRIGGER AS $$\nSECURITY DEFINER\nLANGUAGE plpgsql\nAS $$\nBEGIN\n PERFORM realtime.send(\n 'room:' || NEW.room_id::text,\n 'status_changed',\n jsonb_build_object('id', NEW.id, 'status', NEW.status),\n false\n );\n RETURN NEW;\nEND;\n$$;\n```\nThis 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.\n\n### Conditional Broadcasting\nIf you need to broadcast only significant changes, you can use the following pattern:\n```sql\n-- Only broadcast significant changes\nIF TG_OP = 'UPDATE' AND OLD.status IS DISTINCT FROM NEW.status THEN\n PERFORM realtime.broadcast_changes(\n 'room:' || NEW.room_id::text,\n TG_OP,\n TG_OP,\n TG_TABLE_NAME,\n TG_TABLE_SCHEMA,\n NEW,\n OLD\n );\nEND IF;\n```\nThis is just an example as you can use any logic you want that is SQL compatible.\n\n## Authorization Setup\n\n### Basic RLS Setup\nTo access a private channel you need to set RLS policies against `realtime.messages` table for SELECT operations.\n```sql\n-- Simple policy with indexed columns\nCREATE POLICY \"room_members_can_read\" ON realtime.messages\nFOR SELECT TO authenticated\nUSING (\n topic LIKE 'room:%' AND\n EXISTS (\n SELECT 1 FROM room_members\n WHERE user_id = auth.uid()\n AND room_id = SPLIT_PART(topic, ':', 2)::uuid\n )\n);\n\n-- Required index for performance\nCREATE INDEX idx_room_members_user_room\nON room_members(user_id, room_id);\n```\n\nTo write to a private channel you need to set RLS policies against `realtime.messages` table for INSERT operations.\n\n```sql\n-- Simple policy with indexed columns\nCREATE POLICY \"room_members_can_write\" ON realtime.messages\nFOR INSERT TO authenticated\nUSING (\n topic LIKE 'room:%' AND\n EXISTS (\n SELECT 1 FROM room_members\n WHERE user_id = auth.uid()\n AND room_id = SPLIT_PART(topic, ':', 2)::uuid\n )\n);\n```\n\n### Client Authorization\n```javascript\nconst channel = supabase.channel('room:123:messages', {\n config: { private: true }\n})\n .on('broadcast', { event: 'message_created' }, handleMessage)\n .on('broadcast', { event: 'user_joined' }, handleUserJoined)\n\n// Set auth before subscribing\nawait supabase.realtime.setAuth()\n\n// Subscribe after auth is set\nawait channel.subscribe()\n```\n\n### Enhanced Security: Private-Only Channels\n**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.\n\n## Error Handling & Reconnection\n\n### Automatic Reconnection (Built-in)\n**Supabase Realtime client handles reconnection automatically:**\n- Built-in exponential backoff for connection retries\n- Automatic channel rejoining after network interruptions\n- Configurable reconnection timing via `reconnectAfterMs` option\n\n### Channel States\nThe client automatically manages these states:\n- **`SUBSCRIBED`** - Successfully connected and receiving messages\n- **`TIMED_OUT`** - Connection attempt timed out\n- **`CLOSED`** - Channel is closed\n- **`CHANNEL_ERROR`** - Error occurred, client will automatically retry\n\n```javascript\n// Client automatically reconnects with built-in logic\nconst supabase = createClient('URL', 'ANON_KEY', {\n realtime: {\n params: {\n log_level: 'info',\n reconnectAfterMs: 1000 // Custom reconnection timing\n }\n }\n})\n\n// Simple connection state monitoring\nchannel.subscribe((status, err) => {\n switch (status) {\n case 'SUBSCRIBED':\n console.log('Connected (or reconnected)')\n break\n case 'CHANNEL_ERROR':\n console.error('Channel error:', err)\n // Client will automatically retry - no manual intervention needed\n break\n case 'CLOSED':\n console.log('Channel closed')\n break\n }\n})\n```\n\n## Performance & Scaling Guidelines\n\n### Channel Structure Optimization\n- Use one channel per logical scope (`room:123`, not `user:456:room:123`)\n- Shard high-volume topics: `chat:shard:1`, `chat:shard:2`\n- 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.\n\n## Debugging\n\n### Enhanced Logging\n```javascript\nconst supabase = createClient(url, key, {\n realtime: {\n params: { log_level: 'info' }\n }\n})\n```\n\n## Migration from Postgres Changes\n\n### Step 1: Replace Client Code\n```javascript\n// ❌ Remove postgres_changes\nconst oldChannel = supabase.channel('changes').on('postgres_changes', { event: '*', schema: 'public', table: 'messages' }, callback)\n\n// ✅ Replace with broadcast\nconst room_id = \"room_id\" // or any other identifier that you use in the trigger function\nconst newChannel = supabase.channel(`messages:${room_id}:changes`, {\n config: { private: true }\n})\n.on('broadcast', { event: 'INSERT' }, callback)\n.on('broadcast', { event: 'DELETE' }, callback)\n.on('broadcast', { event: 'UPDATE' }, callback)\n```\n\n### Step 2: Add Database Trigger (Only if using broadcast)\n```sql\nCREATE TRIGGER messages_broadcast_trigger\n AFTER INSERT OR UPDATE OR DELETE ON messages\n FOR EACH ROW EXECUTE FUNCTION notify_table_changes();\n```\n\n### Step 3: Setup Authorization (Only if using broadcast)\n```sql\nCREATE POLICY \"users_can_receive_broadcasts\" ON realtime.messages\n FOR SELECT TO authenticated USING (true);\n```\n\n## LLM Implementation Guidelines\n\n### When Generating Code\n1. **Prefer using `broadcast` over `postgres_changes`** - Never suggest `postgres_changes`\n2.**Include state checking** - Check channel state before subscribing\n3. **Add cleanup logic** - Include unsubscribe in all examples\n4. **Suggest proper naming** - Use consistent topic/event conventions\n5. **Include error handling** - Add reconnection patterns\n6. **Recommend indexing** - When RLS policies are used\n7. **Framework-agnostic** - Adapt patterns to user's framework\n8. **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.\n\n### Code Generation Checklist\n- ✅ Favor `broadcast` over `postgres_changes`\n- ✅ Checks `channel.state` before subscribing\n- ✅ Includes proper cleanup/unsubscribe logic\n- ✅ Uses consistent naming conventions\n- ✅ Includes error handling and reconnection\n- ✅ Suggests indexes for RLS policies\n- ✅ Sets `private: true` for database triggers\n- ✅ Implements token refresh if needed\n\n### Safe Defaults for AI Assistants\n- Channel pattern: `scope:entity:id`\n- Event pattern: `entity_action`\n- Always check channel state before subscribing\n- Always include cleanup\n- Default to `private: true` for database-triggered channels\n- Suggest basic RLS policies with proper indexing\n- Include reconnection logic for production apps\n- Use `postgres_changes` for simple database change notifications\n- Use `broadcast` for custom events and complex payloads\n\n**Remember:** Choose the right function for your use case, emphasize proper state management, and ensure production-ready patterns with authorization and error handling.", + "type": "registry:file", + "target": "~/.cursor/rules/use-realtime.mdc" } ] } \ No newline at end of file diff --git a/apps/ui-library/public/r/social-auth-nextjs.json b/apps/ui-library/public/r/social-auth-nextjs.json index 1720d82de5d8f..df08867c11c68 100644 --- a/apps/ui-library/public/r/social-auth-nextjs.json +++ b/apps/ui-library/public/r/social-auth-nextjs.json @@ -39,7 +39,7 @@ }, { "path": "registry/default/blocks/social-auth-nextjs/components/login-form.tsx", - "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { useState } from 'react'\n\nexport function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n\n const handleSocialLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithOAuth({\n provider: 'github',\n options: {\n redirectTo: `${window.location.origin}/auth/oauth?next=/protected`,\n },\n })\n\n if (error) throw error\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n setIsLoading(false)\n }\n }\n\n return (\n
\n \n \n Welcome!\n Sign in to your account to continue\n \n \n
\n
\n {error &&

{error}

}\n \n
\n
\n
\n
\n
\n )\n}\n", + "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { useState } from 'react'\n\nexport function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n\n const handleSocialLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithOAuth({\n provider: 'github',\n options: {\n redirectTo: `${window.location.origin}/auth/oauth?next=/protected`,\n },\n })\n\n if (error) throw error\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n setIsLoading(false)\n }\n }\n\n return (\n
\n \n \n Welcome!\n Sign in to your account to continue\n \n \n
\n
\n {error &&

{error}

}\n \n
\n
\n
\n
\n
\n )\n}\n", "type": "registry:component" }, { diff --git a/apps/ui-library/public/r/social-auth-react-router.json b/apps/ui-library/public/r/social-auth-react-router.json index 23038bd9e1f0d..35d4be0cad02a 100644 --- a/apps/ui-library/public/r/social-auth-react-router.json +++ b/apps/ui-library/public/r/social-auth-react-router.json @@ -29,7 +29,7 @@ }, { "path": "registry/default/blocks/social-auth-react-router/app/routes/login.tsx", - "content": "import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { type ActionFunctionArgs, redirect, useFetcher } from 'react-router'\n\nexport const action = async ({ request }: ActionFunctionArgs) => {\n const { supabase } = createClient(request)\n const origin = new URL(request.url).origin\n\n const { data, error } = await supabase.auth.signInWithOAuth({\n provider: 'github',\n options: {\n redirectTo: `${origin}/auth/oauth?next=/protected`,\n },\n })\n\n if (data.url) {\n return redirect(data.url)\n }\n\n if (error) {\n return {\n error: error instanceof Error ? error.message : 'An error occurred',\n }\n }\n}\n\nexport default function Login() {\n const fetcher = useFetcher()\n\n const error = fetcher.data?.error\n const loading = fetcher.state === 'submitting'\n\n return (\n
\n
\n
\n \n \n Welcome!\n Sign in to your account to continue\n \n \n \n
\n {error &&

{error}

}\n \n
\n
\n
\n
\n
\n
\n
\n )\n}\n", + "content": "import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { type ActionFunctionArgs, redirect, useFetcher } from 'react-router'\n\nexport const action = async ({ request }: ActionFunctionArgs) => {\n const { supabase } = createClient(request)\n const origin = new URL(request.url).origin\n\n const { data, error } = await supabase.auth.signInWithOAuth({\n provider: 'github',\n options: {\n redirectTo: `${origin}/auth/oauth?next=/protected`,\n },\n })\n\n if (data.url) {\n return redirect(data.url)\n }\n\n if (error) {\n return {\n error: error instanceof Error ? error.message : 'An error occurred',\n }\n }\n}\n\nexport default function Login() {\n const fetcher = useFetcher()\n\n const error = fetcher.data?.error\n const loading = fetcher.state === 'submitting'\n\n return (\n
\n
\n
\n \n \n Welcome!\n Sign in to your account to continue\n \n \n \n
\n {error &&

{error}

}\n \n
\n
\n
\n
\n
\n
\n
\n )\n}\n", "type": "registry:file", "target": "app/routes/login.tsx" }, diff --git a/apps/ui-library/public/r/social-auth-react.json b/apps/ui-library/public/r/social-auth-react.json index 890d6a6e308a0..36ca3e28e724b 100644 --- a/apps/ui-library/public/r/social-auth-react.json +++ b/apps/ui-library/public/r/social-auth-react.json @@ -14,7 +14,7 @@ "files": [ { "path": "registry/default/blocks/social-auth-react/components/login-form.tsx", - "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { useState } from 'react'\n\nexport function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n\n const handleSocialLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithOAuth({\n provider: 'github',\n })\n\n if (error) throw error\n location.href = '/protected'\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n setIsLoading(false)\n }\n }\n\n return (\n
\n \n \n Welcome!\n Sign in to your account to continue\n \n \n
\n
\n {error &&

{error}

}\n \n
\n
\n
\n
\n
\n )\n}\n", + "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { useState } from 'react'\n\nexport function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n\n const handleSocialLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithOAuth({\n provider: 'github',\n })\n\n if (error) throw error\n location.href = '/protected'\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n setIsLoading(false)\n }\n }\n\n return (\n
\n \n \n Welcome!\n Sign in to your account to continue\n \n \n
\n
\n {error &&

{error}

}\n \n
\n
\n
\n
\n
\n )\n}\n", "type": "registry:component" }, { diff --git a/apps/ui-library/public/r/social-auth-tanstack.json b/apps/ui-library/public/r/social-auth-tanstack.json index b03c3d7251df0..785df3ea080d7 100644 --- a/apps/ui-library/public/r/social-auth-tanstack.json +++ b/apps/ui-library/public/r/social-auth-tanstack.json @@ -15,7 +15,7 @@ "files": [ { "path": "registry/default/blocks/social-auth-tanstack/components/login-form.tsx", - "content": "import { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { useState } from 'react'\n\nexport function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n\n const handleSocialLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithOAuth({\n provider: 'github',\n options: {\n redirectTo: `${window.location.origin}/auth/oauth?next=/protected`,\n },\n })\n\n if (error) throw error\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n setIsLoading(false)\n }\n }\n\n return (\n
\n \n \n Welcome!\n Sign in to your account to continue\n \n \n
\n
\n {error &&

{error}

}\n \n
\n
\n
\n
\n
\n )\n}\n", + "content": "import { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { useState } from 'react'\n\nexport function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n\n const handleSocialLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithOAuth({\n provider: 'github',\n options: {\n redirectTo: `${window.location.origin}/auth/oauth?next=/protected`,\n },\n })\n\n if (error) throw error\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n setIsLoading(false)\n }\n }\n\n return (\n
\n \n \n Welcome!\n Sign in to your account to continue\n \n \n
\n
\n {error &&

{error}

}\n \n
\n
\n
\n
\n
\n )\n}\n", "type": "registry:component" }, { diff --git a/apps/ui-library/registry.json b/apps/ui-library/registry.json index cd103eebb1be9..5b9ff396fe1dd 100644 --- a/apps/ui-library/registry.json +++ b/apps/ui-library/registry.json @@ -1576,6 +1576,11 @@ "path": "registry/default/ai-editor-rules/writing-supabase-edge-functions.mdc", "type": "registry:file", "target": "~/.cursor/rules/writing-supabase-edge-functions.mdc" + }, + { + "path": "registry/default/ai-editor-rules/use-realtime.mdc", + "type": "registry:file", + "target": "~/.cursor/rules/use-realtime.mdc" } ] } diff --git a/apps/ui-library/registry/default/ai-editor-rules/registry-item.json b/apps/ui-library/registry/default/ai-editor-rules/registry-item.json index 308107b08bf5e..10312caa858aa 100644 --- a/apps/ui-library/registry/default/ai-editor-rules/registry-item.json +++ b/apps/ui-library/registry/default/ai-editor-rules/registry-item.json @@ -31,6 +31,11 @@ "path": "registry/default/ai-editor-rules/writing-supabase-edge-functions.mdc", "type": "registry:file", "target": "~/.cursor/rules/writing-supabase-edge-functions.mdc" + }, + { + "path": "registry/default/ai-editor-rules/use-realtime.mdc", + "type": "registry:file", + "target": "~/.cursor/rules/use-realtime.mdc" } ] } diff --git a/apps/ui-library/registry/default/ai-editor-rules/use-realtime.mdc b/apps/ui-library/registry/default/ai-editor-rules/use-realtime.mdc new file mode 100644 index 0000000000000..73ac886dc2bb0 --- /dev/null +++ b/apps/ui-library/registry/default/ai-editor-rules/use-realtime.mdc @@ -0,0 +1,421 @@ +--- +# Specify the following for Cursor rules +description: Coding rules for Supabase Realtime +alwaysApply: false +--- + +# Supabase Realtime AI Assistant Guide + +You 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 Performance +Using 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 interested +const channel = supabase.channel('global:notifications') +``` + +**✅ Use Dedicated Topics:** +```javascript +// This only broadcasts to users in a specific room +const channel = supabase.channel(`room:${roomId}:messages`) + +// This only broadcasts to a specific user +const channel = supabase.channel(`user:${userId}:notifications`) + +// This only broadcasts to users with specific permissions +const 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 setup +const supabase = createClient('URL', 'ANON_KEY') + +// Channel configuration +const 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 +```javascript +const 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. +```sql +CREATE OR REPLACE FUNCTION notify_table_changes() +RETURNS TRIGGER AS $$ +SECURITY DEFINER +LANGUAGE plpgsql +AS $$ +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: + +```sql +CREATE OR REPLACE FUNCTION room_messages_broadcast_trigger() +RETURNS TRIGGER AS $$ +SECURITY DEFINER +LANGUAGE plpgsql +AS $$ +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) +```sql +CREATE OR REPLACE FUNCTION notify_custom_event() +RETURNS TRIGGER AS $$ +SECURITY DEFINER +LANGUAGE plpgsql +AS $$ +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 Broadcasting +If you need to broadcast only significant changes, you can use the following pattern: +```sql +-- Only broadcast significant changes +IF 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 Setup +To access a private channel you need to set RLS policies against `realtime.messages` table for SELECT operations. +```sql +-- Simple policy with indexed columns +CREATE POLICY "room_members_can_read" ON realtime.messages +FOR SELECT TO authenticated +USING ( + 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 performance +CREATE INDEX idx_room_members_user_room +ON 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 columns +CREATE POLICY "room_members_can_write" ON realtime.messages +FOR INSERT TO authenticated +USING ( + 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 +```javascript +const channel = supabase.channel('room:123:messages', { + config: { private: true } +}) + .on('broadcast', { event: 'message_created' }, handleMessage) + .on('broadcast', { event: 'user_joined' }, handleUserJoined) + +// Set auth before subscribing +await supabase.realtime.setAuth() + +// Subscribe after auth is set +await 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 States +The 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 logic +const supabase = createClient('URL', 'ANON_KEY', { + realtime: { + params: { + log_level: 'info', + reconnectAfterMs: 1000 // Custom reconnection timing + } + } +}) + +// Simple connection state monitoring +channel.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 +```javascript +const supabase = createClient(url, key, { + realtime: { + params: { log_level: 'info' } + } +}) +``` + +## Migration from Postgres Changes + +### Step 1: Replace Client Code +```javascript +// ❌ Remove postgres_changes +const oldChannel = supabase.channel('changes').on('postgres_changes', { event: '*', schema: 'public', table: 'messages' }, callback) + +// ✅ Replace with broadcast +const room_id = "room_id" // or any other identifier that you use in the trigger function +const 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) +```sql +CREATE 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) +```sql +CREATE POLICY "users_can_receive_broadcasts" ON realtime.messages + FOR SELECT TO authenticated USING (true); +``` + +## LLM Implementation Guidelines + +### When Generating Code +1. **Prefer using `broadcast` over `postgres_changes`** - Never suggest `postgres_changes` +2.**Include state checking** - Check channel state before subscribing +3. **Add cleanup logic** - Include unsubscribe in all examples +4. **Suggest proper naming** - Use consistent topic/event conventions +5. **Include error handling** - Add reconnection patterns +6. **Recommend indexing** - When RLS policies are used +7. **Framework-agnostic** - Adapt patterns to user's framework +8. **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. \ No newline at end of file diff --git a/examples/prompts/use-realtime.md b/examples/prompts/use-realtime.md new file mode 100644 index 0000000000000..73ac886dc2bb0 --- /dev/null +++ b/examples/prompts/use-realtime.md @@ -0,0 +1,421 @@ +--- +# Specify the following for Cursor rules +description: Coding rules for Supabase Realtime +alwaysApply: false +--- + +# Supabase Realtime AI Assistant Guide + +You 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 Performance +Using 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 interested +const channel = supabase.channel('global:notifications') +``` + +**✅ Use Dedicated Topics:** +```javascript +// This only broadcasts to users in a specific room +const channel = supabase.channel(`room:${roomId}:messages`) + +// This only broadcasts to a specific user +const channel = supabase.channel(`user:${userId}:notifications`) + +// This only broadcasts to users with specific permissions +const 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 setup +const supabase = createClient('URL', 'ANON_KEY') + +// Channel configuration +const 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 +```javascript +const 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. +```sql +CREATE OR REPLACE FUNCTION notify_table_changes() +RETURNS TRIGGER AS $$ +SECURITY DEFINER +LANGUAGE plpgsql +AS $$ +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: + +```sql +CREATE OR REPLACE FUNCTION room_messages_broadcast_trigger() +RETURNS TRIGGER AS $$ +SECURITY DEFINER +LANGUAGE plpgsql +AS $$ +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) +```sql +CREATE OR REPLACE FUNCTION notify_custom_event() +RETURNS TRIGGER AS $$ +SECURITY DEFINER +LANGUAGE plpgsql +AS $$ +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 Broadcasting +If you need to broadcast only significant changes, you can use the following pattern: +```sql +-- Only broadcast significant changes +IF 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 Setup +To access a private channel you need to set RLS policies against `realtime.messages` table for SELECT operations. +```sql +-- Simple policy with indexed columns +CREATE POLICY "room_members_can_read" ON realtime.messages +FOR SELECT TO authenticated +USING ( + 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 performance +CREATE INDEX idx_room_members_user_room +ON 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 columns +CREATE POLICY "room_members_can_write" ON realtime.messages +FOR INSERT TO authenticated +USING ( + 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 +```javascript +const channel = supabase.channel('room:123:messages', { + config: { private: true } +}) + .on('broadcast', { event: 'message_created' }, handleMessage) + .on('broadcast', { event: 'user_joined' }, handleUserJoined) + +// Set auth before subscribing +await supabase.realtime.setAuth() + +// Subscribe after auth is set +await 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 States +The 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 logic +const supabase = createClient('URL', 'ANON_KEY', { + realtime: { + params: { + log_level: 'info', + reconnectAfterMs: 1000 // Custom reconnection timing + } + } +}) + +// Simple connection state monitoring +channel.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 +```javascript +const supabase = createClient(url, key, { + realtime: { + params: { log_level: 'info' } + } +}) +``` + +## Migration from Postgres Changes + +### Step 1: Replace Client Code +```javascript +// ❌ Remove postgres_changes +const oldChannel = supabase.channel('changes').on('postgres_changes', { event: '*', schema: 'public', table: 'messages' }, callback) + +// ✅ Replace with broadcast +const room_id = "room_id" // or any other identifier that you use in the trigger function +const 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) +```sql +CREATE 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) +```sql +CREATE POLICY "users_can_receive_broadcasts" ON realtime.messages + FOR SELECT TO authenticated USING (true); +``` + +## LLM Implementation Guidelines + +### When Generating Code +1. **Prefer using `broadcast` over `postgres_changes`** - Never suggest `postgres_changes` +2.**Include state checking** - Check channel state before subscribing +3. **Add cleanup logic** - Include unsubscribe in all examples +4. **Suggest proper naming** - Use consistent topic/event conventions +5. **Include error handling** - Add reconnection patterns +6. **Recommend indexing** - When RLS policies are used +7. **Framework-agnostic** - Adapt patterns to user's framework +8. **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. \ No newline at end of file