From 3cfd5f9ed80b6b8c1c89ce7858a66d7d5f1eb503 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Tue, 27 Jan 2026 22:19:10 +0000 Subject: [PATCH] feat: realtime agent references --- skills/supabase/AGENTS.md | 25 ++++- skills/supabase/SKILL.md | 11 ++- .../supabase/references/realtime/_sections.md | 31 ++++++ .../references/realtime/broadcast-basics.md | 98 ++++++++++++++++++ .../references/realtime/broadcast-database.md | 96 ++++++++++++++++++ .../references/realtime/patterns-cleanup.md | 94 ++++++++++++++++++ .../references/realtime/patterns-errors.md | 98 ++++++++++++++++++ .../references/realtime/postgres-changes.md | 99 +++++++++++++++++++ .../references/realtime/presence-tracking.md | 87 ++++++++++++++++ .../references/realtime/setup-auth.md | 83 ++++++++++++++++ .../references/realtime/setup-channels.md | 70 +++++++++++++ 11 files changed, 788 insertions(+), 4 deletions(-) create mode 100644 skills/supabase/references/realtime/_sections.md create mode 100644 skills/supabase/references/realtime/broadcast-basics.md create mode 100644 skills/supabase/references/realtime/broadcast-database.md create mode 100644 skills/supabase/references/realtime/patterns-cleanup.md create mode 100644 skills/supabase/references/realtime/patterns-errors.md create mode 100644 skills/supabase/references/realtime/postgres-changes.md create mode 100644 skills/supabase/references/realtime/presence-tracking.md create mode 100644 skills/supabase/references/realtime/setup-auth.md create mode 100644 skills/supabase/references/realtime/setup-channels.md diff --git a/skills/supabase/AGENTS.md b/skills/supabase/AGENTS.md index 67c3056..9211248 100644 --- a/skills/supabase/AGENTS.md +++ b/skills/supabase/AGENTS.md @@ -24,10 +24,15 @@ supabase/ | Priority | Category | Impact | Prefix | |----------|----------|--------|--------| | 1 | Row Level Security | CRITICAL | `rls-` | +| 1 | Channel Setup | HIGH | `setup-` | | 2 | Connection Pooling | CRITICAL | `conn-` | +| 2 | Broadcast Messaging | CRITICAL | `broadcast-` | | 3 | Schema Design | HIGH | `schema-` | +| 3 | Presence Tracking | MEDIUM | `presence-` | | 4 | Migrations | HIGH | `migrations-` | +| 4 | Postgres Changes | MEDIUM | `postgres-` | | 5 | Performance | CRITICAL | `perf-` | +| 5 | Implementation Patterns | CRITICAL | `patterns-` | | 6 | Security | CRITICAL | `security-` | Reference files are named `{prefix}-{topic}.md` (e.g., `query-missing-indexes.md`). @@ -64,6 +69,24 @@ Reference files are named `{prefix}-{topic}.md` (e.g., `query-missing-indexes.md - `references/db/security-functions.md` - `references/db/security-service-role.md` +**Broadcast Messaging** (`broadcast-`): +- `references/realtime/broadcast-basics.md` +- `references/realtime/broadcast-database.md` + +**Implementation Patterns** (`patterns-`): +- `references/realtime/patterns-cleanup.md` +- `references/realtime/patterns-errors.md` + +**Postgres Changes** (`postgres-`): +- `references/realtime/postgres-changes.md` + +**Presence Tracking** (`presence-`): +- `references/realtime/presence-tracking.md` + +**Channel Setup** (`setup-`): +- `references/realtime/setup-auth.md` +- `references/realtime/setup-channels.md` + --- -*18 reference files across 6 categories* \ No newline at end of file +*26 reference files across 11 categories* \ No newline at end of file diff --git a/skills/supabase/SKILL.md b/skills/supabase/SKILL.md index bf4ec48..dd895ec 100644 --- a/skills/supabase/SKILL.md +++ b/skills/supabase/SKILL.md @@ -71,9 +71,14 @@ Reference the appropriate resource file based on the user's needs: ### Realtime -| Area | Resource | When to Use | -| -------- | ------------------------ | -------------------------------------------- | -| Realtime | `references/realtime.md` | Real-time subscriptions, presence, broadcast | +| Area | Resource | When to Use | +| ---------------- | ----------------------------------- | --------------------------------------------------- | +| Realtime | `references/realtime.md` | Real-time subscriptions, presence, broadcast | +| Channel Setup | `references/realtime/setup-*.md` | Creating channels, naming conventions, auth | +| Broadcast | `references/realtime/broadcast-*.md`| Client messaging, database-triggered broadcasts | +| Presence | `references/realtime/presence-*.md` | User online status, shared state tracking | +| Postgres Changes | `references/realtime/postgres-*.md` | Database change listeners (prefer Broadcast) | +| Patterns | `references/realtime/patterns-*.md` | Cleanup, error handling, React integration | ### Client Libraries & CLI diff --git a/skills/supabase/references/realtime/_sections.md b/skills/supabase/references/realtime/_sections.md new file mode 100644 index 0000000..207ce22 --- /dev/null +++ b/skills/supabase/references/realtime/_sections.md @@ -0,0 +1,31 @@ +# Section Definitions + +Reference files are grouped by prefix. Claude loads specific files based on user +queries. + +--- + +## 1. Channel Setup (setup) + +**Impact:** HIGH +**Description:** Channel creation, naming conventions, configuration options, and authentication setup for private channels. + +## 2. Broadcast Messaging (broadcast) + +**Impact:** CRITICAL +**Description:** Sending and receiving real-time messages between clients, database-triggered broadcasts using `realtime.broadcast_changes()` and `realtime.send()`. + +## 3. Presence Tracking (presence) + +**Impact:** MEDIUM +**Description:** Tracking user online status, shared state synchronization, and presence lifecycle management. + +## 4. Postgres Changes (postgres) + +**Impact:** MEDIUM +**Description:** Database change listeners via logical replication. Note: Broadcast is recommended for new applications due to better scalability. + +## 5. Implementation Patterns (patterns) + +**Impact:** CRITICAL +**Description:** Channel cleanup, React integration patterns, error handling, and connection management best practices. diff --git a/skills/supabase/references/realtime/broadcast-basics.md b/skills/supabase/references/realtime/broadcast-basics.md new file mode 100644 index 0000000..c57d6ad --- /dev/null +++ b/skills/supabase/references/realtime/broadcast-basics.md @@ -0,0 +1,98 @@ +--- +title: Send and Receive Broadcast Messages +impact: HIGH +impactDescription: Core pattern for real-time client-to-client messaging +tags: realtime, broadcast, send, receive, subscribe +--- + +## Send and Receive Broadcast Messages + +Broadcast enables low-latency pub/sub messaging between clients. Prefer Broadcast over Postgres Changes for production applications. + +## Subscribe to Broadcast Events + +```javascript +const channel = supabase.channel('room:123:messages', { + config: { private: true }, +}) + +channel + .on('broadcast', { event: 'message_created' }, (payload) => { + console.log('New message:', payload.payload) + }) + .on('broadcast', { event: '*' }, (payload) => { + // Listen to all events on this channel + }) + .subscribe((status) => { + if (status === 'SUBSCRIBED') { + console.log('Connected!') + } + }) +``` + +## Send Messages + +**After subscribing (WebSocket - lower latency):** + +```javascript +channel.send({ + type: 'broadcast', + event: 'message_created', + payload: { text: 'Hello!', user_id: '123' }, +}) +``` + +**Before subscribing or one-off (HTTP):** + +```javascript +const { error } = await channel.send({ + type: 'broadcast', + event: 'message_created', + payload: { text: 'Hello!' }, +}) +``` + +## Receive Own Messages + +By default, senders don't receive their own broadcasts. + +**Incorrect:** + +```javascript +// Won't receive own messages +const channel = supabase.channel('room:123') +``` + +**Correct:** + +```javascript +// Enable self-receive when needed (e.g., optimistic UI confirmation) +const channel = supabase.channel('room:123', { + config: { + broadcast: { self: true }, + }, +}) +``` + +## Get Server Acknowledgment + +```javascript +const channel = supabase.channel('room:123', { + config: { + broadcast: { ack: true }, + }, +}) + +// Returns 'ok' when server confirms receipt +const status = await channel.send({ + type: 'broadcast', + event: 'message_created', + payload: { text: 'Hello!' }, +}) +``` + +## Related + +- [broadcast-database.md](broadcast-database.md) +- [patterns-cleanup.md](patterns-cleanup.md) +- [Docs](https://supabase.com/docs/guides/realtime/broadcast) diff --git a/skills/supabase/references/realtime/broadcast-database.md b/skills/supabase/references/realtime/broadcast-database.md new file mode 100644 index 0000000..2fc82b4 --- /dev/null +++ b/skills/supabase/references/realtime/broadcast-database.md @@ -0,0 +1,96 @@ +--- +title: Broadcast from Database Triggers +impact: CRITICAL +impactDescription: Scalable pattern for notifying clients of database changes +tags: realtime, broadcast, database, triggers, realtime.send, realtime.broadcast_changes +--- + +## Broadcast from Database Triggers + +Use database triggers with `realtime.broadcast_changes()` instead of `postgres_changes` for better scalability. This avoids per-subscriber RLS checks. + +## realtime.broadcast_changes() + +Broadcasts database changes in a standard format. + +```sql +create or replace function room_messages_broadcast() +returns trigger +security definer +language plpgsql +as $$ +begin + perform realtime.broadcast_changes( + 'room:' || coalesce(new.room_id, old.room_id)::text, -- topic + tg_op, -- event (INSERT/UPDATE/DELETE) + tg_op, -- operation + tg_table_name, -- table + tg_table_schema, -- schema + new, -- new record + old -- old record + ); + return coalesce(new, old); +end; +$$; + +create trigger messages_broadcast_trigger + after insert or update or delete on messages + for each row execute function room_messages_broadcast(); +``` + +**Client subscription:** + +```javascript +await supabase.realtime.setAuth() +const channel = supabase + .channel('room:123', { config: { private: true } }) + .on('broadcast', { event: 'INSERT' }, (payload) => console.log('Insert:', payload)) + .on('broadcast', { event: 'UPDATE' }, (payload) => console.log('Update:', payload)) + .on('broadcast', { event: 'DELETE' }, (payload) => console.log('Delete:', payload)) + .subscribe() +``` + +## realtime.send() + +Sends custom payloads without table binding. + +```sql +select realtime.send( + jsonb_build_object('message', 'Custom notification'), -- payload + 'notification_sent', -- event + 'user:456:notifications', -- topic + true -- private (true = requires auth) +); +``` + +## Public vs Private Mismatch + +**Incorrect:** + +```sql +-- Database sends to public channel +select realtime.send('{}', 'event', 'topic', false); -- private = false +``` + +```javascript +// Client expects private channel - won't receive message +const channel = supabase.channel('topic', { config: { private: true } }) +``` + +**Correct:** + +```sql +-- Database sends to private channel +select realtime.send('{}', 'event', 'topic', true); -- private = true +``` + +```javascript +// Client matches +const channel = supabase.channel('topic', { config: { private: true } }) +``` + +## Related + +- [setup-auth.md](setup-auth.md) +- [broadcast-basics.md](broadcast-basics.md) +- [Docs](https://supabase.com/docs/guides/realtime/broadcast) diff --git a/skills/supabase/references/realtime/patterns-cleanup.md b/skills/supabase/references/realtime/patterns-cleanup.md new file mode 100644 index 0000000..3a1152a --- /dev/null +++ b/skills/supabase/references/realtime/patterns-cleanup.md @@ -0,0 +1,94 @@ +--- +title: Clean Up Channels to Prevent Memory Leaks +impact: CRITICAL +impactDescription: Prevents memory leaks and connection quota exhaustion +tags: realtime, cleanup, react, lifecycle, removeChannel +--- + +## Clean Up Channels to Prevent Memory Leaks + +Always remove channels when components unmount or subscriptions are no longer needed. + +## React Pattern + +**Incorrect:** + +```javascript +function ChatRoom({ roomId }) { + useEffect(() => { + const channel = supabase.channel(`room:${roomId}`) + channel.on('broadcast', { event: 'message' }, handleMessage).subscribe() + // Missing cleanup - channel persists after unmount + }, [roomId]) +} +``` + +**Correct:** + +```javascript +function ChatRoom({ roomId }) { + const channelRef = useRef(null) + + useEffect(() => { + // Prevent duplicate subscriptions + if (channelRef.current?.state === 'subscribed') return + + const channel = supabase.channel(`room:${roomId}:messages`, { + config: { private: true }, + }) + channelRef.current = channel + + supabase.realtime.setAuth() + + channel + .on('broadcast', { event: 'message_created' }, handleMessage) + .subscribe() + + return () => { + if (channelRef.current) { + supabase.removeChannel(channelRef.current) + channelRef.current = null + } + } + }, [roomId]) +} +``` + +## Channel Lifecycle Methods + +```javascript +// Remove specific channel +supabase.removeChannel(channel) + +// Remove all channels (e.g., on logout) +supabase.removeAllChannels() + +// Get active channels +const channels = supabase.getChannels() +``` + +## Check Channel State Before Subscribing + +```javascript +// Prevent duplicate subscriptions +if (channel.state === 'subscribed') { + return +} +channel.subscribe() +``` + +## Connection Quotas + +| Plan | Max Connections | Channels per Connection | +|------|-----------------|------------------------| +| Free | 200 | 100 | +| Pro | 500 | 100 | +| Team | 10,000 | 100 | + +Leaked channels count against quotas even when inactive. + +## Related + +- [patterns-errors.md](patterns-errors.md) +- [setup-channels.md](setup-channels.md) +- [Docs](https://supabase.com/docs/guides/realtime/quotas) diff --git a/skills/supabase/references/realtime/patterns-errors.md b/skills/supabase/references/realtime/patterns-errors.md new file mode 100644 index 0000000..0db55ad --- /dev/null +++ b/skills/supabase/references/realtime/patterns-errors.md @@ -0,0 +1,98 @@ +--- +title: Handle Realtime Errors and Connection Issues +impact: HIGH +impactDescription: Enables graceful handling of connection failures +tags: realtime, errors, subscribe, status, reconnection +--- + +## Handle Realtime Errors and Connection Issues + +Handle subscription status and errors to provide reliable user experiences. + +## Subscription Status Handling + +**Incorrect:** + +```javascript +// Ignoring subscription status - no visibility into connection issues +channel.subscribe() +``` + +**Correct:** + +```javascript +channel.subscribe((status, err) => { + switch (status) { + case 'SUBSCRIBED': + console.log('Connected!') + break + case 'CHANNEL_ERROR': + console.error('Channel error:', err) + // Client retries automatically + break + case 'TIMED_OUT': + console.error('Connection timed out') + break + case 'CLOSED': + console.log('Channel closed') + break + } +}) +``` + +## Common Error Codes + +| Error | Cause | Solution | +|-------|-------|----------| +| `too_many_connections` | Connection limit exceeded | Clean up unused channels, upgrade plan | +| `too_many_joins` | Channel join rate exceeded | Reduce join frequency | +| `ConnectionRateLimitReached` | Max connections reached | Upgrade plan | +| `DatabaseLackOfConnections` | No available DB connections | Increase compute size | +| `TenantNotFound` | Invalid project reference | Verify project URL | + +## Automatic Reconnection + +Supabase handles reconnection with exponential backoff. Configure timing: + +```javascript +const supabase = createClient(url, key, { + realtime: { + params: { + log_level: 'info', // 'debug' | 'info' | 'warn' | 'error' + }, + }, +}) +``` + +## Silent Disconnections in Background + +WebSocket connections can disconnect when apps are backgrounded (mobile, inactive tabs). + +**Solution:** Monitor connection state and re-subscribe when needed: + +```javascript +channel.subscribe((status) => { + if (status === 'SUBSCRIBED') { + // Re-track presence if needed + channel.track({ user_id: userId, online_at: new Date().toISOString() }) + } +}) +``` + +## Authorization Errors + +Private channel authorization fails when: +- User not authenticated (`setAuth()` not called) +- Missing RLS policies on `realtime.messages` +- Token expired (refresh before expiry) + +```javascript +// Refresh auth before token expires +await supabase.realtime.setAuth('fresh-jwt-token') +``` + +## Related + +- [patterns-cleanup.md](patterns-cleanup.md) +- [setup-auth.md](setup-auth.md) +- [Docs](https://supabase.com/docs/guides/realtime/troubleshooting) diff --git a/skills/supabase/references/realtime/postgres-changes.md b/skills/supabase/references/realtime/postgres-changes.md new file mode 100644 index 0000000..f3f0320 --- /dev/null +++ b/skills/supabase/references/realtime/postgres-changes.md @@ -0,0 +1,99 @@ +--- +title: Listen to Database Changes with Postgres Changes +impact: MEDIUM +impactDescription: Simple database change listeners with scaling limitations +tags: realtime, postgres_changes, database, subscribe, publication +--- + +## Listen to Database Changes with Postgres Changes + +Postgres Changes streams database changes via logical replication. Note: **Broadcast is recommended for new applications** due to better scalability. + +## When to Use Postgres Changes + +- Quick prototyping and development +- Low user counts (< 100 concurrent subscribers per table) +- When simplicity is more important than scale + +## Basic Setup + +**1. Add table to publication:** + +```sql +alter publication supabase_realtime add table messages; +``` + +**2. Subscribe to changes:** + +```javascript +const channel = supabase + .channel('db-changes') + .on( + 'postgres_changes', + { + event: 'INSERT', // 'INSERT' | 'UPDATE' | 'DELETE' | '*' + schema: 'public', + table: 'messages', + }, + (payload) => console.log('New row:', payload.new) + ) + .subscribe() +``` + +## Filter Syntax + +```javascript +.on('postgres_changes', { + event: '*', + schema: 'public', + table: 'messages', + filter: 'room_id=eq.123', // Only changes where room_id = 123 +}, callback) +``` + +| Filter | Example | +|--------|---------| +| `eq` | `id=eq.1` | +| `neq` | `status=neq.deleted` | +| `lt`, `lte` | `age=lt.65` | +| `gt`, `gte` | `quantity=gt.10` | +| `in` | `name=in.(red,blue,yellow)` (max 100 values) | + +## Receive Old Records on UPDATE/DELETE + +By default, only `new` records are sent. + +**Incorrect:** + +```sql +-- Only new record available in payload +alter publication supabase_realtime add table messages; +``` + +**Correct:** + +```sql +-- Enable old record in payload +alter table messages replica identity full; +alter publication supabase_realtime add table messages; +``` + +## Scaling Limitation + +Each change triggers RLS checks for every subscriber: + +```text +100 subscribers = 100 database reads per change +``` + +For high-traffic tables, migrate to [broadcast-database.md](broadcast-database.md). + +## DELETE Events Not Filterable + +Filters don't work on DELETE events due to how Postgres logical replication works. + +## Related + +- [broadcast-database.md](broadcast-database.md) +- [patterns-cleanup.md](patterns-cleanup.md) +- [Docs](https://supabase.com/docs/guides/realtime/postgres-changes) diff --git a/skills/supabase/references/realtime/presence-tracking.md b/skills/supabase/references/realtime/presence-tracking.md new file mode 100644 index 0000000..1cc8eeb --- /dev/null +++ b/skills/supabase/references/realtime/presence-tracking.md @@ -0,0 +1,87 @@ +--- +title: Track User Presence and Online Status +impact: MEDIUM +impactDescription: Enables features like online indicators and typing status +tags: realtime, presence, track, online, state +--- + +## Track User Presence and Online Status + +Presence synchronizes shared state between users. Use sparingly due to computational overhead. + +## Track Presence + +```javascript +const channel = supabase.channel('room:123', { + config: { private: true }, +}) + +channel + .on('presence', { event: 'sync' }, () => { + const state = channel.presenceState() + console.log('Online users:', Object.keys(state)) + }) + .on('presence', { event: 'join' }, ({ key, newPresences }) => { + console.log('User joined:', key, newPresences) + }) + .on('presence', { event: 'leave' }, ({ key, leftPresences }) => { + console.log('User left:', key, leftPresences) + }) + .subscribe(async (status) => { + if (status === 'SUBSCRIBED') { + await channel.track({ + user_id: 'user-123', + online_at: new Date().toISOString(), + }) + } + }) +``` + +## Get Current State + +```javascript +const state = channel.presenceState() +// Returns: { "key1": [{ user_id: "123" }], "key2": [{ user_id: "456" }] } +``` + +## Stop Tracking + +```javascript +await channel.untrack() +``` + +## Custom Presence Key + +By default, presence uses a UUIDv1 key. Override for user-specific tracking. + +**Incorrect:** + +```javascript +// Each browser tab gets separate presence entry +const channel = supabase.channel('room:123') +``` + +**Correct:** + +```javascript +// Same user shows once across tabs +const channel = supabase.channel('room:123', { + config: { + presence: { key: `user:${userId}` }, + }, +}) +``` + +## Quotas + +| Plan | Presence Messages/Second | +|------|-------------------------| +| Free | 20 | +| Pro | 50 | +| Team/Enterprise | 1,000 | + +## Related + +- [setup-channels.md](setup-channels.md) +- [patterns-cleanup.md](patterns-cleanup.md) +- [Docs](https://supabase.com/docs/guides/realtime/presence) diff --git a/skills/supabase/references/realtime/setup-auth.md b/skills/supabase/references/realtime/setup-auth.md new file mode 100644 index 0000000..c98d652 --- /dev/null +++ b/skills/supabase/references/realtime/setup-auth.md @@ -0,0 +1,83 @@ +--- +title: Configure Private Channels with Authentication +impact: CRITICAL +impactDescription: Prevents unauthorized access to real-time messages +tags: realtime, auth, private, rls, security, setAuth +--- + +## Configure Private Channels with Authentication + +Always use private channels in production. Public channels allow any client to subscribe. + +## Enable Private Channels + +**Incorrect:** + +```javascript +// Public channel - anyone can subscribe +const channel = supabase.channel('room:123:messages') +``` + +**Correct:** + +```javascript +// Private channel requires authentication +await supabase.realtime.setAuth() // Must call before subscribing +const channel = supabase.channel('room:123:messages', { + config: { private: true }, +}) +``` + +## RLS Policies on realtime.messages + +Private channels require RLS policies on the `realtime.messages` table. + +**Read access (subscribe to channel):** + +```sql +create policy "authenticated_users_can_receive" +on realtime.messages for select +to authenticated +using (true); +``` + +**Write access (send to channel):** + +```sql +create policy "authenticated_users_can_send" +on realtime.messages for insert +to authenticated +with check (true); +``` + +**Topic-specific access:** + +```sql +-- Only room members can receive messages +create policy "room_members_can_read" +on realtime.messages for select +to authenticated +using ( + extension in ('broadcast', 'presence') + and exists ( + select 1 from room_members + where user_id = (select auth.uid()) + and room_id = split_part(realtime.topic(), ':', 2)::uuid + ) +); +``` + +## Index RLS Policy Columns + +Missing indexes slow channel joins significantly. + +```sql +create index idx_room_members_user_room +on room_members(user_id, room_id); +``` + +## Related + +- [setup-channels.md](setup-channels.md) +- [broadcast-database.md](broadcast-database.md) +- [Docs](https://supabase.com/docs/guides/realtime/authorization) diff --git a/skills/supabase/references/realtime/setup-channels.md b/skills/supabase/references/realtime/setup-channels.md new file mode 100644 index 0000000..e004651 --- /dev/null +++ b/skills/supabase/references/realtime/setup-channels.md @@ -0,0 +1,70 @@ +--- +title: Create and Configure Realtime Channels +impact: HIGH +impactDescription: Proper channel setup enables reliable real-time communication +tags: realtime, channels, configuration, topics, naming +--- + +## Create and Configure Realtime Channels + +Channels are rooms where clients communicate. Use consistent naming and appropriate configuration. + +## Topic Naming Convention + +Use `scope:entity:id` format for predictable, filterable topics. + +**Incorrect:** + +```javascript +// Generic names make filtering impossible +const channel = supabase.channel('messages') +const channel = supabase.channel('room1') +``` + +**Correct:** + +```javascript +// Structured naming enables topic-based RLS policies +const channel = supabase.channel('room:123:messages') +const channel = supabase.channel('user:456:notifications') +const channel = supabase.channel('game:789:moves') +``` + +## Channel Configuration Options + +```javascript +const channel = supabase.channel('room:123:messages', { + config: { + private: true, // Require authentication (recommended) + broadcast: { + self: true, // Receive own messages + ack: true, // Get server acknowledgment + }, + presence: { + key: 'user-session-id', // Custom presence key (default: UUIDv1) + }, + }, +}) +``` + +## Event Naming + +Use snake_case for event names. + +**Incorrect:** + +```javascript +channel.send({ type: 'broadcast', event: 'newMessage', payload: {} }) +``` + +**Correct:** + +```javascript +channel.send({ type: 'broadcast', event: 'message_created', payload: {} }) +channel.send({ type: 'broadcast', event: 'user_joined', payload: {} }) +``` + +## Related + +- [setup-auth.md](setup-auth.md) +- [Docs](https://supabase.com/docs/guides/realtime/concepts)