
Supabase Admin
- 136 installs
- 178 repo stars
- Updated July 14, 2026
- erichowens/some_claude_skills
Administer a live Supabase project—manage tables, policies, auth settings, storage buckets, and environment configuration without leaving the agent workflow during maintenance or incident response.
About
The supabase-admin skill equips agents to manage Supabase-backed applications during ongoing operations. It covers practical administration across PostgreSQL schemas, authentication, storage, and project settings so teams can apply policy updates, inspect data structures, and adjust backend configuration safely. It is suited to SaaS and API products that rely on Supabase as their primary datastore and need repeatable, agent-assisted maintenance without rebuilding application code from scratch.
- Guides Supabase project and schema administration tasks
- Covers Row Level Security, auth, and storage configuration
- Supports operational changes to tables, roles, and policies
- Reduces context switching between CLI, dashboard, and code
- Helps stabilize backend data services in production
Supabase Admin by the numbers
- 136 all-time installs (skills.sh)
- Ranked #288 of 911 Databases skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/erichowens/some_claude_skills --skill supabase-adminAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 136 |
|---|---|
| repo stars | ★ 178 |
| Last updated | July 14, 2026 |
| Repository | erichowens/some_claude_skills ↗ |
What it does
Administer a live Supabase project—manage tables, policies, auth settings, storage buckets, and environment configuration without leaving the agent workflow during maintenance or incident response.
Files
Supabase Administration Expert
Master Supabase schema design, Row Level Security policies, migrations, and performance optimization for production applications.
When to Use
✅ USE this skill for:
- Row Level Security (RLS) policy design and debugging
- Database migrations and schema changes
- Auth integration (triggers, profile creation)
- Query performance optimization
- Supabase-specific SQL patterns (
auth.uid(),auth.jwt())
❌ DO NOT use for:
- Supabase Auth UI configuration → use Supabase dashboard docs
- Edge Functions → use
cloudflare-worker-devskill - General PostgreSQL without Supabase context → use standard SQL resources
- Client-side Supabase SDK usage → use Supabase JS docs
Core Competencies
1. Row Level Security (RLS)
Always Enable RLS on User Tables:
ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;Policy Patterns:
-- Public read, authenticated write
CREATE POLICY "Public read" ON posts FOR SELECT USING (true);
CREATE POLICY "Owners can write" ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Owner-only access
CREATE POLICY "Users own their data" ON profiles
FOR ALL USING (auth.uid() = id);
-- Role-based access
CREATE POLICY "Admins can do anything" ON content
FOR ALL USING (
EXISTS (
SELECT 1 FROM profiles
WHERE profiles.id = auth.uid()
AND profiles.role = 'admin'
)
);Performance-Critical: Index auth.uid() Columns:
-- 100x performance improvement for RLS policies
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_profiles_id ON profiles(id);Subquery Optimization for JWT Functions:
-- BAD: JWT parsed for every row
CREATE POLICY "slow" ON posts FOR SELECT
USING (user_id = auth.uid());
-- GOOD: JWT parsed once via subquery
CREATE POLICY "fast" ON posts FOR SELECT
USING (user_id = (SELECT auth.uid()));2. Migration Best Practices
File Naming Convention:
supabase/migrations/
├── 001_initial_schema.sql
├── 002_add_profiles_trigger.sql
├── 003_forum_tables.sql
└── 004_add_rls_policies.sqlMigration Template:
-- Migration: 005_feature_name
-- Description: What this migration does
-- Author: name
-- Date: YYYY-MM-DD
-- Up migration
BEGIN;
-- Your DDL here
CREATE TABLE ...;
ALTER TABLE ...;
CREATE POLICY ...;
COMMIT;
-- Down migration (as comment for reference)
-- DROP TABLE ...;
-- DROP POLICY ...;Safe Migration Patterns:
-- Add column with default (no table lock)
ALTER TABLE users ADD COLUMN status text DEFAULT 'active';
-- Add NOT NULL constraint safely
ALTER TABLE users ADD COLUMN email text;
UPDATE users SET email = 'unknown@example.com' WHERE email IS NULL;
ALTER TABLE users ALTER COLUMN email SET NOT NULL;
-- Create index concurrently (no lock)
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);3. Auth Integration
Auto-create Profile on Signup:
-- Function to create profile
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.profiles (id, email, display_name)
VALUES (
NEW.id,
NEW.email,
COALESCE(NEW.raw_user_meta_data->>'display_name', split_part(NEW.email, '@', 1))
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Trigger on auth.users
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();Check Auth Status in Policies:
-- Authenticated users only
CREATE POLICY "Authenticated access" ON data
FOR SELECT USING (auth.role() = 'authenticated');
-- Get current user's ID
SELECT auth.uid();
-- Get current user's JWT claims
SELECT auth.jwt();4. Common Schema Patterns
Timestamps with Defaults:
CREATE TABLE posts (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid REFERENCES auth.users(id) ON DELETE CASCADE,
content text NOT NULL,
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);
-- Auto-update updated_at
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_posts_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW EXECUTE FUNCTION update_updated_at();Soft Delete Pattern:
ALTER TABLE posts ADD COLUMN deleted_at timestamptz;
CREATE POLICY "Hide deleted" ON posts
FOR SELECT USING (deleted_at IS NULL);Full-Text Search:
-- Add search vector column
ALTER TABLE posts ADD COLUMN search_vector tsvector;
-- Create GIN index
CREATE INDEX idx_posts_search ON posts USING GIN(search_vector);
-- Update function
CREATE OR REPLACE FUNCTION posts_search_update()
RETURNS TRIGGER AS $$
BEGIN
NEW.search_vector := to_tsvector('english', COALESCE(NEW.title, '') || ' ' || COALESCE(NEW.content, ''));
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Search query
SELECT * FROM posts
WHERE search_vector @@ plainto_tsquery('english', 'search terms');5. Debugging RLS Issues
Common Problem: Empty Results, No Error
-- Check if RLS is enabled
SELECT tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public';
-- List all policies
SELECT * FROM pg_policies WHERE tablename = 'your_table';
-- Test as specific role
SET ROLE anon;
SELECT * FROM your_table LIMIT 1;
RESET ROLE;
-- Test with specific user
SET request.jwt.claims TO '{"sub": "user-uuid-here"}';
SELECT * FROM your_table;Diagnostic Query:
-- Check what the current user can see
SELECT
auth.uid() as current_user,
auth.role() as current_role,
(SELECT count(*) FROM your_table) as visible_rows;Quick Reference
| Task | Command |
|---|---|
| Enable RLS | ALTER TABLE t ENABLE ROW LEVEL SECURITY; |
| Create policy | CREATE POLICY "name" ON t FOR action USING (condition); |
| Drop policy | DROP POLICY "name" ON t; |
| Check policies | SELECT * FROM pg_policies WHERE tablename = 't'; |
| Current user | SELECT auth.uid(); |
| Force RLS for owner | ALTER TABLE t FORCE ROW LEVEL SECURITY; |
References
See /references/ for detailed guides:
rls-patterns.md- Advanced RLS policy patternsmigration-checklist.md- Pre-deployment checklistperformance-tuning.md- Query and index optimizationsocial-schema.md- Schema patterns for social features
Advanced RLS Policy Patterns
Policy Types
| Type | Use Case |
|---|---|
FOR SELECT | Read access |
FOR INSERT | Create access (use WITH CHECK) |
FOR UPDATE | Modify access |
FOR DELETE | Remove access |
FOR ALL | All operations |
Pattern 1: Owner-Based Access
-- Only owner can see/modify their data
CREATE POLICY "Owner access" ON user_data
FOR ALL USING (auth.uid() = user_id);Pattern 2: Public Read, Authenticated Write
-- Anyone can read
CREATE POLICY "Public read" ON posts
FOR SELECT USING (true);
-- Only authenticated users can create (owning their posts)
CREATE POLICY "Auth write" ON posts
FOR INSERT WITH CHECK (
auth.role() = 'authenticated'
AND auth.uid() = author_id
);
-- Only author can update
CREATE POLICY "Author update" ON posts
FOR UPDATE USING (auth.uid() = author_id);Pattern 3: Role-Based Access Control (RBAC)
-- Store roles in profiles
ALTER TABLE profiles ADD COLUMN role text DEFAULT 'user';
-- Admin can do anything
CREATE POLICY "Admin full access" ON content
FOR ALL USING (
EXISTS (
SELECT 1 FROM profiles
WHERE profiles.id = (SELECT auth.uid())
AND profiles.role = 'admin'
)
);
-- Moderators can read and update
CREATE POLICY "Mod read" ON content
FOR SELECT USING (
EXISTS (
SELECT 1 FROM profiles
WHERE profiles.id = (SELECT auth.uid())
AND profiles.role IN ('admin', 'moderator')
)
);Pattern 4: Team/Organization Access
-- Users belong to teams
CREATE TABLE team_members (
team_id uuid REFERENCES teams(id),
user_id uuid REFERENCES profiles(id),
role text DEFAULT 'member',
PRIMARY KEY (team_id, user_id)
);
-- Team members can access team resources
CREATE POLICY "Team access" ON team_resources
FOR SELECT USING (
EXISTS (
SELECT 1 FROM team_members
WHERE team_members.team_id = team_resources.team_id
AND team_members.user_id = (SELECT auth.uid())
)
);Pattern 5: Privacy Settings
-- Users control their own visibility
ALTER TABLE profiles ADD COLUMN privacy text DEFAULT 'public';
CREATE POLICY "Respect privacy" ON profiles
FOR SELECT USING (
privacy = 'public'
OR id = (SELECT auth.uid())
OR (
privacy = 'friends'
AND EXISTS (
SELECT 1 FROM friendships
WHERE status = 'accepted'
AND (
(requester_id = profiles.id AND addressee_id = (SELECT auth.uid()))
OR (addressee_id = profiles.id AND requester_id = (SELECT auth.uid()))
)
)
)
);Pattern 6: Temporal Access (Time-Based)
-- Content visible only during certain times
CREATE POLICY "Time-limited access" ON events
FOR SELECT USING (
starts_at <= now()
AND (ends_at IS NULL OR ends_at > now())
);
-- Soft-deleted items hidden
CREATE POLICY "Hide deleted" ON posts
FOR SELECT USING (deleted_at IS NULL);Pattern 7: Hierarchical Access (Parent-Child)
-- Comments inherit visibility from posts
CREATE POLICY "Comments follow posts" ON comments
FOR SELECT USING (
EXISTS (
SELECT 1 FROM posts
WHERE posts.id = comments.post_id
AND (
posts.visibility = 'public'
OR posts.author_id = (SELECT auth.uid())
)
)
);Performance Optimization
Always Use Subquery for auth.uid()
-- SLOW: JWT parsed for every row
CREATE POLICY "slow" ON data
FOR SELECT USING (user_id = auth.uid());
-- FAST: JWT parsed once
CREATE POLICY "fast" ON data
FOR SELECT USING (user_id = (SELECT auth.uid()));Index Policy Columns
-- Create indexes on columns used in policies
CREATE INDEX idx_posts_author ON posts(author_id);
CREATE INDEX idx_team_members_lookup ON team_members(team_id, user_id);
CREATE INDEX idx_profiles_role ON profiles(role);Use Security Definer Functions for Complex Logic
-- Move complex logic to a function
CREATE OR REPLACE FUNCTION can_access_resource(resource_id uuid)
RETURNS boolean AS $$
-- Complex access logic here
SELECT EXISTS (
SELECT 1 FROM permissions
WHERE permissions.resource_id = $1
AND permissions.user_id = (SELECT auth.uid())
AND permissions.expires_at > now()
);
$$ LANGUAGE sql STABLE SECURITY DEFINER;
-- Simple policy using function
CREATE POLICY "Check permissions" ON resources
FOR SELECT USING (can_access_resource(id));Debugging Policies
-- See all policies on a table
SELECT * FROM pg_policies WHERE tablename = 'your_table';
-- Test as anonymous
SET ROLE anon;
SELECT * FROM your_table LIMIT 1;
RESET ROLE;
-- Test as authenticated with specific user
SET request.jwt.claims TO '{"sub": "user-uuid", "role": "authenticated"}';
SELECT * FROM your_table;
-- Check current auth context
SELECT
auth.uid() as user_id,
auth.role() as role,
current_user as db_user;Common Mistakes
1. Forgetting to enable RLS: ALTER TABLE t ENABLE ROW LEVEL SECURITY; 2. Not indexing policy columns: Causes full table scans 3. Using `auth.uid()` directly: Use (SELECT auth.uid()) instead 4. Overly permissive policies: Start restrictive, add permissions 5. Circular references: Policy A depends on B depends on A 6. Missing INSERT policies: WITH CHECK is required for INSERT 7. Testing only as superuser: Superuser bypasses RLS
Social Features Schema Patterns
Proven database patterns for social features in Supabase.
Friend/Connection System
Basic Friend Request Schema
-- Friend relationships (bidirectional once accepted)
CREATE TABLE friendships (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
requester_id uuid REFERENCES profiles(id) ON DELETE CASCADE,
addressee_id uuid REFERENCES profiles(id) ON DELETE CASCADE,
status text CHECK (status IN ('pending', 'accepted', 'blocked')) DEFAULT 'pending',
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now(),
UNIQUE(requester_id, addressee_id)
);
-- Indexes for fast lookups
CREATE INDEX idx_friendships_requester ON friendships(requester_id);
CREATE INDEX idx_friendships_addressee ON friendships(addressee_id);
CREATE INDEX idx_friendships_status ON friendships(status);
-- RLS Policies
ALTER TABLE friendships ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users see their friendships" ON friendships
FOR SELECT USING (
auth.uid() IN (requester_id, addressee_id)
);
CREATE POLICY "Users can request friendship" ON friendships
FOR INSERT WITH CHECK (
auth.uid() = requester_id
AND requester_id != addressee_id
);
CREATE POLICY "Addressee can accept/reject" ON friendships
FOR UPDATE USING (
auth.uid() = addressee_id
AND status = 'pending'
);
CREATE POLICY "Either party can delete" ON friendships
FOR DELETE USING (
auth.uid() IN (requester_id, addressee_id)
);Query Helpers
-- Get all friends for a user (accepted only)
CREATE OR REPLACE FUNCTION get_friends(user_uuid uuid)
RETURNS TABLE(friend_id uuid, friend_since timestamptz) AS $$
SELECT
CASE
WHEN requester_id = user_uuid THEN addressee_id
ELSE requester_id
END as friend_id,
updated_at as friend_since
FROM friendships
WHERE status = 'accepted'
AND (requester_id = user_uuid OR addressee_id = user_uuid)
$$ LANGUAGE sql STABLE;
-- Check if two users are friends
CREATE OR REPLACE FUNCTION are_friends(user1 uuid, user2 uuid)
RETURNS boolean AS $$
SELECT EXISTS (
SELECT 1 FROM friendships
WHERE status = 'accepted'
AND ((requester_id = user1 AND addressee_id = user2)
OR (requester_id = user2 AND addressee_id = user1))
)
$$ LANGUAGE sql STABLE;Sponsor/Sponsee Relationships
Hierarchical Mentor Relationship
-- Sponsor relationships (one-to-many)
CREATE TABLE sponsorships (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
sponsor_id uuid REFERENCES profiles(id) ON DELETE CASCADE,
sponsee_id uuid REFERENCES profiles(id) ON DELETE CASCADE,
invite_code text UNIQUE,
status text CHECK (status IN ('pending', 'active', 'ended')) DEFAULT 'pending',
program text, -- 'aa', 'na', 'cma', etc.
started_at timestamptz,
ended_at timestamptz,
created_at timestamptz DEFAULT now(),
UNIQUE(sponsor_id, sponsee_id)
);
-- Invite code generation function
CREATE OR REPLACE FUNCTION generate_sponsor_invite()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.invite_code IS NULL THEN
NEW.invite_code := upper(substr(md5(random()::text), 1, 8));
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER set_sponsor_invite
BEFORE INSERT ON sponsorships
FOR EACH ROW EXECUTE FUNCTION generate_sponsor_invite();
-- RLS Policies
ALTER TABLE sponsorships ENABLE ROW LEVEL SECURITY;
CREATE POLICY "View own sponsorships" ON sponsorships
FOR SELECT USING (auth.uid() IN (sponsor_id, sponsee_id));
CREATE POLICY "Sponsors create invites" ON sponsorships
FOR INSERT WITH CHECK (auth.uid() = sponsor_id);
CREATE POLICY "Either party can update" ON sponsorships
FOR UPDATE USING (auth.uid() IN (sponsor_id, sponsee_id));Accept Invite by Code
CREATE OR REPLACE FUNCTION accept_sponsor_invite(code text)
RETURNS sponsorships AS $$
DECLARE
result sponsorships;
BEGIN
UPDATE sponsorships
SET
sponsee_id = auth.uid(),
status = 'active',
started_at = now(),
invite_code = NULL -- Clear code after use
WHERE invite_code = code
AND status = 'pending'
AND sponsee_id IS NULL
RETURNING * INTO result;
IF result IS NULL THEN
RAISE EXCEPTION 'Invalid or expired invite code';
END IF;
RETURN result;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;Ad-Hoc Groups (Meeting-Based)
Ephemeral Group Schema
-- Groups that form around meetings or topics
CREATE TABLE groups (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL,
description text,
type text CHECK (type IN ('meeting', 'topic', 'support', 'private')) DEFAULT 'topic',
visibility text CHECK (visibility IN ('public', 'private', 'invite')) DEFAULT 'public',
meeting_id uuid REFERENCES meetings(id) ON DELETE SET NULL, -- Optional meeting link
creator_id uuid REFERENCES profiles(id) ON DELETE SET NULL,
max_members int DEFAULT 50,
expires_at timestamptz, -- For ephemeral groups
created_at timestamptz DEFAULT now()
);
-- Group membership
CREATE TABLE group_members (
group_id uuid REFERENCES groups(id) ON DELETE CASCADE,
user_id uuid REFERENCES profiles(id) ON DELETE CASCADE,
role text CHECK (role IN ('owner', 'admin', 'member')) DEFAULT 'member',
joined_at timestamptz DEFAULT now(),
PRIMARY KEY (group_id, user_id)
);
-- Indexes
CREATE INDEX idx_groups_meeting ON groups(meeting_id) WHERE meeting_id IS NOT NULL;
CREATE INDEX idx_groups_type ON groups(type);
CREATE INDEX idx_group_members_user ON group_members(user_id);
-- RLS Policies
ALTER TABLE groups ENABLE ROW LEVEL SECURITY;
ALTER TABLE group_members ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Public groups visible" ON groups
FOR SELECT USING (
visibility = 'public'
OR creator_id = auth.uid()
OR EXISTS (
SELECT 1 FROM group_members
WHERE group_members.group_id = groups.id
AND group_members.user_id = auth.uid()
)
);
CREATE POLICY "Members see membership" ON group_members
FOR SELECT USING (
user_id = auth.uid()
OR EXISTS (
SELECT 1 FROM group_members gm
WHERE gm.group_id = group_members.group_id
AND gm.user_id = auth.uid()
)
);Auto-Create Group for Meeting
CREATE OR REPLACE FUNCTION create_meeting_group(
meeting_uuid uuid,
group_name text DEFAULT NULL
)
RETURNS groups AS $$
DECLARE
meeting_record meetings;
new_group groups;
BEGIN
-- Get meeting info
SELECT * INTO meeting_record FROM meetings WHERE id = meeting_uuid;
IF meeting_record IS NULL THEN
RAISE EXCEPTION 'Meeting not found';
END IF;
-- Create group
INSERT INTO groups (name, type, meeting_id, creator_id, expires_at)
VALUES (
COALESCE(group_name, meeting_record.name || ' Group'),
'meeting',
meeting_uuid,
auth.uid(),
now() + interval '24 hours' -- Ephemeral by default
)
RETURNING * INTO new_group;
-- Add creator as owner
INSERT INTO group_members (group_id, user_id, role)
VALUES (new_group.id, auth.uid(), 'owner');
RETURN new_group;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;Direct Messaging
Conversation Schema
-- Conversations (1:1 or group)
CREATE TABLE conversations (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
type text CHECK (type IN ('direct', 'group')) DEFAULT 'direct',
group_id uuid REFERENCES groups(id) ON DELETE CASCADE,
created_at timestamptz DEFAULT now()
);
-- Conversation participants
CREATE TABLE conversation_participants (
conversation_id uuid REFERENCES conversations(id) ON DELETE CASCADE,
user_id uuid REFERENCES profiles(id) ON DELETE CASCADE,
last_read_at timestamptz DEFAULT now(),
muted_until timestamptz,
PRIMARY KEY (conversation_id, user_id)
);
-- Messages
CREATE TABLE messages (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
conversation_id uuid REFERENCES conversations(id) ON DELETE CASCADE,
sender_id uuid REFERENCES profiles(id) ON DELETE SET NULL,
content text NOT NULL,
message_type text DEFAULT 'text', -- 'text', 'image', 'system'
created_at timestamptz DEFAULT now(),
edited_at timestamptz,
deleted_at timestamptz
);
-- Indexes
CREATE INDEX idx_messages_conversation ON messages(conversation_id, created_at DESC);
CREATE INDEX idx_conversation_participants_user ON conversation_participants(user_id);
-- RLS Policies
ALTER TABLE conversations ENABLE ROW LEVEL SECURITY;
ALTER TABLE conversation_participants ENABLE ROW LEVEL SECURITY;
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Participants see conversations" ON conversations
FOR SELECT USING (
EXISTS (
SELECT 1 FROM conversation_participants
WHERE conversation_participants.conversation_id = conversations.id
AND conversation_participants.user_id = auth.uid()
)
);
CREATE POLICY "Participants see messages" ON messages
FOR SELECT USING (
EXISTS (
SELECT 1 FROM conversation_participants
WHERE conversation_participants.conversation_id = messages.conversation_id
AND conversation_participants.user_id = auth.uid()
)
AND deleted_at IS NULL
);
CREATE POLICY "Participants send messages" ON messages
FOR INSERT WITH CHECK (
auth.uid() = sender_id
AND EXISTS (
SELECT 1 FROM conversation_participants
WHERE conversation_participants.conversation_id = messages.conversation_id
AND conversation_participants.user_id = auth.uid()
)
);Get or Create DM Conversation
CREATE OR REPLACE FUNCTION get_or_create_dm(other_user_id uuid)
RETURNS conversations AS $$
DECLARE
existing_conversation conversations;
new_conversation conversations;
BEGIN
-- Check for existing DM
SELECT c.* INTO existing_conversation
FROM conversations c
WHERE c.type = 'direct'
AND EXISTS (
SELECT 1 FROM conversation_participants cp1
WHERE cp1.conversation_id = c.id AND cp1.user_id = auth.uid()
)
AND EXISTS (
SELECT 1 FROM conversation_participants cp2
WHERE cp2.conversation_id = c.id AND cp2.user_id = other_user_id
)
AND (SELECT count(*) FROM conversation_participants WHERE conversation_id = c.id) = 2
LIMIT 1;
IF existing_conversation IS NOT NULL THEN
RETURN existing_conversation;
END IF;
-- Create new conversation
INSERT INTO conversations (type)
VALUES ('direct')
RETURNING * INTO new_conversation;
-- Add participants
INSERT INTO conversation_participants (conversation_id, user_id)
VALUES
(new_conversation.id, auth.uid()),
(new_conversation.id, other_user_id);
RETURN new_conversation;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;Real-Time Subscriptions
// Subscribe to new messages
const subscription = supabase
.channel('messages')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
filter: `conversation_id=eq.${conversationId}`
},
(payload) => {
addMessage(payload.new);
}
)
.subscribe();
// Subscribe to friend requests
const friendRequests = supabase
.channel('friend-requests')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'friendships',
filter: `addressee_id=eq.${userId}`
},
(payload) => {
showNotification('New friend request!');
}
)
.subscribe();Performance Tips
1. Index foreign keys and filter columns 2. Use `(SELECT auth.uid())` in policies for JWT caching 3. Paginate messages with cursor-based pagination 4. Consider materialized views for friend counts 5. Use database functions for complex operations 6. Clean up expired ephemeral groups with scheduled job