mirror of
https://github.com/supabase/agent-skills.git
synced 2026-03-27 10:09:26 +08:00
feat: supabase db references
This commit is contained in:
@@ -1,45 +1,36 @@
|
|||||||
# Reference Sections
|
# Section Definitions
|
||||||
|
|
||||||
|
Reference files are grouped by prefix. Claude loads specific files based on user
|
||||||
|
queries.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Row Level Security (rls)
|
||||||
|
|
||||||
## 1. Getting Started (getting)
|
|
||||||
**Impact:** CRITICAL
|
**Impact:** CRITICAL
|
||||||
**Description:** Local development setup and CLI workflow guides
|
**Description:** RLS policies, common mistakes, performance optimizations, and security patterns specific to Supabase's auth.uid() integration.
|
||||||
|
|
||||||
|
## 2. Connection Pooling (conn)
|
||||||
|
|
||||||
## 2. Database (db)
|
|
||||||
**Impact:** CRITICAL
|
**Impact:** CRITICAL
|
||||||
**Description:** Database schema, RLS, migrations, and performance
|
**Description:** Supabase-specific connection pooling with Supavisor. Transaction mode (port 6543) vs Session mode (port 5432).
|
||||||
|
|
||||||
## 3. Authentication (auth)
|
## 3. Schema Design (schema)
|
||||||
**Impact:** CRITICAL
|
|
||||||
**Description:** Auth flows, sessions, OAuth, MFA, and SSO
|
|
||||||
|
|
||||||
## 4. Storage (storage)
|
|
||||||
**Impact:** HIGH
|
**Impact:** HIGH
|
||||||
**Description:** File uploads, access control, and CDN
|
**Description:** Supabase-specific schema patterns including auth.users foreign keys, timestamptz, JSONB usage, extensions, and Realtime.
|
||||||
|
|
||||||
|
## 4. Migrations (migrations)
|
||||||
|
|
||||||
## 5. Edge Functions (edge)
|
|
||||||
**Impact:** HIGH
|
**Impact:** HIGH
|
||||||
**Description:** Serverless functions, deployment, and patterns
|
**Description:** Migration workflows using Supabase CLI, idempotent patterns, supabase db diff, and local testing strategies.
|
||||||
|
|
||||||
## 6. Realtime (realtime)
|
## 5. Performance (perf)
|
||||||
**Impact:** MEDIUM-HIGH
|
|
||||||
**Description:** Subscriptions, presence, and broadcast
|
|
||||||
|
|
||||||
## 7. SDK (sdk)
|
|
||||||
**Impact:** HIGH
|
|
||||||
**Description:** Client libraries, queries, and TypeScript
|
|
||||||
|
|
||||||
## 8. CLI (cli)
|
|
||||||
**Impact:** CRITICAL
|
**Impact:** CRITICAL
|
||||||
**Description:** CLI commands, migrations, and local development
|
**Description:** Index strategies (BRIN, GIN, partial), query optimization for PostgREST, and Supabase-specific performance patterns.
|
||||||
|
|
||||||
## 9. MCP (mcp)
|
## 6. Security (security)
|
||||||
**Impact:** MEDIUM
|
|
||||||
**Description:** MCP server setup and configuration
|
|
||||||
|
|
||||||
## 10. Tooling (tooling)
|
**Impact:** CRITICAL
|
||||||
**Impact:** MEDIUM
|
**Description:** Service role key handling, security definer functions in private schemas, and Supabase-specific security patterns.
|
||||||
**Description:** Tool selection and workflow guides
|
|
||||||
|
|
||||||
## 11. Vectors (vectors)
|
|
||||||
**Impact:** MEDIUM
|
|
||||||
**Description:** pgvector, embeddings, and semantic search
|
|
||||||
|
|||||||
106
skills/supabase/references/conn-pooling.md
Normal file
106
skills/supabase/references/conn-pooling.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
---
|
||||||
|
title: Use Correct Connection Pooling Mode
|
||||||
|
impact: CRITICAL
|
||||||
|
impactDescription: Prevents connection exhaustion and enables 10-100x scalability
|
||||||
|
tags: connection-pooling, supavisor, transaction-mode, session-mode
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use Correct Connection Pooling Mode
|
||||||
|
|
||||||
|
Supabase provides Supavisor for connection pooling. Choose the right mode based
|
||||||
|
on your application type.
|
||||||
|
|
||||||
|
## Transaction Mode (Port 6543)
|
||||||
|
|
||||||
|
Best for: Serverless functions, edge computing, stateless APIs.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
## Transaction mode connection string
|
||||||
|
postgres://postgres.[ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
**Limitations:**
|
||||||
|
|
||||||
|
- No prepared statements
|
||||||
|
- No SET commands
|
||||||
|
- No LISTEN/NOTIFY
|
||||||
|
- No temp tables
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Prisma - disable prepared statements
|
||||||
|
const prisma = new PrismaClient({
|
||||||
|
datasources: {
|
||||||
|
db: {
|
||||||
|
url: process.env.DATABASE_URL + "?pgbouncer=true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Session Mode (Port 5432)
|
||||||
|
|
||||||
|
Best for: Long-running servers, apps needing prepared statements.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
## Session mode (via pooler for IPv4)
|
||||||
|
postgres://postgres.[ref]:[password]@aws-0-[region].pooler.supabase.com:5432/postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
## Direct Connection (Port 5432)
|
||||||
|
|
||||||
|
Best for: Migrations, admin tasks, persistent servers.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
## Direct connection (IPv6 only unless IPv4 add-on enabled)
|
||||||
|
postgres://postgres.[ref]:[password]@db.[ref].supabase.co:5432/postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Mistakes
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Serverless with session mode - exhausts connections
|
||||||
|
const pool = new Pool({
|
||||||
|
connectionString: "...pooler.supabase.com:5432/postgres",
|
||||||
|
max: 20, // Too many connections per instance!
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Serverless with transaction mode
|
||||||
|
const pool = new Pool({
|
||||||
|
connectionString: "...pooler.supabase.com:6543/postgres",
|
||||||
|
max: 1, // Single connection per serverless instance
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
## Transaction mode with prepared statements
|
||||||
|
DATABASE_URL="...pooler.supabase.com:6543/postgres"
|
||||||
|
## Error: prepared statement already exists
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
## Add pgbouncer=true to disable prepared statements
|
||||||
|
DATABASE_URL="...pooler.supabase.com:6543/postgres?pgbouncer=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Connection Limits by Compute Size
|
||||||
|
|
||||||
|
| Compute | Direct Connections | Pooler Clients |
|
||||||
|
| ------- | ------------------ | -------------- |
|
||||||
|
| Nano | 60 | 200 |
|
||||||
|
| Small | 90 | 400 |
|
||||||
|
| Medium | 120 | 600 |
|
||||||
|
| Large | 160 | 800 |
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Docs](https://supabase.com/docs/guides/database/connecting-to-postgres)
|
||||||
97
skills/supabase/references/migrations-diff.md
Normal file
97
skills/supabase/references/migrations-diff.md
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
---
|
||||||
|
title: Use supabase db diff for Dashboard Changes
|
||||||
|
impact: HIGH
|
||||||
|
impactDescription: Captures manual changes into version-controlled migrations
|
||||||
|
tags: migrations, supabase-cli, db-diff, dashboard
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use supabase db diff for Dashboard Changes
|
||||||
|
|
||||||
|
When making schema changes via Dashboard, use `supabase db diff` to generate
|
||||||
|
migration files for version control.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Making changes in Dashboard without capturing them
|
||||||
|
-- Changes exist in remote but not in version control
|
||||||
|
-- Team members can't reproduce the database state
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# After making Dashboard changes, generate migration
|
||||||
|
supabase db diff -f add_profiles_table
|
||||||
|
|
||||||
|
# Review and test
|
||||||
|
supabase db reset
|
||||||
|
|
||||||
|
# Commit to version control
|
||||||
|
git add supabase/migrations/
|
||||||
|
git commit -m "Add profiles table migration"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Make changes in Supabase Dashboard (create tables, add columns, etc.)
|
||||||
|
2. Generate migration from diff:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
supabase db diff -f add_profiles_table
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Review generated migration in `supabase/migrations/`
|
||||||
|
4. Test locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
supabase db reset
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Commit migration to version control
|
||||||
|
|
||||||
|
## Diff Against Local Database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start local Supabase
|
||||||
|
supabase start
|
||||||
|
|
||||||
|
# Make changes via Dashboard or SQL
|
||||||
|
# Generate diff
|
||||||
|
supabase db diff -f my_changes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Diff Against Remote Database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Link to remote project
|
||||||
|
supabase link --project-ref your-project-ref
|
||||||
|
|
||||||
|
# Pull remote schema and generate diff
|
||||||
|
supabase db diff --linked -f sync_remote_changes
|
||||||
|
```
|
||||||
|
|
||||||
|
## What diff Captures
|
||||||
|
|
||||||
|
- Tables and columns
|
||||||
|
- Indexes
|
||||||
|
- Constraints
|
||||||
|
- Functions and triggers
|
||||||
|
- RLS policies
|
||||||
|
- Extensions
|
||||||
|
|
||||||
|
## What diff Does NOT Capture
|
||||||
|
|
||||||
|
- DML (INSERT, UPDATE, DELETE)
|
||||||
|
- View ownership changes
|
||||||
|
- Materialized views
|
||||||
|
- Partitions
|
||||||
|
- Comments
|
||||||
|
|
||||||
|
For these, write manual migrations.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [migrations-idempotent.md](migrations-idempotent.md)
|
||||||
|
- [migrations-testing.md](migrations-testing.md)
|
||||||
|
- [Docs](https://supabase.com/docs/guides/deployment/database-migrations)
|
||||||
90
skills/supabase/references/migrations-idempotent.md
Normal file
90
skills/supabase/references/migrations-idempotent.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
---
|
||||||
|
title: Write Idempotent Migrations
|
||||||
|
impact: HIGH
|
||||||
|
impactDescription: Safe to run multiple times, prevents migration failures
|
||||||
|
tags: migrations, idempotent, supabase-cli
|
||||||
|
---
|
||||||
|
|
||||||
|
## Write Idempotent Migrations
|
||||||
|
|
||||||
|
Migrations should be safe to run multiple times without errors. Use
|
||||||
|
`IF NOT EXISTS` and `IF EXISTS` clauses.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Fails on second run: "relation already exists"
|
||||||
|
create table users (
|
||||||
|
id uuid primary key,
|
||||||
|
email text not null
|
||||||
|
);
|
||||||
|
|
||||||
|
create index idx_users_email on users(email);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Safe to run multiple times
|
||||||
|
create table if not exists users (
|
||||||
|
id uuid primary key,
|
||||||
|
email text not null
|
||||||
|
);
|
||||||
|
|
||||||
|
create index if not exists idx_users_email on users(email);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Idempotent Column Additions
|
||||||
|
|
||||||
|
```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 $$;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Idempotent Drops
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Safe drops
|
||||||
|
drop table if exists old_table;
|
||||||
|
drop index if exists old_index;
|
||||||
|
drop function if exists old_function();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Idempotent Policies
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Drop and recreate to update policy
|
||||||
|
drop policy if exists "Users see own data" on users;
|
||||||
|
|
||||||
|
create policy "Users see own data" on users
|
||||||
|
for select to authenticated
|
||||||
|
using ((select auth.uid()) = id);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration File Naming
|
||||||
|
|
||||||
|
Migrations in `supabase/migrations/` are named with timestamps:
|
||||||
|
|
||||||
|
```
|
||||||
|
20240315120000_create_users.sql
|
||||||
|
20240315130000_add_profiles.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
Create new migration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
supabase migration new create_users
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [migrations-testing.md](migrations-testing.md)
|
||||||
|
- [Docs](https://supabase.com/docs/guides/deployment/database-migrations)
|
||||||
116
skills/supabase/references/migrations-testing.md
Normal file
116
skills/supabase/references/migrations-testing.md
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
---
|
||||||
|
title: Test Migrations with supabase db reset
|
||||||
|
impact: MEDIUM-HIGH
|
||||||
|
impactDescription: Catch migration errors before production deployment
|
||||||
|
tags: migrations, testing, supabase-cli, local-development
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Migrations with supabase db reset
|
||||||
|
|
||||||
|
Always test migrations locally before deploying to production. Use
|
||||||
|
`supabase db reset` to verify migrations run cleanly from scratch.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Deploying directly without testing
|
||||||
|
supabase db push # Migration fails in production!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test migrations locally first
|
||||||
|
supabase db reset # Runs all migrations from scratch
|
||||||
|
|
||||||
|
# Verify success, then deploy
|
||||||
|
supabase db push
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start local Supabase
|
||||||
|
supabase start
|
||||||
|
|
||||||
|
# Reset database and run all migrations
|
||||||
|
supabase db reset
|
||||||
|
|
||||||
|
# Verify tables and data
|
||||||
|
supabase inspect db table-sizes
|
||||||
|
```
|
||||||
|
|
||||||
|
## What db reset Does
|
||||||
|
|
||||||
|
1. Drops the local database
|
||||||
|
2. Creates a fresh database
|
||||||
|
3. Runs all migrations in order
|
||||||
|
4. Runs `supabase/seed.sql` if present
|
||||||
|
|
||||||
|
## Seed Data for Testing
|
||||||
|
|
||||||
|
Create `supabase/seed.sql` for test data:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- supabase/seed.sql
|
||||||
|
-- Runs after migrations on db reset
|
||||||
|
|
||||||
|
-- Use ON CONFLICT for idempotency
|
||||||
|
insert into categories (name)
|
||||||
|
values ('Action'), ('Comedy'), ('Drama')
|
||||||
|
on conflict (name) do nothing;
|
||||||
|
|
||||||
|
-- Test users (only in local development!)
|
||||||
|
insert into profiles (id, username)
|
||||||
|
values ('00000000-0000-0000-0000-000000000001', 'testuser')
|
||||||
|
on conflict (id) do nothing;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Specific Migration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Apply single pending migration
|
||||||
|
supabase migration up
|
||||||
|
|
||||||
|
# Check migration status
|
||||||
|
supabase migration list
|
||||||
|
```
|
||||||
|
|
||||||
|
## Repair Failed Migration
|
||||||
|
|
||||||
|
If a migration partially fails:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fix the migration file
|
||||||
|
# Then repair the migration history
|
||||||
|
supabase migration repair --status applied 20240315120000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Inspect Database State
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View tables
|
||||||
|
supabase inspect db table-sizes
|
||||||
|
|
||||||
|
# View indexes
|
||||||
|
supabase inspect db index-usage
|
||||||
|
|
||||||
|
# View cache hit rate
|
||||||
|
supabase inspect db cache-hit
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI/CD Integration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# GitHub Actions example
|
||||||
|
- name: Test migrations
|
||||||
|
run: |
|
||||||
|
supabase start
|
||||||
|
supabase db reset
|
||||||
|
supabase test db # Run pgTAP tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [migrations-idempotent.md](migrations-idempotent.md)
|
||||||
|
- [Docs](https://supabase.com/docs/guides/local-development/overview)
|
||||||
114
skills/supabase/references/perf-indexes.md
Normal file
114
skills/supabase/references/perf-indexes.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
---
|
||||||
|
title: Choose the Right Index Type
|
||||||
|
impact: CRITICAL
|
||||||
|
impactDescription: 10-1000x query performance improvements with proper indexing
|
||||||
|
tags: indexes, performance, btree, brin, gin, partial
|
||||||
|
---
|
||||||
|
|
||||||
|
## Choose the Right Index Type
|
||||||
|
|
||||||
|
Supabase uses PostgreSQL indexes. Choose the right type for your query patterns.
|
||||||
|
|
||||||
|
## B-Tree (Default)
|
||||||
|
|
||||||
|
Best for: Equality, range queries, sorting.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Equality and range queries
|
||||||
|
create index idx_users_email on users(email);
|
||||||
|
create index idx_orders_created on orders(created_at);
|
||||||
|
|
||||||
|
-- Composite index for multi-column queries
|
||||||
|
create index idx_orders_user_status on orders(user_id, status);
|
||||||
|
```
|
||||||
|
|
||||||
|
## BRIN (Block Range Index)
|
||||||
|
|
||||||
|
Best for: Large tables with naturally ordered data (timestamps, sequential IDs).
|
||||||
|
10x+ smaller than B-tree.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Perfect for append-only timestamp columns
|
||||||
|
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.
|
||||||
|
|
||||||
|
## GIN (Generalized Inverted Index)
|
||||||
|
|
||||||
|
Best for: JSONB, arrays, full-text search.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- JSONB containment queries
|
||||||
|
create index idx_users_metadata on users using gin(metadata);
|
||||||
|
|
||||||
|
-- Full-text search
|
||||||
|
create index idx_posts_search on posts using gin(to_tsvector('english', title || ' ' || content));
|
||||||
|
|
||||||
|
-- Array containment
|
||||||
|
create index idx_tags on posts using gin(tags);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Partial Index
|
||||||
|
|
||||||
|
Best for: Queries that filter on specific values.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Only index active users (smaller, faster)
|
||||||
|
create index idx_active_users on users(email)
|
||||||
|
where status = 'active';
|
||||||
|
|
||||||
|
-- Only index unprocessed orders
|
||||||
|
create index idx_pending_orders on orders(created_at)
|
||||||
|
where processed = false;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Requirement:** Query WHERE clause must match index condition.
|
||||||
|
|
||||||
|
## Common Mistakes
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Over-indexing: slows writes, wastes space
|
||||||
|
create index idx_users_1 on users(email);
|
||||||
|
create index idx_users_2 on users(email, name);
|
||||||
|
create index idx_users_3 on users(name, email);
|
||||||
|
create index idx_users_4 on users(name);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Minimal indexes based on actual queries
|
||||||
|
create index idx_users_email on users(email); -- For login
|
||||||
|
create index idx_users_name on users(name); -- For search
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verify Index Usage
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Check if query uses index
|
||||||
|
explain analyze
|
||||||
|
select * from users where email = 'test@example.com';
|
||||||
|
|
||||||
|
-- Find unused indexes
|
||||||
|
select * from pg_stat_user_indexes
|
||||||
|
where idx_scan = 0 and indexrelname not like '%_pkey';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Concurrently Create Indexes
|
||||||
|
|
||||||
|
For production tables, avoid locking:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Doesn't block writes
|
||||||
|
create index concurrently idx_users_email on users(email);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [rls-performance.md](rls-performance.md)
|
||||||
|
- [schema-jsonb.md](schema-jsonb.md)
|
||||||
|
- [Docs](https://supabase.com/docs/guides/database/postgres/indexes)
|
||||||
149
skills/supabase/references/perf-query-optimization.md
Normal file
149
skills/supabase/references/perf-query-optimization.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
---
|
||||||
|
title: Optimize Queries for PostgREST
|
||||||
|
impact: HIGH
|
||||||
|
impactDescription: Faster API responses and reduced database load
|
||||||
|
tags: postgrest, queries, performance, optimization, supabase-js
|
||||||
|
---
|
||||||
|
|
||||||
|
## Optimize Queries for PostgREST
|
||||||
|
|
||||||
|
Supabase uses PostgREST to generate REST APIs. Optimize queries for better
|
||||||
|
performance.
|
||||||
|
|
||||||
|
## Select Only Needed Columns
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Fetches all columns including large text/blobs
|
||||||
|
const { data } = await supabase.from("posts").select("*");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Only fetch needed columns
|
||||||
|
const { data } = await supabase.from("posts").select("id, title, author_id");
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use Explicit Filters
|
||||||
|
|
||||||
|
Explicit filters help the query planner, even with RLS.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Relies only on RLS - query planner has less info
|
||||||
|
const { data } = await supabase.from("posts").select("*");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Explicit filter improves query plan
|
||||||
|
const { data } = await supabase
|
||||||
|
.from("posts")
|
||||||
|
.select("*")
|
||||||
|
.eq("author_id", userId);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Always Paginate
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Could return thousands of rows
|
||||||
|
const { data } = await supabase.from("posts").select("*");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Paginate results
|
||||||
|
const { data } = await supabase
|
||||||
|
.from("posts")
|
||||||
|
.select("*")
|
||||||
|
.range(0, 19) // First 20 rows
|
||||||
|
.order("created_at", { ascending: false });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Efficient Joins
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// N+1: One query per post for author
|
||||||
|
const { data: posts } = await supabase.from("posts").select("*");
|
||||||
|
for (const post of posts) {
|
||||||
|
const { data: author } = await supabase
|
||||||
|
.from("users")
|
||||||
|
.select("*")
|
||||||
|
.eq("id", post.author_id)
|
||||||
|
.single();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Single query with embedded join
|
||||||
|
const { data } = await supabase.from("posts").select(`
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
author:users (
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
avatar_url
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use count Option Efficiently
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Counts ALL rows (slow on large tables)
|
||||||
|
const { count } = await supabase
|
||||||
|
.from("posts")
|
||||||
|
.select("*", { count: "exact", head: true });
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Estimated count (fast)
|
||||||
|
const { count } = await supabase
|
||||||
|
.from("posts")
|
||||||
|
.select("*", { count: "estimated", head: true });
|
||||||
|
|
||||||
|
// Or planned count (uses query planner estimate)
|
||||||
|
const { count } = await supabase
|
||||||
|
.from("posts")
|
||||||
|
.select("*", { count: "planned", head: true });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debug Query Performance
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Get query execution plan
|
||||||
|
const { data } = await supabase
|
||||||
|
.from("posts")
|
||||||
|
.select("*")
|
||||||
|
.eq("author_id", userId)
|
||||||
|
.explain({ analyze: true, verbose: true });
|
||||||
|
|
||||||
|
console.log(data); // Shows execution plan
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable explain in database:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
alter role authenticator set pgrst.db_plan_enabled to true;
|
||||||
|
notify pgrst, 'reload config';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [perf-indexes.md](perf-indexes.md)
|
||||||
|
- [Docs](https://supabase.com/docs/guides/database/query-optimization)
|
||||||
97
skills/supabase/references/rls-common-mistakes.md
Normal file
97
skills/supabase/references/rls-common-mistakes.md
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
---
|
||||||
|
title: Avoid Common RLS Policy Mistakes
|
||||||
|
impact: CRITICAL
|
||||||
|
impactDescription: Prevents security vulnerabilities and unintended data exposure
|
||||||
|
tags: rls, security, auth.uid, policies, common-mistakes
|
||||||
|
---
|
||||||
|
|
||||||
|
## Avoid Common RLS Policy Mistakes
|
||||||
|
|
||||||
|
## 1. Missing TO Clause
|
||||||
|
|
||||||
|
Without `TO`, policies apply to all roles including `anon`.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Runs for both anon and authenticated users
|
||||||
|
create policy "Users see own data" on profiles
|
||||||
|
using (auth.uid() = user_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Only runs for authenticated users
|
||||||
|
create policy "Users see own data" on profiles
|
||||||
|
to authenticated
|
||||||
|
using (auth.uid() = user_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Using user_metadata for Authorization
|
||||||
|
|
||||||
|
Users can modify their own `user_metadata`. Use `app_metadata` instead.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- DANGEROUS: users can set their own role!
|
||||||
|
using ((auth.jwt() -> 'user_metadata' ->> 'role') = 'admin')
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- app_metadata cannot be modified by users
|
||||||
|
using ((auth.jwt() -> 'app_metadata' ->> 'role') = 'admin')
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Not Checking NULL auth.uid()
|
||||||
|
|
||||||
|
For unauthenticated users, `auth.uid()` returns NULL.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- NULL = NULL is NULL (not true), but confusing behavior
|
||||||
|
using (auth.uid() = user_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Explicit NULL check
|
||||||
|
using (auth.uid() is not null and auth.uid() = user_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Missing SELECT Policy for UPDATE
|
||||||
|
|
||||||
|
UPDATE operations require a SELECT policy to find rows to update.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- UPDATE silently fails - no rows found
|
||||||
|
create policy "Users can update" on profiles
|
||||||
|
for update to authenticated
|
||||||
|
using (auth.uid() = user_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Need both SELECT and UPDATE policies
|
||||||
|
create policy "Users can view" on profiles
|
||||||
|
for select to authenticated
|
||||||
|
using (auth.uid() = user_id);
|
||||||
|
|
||||||
|
create policy "Users can update" on profiles
|
||||||
|
for update to authenticated
|
||||||
|
using (auth.uid() = user_id)
|
||||||
|
with check (auth.uid() = user_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [rls-mandatory.md](rls-mandatory.md)
|
||||||
|
- [Docs](https://supabase.com/docs/guides/database/postgres/row-level-security)
|
||||||
50
skills/supabase/references/rls-mandatory.md
Normal file
50
skills/supabase/references/rls-mandatory.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
title: Enable RLS on All Exposed Schemas
|
||||||
|
impact: CRITICAL
|
||||||
|
impactDescription: Prevents unauthorized data access at the database level
|
||||||
|
tags: rls, security, auth, policies
|
||||||
|
---
|
||||||
|
|
||||||
|
## Enable RLS on All Exposed Schemas
|
||||||
|
|
||||||
|
RLS must be enabled on every table in exposed schemas (default: `public`). Without
|
||||||
|
RLS, any user with the anon key can read and write all data.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Table without RLS - anyone can read/write everything
|
||||||
|
create table profiles (
|
||||||
|
id uuid primary key,
|
||||||
|
user_id uuid,
|
||||||
|
bio text
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
create table profiles (
|
||||||
|
id uuid primary key,
|
||||||
|
user_id uuid references auth.users(id) on delete cascade,
|
||||||
|
bio text
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Enable RLS
|
||||||
|
alter table profiles enable row level security;
|
||||||
|
|
||||||
|
-- Create policy
|
||||||
|
create policy "Users can view own profile"
|
||||||
|
on profiles for select
|
||||||
|
to authenticated
|
||||||
|
using (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.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Docs](https://supabase.com/docs/guides/database/postgres/row-level-security)
|
||||||
108
skills/supabase/references/rls-performance.md
Normal file
108
skills/supabase/references/rls-performance.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
---
|
||||||
|
title: Optimize RLS Policy Performance
|
||||||
|
impact: CRITICAL
|
||||||
|
impactDescription: Achieve 100x-99,000x query performance improvements
|
||||||
|
tags: rls, performance, optimization, indexes, auth.uid
|
||||||
|
---
|
||||||
|
|
||||||
|
## Optimize RLS Policy Performance
|
||||||
|
|
||||||
|
RLS policies run on every row access. Unoptimized policies cause severe
|
||||||
|
performance degradation.
|
||||||
|
|
||||||
|
## 1. Wrap auth.uid() in SELECT (94-99% improvement)
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- auth.uid() called for every row
|
||||||
|
create policy "Users see own data" on profiles
|
||||||
|
to authenticated
|
||||||
|
using (auth.uid() = user_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Cached once per statement via initPlan
|
||||||
|
create policy "Users see own data" on profiles
|
||||||
|
to authenticated
|
||||||
|
using ((select auth.uid()) = user_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Add Indexes on Policy Columns (99% improvement)
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Full table scan for every query
|
||||||
|
create policy "Users see own data" on profiles
|
||||||
|
using ((select auth.uid()) = user_id);
|
||||||
|
-- No index on user_id
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
create policy "Users see own data" on profiles
|
||||||
|
using ((select auth.uid()) = user_id);
|
||||||
|
|
||||||
|
-- Add index on filtered column
|
||||||
|
create index idx_profiles_user_id on profiles(user_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Use Explicit Filters in Queries (94% improvement)
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Relies only on implicit RLS filter
|
||||||
|
const { data } = await supabase.from("profiles").select("*");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Add explicit filter - helps query planner
|
||||||
|
const { data } = await supabase
|
||||||
|
.from("profiles")
|
||||||
|
.select("*")
|
||||||
|
.eq("user_id", userId);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Use Security Definer Functions for Joins
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Join in policy - executed per row
|
||||||
|
using (
|
||||||
|
user_id in (
|
||||||
|
select user_id from team_members
|
||||||
|
where team_id = teams.id -- joins!
|
||||||
|
)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Function in private schema
|
||||||
|
create function private.user_team_ids()
|
||||||
|
returns setof uuid
|
||||||
|
language sql
|
||||||
|
security definer
|
||||||
|
stable
|
||||||
|
as $$
|
||||||
|
select team_id from team_members
|
||||||
|
where user_id = (select auth.uid())
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Policy uses cached function result
|
||||||
|
using (team_id in (select private.user_team_ids()))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [security-functions.md](security-functions.md)
|
||||||
|
- [Supabase RLS Performance Guide](https://github.com/orgs/supabase/discussions/14576)
|
||||||
81
skills/supabase/references/rls-policy-types.md
Normal file
81
skills/supabase/references/rls-policy-types.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
title: Use RESTRICTIVE vs PERMISSIVE Policies
|
||||||
|
impact: MEDIUM-HIGH
|
||||||
|
impactDescription: Controls policy combination logic to prevent unintended access
|
||||||
|
tags: rls, policies, permissive, restrictive
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use RESTRICTIVE vs PERMISSIVE Policies
|
||||||
|
|
||||||
|
Supabase RLS supports two policy types with different combination logic.
|
||||||
|
|
||||||
|
## PERMISSIVE (Default)
|
||||||
|
|
||||||
|
Multiple permissive policies combine with OR logic. If ANY policy passes, access
|
||||||
|
is granted.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- User can access if they own it OR are an admin
|
||||||
|
create policy "Owner access" on documents
|
||||||
|
for select to authenticated
|
||||||
|
using (owner_id = (select auth.uid()));
|
||||||
|
|
||||||
|
create policy "Admin access" on documents
|
||||||
|
for select to authenticated
|
||||||
|
using ((select auth.jwt() -> 'app_metadata' ->> 'role') = 'admin');
|
||||||
|
```
|
||||||
|
|
||||||
|
## RESTRICTIVE
|
||||||
|
|
||||||
|
Restrictive policies combine with AND logic. ALL restrictive policies must pass.
|
||||||
|
|
||||||
|
**Use Case: Enforce MFA for sensitive operations**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Base access policy (permissive)
|
||||||
|
create policy "Users can view own data" on sensitive_data
|
||||||
|
for select to authenticated
|
||||||
|
using (user_id = (select auth.uid()));
|
||||||
|
|
||||||
|
-- MFA requirement (restrictive) - MUST also pass
|
||||||
|
create policy "Require MFA" on sensitive_data
|
||||||
|
as restrictive
|
||||||
|
for select to authenticated
|
||||||
|
using ((select auth.jwt() ->> 'aal') = 'aal2');
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use Case: Block OAuth client access**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Allow direct session access
|
||||||
|
create policy "Direct access only" on payment_methods
|
||||||
|
as restrictive
|
||||||
|
for all to authenticated
|
||||||
|
using ((select auth.jwt() ->> 'client_id') is null);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Mistake
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Intended as additional requirement, but PERMISSIVE means OR
|
||||||
|
create policy "Require MFA" on sensitive_data
|
||||||
|
for select to authenticated
|
||||||
|
using ((select auth.jwt() ->> 'aal') = 'aal2');
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- AS RESTRICTIVE makes it an AND requirement
|
||||||
|
create policy "Require MFA" on sensitive_data
|
||||||
|
as restrictive
|
||||||
|
for select to authenticated
|
||||||
|
using ((select auth.jwt() ->> 'aal') = 'aal2');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [rls-common-mistakes.md](rls-common-mistakes.md)
|
||||||
|
- [Docs](https://supabase.com/docs/guides/database/postgres/row-level-security)
|
||||||
65
skills/supabase/references/rls-views.md
Normal file
65
skills/supabase/references/rls-views.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
title: Use security_invoker for Views with RLS
|
||||||
|
impact: HIGH
|
||||||
|
impactDescription: Ensures views respect RLS policies instead of bypassing them
|
||||||
|
tags: rls, views, security_invoker, security
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use security_invoker for Views with RLS
|
||||||
|
|
||||||
|
By default, views run as the view owner (security definer), bypassing RLS on
|
||||||
|
underlying tables.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- View bypasses RLS - exposes all data!
|
||||||
|
create view public_profiles as
|
||||||
|
select id, username, avatar_url
|
||||||
|
from profiles;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (Postgres 15+):**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- View respects RLS of querying user
|
||||||
|
create view public_profiles
|
||||||
|
with (security_invoker = true)
|
||||||
|
as
|
||||||
|
select id, username, avatar_url
|
||||||
|
from profiles;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (Older Postgres):**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Option 1: Revoke direct access, create RLS on view
|
||||||
|
revoke all on public_profiles from anon, authenticated;
|
||||||
|
|
||||||
|
-- Option 2: Create view in unexposed schema
|
||||||
|
create schema private;
|
||||||
|
create view private.profiles_view as
|
||||||
|
select * from profiles;
|
||||||
|
```
|
||||||
|
|
||||||
|
## When to Use security_definer
|
||||||
|
|
||||||
|
Use `security_definer = true` (default) when the view intentionally aggregates
|
||||||
|
or filters data that users shouldn't access directly:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Intentionally exposes limited public data
|
||||||
|
create view leaderboard as
|
||||||
|
select username, score
|
||||||
|
from profiles
|
||||||
|
order by score desc
|
||||||
|
limit 100;
|
||||||
|
|
||||||
|
-- Grant read access
|
||||||
|
grant select on leaderboard to anon;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [rls-mandatory.md](rls-mandatory.md)
|
||||||
|
- [Docs](https://supabase.com/docs/guides/database/postgres/row-level-security)
|
||||||
80
skills/supabase/references/schema-auth-fk.md
Normal file
80
skills/supabase/references/schema-auth-fk.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
title: Add CASCADE to auth.users Foreign Keys
|
||||||
|
impact: HIGH
|
||||||
|
impactDescription: Prevents orphaned records and user deletion failures
|
||||||
|
tags: foreign-keys, auth.users, cascade, schema-design
|
||||||
|
---
|
||||||
|
|
||||||
|
## Add CASCADE to auth.users Foreign Keys
|
||||||
|
|
||||||
|
When referencing `auth.users`, always specify `ON DELETE CASCADE`. Without it,
|
||||||
|
deleting users fails with foreign key violations.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- User deletion fails: "foreign key violation"
|
||||||
|
create table profiles (
|
||||||
|
id uuid primary key references auth.users(id),
|
||||||
|
username text,
|
||||||
|
avatar_url text
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Profile deleted automatically when user is deleted
|
||||||
|
create table profiles (
|
||||||
|
id uuid primary key references auth.users(id) on delete cascade,
|
||||||
|
username text,
|
||||||
|
avatar_url text
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Alternative: SET NULL for Optional Relationships
|
||||||
|
|
||||||
|
Use `ON DELETE SET NULL` when the record should persist without the user:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
create table comments (
|
||||||
|
id bigint primary key generated always as identity,
|
||||||
|
author_id uuid references auth.users(id) on delete set null,
|
||||||
|
content text not null,
|
||||||
|
created_at timestamptz default now()
|
||||||
|
);
|
||||||
|
-- Comment remains with author_id = NULL after user deletion
|
||||||
|
```
|
||||||
|
|
||||||
|
## Auto-Create Profile on Signup
|
||||||
|
|
||||||
|
```sql
|
||||||
|
create or replace function public.handle_new_user()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql
|
||||||
|
security definer
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
insert into public.profiles (id, email, full_name)
|
||||||
|
values (
|
||||||
|
new.id,
|
||||||
|
new.email,
|
||||||
|
new.raw_user_meta_data ->> 'full_name'
|
||||||
|
);
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
create trigger on_auth_user_created
|
||||||
|
after insert on auth.users
|
||||||
|
for each row execute function public.handle_new_user();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** Use `security definer` and `set search_path = ''` for triggers on
|
||||||
|
auth.users.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [security-functions.md](security-functions.md)
|
||||||
|
- [Docs](https://supabase.com/docs/guides/database/postgres/cascade-deletes)
|
||||||
80
skills/supabase/references/schema-extensions.md
Normal file
80
skills/supabase/references/schema-extensions.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
title: Install Extensions in extensions Schema
|
||||||
|
impact: MEDIUM
|
||||||
|
impactDescription: Keeps public schema clean and simplifies migrations
|
||||||
|
tags: extensions, schema-design, best-practices
|
||||||
|
---
|
||||||
|
|
||||||
|
## Install Extensions in extensions Schema
|
||||||
|
|
||||||
|
Install PostgreSQL extensions in the `extensions` schema to keep the `public`
|
||||||
|
schema clean and avoid conflicts with application tables.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Installs in public schema by default
|
||||||
|
create extension pg_trgm;
|
||||||
|
create extension pgvector;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Install in extensions schema
|
||||||
|
create extension if not exists pg_trgm with schema extensions;
|
||||||
|
create extension if not exists vector with schema extensions;
|
||||||
|
|
||||||
|
-- Reference with schema prefix
|
||||||
|
create index idx_name_trgm on users
|
||||||
|
using gin(name extensions.gin_trgm_ops);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Supabase Extensions
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Vector similarity search (AI embeddings)
|
||||||
|
create extension if not exists vector with schema extensions;
|
||||||
|
|
||||||
|
-- Scheduled jobs
|
||||||
|
create extension if not exists pg_cron with schema extensions;
|
||||||
|
|
||||||
|
-- HTTP requests from database
|
||||||
|
create extension if not exists pg_net with schema extensions;
|
||||||
|
|
||||||
|
-- Full-text search improvements
|
||||||
|
create extension if not exists pg_trgm with schema extensions;
|
||||||
|
|
||||||
|
-- Geospatial data
|
||||||
|
create extension if not exists postgis with schema extensions;
|
||||||
|
|
||||||
|
-- UUID generation (enabled by default)
|
||||||
|
create extension if not exists "uuid-ossp" with schema extensions;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Check Available Extensions
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- List available extensions
|
||||||
|
select * from pg_available_extensions;
|
||||||
|
|
||||||
|
-- List installed extensions
|
||||||
|
select * from pg_extension;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using Extensions
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- pgvector example
|
||||||
|
create table documents (
|
||||||
|
id bigint primary key generated always as identity,
|
||||||
|
content text,
|
||||||
|
embedding vector(1536) -- OpenAI ada-002 dimensions
|
||||||
|
);
|
||||||
|
|
||||||
|
create index on documents using ivfflat (embedding vector_cosine_ops);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Docs](https://supabase.com/docs/guides/database/extensions)
|
||||||
95
skills/supabase/references/schema-jsonb.md
Normal file
95
skills/supabase/references/schema-jsonb.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
---
|
||||||
|
title: Use Structured Columns Over JSONB When Possible
|
||||||
|
impact: MEDIUM
|
||||||
|
impactDescription: Improves query performance, type safety, and data integrity
|
||||||
|
tags: jsonb, json, schema-design, performance
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use Structured Columns Over JSONB When Possible
|
||||||
|
|
||||||
|
JSONB is flexible but should not replace proper schema design. Use structured
|
||||||
|
columns for known fields, JSONB for truly dynamic data.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Everything in JSONB - loses type safety and performance
|
||||||
|
create table users (
|
||||||
|
id uuid primary key,
|
||||||
|
data jsonb -- contains email, name, role, etc.
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Querying is verbose and slow without indexes
|
||||||
|
select data ->> 'email' from users
|
||||||
|
where data ->> 'role' = 'admin';
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Structured columns for known fields
|
||||||
|
create table users (
|
||||||
|
id uuid primary key,
|
||||||
|
email text not null,
|
||||||
|
name text,
|
||||||
|
role text check (role in ('admin', 'user', 'guest')),
|
||||||
|
-- JSONB only for truly flexible data
|
||||||
|
preferences jsonb default '{}'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Fast, type-safe queries
|
||||||
|
select email from users where role = 'admin';
|
||||||
|
```
|
||||||
|
|
||||||
|
## When JSONB is Appropriate
|
||||||
|
|
||||||
|
- Webhook payloads
|
||||||
|
- User-defined fields
|
||||||
|
- API responses to cache
|
||||||
|
- Rapid prototyping (migrate to columns later)
|
||||||
|
|
||||||
|
## Indexing JSONB
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- GIN index for containment queries
|
||||||
|
create index idx_users_preferences on users using gin(preferences);
|
||||||
|
|
||||||
|
-- Query using containment operator
|
||||||
|
select * from users
|
||||||
|
where preferences @> '{"theme": "dark"}';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validate JSONB with pg_jsonschema
|
||||||
|
|
||||||
|
```sql
|
||||||
|
create extension if not exists pg_jsonschema with schema extensions;
|
||||||
|
|
||||||
|
alter table users
|
||||||
|
add constraint check_preferences check (
|
||||||
|
jsonb_matches_schema(
|
||||||
|
'{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"theme": {"type": "string", "enum": ["light", "dark"]},
|
||||||
|
"notifications": {"type": "boolean"}
|
||||||
|
}
|
||||||
|
}',
|
||||||
|
preferences
|
||||||
|
)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Querying JSONB
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// supabase-js
|
||||||
|
const { data } = await supabase
|
||||||
|
.from("users")
|
||||||
|
.select("email, preferences->theme")
|
||||||
|
.eq("preferences->>notifications", "true");
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [perf-indexes.md](perf-indexes.md)
|
||||||
|
- [Docs](https://supabase.com/docs/guides/database/json)
|
||||||
91
skills/supabase/references/schema-realtime.md
Normal file
91
skills/supabase/references/schema-realtime.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
---
|
||||||
|
title: Realtime Requires Primary Keys
|
||||||
|
impact: MEDIUM-HIGH
|
||||||
|
impactDescription: Prevents Realtime subscription failures and data sync issues
|
||||||
|
tags: realtime, primary-keys, subscriptions
|
||||||
|
---
|
||||||
|
|
||||||
|
## Realtime Requires Primary Keys
|
||||||
|
|
||||||
|
Supabase Realtime uses primary keys to track row changes. Tables without primary
|
||||||
|
keys cannot be subscribed to.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- No primary key - Realtime subscriptions will fail
|
||||||
|
create table messages (
|
||||||
|
user_id uuid,
|
||||||
|
content text,
|
||||||
|
created_at timestamptz default now()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
create table messages (
|
||||||
|
id bigint primary key generated always as identity,
|
||||||
|
user_id uuid references auth.users(id) on delete cascade,
|
||||||
|
content text not null,
|
||||||
|
created_at timestamptz default now()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Enable Realtime for a Table
|
||||||
|
|
||||||
|
**Via SQL:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Add table to realtime publication
|
||||||
|
alter publication supabase_realtime add table messages;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Via Dashboard:**
|
||||||
|
|
||||||
|
Database > Publications > supabase_realtime > Add table
|
||||||
|
|
||||||
|
## Realtime with RLS
|
||||||
|
|
||||||
|
RLS policies apply to Realtime subscriptions. Users only receive changes they
|
||||||
|
have access to.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Policy applies to realtime
|
||||||
|
create policy "Users see own messages" on messages
|
||||||
|
for select to authenticated
|
||||||
|
using (user_id = (select auth.uid()));
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Subscribe with RLS filtering
|
||||||
|
const channel = supabase
|
||||||
|
.channel("messages")
|
||||||
|
.on(
|
||||||
|
"postgres_changes",
|
||||||
|
{ event: "*", schema: "public", table: "messages" },
|
||||||
|
(payload) => console.log(payload)
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
- Add indexes on columns used in Realtime filters
|
||||||
|
- Keep RLS policies simple for subscribed tables
|
||||||
|
- Monitor "Realtime Private Channel RLS Execution Time" in Dashboard
|
||||||
|
|
||||||
|
## Replica Identity
|
||||||
|
|
||||||
|
By default, only the primary key is sent in UPDATE/DELETE payloads. To receive
|
||||||
|
all columns:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Send all columns in change events (increases bandwidth)
|
||||||
|
alter table messages replica identity full;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [rls-mandatory.md](rls-mandatory.md)
|
||||||
|
- [Docs](https://supabase.com/docs/guides/realtime)
|
||||||
79
skills/supabase/references/schema-timestamps.md
Normal file
79
skills/supabase/references/schema-timestamps.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
title: Always Use timestamptz Not timestamp
|
||||||
|
impact: MEDIUM-HIGH
|
||||||
|
impactDescription: Prevents timezone-related bugs and data inconsistencies
|
||||||
|
tags: timestamps, timestamptz, timezone, data-types
|
||||||
|
---
|
||||||
|
|
||||||
|
## Always Use timestamptz Not timestamp
|
||||||
|
|
||||||
|
Use `timestamptz` (timestamp with time zone) instead of `timestamp`. The latter
|
||||||
|
loses timezone information, causing bugs when users are in different timezones.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
create table events (
|
||||||
|
id bigint primary key generated always as identity,
|
||||||
|
name text not null,
|
||||||
|
-- Stores time without timezone context
|
||||||
|
created_at timestamp default now(),
|
||||||
|
starts_at timestamp
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
create table events (
|
||||||
|
id bigint primary key generated always as identity,
|
||||||
|
name text not null,
|
||||||
|
-- Stores time in UTC, converts on retrieval
|
||||||
|
created_at timestamptz default now(),
|
||||||
|
starts_at timestamptz
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## How timestamptz Works
|
||||||
|
|
||||||
|
- Stores time in UTC internally
|
||||||
|
- Converts to/from session timezone automatically
|
||||||
|
- `now()` returns current time in session timezone, stored as UTC
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Insert with timezone
|
||||||
|
insert into events (name, starts_at)
|
||||||
|
values ('Launch', '2024-03-15 10:00:00-05'); -- EST
|
||||||
|
|
||||||
|
-- Retrieved in UTC by default in Supabase
|
||||||
|
select starts_at from events;
|
||||||
|
-- 2024-03-15 15:00:00+00
|
||||||
|
```
|
||||||
|
|
||||||
|
## Auto-Update updated_at Column
|
||||||
|
|
||||||
|
```sql
|
||||||
|
create table posts (
|
||||||
|
id bigint primary key generated always as identity,
|
||||||
|
title text not null,
|
||||||
|
created_at timestamptz default now(),
|
||||||
|
updated_at timestamptz default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Trigger to auto-update
|
||||||
|
create or replace function update_updated_at()
|
||||||
|
returns trigger as $$
|
||||||
|
begin
|
||||||
|
new.updated_at = now();
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$ language plpgsql;
|
||||||
|
|
||||||
|
create trigger posts_updated_at
|
||||||
|
before update on posts
|
||||||
|
for each row execute function update_updated_at();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Docs](https://supabase.com/docs/guides/database/tables)
|
||||||
125
skills/supabase/references/security-functions.md
Normal file
125
skills/supabase/references/security-functions.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
---
|
||||||
|
title: Use security_definer Functions in Private Schema
|
||||||
|
impact: HIGH
|
||||||
|
impactDescription: Controlled privilege escalation without exposing service role
|
||||||
|
tags: functions, security_definer, security, private-schema
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use security_definer Functions in Private Schema
|
||||||
|
|
||||||
|
`security definer` functions run with the privileges of the function owner, not
|
||||||
|
the caller. Place them in a private schema to prevent direct API access.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- DANGEROUS: Exposed via API, can be called directly
|
||||||
|
create function public.get_all_users()
|
||||||
|
returns setof auth.users
|
||||||
|
language sql
|
||||||
|
security definer
|
||||||
|
as $$
|
||||||
|
select * from auth.users; -- Bypasses RLS!
|
||||||
|
$$;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Create private schema (not exposed to API)
|
||||||
|
create schema if not exists private;
|
||||||
|
|
||||||
|
-- Function in private schema
|
||||||
|
create function private.get_all_users()
|
||||||
|
returns setof auth.users
|
||||||
|
language sql
|
||||||
|
security definer
|
||||||
|
set search_path = '' -- Prevent search_path injection
|
||||||
|
as $$
|
||||||
|
select * from auth.users;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Wrapper in public schema with access control
|
||||||
|
create function public.get_user_count()
|
||||||
|
returns bigint
|
||||||
|
language sql
|
||||||
|
security invoker -- Runs as caller
|
||||||
|
as $$
|
||||||
|
select count(*) from private.get_all_users()
|
||||||
|
where (select auth.jwt() -> 'app_metadata' ->> 'role') = 'admin';
|
||||||
|
$$;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Use Cases
|
||||||
|
|
||||||
|
### 1. Admin Operations
|
||||||
|
|
||||||
|
```sql
|
||||||
|
create function private.admin_delete_user(target_user_id uuid)
|
||||||
|
returns void
|
||||||
|
language plpgsql
|
||||||
|
security definer
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
-- Verify caller is admin
|
||||||
|
if (select auth.jwt() -> 'app_metadata' ->> 'role') != 'admin' then
|
||||||
|
raise exception 'Unauthorized';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
delete from auth.users where id = target_user_id;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Cross-User Data Access
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Function returns team IDs the current user belongs to
|
||||||
|
create function private.user_teams()
|
||||||
|
returns setof uuid
|
||||||
|
language sql
|
||||||
|
security definer
|
||||||
|
stable
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
select team_id from public.team_members
|
||||||
|
where user_id = (select auth.uid());
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- RLS policy uses cached function result (no per-row join)
|
||||||
|
create policy "Team members see team data" on team_data
|
||||||
|
for select to authenticated
|
||||||
|
using (team_id in (select private.user_teams()));
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
```sql
|
||||||
|
create function private.sensitive_operation()
|
||||||
|
returns void
|
||||||
|
language plpgsql
|
||||||
|
security definer
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
-- Log the operation
|
||||||
|
insert into audit_log (user_id, action, timestamp)
|
||||||
|
values ((select auth.uid()), 'sensitive_operation', now());
|
||||||
|
|
||||||
|
-- Perform operation
|
||||||
|
-- ...
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [security-service-role.md](security-service-role.md)
|
||||||
|
- [rls-performance.md](rls-performance.md)
|
||||||
|
- [Docs](https://supabase.com/docs/guides/database/functions)
|
||||||
97
skills/supabase/references/security-service-role.md
Normal file
97
skills/supabase/references/security-service-role.md
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
---
|
||||||
|
title: Never Expose Service Role Key to Browser
|
||||||
|
impact: CRITICAL
|
||||||
|
impactDescription: Prevents complete database compromise and data breach
|
||||||
|
tags: service-role, security, api-keys, anon-key
|
||||||
|
---
|
||||||
|
|
||||||
|
## Never Expose Service Role Key to Browser
|
||||||
|
|
||||||
|
The service role key bypasses ALL Row Level Security. Exposing it gives complete
|
||||||
|
database access to anyone.
|
||||||
|
|
||||||
|
**Incorrect:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// NEVER do this - service key in frontend code!
|
||||||
|
const supabase = createClient(
|
||||||
|
"https://xxx.supabase.co",
|
||||||
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." // service_role key
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Browser: Use anon key (respects RLS)
|
||||||
|
const supabase = createClient(
|
||||||
|
"https://xxx.supabase.co",
|
||||||
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." // anon key
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## When to Use Service Role Key
|
||||||
|
|
||||||
|
Only in server-side code that users cannot access:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Edge Function or backend server
|
||||||
|
import { createClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
const supabaseAdmin = createClient(
|
||||||
|
process.env.SUPABASE_URL,
|
||||||
|
process.env.SUPABASE_SERVICE_ROLE_KEY // Only in secure backend
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bypass RLS for admin operations
|
||||||
|
const { data } = await supabaseAdmin.from("users").select("*");
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
## .env.local (never commit to git!)
|
||||||
|
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... # Safe to expose
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY=eyJ... # NEVER prefix with NEXT_PUBLIC_
|
||||||
|
```
|
||||||
|
|
||||||
|
## Identifying Keys
|
||||||
|
|
||||||
|
- **Anon key**: `role` claim is `anon`
|
||||||
|
- **Service role key**: `role` claim is `service_role`
|
||||||
|
|
||||||
|
Decode JWT at [jwt.io](https://jwt.io) to verify.
|
||||||
|
|
||||||
|
## If Service Key is Exposed
|
||||||
|
|
||||||
|
1. Immediately rotate keys in Dashboard > Settings > API
|
||||||
|
2. Review database for unauthorized changes
|
||||||
|
3. Check logs for suspicious activity
|
||||||
|
4. Update all backend services with new key
|
||||||
|
|
||||||
|
## Alternative: Security Definer Functions
|
||||||
|
|
||||||
|
Instead of service role, use security definer functions for specific elevated
|
||||||
|
operations:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Runs with function owner's privileges
|
||||||
|
create function admin_get_user_count()
|
||||||
|
returns bigint
|
||||||
|
language sql
|
||||||
|
security definer
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
select count(*) from auth.users;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Grant to authenticated users
|
||||||
|
grant execute on function admin_get_user_count to authenticated;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [security-functions.md](security-functions.md)
|
||||||
|
- [rls-mandatory.md](rls-mandatory.md)
|
||||||
|
- [Docs](https://supabase.com/docs/guides/api/api-keys)
|
||||||
Reference in New Issue
Block a user