diff --git a/skills/supabase/references/db-conn-pooling.md b/skills/supabase/references/db-conn-pooling.md index 9e5521b..a1c2963 100644 --- a/skills/supabase/references/db-conn-pooling.md +++ b/skills/supabase/references/db-conn-pooling.md @@ -39,7 +39,9 @@ const prisma = new PrismaClient({ ## Session Mode (Port 5432) -Best for: Long-running servers, apps needing prepared statements. +Alternative to direct connection when IPv4 is required. Supports prepared +statements, SET commands, and LISTEN/NOTIFY. Recommended for migrations +when direct connection is unavailable. ```bash ## Session mode (via pooler for IPv4) @@ -48,11 +50,21 @@ postgres://postgres.[ref]:[password]@aws-0-[region].pooler.supabase.com:5432/pos ## Direct Connection (Port 5432) -Best for: Migrations, admin tasks, persistent servers. +Best for: Admin tasks, persistent servers. ```bash ## Direct connection (IPv6 only unless IPv4 add-on enabled) -postgres://postgres.[ref]:[password]@db.[ref].supabase.co:5432/postgres +postgres://postgres:[password]@db.[ref].supabase.co:5432/postgres +``` + +## Dedicated Pooler (PgBouncer) + +For paying customers. Co-located with Postgres for best latency. Transaction +mode only. Requires IPv6 or IPv4 add-on. + +```bash +## Dedicated pooler (port 6543) +postgres://postgres.[ref]:[password]@db.[ref].supabase.co:6543/postgres ``` ## Common Mistakes diff --git a/skills/supabase/references/db-migrations-diff.md b/skills/supabase/references/db-migrations-diff.md index 8d540c7..82b06d1 100644 --- a/skills/supabase/references/db-migrations-diff.md +++ b/skills/supabase/references/db-migrations-diff.md @@ -77,16 +77,18 @@ npx supabase db diff --linked -f sync_remote_changes - Indexes - Constraints - Functions and triggers -- RLS policies +- RLS policies (new policies only; `alter policy` may not diff correctly) - Extensions ## What diff Does NOT Capture +- Publications +- Storage buckets +- Views with `security_invoker` attributes - DML (INSERT, UPDATE, DELETE) -- View ownership changes -- Materialized views -- Partitions -- Comments + +**Caveat:** `alter policy` changes may not be captured correctly. Use versioned +migrations for RLS policy modifications. For these, write manual migrations. diff --git a/skills/supabase/references/db-migrations-idempotent.md b/skills/supabase/references/db-migrations-idempotent.md index dcd0970..5c7b294 100644 --- a/skills/supabase/references/db-migrations-idempotent.md +++ b/skills/supabase/references/db-migrations-idempotent.md @@ -38,15 +38,7 @@ create index if not exists idx_users_email on users(email); ```sql -- Add column only if it doesn't exist -do $$ -begin - if not exists ( - select 1 from information_schema.columns - where table_name = 'users' and column_name = 'phone' - ) then - alter table users add column phone text; - end if; -end $$; +alter table users add column if not exists phone text; ``` ## Idempotent Drops diff --git a/skills/supabase/references/db-migrations-testing.md b/skills/supabase/references/db-migrations-testing.md index 25f9fe8..7c8908c 100644 --- a/skills/supabase/references/db-migrations-testing.md +++ b/skills/supabase/references/db-migrations-testing.md @@ -72,18 +72,21 @@ on conflict (id) do nothing; # Apply all pending migrations npx supabase migration up -# Check migration status +# Check migration status (requires supabase link for remote) npx supabase migration list ``` ## Repair Failed Migration -If a migration partially fails: +If local and remote migration histories diverge, use `migration repair` to +manually update the remote history table without re-executing migrations: ```bash -# Fix the migration file -# Then repair the migration history +# Mark a migration as applied (inserts record without running it) npx supabase migration repair --status applied 20240315120000 + +# Mark a migration as reverted (removes record from history) +npx supabase migration repair --status reverted 20240315120000 ``` ## Inspect Database State diff --git a/skills/supabase/references/db-perf-indexes.md b/skills/supabase/references/db-perf-indexes.md index 6cb750c..8cda7c7 100644 --- a/skills/supabase/references/db-perf-indexes.md +++ b/skills/supabase/references/db-perf-indexes.md @@ -33,7 +33,9 @@ create index idx_logs_created on logs using brin(created_at); create index idx_events_id on events using brin(id); ``` -**When to use:** Tables with millions of rows where data is inserted in order. +**When to use:** Tables with millions of rows where data is inserted in order and +updated infrequently. Ideal for `created_at` on append-only tables like orders +or logs. Routinely 10x+ smaller than equivalent B-tree indexes. ## GIN (Generalized Inverted Index) diff --git a/skills/supabase/references/db-perf-query-optimization.md b/skills/supabase/references/db-perf-query-optimization.md index ce7b2e0..155a23e 100644 --- a/skills/supabase/references/db-perf-query-optimization.md +++ b/skills/supabase/references/db-perf-query-optimization.md @@ -125,21 +125,35 @@ const { count } = await supabase ## Debug Query Performance +`explain()` is disabled by default to protect sensitive database information. +Enable it first: + +```sql +alter role authenticator set pgrst.db_plan_enabled to 'true'; +notify pgrst, 'reload config'; +``` + +Then use it in queries: + ```javascript -// Get query execution plan +// Get query execution plan (text format by default) const { data } = await supabase .from("posts") .select("*") .eq("author_id", userId) .explain({ analyze: true, verbose: true }); -console.log(data); // Shows execution plan +// JSON format +const { data: jsonPlan } = await supabase + .from("posts") + .select("*") + .explain({ analyze: true, format: "json" }); ``` -Enable explain in database: +Disable after use: ```sql -alter role authenticator set pgrst.db_plan_enabled to true; +alter role authenticator set pgrst.db_plan_enabled to 'false'; notify pgrst, 'reload config'; ``` diff --git a/skills/supabase/references/db-rls-mandatory.md b/skills/supabase/references/db-rls-mandatory.md index 1308837..91fd489 100644 --- a/skills/supabase/references/db-rls-mandatory.md +++ b/skills/supabase/references/db-rls-mandatory.md @@ -37,13 +37,16 @@ alter table profiles enable row level security; create policy "Users can view own profile" on profiles for select to authenticated - using (auth.uid() = user_id); + using ((select auth.uid()) = user_id); ``` Tables created via Dashboard have RLS enabled by default. Tables created via SQL require manual enablement. Supabase sends daily warnings for tables without RLS. -**Note:** Service role key bypasses ALL RLS policies. Never expose it to browsers. +**Note:** Service role key bypasses RLS policies only when no user is signed in. +If a user is authenticated, Supabase adheres to that user's RLS policies even +when the client is initialized with the service role key. Never expose the +service role key to browsers. ## Related diff --git a/skills/supabase/references/db-rls-performance.md b/skills/supabase/references/db-rls-performance.md index c559e53..23cb835 100644 --- a/skills/supabase/references/db-rls-performance.md +++ b/skills/supabase/references/db-rls-performance.md @@ -90,12 +90,15 @@ using ( -- Function in private schema create function private.user_team_ids() returns setof uuid -language sql +language plpgsql security definer stable +set search_path = '' as $$ - select team_id from team_members - where user_id = (select auth.uid()) +begin + return query select team_id from public.team_members + where user_id = (select auth.uid()); +end; $$; -- Policy uses cached function result diff --git a/skills/supabase/references/db-rls-views.md b/skills/supabase/references/db-rls-views.md index 7aa5d7e..3b5d309 100644 --- a/skills/supabase/references/db-rls-views.md +++ b/skills/supabase/references/db-rls-views.md @@ -30,14 +30,16 @@ as from profiles; ``` -**Correct (Older Postgres):** +**Correct (Pre-Postgres 15):** + +`security_invoker` is not available before PG15. Use one of these alternatives: ```sql --- Option 1: Revoke direct access, create RLS on view -revoke all on public_profiles from anon, authenticated; +-- Option 1: Create a public table synced via trigger instead of a view +-- This avoids the security definer bypass entirely -- Option 2: Create view in unexposed schema -create schema private; +create schema if not exists private; create view private.profiles_view as select * from profiles; ``` diff --git a/skills/supabase/references/db-schema-auth-fk.md b/skills/supabase/references/db-schema-auth-fk.md index cb0609a..3241f8f 100644 --- a/skills/supabase/references/db-schema-auth-fk.md +++ b/skills/supabase/references/db-schema-auth-fk.md @@ -30,6 +30,14 @@ create table profiles ( username text, avatar_url text ); + +-- Enable RLS on profiles +alter table profiles enable row level security; + +create policy "Users can view own profile" + on profiles for select + to authenticated + using ((select auth.uid()) = id); ``` ## Alternative: SET NULL for Optional Relationships @@ -72,7 +80,7 @@ create trigger on_auth_user_created ``` **Important:** Use `security definer` and `set search_path = ''` for triggers on -auth.users. +auth.users. If the trigger fails, it will block signups — test thoroughly. ## Related diff --git a/skills/supabase/references/db-schema-extensions.md b/skills/supabase/references/db-schema-extensions.md index 6a8e31a..76606ad 100644 --- a/skills/supabase/references/db-schema-extensions.md +++ b/skills/supabase/references/db-schema-extensions.md @@ -39,8 +39,8 @@ create extension if not exists vector with schema extensions; -- Scheduled jobs (pg_cron requires pg_catalog, not extensions) create extension if not exists pg_cron with schema pg_catalog; --- HTTP requests from database -create extension if not exists pg_net with schema extensions; +-- HTTP requests from database (pg_net creates its own 'net' schema) +create extension if not exists pg_net; -- Full-text search improvements create extension if not exists pg_trgm with schema extensions; @@ -65,14 +65,19 @@ select * from pg_extension; ## Using Extensions ```sql --- pgvector example +-- pgvector example (use extensions. prefix for type) create table documents ( id bigint primary key generated always as identity, content text, - embedding vector(1536) -- OpenAI ada-002 dimensions + embedding extensions.vector(1536) -- OpenAI ada-002 dimensions ); -create index on documents using ivfflat (embedding vector_cosine_ops); +-- HNSW is recommended over IVFFlat for most use cases +create index on documents using hnsw (embedding extensions.vector_cosine_ops); + +-- If using IVFFlat, lists parameter is required +create index on documents using ivfflat (embedding extensions.vector_cosine_ops) + with (lists = 100); ``` ## Related diff --git a/skills/supabase/references/db-schema-jsonb.md b/skills/supabase/references/db-schema-jsonb.md index 1a36b6f..957c2e9 100644 --- a/skills/supabase/references/db-schema-jsonb.md +++ b/skills/supabase/references/db-schema-jsonb.md @@ -87,6 +87,18 @@ const { data } = await supabase .from("users") .select("email, preferences->theme") .eq("preferences->>notifications", "true"); + +// containment queries +const { data: darkUsers } = await supabase + .from("users") + .select("*") + .contains("preferences", { theme: "dark" }); + +// contained-by query +const { data: subset } = await supabase + .from("users") + .select("*") + .containedBy("preferences", { theme: "dark", notifications: true }); ``` ## Related diff --git a/skills/supabase/references/db-schema-realtime.md b/skills/supabase/references/db-schema-realtime.md index db4a9fb..b58a43a 100644 --- a/skills/supabase/references/db-schema-realtime.md +++ b/skills/supabase/references/db-schema-realtime.md @@ -43,7 +43,7 @@ alter publication supabase_realtime add table messages; **Via Dashboard:** -Database > Publications > supabase_realtime > Add table +Stop and ask the user to navigate to the Supabase Dashboard and add the table under Database > Publications > supabase_realtime. ## Realtime with RLS @@ -73,7 +73,9 @@ const channel = supabase - Add indexes on columns used in Realtime filters - Keep RLS policies simple for subscribed tables -- Monitor "Realtime Private Channel RLS Execution Time" in Dashboard +- Stop and ask the user to monitor "Realtime Private Channel RLS Execution Time" in the Supabase Dashboard +- Prefer Broadcast over Postgres Changes for scalability — Postgres Changes + has limitations at scale due to single-thread processing ## Replica Identity @@ -85,6 +87,10 @@ all columns: alter table messages replica identity full; ``` +**Caveat:** RLS policies are not applied to DELETE events — there is no way for +Postgres to verify user access to a deleted record. With `replica identity full`, +DELETE events only include primary key columns in the `old` record. + ## Related - [rls-mandatory.md](rls-mandatory.md) diff --git a/skills/supabase/references/db-schema-timestamps.md b/skills/supabase/references/db-schema-timestamps.md index 2e65834..a9257c6 100644 --- a/skills/supabase/references/db-schema-timestamps.md +++ b/skills/supabase/references/db-schema-timestamps.md @@ -39,8 +39,15 @@ create table events ( - Stores time in UTC internally - Converts to/from session timezone automatically - `now()` returns current time in session timezone, stored as UTC +- Supabase databases are set to UTC by default — keep it that way ```sql +-- Check current timezone +show timezone; + +-- Change database timezone (not recommended) +alter database postgres set timezone to 'America/New_York'; + -- Insert with timezone insert into events (name, starts_at) values ('Launch', '2024-03-15 10:00:00-05'); -- EST diff --git a/skills/supabase/references/db-security-functions.md b/skills/supabase/references/db-security-functions.md index cd1f4fe..f86d9ef 100644 --- a/skills/supabase/references/db-security-functions.md +++ b/skills/supabase/references/db-security-functions.md @@ -67,7 +67,9 @@ begin raise exception 'Unauthorized'; end if; - delete from auth.users where id = target_user_id; + -- Use the Supabase Admin API to delete users instead of direct table access + -- Direct DML on auth.users is unsupported and may break auth internals + delete from public.profiles where id = target_user_id; end; $$; ``` @@ -87,6 +89,10 @@ as $$ where user_id = (select auth.uid()); $$; +-- Revoke default public access, grant only to authenticated +revoke execute on function private.user_teams from public; +grant execute on function private.user_teams to authenticated; + -- RLS policy uses cached function result (no per-row join) create policy "Team members see team data" on team_data for select to authenticated @@ -95,10 +101,13 @@ create policy "Team members see team data" on team_data ## Security Best Practices -1. **Always set search_path = ''** - Prevents search_path injection attacks -2. **Validate caller permissions** - Don't assume caller is authorized -3. **Keep functions minimal** - Only expose necessary operations -4. **Log sensitive operations** - Audit trail for admin actions +1. **Always set search_path = ''** - Prevents search_path injection attacks. Qualify all table references (e.g., `public.my_table`) +2. **Revoke default execute permissions** - `revoke execute on function my_func from public;` then grant selectively +3. **Validate caller permissions** - Don't assume caller is authorized +4. **Keep functions minimal** - Only expose necessary operations +5. **Log sensitive operations** - Audit trail for admin actions +6. **Never directly modify `auth.users`** - Use the Supabase Admin API instead +7. **JWT freshness caveat** - `auth.jwt()` values reflect the JWT at issuance time. Changes to `app_metadata` (e.g., removing a role) are not reflected until the JWT is refreshed ```sql create function private.sensitive_operation() @@ -109,7 +118,7 @@ set search_path = '' as $$ begin -- Log the operation - insert into audit_log (user_id, action, timestamp) + insert into public.audit_log (user_id, action, timestamp) values ((select auth.uid()), 'sensitive_operation', now()); -- Perform operation diff --git a/skills/supabase/references/db-security-service-role.md b/skills/supabase/references/db-security-service-role.md index 4152f26..30d7ddc 100644 --- a/skills/supabase/references/db-security-service-role.md +++ b/skills/supabase/references/db-security-service-role.md @@ -71,10 +71,16 @@ The publishable and secret keys are replacing the legacy JWT-based keys. Decode ## If Service Key is Exposed -1. Immediately rotate keys in Dashboard > Settings > API Keys -2. Review database for unauthorized changes -3. Check logs for suspicious activity -4. Update all backend services with new key +Don't rush. Remediate the root cause first, then: + +1. Stop and ask the user to create a new secret API key in the Supabase Dashboard under Settings > API Keys +2. Replace the compromised key across all backend services +3. Delete the old key (irreversible) +4. Review database for unauthorized changes +5. Check logs for suspicious activity + +For legacy JWT `service_role` keys, transition to the new secret key format +first, then rotate the JWT secret if it was also compromised. ## Alternative: Security Definer Functions diff --git a/skills/supabase/references/realtime-broadcast-basics.md b/skills/supabase/references/realtime-broadcast-basics.md index 96d08d6..011826e 100644 --- a/skills/supabase/references/realtime-broadcast-basics.md +++ b/skills/supabase/references/realtime-broadcast-basics.md @@ -7,7 +7,9 @@ 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 applications that require more concurrent connections. +Broadcast enables low-latency pub/sub messaging between clients. Prefer Broadcast +over Postgres Changes for scalability — Postgres Changes triggers per-subscriber +RLS checks and processes on a single thread. ## Subscribe to Broadcast Events @@ -42,10 +44,12 @@ channel.send({ }) ``` -**Before subscribing or one-off (HTTP):** +**Before subscribing or one-off (HTTP — no subscribe needed):** ```javascript -await channel.httpSend('message_created', { text: 'Hello!' }) +supabase + .channel('room:123') + .httpSend('message_created', { text: 'Hello!' }) ``` ## Receive Own Messages @@ -79,7 +83,7 @@ const channel = supabase.channel('room:123', { }, }) -// Returns 'ok' when server confirms receipt +// Server confirms receipt when ack is enabled const status = await channel.send({ type: 'broadcast', event: 'message_created', diff --git a/skills/supabase/references/realtime-broadcast-database.md b/skills/supabase/references/realtime-broadcast-database.md index 3d4c57d..eadc232 100644 --- a/skills/supabase/references/realtime-broadcast-database.md +++ b/skills/supabase/references/realtime-broadcast-database.md @@ -17,6 +17,7 @@ Broadcasts database changes in a standard format. create or replace function room_messages_broadcast() returns trigger security definer +set search_path = '' language plpgsql as $$ begin @@ -29,7 +30,7 @@ begin new, -- new record old -- old record ); - return coalesce(new, old); + return null; -- AFTER trigger return value is ignored end; $$; diff --git a/skills/supabase/references/realtime-patterns-cleanup.md b/skills/supabase/references/realtime-patterns-cleanup.md index 59a3c17..4c4a456 100644 --- a/skills/supabase/references/realtime-patterns-cleanup.md +++ b/skills/supabase/references/realtime-patterns-cleanup.md @@ -44,7 +44,7 @@ function ChatRoom({ roomId }) { return () => { if (channelRef.current) { - supabase.removeChannel(channelRef.current) + channelRef.current.unsubscribe() channelRef.current = null } } @@ -81,10 +81,11 @@ channel.subscribe() |------|-----------------|------------------------| | Free | 200 | 100 | | Pro | 500 | 100 | +| Pro (no spend cap) | 10,000 | 100 | | Team | 10,000 | 100 | Leaked channels count against quotas even when inactive. -For Pay as you go customers you can edit these limits on [Realtime Settings](https://supabase.com/dashboard/project/_/realtime/settings) +For Pay as you go customers, stop and ask the user to edit these limits in the Supabase Dashboard under Realtime Settings. ## Related - [patterns-errors.md](patterns-errors.md) diff --git a/skills/supabase/references/realtime-patterns-debugging.md b/skills/supabase/references/realtime-patterns-debugging.md index 3763ed1..c9fa2ab 100644 --- a/skills/supabase/references/realtime-patterns-debugging.md +++ b/skills/supabase/references/realtime-patterns-debugging.md @@ -41,14 +41,12 @@ Log message types: ## Server-Side Log Level -Configure Realtime server log verbosity via client params: +Configure Realtime server log verbosity: ```javascript const supabase = createClient(url, key, { realtime: { - params: { - log_level: 'info', // 'debug' | 'info' | 'warn' | 'error' - }, + logLevel: 'info', // 'info' | 'warn' | 'error' }, }) ``` diff --git a/skills/supabase/references/realtime-patterns-errors.md b/skills/supabase/references/realtime-patterns-errors.md index ba599fb..59ad5ca 100644 --- a/skills/supabase/references/realtime-patterns-errors.md +++ b/skills/supabase/references/realtime-patterns-errors.md @@ -45,9 +45,9 @@ channel.subscribe((status, err) => { | Error | Cause | Solution | |-------|-------|----------| | `too_many_connections` | Connection limit exceeded | Clean up unused channels, upgrade plan | +| `too_many_channels` | Too many channels per connection | Remove unused channels (limit: 100/connection) | | `too_many_joins` | Channel join rate exceeded | Reduce join frequency | -| `ConnectionRateLimitReached` | Max connections reached | Upgrade plan | -| `DatabaseLackOfConnections` | No available DB connections | Increase compute size | +| `tenant_events` | Too many messages/second | Reduce message rate or upgrade plan | | `TenantNotFound` | Invalid project reference | Verify project URL | ## Automatic Reconnection @@ -72,12 +72,30 @@ Log message types include `push`, `receive`, `transport`, `error`, and `worker`. ## Silent Disconnections in Background -WebSocket connections can disconnect when apps are backgrounded (mobile, inactive tabs). Supabase reconnects automatically. Re-track presence after reconnection if needed: +WebSocket connections can disconnect when apps are backgrounded (mobile, inactive +tabs) due to browser throttling of timers. Two solutions: + +```javascript +const supabase = createClient(url, key, { + realtime: { + // 1. Use Web Worker to prevent browser throttling of heartbeats + worker: true, + // 2. Detect disconnections and reconnect + heartbeatCallback: (client) => { + if (client.connectionState() === 'disconnected') { + client.connect() + } + }, + }, +}) +``` + +Use both together: `worker` prevents throttling, `heartbeatCallback` handles +network-level disconnections. Re-track presence after reconnection if needed: ```javascript channel.subscribe((status) => { if (status === 'SUBSCRIBED') { - // Re-track presence after reconnection channel.track({ user_id: userId, online_at: new Date().toISOString() }) } }) @@ -94,4 +112,4 @@ Private channel authorization fails when: - [patterns-cleanup.md](patterns-cleanup.md) - [setup-auth.md](setup-auth.md) -- [Docs](https://supabase.com/docs/guides/realtime/troubleshooting) +- [Docs](https://supabase.com/docs/guides/realtime/limits) diff --git a/skills/supabase/references/realtime-postgres-changes.md b/skills/supabase/references/realtime-postgres-changes.md index 92e266d..1ff93e7 100644 --- a/skills/supabase/references/realtime-postgres-changes.md +++ b/skills/supabase/references/realtime-postgres-changes.md @@ -78,6 +78,9 @@ alter table messages replica identity full; alter publication supabase_realtime add table messages; ``` +**Caveat:** RLS policies are not applied to DELETE events. With `replica identity +full`, DELETE events still only include primary key columns in the `old` record. + ## Scaling Limitation Each change triggers RLS checks for every subscriber: @@ -86,6 +89,10 @@ Each change triggers RLS checks for every subscriber: 100 subscribers = 100 database reads per change ``` +Database changes are processed on a single thread to maintain order. Compute +upgrades do not significantly improve Postgres Changes throughput. If your +database cannot authorize changes fast enough, changes are delayed until timeout. + For high-traffic tables, migrate to [broadcast-database.md](broadcast-database.md). ## DELETE Events Not Filterable diff --git a/skills/supabase/references/realtime-presence-tracking.md b/skills/supabase/references/realtime-presence-tracking.md index 8c9ec59..06eea4e 100644 --- a/skills/supabase/references/realtime-presence-tracking.md +++ b/skills/supabase/references/realtime-presence-tracking.md @@ -12,9 +12,9 @@ Presence synchronizes shared state between users. Use sparingly due to computati ## Track Presence ```javascript -const channel = supabase.channel('room:123', { - config: { private: true }, -}) +// private: true is a channel setting, not presence-specific +// Add it for production use (see setup-auth.md) +const channel = supabase.channel('room:123') channel .on('presence', { event: 'sync' }, () => { @@ -78,8 +78,9 @@ const channel = supabase.channel('room:123', { |------|-------------------------| | Free | 20 | | Pro | 50 | +| Pro (no spend cap) | 1,000 | | Team/Enterprise | 1,000 | -For Pay as you go customers you can edit these limits on [Realtime Settings](https://supabase.com/dashboard/project/_/realtime/settings) +For Pay as you go customers, stop and ask the user to edit these limits in the Supabase Dashboard under Realtime Settings. ## Related - [setup-channels.md](setup-channels.md) diff --git a/skills/supabase/references/realtime-setup-auth.md b/skills/supabase/references/realtime-setup-auth.md index 7412514..e8d0e74 100644 --- a/skills/supabase/references/realtime-setup-auth.md +++ b/skills/supabase/references/realtime-setup-auth.md @@ -11,6 +11,10 @@ Always use private channels in production. Public channels allow any client to s ## Enable Private Channels +**Prerequisite:** Stop and ask the user to disable "Allow public access" in the +Supabase Dashboard under Realtime Settings. Without this, channels are public +even with `private: true`. + **Incorrect:** ```javascript @@ -57,11 +61,11 @@ create policy "room_members_can_read" on realtime.messages for select to authenticated using ( - extension in ('broadcast', 'presence') - and exists ( + exists ( select 1 from room_members where user_id = (select auth.uid()) - and room_id = split_part(realtime.topic(), ':', 2)::uuid + and topic = (select realtime.topic()) + and realtime.messages.extension in ('broadcast', 'presence') ) ); ``` diff --git a/skills/supabase/references/realtime-setup-channels.md b/skills/supabase/references/realtime-setup-channels.md index e004651..2435dd3 100644 --- a/skills/supabase/references/realtime-setup-channels.md +++ b/skills/supabase/references/realtime-setup-channels.md @@ -11,7 +11,7 @@ Channels are rooms where clients communicate. Use consistent naming and appropri ## Topic Naming Convention -Use `scope:entity:id` format for predictable, filterable topics. +Use `scope:id:entity` format for predictable, filterable topics. **Incorrect:** @@ -35,7 +35,7 @@ const channel = supabase.channel('game:789:moves') ```javascript const channel = supabase.channel('room:123:messages', { config: { - private: true, // Require authentication (recommended) + private: true, // Require authentication (requires RLS on realtime.messages) broadcast: { self: true, // Receive own messages ack: true, // Get server acknowledgment