Supabase Developer by daffy0208/ai-dev-standards
npx skills add https://github.com/daffy0208/ai-dev-standards --skill 'Supabase Developer'使用 Supabase 构建生产就绪的全栈应用程序。
Supabase 是一个开源的 Firebase 替代方案,提供 PostgreSQL 数据库、身份验证、存储、实时订阅和边缘函数。本技能将指导您使用 Supabase 的完整功能集构建安全、可扩展的应用程序。
Supabase 使用带有扩展的 PostgreSQL:
内置多种提供者的身份验证:
在数据库级别强制执行数据访问的 PostgreSQL 策略:
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
具有 RLS 的 S3 兼容对象存储:
基于 WebSocket 的订阅:
基于 Deno 的无服务器函数:
目标 : 初始化 Supabase 项目并连接到您的应用程序
# 选项 A:Web 仪表板
# 1. 访问 https://supabase.com
# 2. 创建新项目
# 3. 安全保存数据库密码
# 选项 B:CLI
npx supabase init
npx supabase start
# JavaScript/TypeScript
npm install @supabase/supabase-js
# React 助手
npm install @supabase/auth-helpers-react @supabase/auth-helpers-nextjs
# 用于身份验证 UI 组件
npm install @supabase/auth-ui-react @supabase/auth-ui-shared
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key # 仅限服务器端!
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
Next.js 13+ App Router 模式:
// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
// lib/supabase/server.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createClient() {
const cookieStore = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
}
}
}
)
}
目标 : 通过会话管理实现安全的用户身份验证
电子邮件/密码身份验证:
// 注册
async function signUp(email: string, password: string) {
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: 'https://yourapp.com/auth/callback'
}
})
if (error) throw error
return data
}
// 登录
async function signIn(email: string, password: string) {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password
})
if (error) throw error
return data
}
// 登出
async function signOut() {
const { error } = await supabase.auth.signOut()
if (error) throw error
}
OAuth 身份验证:
// Google OAuth
async function signInWithGoogle() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'https://yourapp.com/auth/callback',
queryParams: {
access_type: 'offline',
prompt: 'consent'
}
}
})
if (error) throw error
return data
}
// GitHub, Twitter, Discord 等 - 相同模式
魔法链接:
async function signInWithMagicLink(email: string) {
const { data, error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: 'https://yourapp.com/auth/callback'
}
})
if (error) throw error
return data
}
// 获取当前会话
async function getSession() {
const {
data: { session },
error
} = await supabase.auth.getSession()
return session
}
// 获取当前用户
async function getUser() {
const {
data: { user },
error
} = await supabase.auth.getUser()
return user
}
// 监听身份验证状态变更
supabase.auth.onAuthStateChange((event, session) => {
console.log(event, session)
if (event === 'SIGNED_IN') {
// 用户已登录
}
if (event === 'SIGNED_OUT') {
// 用户已登出
}
if (event === 'TOKEN_REFRESHED') {
// 令牌已刷新
}
})
// middleware.ts
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(req: NextRequest) {
const res = NextResponse.next()
const supabase = createMiddlewareClient({ req, res })
const {
data: { session }
} = await supabase.auth.getSession()
// 受保护的路由
if (!session && req.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', req.url))
}
return res
}
export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*']
}
目标 : 使用行级安全性设计安全的数据库模式
-- 示例:博客应用程序模式
-- 启用 UUID 扩展
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- 用户资料表
CREATE TABLE profiles (
id UUID REFERENCES auth.users(id) PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
full_name TEXT,
avatar_url TEXT,
bio TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 帖子表
CREATE TABLE posts (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
user_id UUID REFERENCES profiles(id) ON DELETE CASCADE NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
published BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 评论表
CREATE TABLE comments (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
post_id UUID REFERENCES posts(id) ON DELETE CASCADE NOT NULL,
user_id UUID REFERENCES profiles(id) ON DELETE CASCADE NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 性能索引
CREATE INDEX posts_user_id_idx ON posts(user_id);
CREATE INDEX posts_created_at_idx ON posts(created_at DESC);
CREATE INDEX comments_post_id_idx ON comments(post_id);
-- 在所有表上启用 RLS
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
ALTER TABLE comments ENABLE ROW LEVEL SECURITY;
-- 用户资料:所有人可读,用户只能更新自己的
CREATE POLICY "Public profiles are viewable by everyone"
ON profiles FOR SELECT
USING (true);
CREATE POLICY "Users can insert their own profile"
ON profiles FOR INSERT
WITH CHECK (auth.uid() = id);
CREATE POLICY "Users can update their own profile"
ON profiles FOR UPDATE
USING (auth.uid() = id);
-- 帖子:公开可读已发布的,用户可以管理自己的
CREATE POLICY "Published posts are viewable by everyone"
ON posts FOR SELECT
USING (published = true OR auth.uid() = user_id);
CREATE POLICY "Users can create their own posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update their own posts"
ON posts FOR UPDATE
USING (auth.uid() = user_id);
CREATE POLICY "Users can delete their own posts"
ON posts FOR DELETE
USING (auth.uid() = user_id);
-- 评论:公开可读,用户可以管理自己的
CREATE POLICY "Comments are viewable by everyone"
ON comments FOR SELECT
USING (true);
CREATE POLICY "Authenticated users can create comments"
ON comments FOR INSERT
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update their own comments"
ON comments FOR UPDATE
USING (auth.uid() = user_id);
CREATE POLICY "Users can delete their own comments"
ON comments FOR DELETE
USING (auth.uid() = user_id);
-- 自动更新 updated_at 时间戳
CREATE OR REPLACE FUNCTION handle_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 应用到表
CREATE TRIGGER handle_profiles_updated_at
BEFORE UPDATE ON profiles
FOR EACH ROW
EXECUTE FUNCTION handle_updated_at();
CREATE TRIGGER handle_posts_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION handle_updated_at();
-- 注册时自动创建用户资料
CREATE OR REPLACE FUNCTION handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.profiles (id, username, full_name, avatar_url)
VALUES (
NEW.id,
NEW.raw_user_meta_data->>'username',
NEW.raw_user_meta_data->>'full_name',
NEW.raw_user_meta_data->>'avatar_url'
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION handle_new_user();
// 插入
const { data, error } = await supabase
.from('posts')
.insert({
title: 'My First Post',
content: 'Hello World!',
user_id: userId
})
.select()
.single()
// 带过滤器的选择
const { data: posts } = await supabase
.from('posts')
.select('*, profiles(*), comments(*)')
.eq('published', true)
.order('created_at', { ascending: false })
.limit(10)
// 更新
const { data, error } = await supabase
.from('posts')
.update({ published: true })
.eq('id', postId)
.select()
// 删除
const { error } = await supabase.from('posts').delete().eq('id', postId)
// 计数
const { count } = await supabase.from('posts').select('*', { count: 'exact', head: true })
// 全文搜索
const { data } = await supabase.from('posts').select('*').textSearch('content', 'supabase', {
type: 'websearch',
config: 'english'
})
目标 : 通过访问控制管理文件上传
-- 创建存储桶
INSERT INTO storage.buckets (id, name, public)
VALUES ('avatars', 'avatars', true);
INSERT INTO storage.buckets (id, name, public)
VALUES ('private-docs', 'private-docs', false);
-- 头像:任何人可读,用户可以上传自己的
CREATE POLICY "Avatar images are publicly accessible"
ON storage.objects FOR SELECT
USING (bucket_id = 'avatars');
CREATE POLICY "Users can upload their own avatar"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'avatars' AND
auth.uid()::text = (storage.foldername(name))[1]
);
CREATE POLICY "Users can update their own avatar"
ON storage.objects FOR UPDATE
USING (
bucket_id = 'avatars' AND
auth.uid()::text = (storage.foldername(name))[1]
);
-- 私有文档:只有所有者可以访问
CREATE POLICY "Users can access their own documents"
ON storage.objects FOR SELECT
USING (
bucket_id = 'private-docs' AND
auth.uid()::text = (storage.foldername(name))[1]
);
CREATE POLICY "Users can upload their own documents"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'private-docs' AND
auth.uid()::text = (storage.foldername(name))[1]
);
// 上传文件
async function uploadFile(bucket: string, path: string, file: File) {
const { data, error } = await supabase.storage.from(bucket).upload(path, file, {
cacheControl: '3600',
upsert: false
})
if (error) throw error
return data
}
// 下载文件
async function downloadFile(bucket: string, path: string) {
const { data, error } = await supabase.storage.from(bucket).download(path)
if (error) throw error
return data
}
// 获取公开 URL
function getPublicUrl(bucket: string, path: string) {
const { data } = supabase.storage.from(bucket).getPublicUrl(path)
return data.publicUrl
}
// 获取签名 URL
async function getSignedUrl(bucket: string, path: string, expiresIn: number = 3600) {
const { data, error } = await supabase.storage.from(bucket).createSignedUrl(path, expiresIn)
if (error) throw error
return data.signedUrl
}
// 删除文件
async function deleteFile(bucket: string, path: string) {
const { error } = await supabase.storage.from(bucket).remove([path])
if (error) throw error
}
// 列出文件
async function listFiles(bucket: string, folder: string = '') {
const { data, error } = await supabase.storage.from(bucket).list(folder, {
limit: 100,
offset: 0,
sortBy: { column: 'created_at', order: 'desc' }
})
if (error) throw error
return data
}
// 获取调整大小的图像 URL
function getTransformedImage(
bucket: string,
path: string,
options: {
width?: number
height?: number
quality?: number
}
) {
const { data } = supabase.storage.from(bucket).getPublicUrl(path, {
transform: {
width: options.width,
height: options.height,
quality: options.quality || 80
}
})
return data.publicUrl
}
目标 : 构建实时、协作功能
// 订阅 INSERT 事件
const subscription = supabase
.channel('posts-channel')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'posts'
},
payload => {
console.log('New post created:', payload.new)
// 使用新帖子更新 UI
}
)
.subscribe()
// 订阅表上的所有事件
supabase
.channel('comments-channel')
.on(
'postgres_changes',
{
event: '*', // INSERT, UPDATE, DELETE
schema: 'public',
table: 'comments'
},
payload => {
console.log('Change detected:', payload)
}
)
.subscribe()
// 带过滤器的订阅
supabase
.channel('my-posts-channel')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'posts',
filter: `user_id=eq.${userId}`
},
payload => {
console.log('My post changed:', payload)
}
)
.subscribe()
// 取消订阅
subscription.unsubscribe()
// 加入房间并广播消息
const channel = supabase.channel('room-1')
// 发送广播消息
channel.send({
type: 'broadcast',
event: 'message',
payload: { text: 'Hello!', user: 'John' }
})
// 接收广播消息
channel
.on('broadcast', { event: 'message' }, payload => {
console.log('Message received:', payload)
})
.subscribe()
// 跟踪在线用户
const channel = supabase.channel('online-users', {
config: {
presence: {
key: userId
}
}
})
// 跟踪当前用户在线状态
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
console.log('Online users:', state)
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('User joined:', key, newPresences)
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('User left:', key, leftPresences)
})
.subscribe(async status => {
if (status === 'SUBSCRIBED') {
await channel.track({
user: userId,
online_at: new Date().toISOString()
})
}
})
// 更新在线状态
await channel.track({ status: 'typing' })
// 停止跟踪
await channel.untrack()
目标 : 无服务器函数和高级功能
// supabase/functions/hello/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
serve(async req => {
try {
// 使用服务角色创建 Supabase 客户端
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
// 从身份验证头获取用户
const authHeader = req.headers.get('Authorization')!
const token = authHeader.replace('Bearer ', '')
const {
data: { user }
} = await supabaseClient.auth.getUser(token)
// 您的逻辑在此处
const { data, error } = await supabaseClient.from('posts').select('*').eq('user_id', user?.id)
if (error) throw error
return new Response(JSON.stringify({ data }), {
headers: { 'Content-Type': 'application/json' }
})
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
})
}
})
部署边缘函数:
# 部署函数
supabase functions deploy hello
# 从客户端调用
const { data, error } = await supabase.functions.invoke('hello', {
body: { name: 'World' },
})
-- 在新帖子时发送 webhook
CREATE OR REPLACE FUNCTION send_post_webhook()
RETURNS TRIGGER AS $$
BEGIN
PERFORM
net.http_post(
url := 'https://your-api.com/webhook',
headers := '{"Content-Type": "application/json"}'::jsonb,
body := jsonb_build_object(
'event', 'new_post',
'post_id', NEW.id,
'title', NEW.title
)
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER on_post_created
AFTER INSERT ON posts
FOR EACH ROW
EXECUTE FUNCTION send_post_webhook();
-- 启用 pgvector 扩展
CREATE EXTENSION IF NOT EXISTS vector;
-- 添加嵌入列
ALTER TABLE posts ADD COLUMN embedding vector(1536);
-- 创建向量索引
CREATE INDEX ON posts USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- 按相似度搜索
SELECT *
FROM posts
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 10;
// 生成并存储嵌入
import OpenAI from 'openai'
const openai = new OpenAI()
async function addEmbedding(postId: string, text: string) {
// 生成嵌入
const response = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: text
})
const embedding = response.data[0].embedding
// 存储在 Supabase 中
await supabase.from('posts').update({ embedding }).eq('id', postId)
}
// 语义搜索
async function semanticSearch(query: string) {
// 生成查询嵌入
const response = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: query
})
const queryEmbedding = response.data[0].embedding
// 搜索
const { data } = await supabase.rpc('match_posts', {
query_embedding: queryEmbedding,
match_threshold: 0.7,
match_count: 10
})
return data
}
# 本地初始化 Supabase
supabase init
supabase start
# 创建新迁移
supabase migration new add_posts_table
# 在 supabase/migrations/ 中编辑迁移文件
# 应用迁移
supabase db reset
# 生成 TypeScript 类型
supabase gen types typescript --local > types/supabase.ts
# 链接到远程项目
supabase link --project-ref your-project-ref
# 将迁移推送到生产环境
supabase db push
# 或应用特定迁移
supabase db remote commit
// ❌ 错误 - 切勿在客户端使用
const supabase = createClient(url, SERVICE_ROLE_KEY)
// ✅ 正确 - 在客户端使用匿名密钥
const supabase = createClient(url, ANON_KEY)
// ✅ 服务角色仅用于服务器端
// app/api/admin/route.ts
const supabase = createClient(url, SERVICE_ROLE_KEY)
-- ❌ 错误 - 没有 RLS 的表
CREATE TABLE sensitive_data (
id UUID PRIMARY KEY,
secret TEXT
);
-- ✅ 正确 - 启用 RLS
CREATE TABLE sensitive_data (
id UUID PRIMARY KEY,
user_id UUID REFERENCES auth.users(id),
secret TEXT
);
ALTER TABLE sensitive_data ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can only access their data"
ON sensitive_data FOR ALL
USING (auth.uid() = user_id);
// ❌ 错误 - 无验证
await supabase.from('posts').insert({ title: userInput })
// ✅ 正确 - 先验证
import { z } from 'zod'
const schema = z.object({
title: z.string().min(1).max(100),
content: z.string().min(10).max(10000)
})
const validated = schema.parse(userInput)
await supabase.from('posts').insert(validated)
// 使用边缘函数进行速率限制
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, key)
// 检查速率限制
const { count } = await supabase
.from('api_calls')
.select('*', { count: 'exact', head: true })
.eq('user_id', userId)
.gte('created_at', oneHourAgo)
if (count >= 100) {
return new Response('Rate limit exceeded', { status: 429 })
}
-- 在频繁查询的列上添加索引
CREATE INDEX posts_user_id_idx ON posts(user_id);
CREATE INDEX posts_created_at_idx ON posts(created_at DESC);
-- 用于多列查询的复合索引
CREATE INDEX posts_user_published_idx ON posts(user_id, published);
-- 全文搜索索引
CREATE INDEX posts_content_fts_idx ON posts USING gin(to_tsvector('english', content));
// ❌ 错误 - 选择所有内容
const { data } = await supabase.from('posts').select('*')
// ✅ 正确 - 选择特定列
const { data } = await supabase.from('posts').select('id, title, created_at')
// 偏移分页
const { data } = await supabase.from('posts').select('*').range(0, 9)
// 游标分页
const { data } = await supabase
.from('posts')
.select('*')
.gt('created_at', lastCreatedAt)
.order('created_at', { ascending: false })
.limit(10)
// 使用 React Query 或 SWR
import { useQuery } from '@tanstack/react-query'
function usePosts() {
return useQuery({
queryKey: ['posts'],
queryFn: async () => {
const { data } = await supabase.from('posts').select('*')
return data
},
staleTime: 5 * 60 * 1000 // 5 分钟
})
}
// 模拟 Supabase 客户端
import { createClient } from '@supabase/supabase-js'
jest.mock('@supabase/supabase-js', () => ({
createClient: jest.fn(() => ({
from: jest.fn(() => ({
select: jest.fn(() => ({
eq: jest.fn(() => ({
single: jest.fn(() =>
Promise.resolve({
data: { id: '1', title: 'Test' },
error: null
})
)
}))
}))
}))
}))
}))
test('fetches post by id', async () => {
const post = await getPostById('1')
expect(post.title).toBe('Test')
})
// 使用测试数据库
const testSupabase = createClient(
process.env.TEST_SUPABASE_URL!,
process.env.TEST_SUPABASE_ANON_KEY!
)
beforeEach(async () => {
// 清理数据库
await testSupabase.from('posts').delete().neq('id', '00000000-0000-0000-0000-000000000000')
})
test('creates post', async () => {
const { data, error } = await testSupabase
.from('posts')
.insert({ title: 'Test Post', content: 'Content' })
.select()
.single()
expect(error).toBeNull()
expect(data.title).toBe('Test Post')
})
import { useMutation, useQueryClient } from '@tanstack/react-query'
function useCreatePost() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async newPost => {
const { data } = await supabase.from('posts').insert(newPost).select().single()
return data
},
onMutate: async newPost => {
// 取消正在进行的重新获取
await queryClient.cancelQueries({ queryKey: ['posts'] })
// 快照之前的值
const previousPosts = queryClient.getQueryData(['posts'])
// 乐观更新
queryClient.setQueryData(['posts'], old => [...old, newPost])
return { previousPosts }
},
onError: (err, newPost, context) => {
// 出错时回滚
queryClient.setQueryData(['posts'], context.previousPosts)
},
onSettled: () => {
// 变更后重新获取
queryClient.invalidateQueries({ queryKey: ['posts'] })
}
})
}
-- 添加 deleted_at 列
ALTER TABLE posts ADD COLUMN deleted_at TIMESTAMP WITH TIME ZONE;
-- 更新 RLS 以排除已删除的
CREATE POLICY "Only show non-deleted posts"
ON posts FOR SELECT
USING (deleted_at IS NULL);
-- 软删除函数
CREATE OR REPLACE FUNCTION soft_delete_post(post_id UUID)
RETURNS void AS $$
BEGIN
UPDATE posts
SET deleted_at = NOW()
WHERE id = post_id AND user_id = auth.uid();
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 创建审计日志表
CREATE TABLE audit_logs (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
table_name TEXT NOT NULL,
record_id UUID NOT NULL,
action TEXT NOT NULL,
old_data JSONB,
new_data JSONB,
user_id UUID REFERENCES auth.users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 审计触发器函数
CREATE OR REPLACE FUNCTION audit_trigger()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit_logs (table_name, record_id, action, old_data, new_data, user_id)
VALUES (
TG_TABLE_NAME,
COALESCE(NEW.id, OLD.id),
TG_OP,
CASE WHEN TG_OP = 'DELETE' THEN row_to_json(OLD) ELSE NULL END,
CASE WHEN TG_OP IN ('INSERT', 'UPDATE') THEN row_to_json(NEW) ELSE NULL END,
auth.uid()
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 应用到表
CREATE TRIGGER audit_posts
AFTER INSERT OR UPDATE OR DELETE ON posts
FOR EACH ROW
EXECUTE FUNCTION audit_trigger();
症状 : 即使策略正确也无法查询数据
解决方案 :
-- 检查是否启用了 RLS
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';
-- 检查策略
SELECT * FROM pg_policies WHERE tablename = 'your_table';
-- 以用户身份测试策略
SET LOCAL ROLE authenticated;
SET LOCAL request.jwt.claims.sub TO 'user-uuid';
SELECT * FROM your_table;
解决方案 :
// 自动刷新令牌
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'TOKEN_REFRESHED') {
// 令牌已自动刷新
}
})
// 手动刷新
const { data, error } = await supabase.auth.refreshSession()
解决方案 :
// 检查文件大小
// 检查 MIME 类型限制
// 验证存储桶是否存在
const { data: buckets } =
Build production-ready full-stack applications with Supabase.
Supabase is an open-source Firebase alternative providing PostgreSQL database, authentication, storage, real-time subscriptions, and edge functions. This skill guides you through building secure, scalable applications using Supabase's full feature set.
Supabase uses PostgreSQL with extensions:
Built-in auth with multiple providers:
PostgreSQL policies that enforce data access at the database level:
S3-compatible object storage with RLS:
WebSocket-based subscriptions:
Deno-based serverless functions:
Goal : Initialize Supabase project and connect to your application
# Option A: Web Dashboard
# 1. Go to https://supabase.com
# 2. Create new project
# 3. Save database password securely
# Option B: CLI (recommended for production)
npx supabase init
npx supabase start
# JavaScript/TypeScript
npm install @supabase/supabase-js
# React helpers (optional)
npm install @supabase/auth-helpers-react @supabase/auth-helpers-nextjs
# For Auth UI components
npm install @supabase/auth-ui-react @supabase/auth-ui-shared
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key # Server-side only!
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
Next.js 13+ App Router Pattern:
// lib/supabase/client.ts (Client Components)
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
// lib/supabase/server.ts (Server Components)
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createClient() {
const cookieStore = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
}
}
}
)
}
Goal : Secure user authentication with session management
Email/Password Authentication:
// Sign up
async function signUp(email: string, password: string) {
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: 'https://yourapp.com/auth/callback'
}
})
if (error) throw error
return data
}
// Sign in
async function signIn(email: string, password: string) {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password
})
if (error) throw error
return data
}
// Sign out
async function signOut() {
const { error } = await supabase.auth.signOut()
if (error) throw error
}
OAuth Authentication:
// Google OAuth
async function signInWithGoogle() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'https://yourapp.com/auth/callback',
queryParams: {
access_type: 'offline',
prompt: 'consent'
}
}
})
if (error) throw error
return data
}
// GitHub, Twitter, Discord, etc. - same pattern
Magic Link (Passwordless):
async function signInWithMagicLink(email: string) {
const { data, error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: 'https://yourapp.com/auth/callback'
}
})
if (error) throw error
return data
}
// Get current session
async function getSession() {
const {
data: { session },
error
} = await supabase.auth.getSession()
return session
}
// Get current user
async function getUser() {
const {
data: { user },
error
} = await supabase.auth.getUser()
return user
}
// Listen to auth changes
supabase.auth.onAuthStateChange((event, session) => {
console.log(event, session)
if (event === 'SIGNED_IN') {
// User signed in
}
if (event === 'SIGNED_OUT') {
// User signed out
}
if (event === 'TOKEN_REFRESHED') {
// Token refreshed
}
})
// middleware.ts
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(req: NextRequest) {
const res = NextResponse.next()
const supabase = createMiddlewareClient({ req, res })
const {
data: { session }
} = await supabase.auth.getSession()
// Protected routes
if (!session && req.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', req.url))
}
return res
}
export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*']
}
Goal : Design secure database schema with Row Level Security
-- Example: Blog application schema
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Profiles table (extends auth.users)
CREATE TABLE profiles (
id UUID REFERENCES auth.users(id) PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
full_name TEXT,
avatar_url TEXT,
bio TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Posts table
CREATE TABLE posts (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
user_id UUID REFERENCES profiles(id) ON DELETE CASCADE NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
published BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Comments table
CREATE TABLE comments (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
post_id UUID REFERENCES posts(id) ON DELETE CASCADE NOT NULL,
user_id UUID REFERENCES profiles(id) ON DELETE CASCADE NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Indexes for performance
CREATE INDEX posts_user_id_idx ON posts(user_id);
CREATE INDEX posts_created_at_idx ON posts(created_at DESC);
CREATE INDEX comments_post_id_idx ON comments(post_id);
-- Enable RLS on all tables
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
ALTER TABLE comments ENABLE ROW LEVEL SECURITY;
-- Profiles: Users can read all, update only their own
CREATE POLICY "Public profiles are viewable by everyone"
ON profiles FOR SELECT
USING (true);
CREATE POLICY "Users can insert their own profile"
ON profiles FOR INSERT
WITH CHECK (auth.uid() = id);
CREATE POLICY "Users can update their own profile"
ON profiles FOR UPDATE
USING (auth.uid() = id);
-- Posts: Public can read published, users can manage their own
CREATE POLICY "Published posts are viewable by everyone"
ON posts FOR SELECT
USING (published = true OR auth.uid() = user_id);
CREATE POLICY "Users can create their own posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update their own posts"
ON posts FOR UPDATE
USING (auth.uid() = user_id);
CREATE POLICY "Users can delete their own posts"
ON posts FOR DELETE
USING (auth.uid() = user_id);
-- Comments: Public can read, users can manage their own
CREATE POLICY "Comments are viewable by everyone"
ON comments FOR SELECT
USING (true);
CREATE POLICY "Authenticated users can create comments"
ON comments FOR INSERT
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update their own comments"
ON comments FOR UPDATE
USING (auth.uid() = user_id);
CREATE POLICY "Users can delete their own comments"
ON comments FOR DELETE
USING (auth.uid() = user_id);
-- Automatic updated_at timestamp
CREATE OR REPLACE FUNCTION handle_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Apply to tables
CREATE TRIGGER handle_profiles_updated_at
BEFORE UPDATE ON profiles
FOR EACH ROW
EXECUTE FUNCTION handle_updated_at();
CREATE TRIGGER handle_posts_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION handle_updated_at();
-- Automatic profile creation on signup
CREATE OR REPLACE FUNCTION handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.profiles (id, username, full_name, avatar_url)
VALUES (
NEW.id,
NEW.raw_user_meta_data->>'username',
NEW.raw_user_meta_data->>'full_name',
NEW.raw_user_meta_data->>'avatar_url'
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION handle_new_user();
// Insert
const { data, error } = await supabase
.from('posts')
.insert({
title: 'My First Post',
content: 'Hello World!',
user_id: userId
})
.select()
.single()
// Select with filters
const { data: posts } = await supabase
.from('posts')
.select('*, profiles(*), comments(*)')
.eq('published', true)
.order('created_at', { ascending: false })
.limit(10)
// Update
const { data, error } = await supabase
.from('posts')
.update({ published: true })
.eq('id', postId)
.select()
// Delete
const { error } = await supabase.from('posts').delete().eq('id', postId)
// Count
const { count } = await supabase.from('posts').select('*', { count: 'exact', head: true })
// Full-text search
const { data } = await supabase.from('posts').select('*').textSearch('content', 'supabase', {
type: 'websearch',
config: 'english'
})
Goal : Manage file uploads with access control
-- Create storage bucket
INSERT INTO storage.buckets (id, name, public)
VALUES ('avatars', 'avatars', true);
INSERT INTO storage.buckets (id, name, public)
VALUES ('private-docs', 'private-docs', false);
-- Avatars: Anyone can read, users can upload their own
CREATE POLICY "Avatar images are publicly accessible"
ON storage.objects FOR SELECT
USING (bucket_id = 'avatars');
CREATE POLICY "Users can upload their own avatar"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'avatars' AND
auth.uid()::text = (storage.foldername(name))[1]
);
CREATE POLICY "Users can update their own avatar"
ON storage.objects FOR UPDATE
USING (
bucket_id = 'avatars' AND
auth.uid()::text = (storage.foldername(name))[1]
);
-- Private docs: Only owner can access
CREATE POLICY "Users can access their own documents"
ON storage.objects FOR SELECT
USING (
bucket_id = 'private-docs' AND
auth.uid()::text = (storage.foldername(name))[1]
);
CREATE POLICY "Users can upload their own documents"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'private-docs' AND
auth.uid()::text = (storage.foldername(name))[1]
);
// Upload file
async function uploadFile(bucket: string, path: string, file: File) {
const { data, error } = await supabase.storage.from(bucket).upload(path, file, {
cacheControl: '3600',
upsert: false
})
if (error) throw error
return data
}
// Download file
async function downloadFile(bucket: string, path: string) {
const { data, error } = await supabase.storage.from(bucket).download(path)
if (error) throw error
return data
}
// Get public URL
function getPublicUrl(bucket: string, path: string) {
const { data } = supabase.storage.from(bucket).getPublicUrl(path)
return data.publicUrl
}
// Get signed URL (private files)
async function getSignedUrl(bucket: string, path: string, expiresIn: number = 3600) {
const { data, error } = await supabase.storage.from(bucket).createSignedUrl(path, expiresIn)
if (error) throw error
return data.signedUrl
}
// Delete file
async function deleteFile(bucket: string, path: string) {
const { error } = await supabase.storage.from(bucket).remove([path])
if (error) throw error
}
// List files
async function listFiles(bucket: string, folder: string = '') {
const { data, error } = await supabase.storage.from(bucket).list(folder, {
limit: 100,
offset: 0,
sortBy: { column: 'created_at', order: 'desc' }
})
if (error) throw error
return data
}
// Get resized image URL
function getTransformedImage(
bucket: string,
path: string,
options: {
width?: number
height?: number
quality?: number
}
) {
const { data } = supabase.storage.from(bucket).getPublicUrl(path, {
transform: {
width: options.width,
height: options.height,
quality: options.quality || 80
}
})
return data.publicUrl
}
Goal : Build live, collaborative features
// Subscribe to INSERT events
const subscription = supabase
.channel('posts-channel')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'posts'
},
payload => {
console.log('New post created:', payload.new)
// Update UI with new post
}
)
.subscribe()
// Subscribe to all events on a table
supabase
.channel('comments-channel')
.on(
'postgres_changes',
{
event: '*', // INSERT, UPDATE, DELETE
schema: 'public',
table: 'comments'
},
payload => {
console.log('Change detected:', payload)
}
)
.subscribe()
// Subscribe with filters
supabase
.channel('my-posts-channel')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'posts',
filter: `user_id=eq.${userId}`
},
payload => {
console.log('My post changed:', payload)
}
)
.subscribe()
// Unsubscribe
subscription.unsubscribe()
// Join a room and broadcast messages
const channel = supabase.channel('room-1')
// Send broadcast message
channel.send({
type: 'broadcast',
event: 'message',
payload: { text: 'Hello!', user: 'John' }
})
// Receive broadcast messages
channel
.on('broadcast', { event: 'message' }, payload => {
console.log('Message received:', payload)
})
.subscribe()
// Track online users
const channel = supabase.channel('online-users', {
config: {
presence: {
key: userId
}
}
})
// Track current user presence
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
console.log('Online users:', state)
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('User joined:', key, newPresences)
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('User left:', key, leftPresences)
})
.subscribe(async status => {
if (status === 'SUBSCRIBED') {
await channel.track({
user: userId,
online_at: new Date().toISOString()
})
}
})
// Update presence
await channel.track({ status: 'typing' })
// Stop tracking
await channel.untrack()
Goal : Serverless functions and advanced capabilities
// supabase/functions/hello/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
serve(async req => {
try {
// Create Supabase client with service role
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
// Get user from auth header
const authHeader = req.headers.get('Authorization')!
const token = authHeader.replace('Bearer ', '')
const {
data: { user }
} = await supabaseClient.auth.getUser(token)
// Your logic here
const { data, error } = await supabaseClient.from('posts').select('*').eq('user_id', user?.id)
if (error) throw error
return new Response(JSON.stringify({ data }), {
headers: { 'Content-Type': 'application/json' }
})
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
})
}
})
Deploy Edge Function:
# Deploy function
supabase functions deploy hello
# Invoke from client
const { data, error } = await supabase.functions.invoke('hello', {
body: { name: 'World' },
})
-- Send webhook on new post
CREATE OR REPLACE FUNCTION send_post_webhook()
RETURNS TRIGGER AS $$
BEGIN
PERFORM
net.http_post(
url := 'https://your-api.com/webhook',
headers := '{"Content-Type": "application/json"}'::jsonb,
body := jsonb_build_object(
'event', 'new_post',
'post_id', NEW.id,
'title', NEW.title
)
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER on_post_created
AFTER INSERT ON posts
FOR EACH ROW
EXECUTE FUNCTION send_post_webhook();
-- Enable pgvector extension
CREATE EXTENSION IF NOT EXISTS vector;
-- Add embedding column
ALTER TABLE posts ADD COLUMN embedding vector(1536);
-- Create vector index
CREATE INDEX ON posts USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- Search by similarity
SELECT *
FROM posts
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 10;
// Generate and store embeddings
import OpenAI from 'openai'
const openai = new OpenAI()
async function addEmbedding(postId: string, text: string) {
// Generate embedding
const response = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: text
})
const embedding = response.data[0].embedding
// Store in Supabase
await supabase.from('posts').update({ embedding }).eq('id', postId)
}
// Semantic search
async function semanticSearch(query: string) {
// Generate query embedding
const response = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: query
})
const queryEmbedding = response.data[0].embedding
// Search
const { data } = await supabase.rpc('match_posts', {
query_embedding: queryEmbedding,
match_threshold: 0.7,
match_count: 10
})
return data
}
# Initialize Supabase locally
supabase init
supabase start
# Create new migration
supabase migration new add_posts_table
# Edit migration file in supabase/migrations/
# Apply migrations
supabase db reset
# Generate TypeScript types
supabase gen types typescript --local > types/supabase.ts
# Link to remote project
supabase link --project-ref your-project-ref
# Push migrations to production
supabase db push
# Or apply specific migration
supabase db remote commit
// ❌ WRONG - Never on client side
const supabase = createClient(url, SERVICE_ROLE_KEY)
// ✅ CORRECT - Use anon key on client
const supabase = createClient(url, ANON_KEY)
// ✅ Service role only on server
// app/api/admin/route.ts
const supabase = createClient(url, SERVICE_ROLE_KEY)
-- ❌ WRONG - Table without RLS
CREATE TABLE sensitive_data (
id UUID PRIMARY KEY,
secret TEXT
);
-- ✅ CORRECT - RLS enabled
CREATE TABLE sensitive_data (
id UUID PRIMARY KEY,
user_id UUID REFERENCES auth.users(id),
secret TEXT
);
ALTER TABLE sensitive_data ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can only access their data"
ON sensitive_data FOR ALL
USING (auth.uid() = user_id);
// ❌ WRONG - No validation
await supabase.from('posts').insert({ title: userInput })
// ✅ CORRECT - Validate first
import { z } from 'zod'
const schema = z.object({
title: z.string().min(1).max(100),
content: z.string().min(10).max(10000)
})
const validated = schema.parse(userInput)
await supabase.from('posts').insert(validated)
// Use Edge Functions for rate limiting
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, key)
// Check rate limit
const { count } = await supabase
.from('api_calls')
.select('*', { count: 'exact', head: true })
.eq('user_id', userId)
.gte('created_at', oneHourAgo)
if (count >= 100) {
return new Response('Rate limit exceeded', { status: 429 })
}
-- Add indexes on frequently queried columns
CREATE INDEX posts_user_id_idx ON posts(user_id);
CREATE INDEX posts_created_at_idx ON posts(created_at DESC);
-- Composite indexes for multi-column queries
CREATE INDEX posts_user_published_idx ON posts(user_id, published);
-- Full-text search indexes
CREATE INDEX posts_content_fts_idx ON posts USING gin(to_tsvector('english', content));
// ❌ WRONG - Select everything
const { data } = await supabase.from('posts').select('*')
// ✅ CORRECT - Select specific columns
const { data } = await supabase.from('posts').select('id, title, created_at')
// Offset pagination
const { data } = await supabase.from('posts').select('*').range(0, 9)
// Cursor pagination (better for large datasets)
const { data } = await supabase
.from('posts')
.select('*')
.gt('created_at', lastCreatedAt)
.order('created_at', { ascending: false })
.limit(10)
// Use React Query or SWR
import { useQuery } from '@tanstack/react-query'
function usePosts() {
return useQuery({
queryKey: ['posts'],
queryFn: async () => {
const { data } = await supabase.from('posts').select('*')
return data
},
staleTime: 5 * 60 * 1000 // 5 minutes
})
}
// Mock Supabase client
import { createClient } from '@supabase/supabase-js'
jest.mock('@supabase/supabase-js', () => ({
createClient: jest.fn(() => ({
from: jest.fn(() => ({
select: jest.fn(() => ({
eq: jest.fn(() => ({
single: jest.fn(() =>
Promise.resolve({
data: { id: '1', title: 'Test' },
error: null
})
)
}))
}))
}))
}))
}))
test('fetches post by id', async () => {
const post = await getPostById('1')
expect(post.title).toBe('Test')
})
// Use test database
const testSupabase = createClient(
process.env.TEST_SUPABASE_URL!,
process.env.TEST_SUPABASE_ANON_KEY!
)
beforeEach(async () => {
// Clean database
await testSupabase.from('posts').delete().neq('id', '00000000-0000-0000-0000-000000000000')
})
test('creates post', async () => {
const { data, error } = await testSupabase
.from('posts')
.insert({ title: 'Test Post', content: 'Content' })
.select()
.single()
expect(error).toBeNull()
expect(data.title).toBe('Test Post')
})
import { useMutation, useQueryClient } from '@tanstack/react-query'
function useCreatePost() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async newPost => {
const { data } = await supabase.from('posts').insert(newPost).select().single()
return data
},
onMutate: async newPost => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['posts'] })
// Snapshot previous value
const previousPosts = queryClient.getQueryData(['posts'])
// Optimistically update
queryClient.setQueryData(['posts'], old => [...old, newPost])
return { previousPosts }
},
onError: (err, newPost, context) => {
// Rollback on error
queryClient.setQueryData(['posts'], context.previousPosts)
},
onSettled: () => {
// Refetch after mutation
queryClient.invalidateQueries({ queryKey: ['posts'] })
}
})
}
-- Add deleted_at column
ALTER TABLE posts ADD COLUMN deleted_at TIMESTAMP WITH TIME ZONE;
-- Update RLS to exclude deleted
CREATE POLICY "Only show non-deleted posts"
ON posts FOR SELECT
USING (deleted_at IS NULL);
-- Soft delete function
CREATE OR REPLACE FUNCTION soft_delete_post(post_id UUID)
RETURNS void AS $$
BEGIN
UPDATE posts
SET deleted_at = NOW()
WHERE id = post_id AND user_id = auth.uid();
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Create audit log table
CREATE TABLE audit_logs (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
table_name TEXT NOT NULL,
record_id UUID NOT NULL,
action TEXT NOT NULL,
old_data JSONB,
new_data JSONB,
user_id UUID REFERENCES auth.users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Audit trigger function
CREATE OR REPLACE FUNCTION audit_trigger()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit_logs (table_name, record_id, action, old_data, new_data, user_id)
VALUES (
TG_TABLE_NAME,
COALESCE(NEW.id, OLD.id),
TG_OP,
CASE WHEN TG_OP = 'DELETE' THEN row_to_json(OLD) ELSE NULL END,
CASE WHEN TG_OP IN ('INSERT', 'UPDATE') THEN row_to_json(NEW) ELSE NULL END,
auth.uid()
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Apply to tables
CREATE TRIGGER audit_posts
AFTER INSERT OR UPDATE OR DELETE ON posts
FOR EACH ROW
EXECUTE FUNCTION audit_trigger();
Symptom : Can't query data even with correct policies
Solution :
-- Check if RLS is enabled
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';
-- Check policies
SELECT * FROM pg_policies WHERE tablename = 'your_table';
-- Test policy as user
SET LOCAL ROLE authenticated;
SET LOCAL request.jwt.claims.sub TO 'user-uuid';
SELECT * FROM your_table;
Solution :
// Auto-refresh tokens
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'TOKEN_REFRESHED') {
// Token refreshed automatically
}
})
// Manual refresh
const { data, error } = await supabase.auth.refreshSession()
Solution :
// Check file size (default: 50MB)
// Check MIME type restrictions
// Verify bucket exists
const { data: buckets } = await supabase.storage.listBuckets()
// Check RLS policies on storage.objects
Solution :
-- Enable replication for table
ALTER PUBLICATION supabase_realtime ADD TABLE posts;
-- Check if table is in publication
SELECT * FROM pg_publication_tables WHERE pubname = 'supabase_realtime';
# Local development
supabase init
supabase start
supabase stop
supabase status
# Migrations
supabase migration new migration_name
supabase db reset
supabase db push
# Type generation
supabase gen types typescript --local > types/supabase.ts
# Edge Functions
supabase functions new function_name
supabase functions serve
supabase functions deploy function_name
# Link to remote
supabase link --project-ref your-ref
// CRUD operations
const { data } = await supabase.from('table').select('*')
const { data } = await supabase.from('table').insert({ ... })
const { data } = await supabase.from('table').update({ ... }).eq('id', id)
const { data } = await supabase.from('table').delete().eq('id', id)
// Filters
.eq('column', value)
.neq('column', value)
.gt('column', value)
.gte('column', value)
.lt('column', value)
.lte('column', value)
.like('column', '%pattern%')
.ilike('column', '%pattern%')
.is('column', null)
.in('column', [1, 2, 3])
.contains('array_column', ['value'])
.textSearch('column', 'query')
// Modifiers
.order('column', { ascending: false })
.limit(10)
.range(0, 9)
.single()
.maybeSingle()
See Phase 1 for client/server setup patterns.
// src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_ANON_KEY
)
// src/hooks/useAuth.ts
export function useAuth() {
const [user, setUser] = useState(null)
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setUser(session?.user ?? null)
})
const {
data: { subscription }
} = supabase.auth.onAuthStateChange((_event, session) => {
setUser(session?.user ?? null)
})
return () => subscription.unsubscribe()
}, [])
return { user }
}
This skill covers the complete Supabase development lifecycle:
Key Takeaway : Supabase provides a complete backend platform with PostgreSQL at its core. Row Level Security is your primary security layer—design RLS policies carefully to ensure data is secure by default.
For complex scenarios, combine this skill with:
Weekly Installs
0
Repository
GitHub Stars
18
First Seen
Jan 1, 1970
Security Audits
Azure RBAC 权限管理工具:查找最小角色、创建自定义角色与自动化分配
117,000 周安装