minor changes to database and realtime reference fiels after docs analysis

This commit is contained in:
Pedro Rodrigues
2026-02-12 14:15:47 +00:00
parent 15fff4da9f
commit 93c12c61f2
25 changed files with 198 additions and 78 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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';
```

View File

@@ -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

View File

@@ -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

View File

@@ -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;
```

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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',

View File

@@ -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;
$$;

View File

@@ -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)

View File

@@ -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'
},
})
```

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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')
)
);
```

View File

@@ -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