mirror of
https://github.com/supabase/agent-skills.git
synced 2026-03-27 10:09:26 +08:00
minor changes to database and realtime reference fiels after docs analysis
This commit is contained in:
@@ -39,7 +39,9 @@ const prisma = new PrismaClient({
|
|||||||
|
|
||||||
## Session Mode (Port 5432)
|
## 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
|
```bash
|
||||||
## Session mode (via pooler for IPv4)
|
## 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)
|
## Direct Connection (Port 5432)
|
||||||
|
|
||||||
Best for: Migrations, admin tasks, persistent servers.
|
Best for: Admin tasks, persistent servers.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
## Direct connection (IPv6 only unless IPv4 add-on enabled)
|
## 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
|
## Common Mistakes
|
||||||
|
|||||||
@@ -77,16 +77,18 @@ npx supabase db diff --linked -f sync_remote_changes
|
|||||||
- Indexes
|
- Indexes
|
||||||
- Constraints
|
- Constraints
|
||||||
- Functions and triggers
|
- Functions and triggers
|
||||||
- RLS policies
|
- RLS policies (new policies only; `alter policy` may not diff correctly)
|
||||||
- Extensions
|
- Extensions
|
||||||
|
|
||||||
## What diff Does NOT Capture
|
## What diff Does NOT Capture
|
||||||
|
|
||||||
|
- Publications
|
||||||
|
- Storage buckets
|
||||||
|
- Views with `security_invoker` attributes
|
||||||
- DML (INSERT, UPDATE, DELETE)
|
- DML (INSERT, UPDATE, DELETE)
|
||||||
- View ownership changes
|
|
||||||
- Materialized views
|
**Caveat:** `alter policy` changes may not be captured correctly. Use versioned
|
||||||
- Partitions
|
migrations for RLS policy modifications.
|
||||||
- Comments
|
|
||||||
|
|
||||||
For these, write manual migrations.
|
For these, write manual migrations.
|
||||||
|
|
||||||
|
|||||||
@@ -38,15 +38,7 @@ create index if not exists idx_users_email on users(email);
|
|||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- Add column only if it doesn't exist
|
-- Add column only if it doesn't exist
|
||||||
do $$
|
alter table users add column if not exists phone text;
|
||||||
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 $$;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Idempotent Drops
|
## Idempotent Drops
|
||||||
|
|||||||
@@ -72,18 +72,21 @@ on conflict (id) do nothing;
|
|||||||
# Apply all pending migrations
|
# Apply all pending migrations
|
||||||
npx supabase migration up
|
npx supabase migration up
|
||||||
|
|
||||||
# Check migration status
|
# Check migration status (requires supabase link for remote)
|
||||||
npx supabase migration list
|
npx supabase migration list
|
||||||
```
|
```
|
||||||
|
|
||||||
## Repair Failed Migration
|
## 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
|
```bash
|
||||||
# Fix the migration file
|
# Mark a migration as applied (inserts record without running it)
|
||||||
# Then repair the migration history
|
|
||||||
npx supabase migration repair --status applied 20240315120000
|
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
|
## Inspect Database State
|
||||||
|
|||||||
@@ -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);
|
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)
|
## GIN (Generalized Inverted Index)
|
||||||
|
|
||||||
|
|||||||
@@ -125,21 +125,35 @@ const { count } = await supabase
|
|||||||
|
|
||||||
## Debug Query Performance
|
## 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
|
```javascript
|
||||||
// Get query execution plan
|
// Get query execution plan (text format by default)
|
||||||
const { data } = await supabase
|
const { data } = await supabase
|
||||||
.from("posts")
|
.from("posts")
|
||||||
.select("*")
|
.select("*")
|
||||||
.eq("author_id", userId)
|
.eq("author_id", userId)
|
||||||
.explain({ analyze: true, verbose: true });
|
.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
|
```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';
|
notify pgrst, 'reload config';
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -37,13 +37,16 @@ alter table profiles enable row level security;
|
|||||||
create policy "Users can view own profile"
|
create policy "Users can view own profile"
|
||||||
on profiles for select
|
on profiles for select
|
||||||
to authenticated
|
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
|
Tables created via Dashboard have RLS enabled by default. Tables created via SQL
|
||||||
require manual enablement. Supabase sends daily warnings for tables without RLS.
|
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
|
## Related
|
||||||
|
|
||||||
|
|||||||
@@ -90,12 +90,15 @@ using (
|
|||||||
-- Function in private schema
|
-- Function in private schema
|
||||||
create function private.user_team_ids()
|
create function private.user_team_ids()
|
||||||
returns setof uuid
|
returns setof uuid
|
||||||
language sql
|
language plpgsql
|
||||||
security definer
|
security definer
|
||||||
stable
|
stable
|
||||||
|
set search_path = ''
|
||||||
as $$
|
as $$
|
||||||
select team_id from team_members
|
begin
|
||||||
where user_id = (select auth.uid())
|
return query select team_id from public.team_members
|
||||||
|
where user_id = (select auth.uid());
|
||||||
|
end;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
-- Policy uses cached function result
|
-- Policy uses cached function result
|
||||||
|
|||||||
@@ -30,14 +30,16 @@ as
|
|||||||
from profiles;
|
from profiles;
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (Older Postgres):**
|
**Correct (Pre-Postgres 15):**
|
||||||
|
|
||||||
|
`security_invoker` is not available before PG15. Use one of these alternatives:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- Option 1: Revoke direct access, create RLS on view
|
-- Option 1: Create a public table synced via trigger instead of a view
|
||||||
revoke all on public_profiles from anon, authenticated;
|
-- This avoids the security definer bypass entirely
|
||||||
|
|
||||||
-- Option 2: Create view in unexposed schema
|
-- Option 2: Create view in unexposed schema
|
||||||
create schema private;
|
create schema if not exists private;
|
||||||
create view private.profiles_view as
|
create view private.profiles_view as
|
||||||
select * from profiles;
|
select * from profiles;
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ create table profiles (
|
|||||||
username text,
|
username text,
|
||||||
avatar_url 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
|
## 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
|
**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
|
## Related
|
||||||
|
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ create extension if not exists vector with schema extensions;
|
|||||||
-- Scheduled jobs (pg_cron requires pg_catalog, not extensions)
|
-- Scheduled jobs (pg_cron requires pg_catalog, not extensions)
|
||||||
create extension if not exists pg_cron with schema pg_catalog;
|
create extension if not exists pg_cron with schema pg_catalog;
|
||||||
|
|
||||||
-- HTTP requests from database
|
-- HTTP requests from database (pg_net creates its own 'net' schema)
|
||||||
create extension if not exists pg_net with schema extensions;
|
create extension if not exists pg_net;
|
||||||
|
|
||||||
-- Full-text search improvements
|
-- Full-text search improvements
|
||||||
create extension if not exists pg_trgm with schema extensions;
|
create extension if not exists pg_trgm with schema extensions;
|
||||||
@@ -65,14 +65,19 @@ select * from pg_extension;
|
|||||||
## Using Extensions
|
## Using Extensions
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- pgvector example
|
-- pgvector example (use extensions. prefix for type)
|
||||||
create table documents (
|
create table documents (
|
||||||
id bigint primary key generated always as identity,
|
id bigint primary key generated always as identity,
|
||||||
content text,
|
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
|
## Related
|
||||||
|
|||||||
@@ -87,6 +87,18 @@ const { data } = await supabase
|
|||||||
.from("users")
|
.from("users")
|
||||||
.select("email, preferences->theme")
|
.select("email, preferences->theme")
|
||||||
.eq("preferences->>notifications", "true");
|
.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
|
## Related
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ alter publication supabase_realtime add table messages;
|
|||||||
|
|
||||||
**Via Dashboard:**
|
**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
|
## Realtime with RLS
|
||||||
|
|
||||||
@@ -73,7 +73,9 @@ const channel = supabase
|
|||||||
|
|
||||||
- Add indexes on columns used in Realtime filters
|
- Add indexes on columns used in Realtime filters
|
||||||
- Keep RLS policies simple for subscribed tables
|
- 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
|
## Replica Identity
|
||||||
|
|
||||||
@@ -85,6 +87,10 @@ all columns:
|
|||||||
alter table messages replica identity full;
|
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
|
## Related
|
||||||
|
|
||||||
- [rls-mandatory.md](rls-mandatory.md)
|
- [rls-mandatory.md](rls-mandatory.md)
|
||||||
|
|||||||
@@ -39,8 +39,15 @@ create table events (
|
|||||||
- Stores time in UTC internally
|
- Stores time in UTC internally
|
||||||
- Converts to/from session timezone automatically
|
- Converts to/from session timezone automatically
|
||||||
- `now()` returns current time in session timezone, stored as UTC
|
- `now()` returns current time in session timezone, stored as UTC
|
||||||
|
- Supabase databases are set to UTC by default — keep it that way
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
|
-- Check current timezone
|
||||||
|
show timezone;
|
||||||
|
|
||||||
|
-- Change database timezone (not recommended)
|
||||||
|
alter database postgres set timezone to 'America/New_York';
|
||||||
|
|
||||||
-- Insert with timezone
|
-- Insert with timezone
|
||||||
insert into events (name, starts_at)
|
insert into events (name, starts_at)
|
||||||
values ('Launch', '2024-03-15 10:00:00-05'); -- EST
|
values ('Launch', '2024-03-15 10:00:00-05'); -- EST
|
||||||
|
|||||||
@@ -67,7 +67,9 @@ begin
|
|||||||
raise exception 'Unauthorized';
|
raise exception 'Unauthorized';
|
||||||
end if;
|
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;
|
end;
|
||||||
$$;
|
$$;
|
||||||
```
|
```
|
||||||
@@ -87,6 +89,10 @@ as $$
|
|||||||
where user_id = (select auth.uid());
|
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)
|
-- RLS policy uses cached function result (no per-row join)
|
||||||
create policy "Team members see team data" on team_data
|
create policy "Team members see team data" on team_data
|
||||||
for select to authenticated
|
for select to authenticated
|
||||||
@@ -95,10 +101,13 @@ create policy "Team members see team data" on team_data
|
|||||||
|
|
||||||
## Security Best Practices
|
## Security Best Practices
|
||||||
|
|
||||||
1. **Always set search_path = ''** - Prevents search_path injection attacks
|
1. **Always set search_path = ''** - Prevents search_path injection attacks. Qualify all table references (e.g., `public.my_table`)
|
||||||
2. **Validate caller permissions** - Don't assume caller is authorized
|
2. **Revoke default execute permissions** - `revoke execute on function my_func from public;` then grant selectively
|
||||||
3. **Keep functions minimal** - Only expose necessary operations
|
3. **Validate caller permissions** - Don't assume caller is authorized
|
||||||
4. **Log sensitive operations** - Audit trail for admin actions
|
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
|
```sql
|
||||||
create function private.sensitive_operation()
|
create function private.sensitive_operation()
|
||||||
@@ -109,7 +118,7 @@ set search_path = ''
|
|||||||
as $$
|
as $$
|
||||||
begin
|
begin
|
||||||
-- Log the operation
|
-- 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());
|
values ((select auth.uid()), 'sensitive_operation', now());
|
||||||
|
|
||||||
-- Perform operation
|
-- Perform operation
|
||||||
|
|||||||
@@ -71,10 +71,16 @@ The publishable and secret keys are replacing the legacy JWT-based keys. Decode
|
|||||||
|
|
||||||
## If Service Key is Exposed
|
## If Service Key is Exposed
|
||||||
|
|
||||||
1. Immediately rotate keys in Dashboard > Settings > API Keys
|
Don't rush. Remediate the root cause first, then:
|
||||||
2. Review database for unauthorized changes
|
|
||||||
3. Check logs for suspicious activity
|
1. Stop and ask the user to create a new secret API key in the Supabase Dashboard under Settings > API Keys
|
||||||
4. Update all backend services with new key
|
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
|
## Alternative: Security Definer Functions
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ tags: realtime, broadcast, send, receive, subscribe
|
|||||||
|
|
||||||
## Send and Receive Broadcast Messages
|
## 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
|
## 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
|
```javascript
|
||||||
await channel.httpSend('message_created', { text: 'Hello!' })
|
supabase
|
||||||
|
.channel('room:123')
|
||||||
|
.httpSend('message_created', { text: 'Hello!' })
|
||||||
```
|
```
|
||||||
|
|
||||||
## Receive Own Messages
|
## 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({
|
const status = await channel.send({
|
||||||
type: 'broadcast',
|
type: 'broadcast',
|
||||||
event: 'message_created',
|
event: 'message_created',
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ Broadcasts database changes in a standard format.
|
|||||||
create or replace function room_messages_broadcast()
|
create or replace function room_messages_broadcast()
|
||||||
returns trigger
|
returns trigger
|
||||||
security definer
|
security definer
|
||||||
|
set search_path = ''
|
||||||
language plpgsql
|
language plpgsql
|
||||||
as $$
|
as $$
|
||||||
begin
|
begin
|
||||||
@@ -29,7 +30,7 @@ begin
|
|||||||
new, -- new record
|
new, -- new record
|
||||||
old -- old record
|
old -- old record
|
||||||
);
|
);
|
||||||
return coalesce(new, old);
|
return null; -- AFTER trigger return value is ignored
|
||||||
end;
|
end;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function ChatRoom({ roomId }) {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (channelRef.current) {
|
if (channelRef.current) {
|
||||||
supabase.removeChannel(channelRef.current)
|
channelRef.current.unsubscribe()
|
||||||
channelRef.current = null
|
channelRef.current = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,10 +81,11 @@ channel.subscribe()
|
|||||||
|------|-----------------|------------------------|
|
|------|-----------------|------------------------|
|
||||||
| Free | 200 | 100 |
|
| Free | 200 | 100 |
|
||||||
| Pro | 500 | 100 |
|
| Pro | 500 | 100 |
|
||||||
|
| Pro (no spend cap) | 10,000 | 100 |
|
||||||
| Team | 10,000 | 100 |
|
| Team | 10,000 | 100 |
|
||||||
|
|
||||||
Leaked channels count against quotas even when inactive.
|
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
|
## Related
|
||||||
|
|
||||||
- [patterns-errors.md](patterns-errors.md)
|
- [patterns-errors.md](patterns-errors.md)
|
||||||
|
|||||||
@@ -41,14 +41,12 @@ Log message types:
|
|||||||
|
|
||||||
## Server-Side Log Level
|
## Server-Side Log Level
|
||||||
|
|
||||||
Configure Realtime server log verbosity via client params:
|
Configure Realtime server log verbosity:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const supabase = createClient(url, key, {
|
const supabase = createClient(url, key, {
|
||||||
realtime: {
|
realtime: {
|
||||||
params: {
|
logLevel: 'info', // 'info' | 'warn' | 'error'
|
||||||
log_level: 'info', // 'debug' | 'info' | 'warn' | 'error'
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -45,9 +45,9 @@ channel.subscribe((status, err) => {
|
|||||||
| Error | Cause | Solution |
|
| Error | Cause | Solution |
|
||||||
|-------|-------|----------|
|
|-------|-------|----------|
|
||||||
| `too_many_connections` | Connection limit exceeded | Clean up unused channels, upgrade plan |
|
| `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 |
|
| `too_many_joins` | Channel join rate exceeded | Reduce join frequency |
|
||||||
| `ConnectionRateLimitReached` | Max connections reached | Upgrade plan |
|
| `tenant_events` | Too many messages/second | Reduce message rate or upgrade plan |
|
||||||
| `DatabaseLackOfConnections` | No available DB connections | Increase compute size |
|
|
||||||
| `TenantNotFound` | Invalid project reference | Verify project URL |
|
| `TenantNotFound` | Invalid project reference | Verify project URL |
|
||||||
|
|
||||||
## Automatic Reconnection
|
## Automatic Reconnection
|
||||||
@@ -72,12 +72,30 @@ Log message types include `push`, `receive`, `transport`, `error`, and `worker`.
|
|||||||
|
|
||||||
## Silent Disconnections in Background
|
## 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
|
```javascript
|
||||||
channel.subscribe((status) => {
|
channel.subscribe((status) => {
|
||||||
if (status === 'SUBSCRIBED') {
|
if (status === 'SUBSCRIBED') {
|
||||||
// Re-track presence after reconnection
|
|
||||||
channel.track({ user_id: userId, online_at: new Date().toISOString() })
|
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)
|
- [patterns-cleanup.md](patterns-cleanup.md)
|
||||||
- [setup-auth.md](setup-auth.md)
|
- [setup-auth.md](setup-auth.md)
|
||||||
- [Docs](https://supabase.com/docs/guides/realtime/troubleshooting)
|
- [Docs](https://supabase.com/docs/guides/realtime/limits)
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ alter table messages replica identity full;
|
|||||||
alter publication supabase_realtime add table messages;
|
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
|
## Scaling Limitation
|
||||||
|
|
||||||
Each change triggers RLS checks for every subscriber:
|
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
|
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).
|
For high-traffic tables, migrate to [broadcast-database.md](broadcast-database.md).
|
||||||
|
|
||||||
## DELETE Events Not Filterable
|
## DELETE Events Not Filterable
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ Presence synchronizes shared state between users. Use sparingly due to computati
|
|||||||
## Track Presence
|
## Track Presence
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const channel = supabase.channel('room:123', {
|
// private: true is a channel setting, not presence-specific
|
||||||
config: { private: true },
|
// Add it for production use (see setup-auth.md)
|
||||||
})
|
const channel = supabase.channel('room:123')
|
||||||
|
|
||||||
channel
|
channel
|
||||||
.on('presence', { event: 'sync' }, () => {
|
.on('presence', { event: 'sync' }, () => {
|
||||||
@@ -78,8 +78,9 @@ const channel = supabase.channel('room:123', {
|
|||||||
|------|-------------------------|
|
|------|-------------------------|
|
||||||
| Free | 20 |
|
| Free | 20 |
|
||||||
| Pro | 50 |
|
| Pro | 50 |
|
||||||
|
| Pro (no spend cap) | 1,000 |
|
||||||
| Team/Enterprise | 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
|
## Related
|
||||||
|
|
||||||
- [setup-channels.md](setup-channels.md)
|
- [setup-channels.md](setup-channels.md)
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ Always use private channels in production. Public channels allow any client to s
|
|||||||
|
|
||||||
## Enable Private Channels
|
## 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:**
|
**Incorrect:**
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
@@ -57,11 +61,11 @@ create policy "room_members_can_read"
|
|||||||
on realtime.messages for select
|
on realtime.messages for select
|
||||||
to authenticated
|
to authenticated
|
||||||
using (
|
using (
|
||||||
extension in ('broadcast', 'presence')
|
exists (
|
||||||
and exists (
|
|
||||||
select 1 from room_members
|
select 1 from room_members
|
||||||
where user_id = (select auth.uid())
|
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')
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Channels are rooms where clients communicate. Use consistent naming and appropri
|
|||||||
|
|
||||||
## Topic Naming Convention
|
## Topic Naming Convention
|
||||||
|
|
||||||
Use `scope:entity:id` format for predictable, filterable topics.
|
Use `scope:id:entity` format for predictable, filterable topics.
|
||||||
|
|
||||||
**Incorrect:**
|
**Incorrect:**
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ const channel = supabase.channel('game:789:moves')
|
|||||||
```javascript
|
```javascript
|
||||||
const channel = supabase.channel('room:123:messages', {
|
const channel = supabase.channel('room:123:messages', {
|
||||||
config: {
|
config: {
|
||||||
private: true, // Require authentication (recommended)
|
private: true, // Require authentication (requires RLS on realtime.messages)
|
||||||
broadcast: {
|
broadcast: {
|
||||||
self: true, // Receive own messages
|
self: true, // Receive own messages
|
||||||
ack: true, // Get server acknowledgment
|
ack: true, // Get server acknowledgment
|
||||||
|
|||||||
Reference in New Issue
Block a user