关于 Supabase Developer
面向使用 Supabase 构建全栈应用的开发团队,指导数据库设计与安全、身份验证、存储、实时功能、Edge Functions 和迁移管理。
主要功能
- 01设计 PostgreSQL 架构与 RLS 策略
- 02实现身份验证和会话管理
- 03管理文件存储与访问控制
- 04构建实时订阅和广播
- 05创建并部署 Edge Functions
- 06管理数据库迁移
适用场景
- 01为应用添加注册与登录
- 02使用 Row Level Security 保护用户数据
- 03构建实时协作功能
- 04处理文件上传和无服务器端点
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ daffy0208/ ai-dev-standards --skill supabase-developer安装后在 Claude Code 中输入/supabase-developer确认已加载。
附带文件manifest.yamlREADME.md
详细介绍
Supabase 开发者
使用 Supabase 构建可用于生产环境的全栈应用。
Supabase 是 Firebase 的开源替代方案,提供 PostgreSQL 数据库、身份验证、存储、实时订阅和边缘函数。本指南将引导你使用 Supabase 的全套功能构建安全、可扩展的应用。
何时使用此技能
- 身份验证:通过电子邮件、OAuth、魔法链接或手机身份验证实现用户注册和登录
- 数据库:设计带有行级安全性(RLS)的 PostgreSQL 架构
- 存储:管理文件上传、下载和访问控制
- 实时功能:通过订阅和广播构建实时功能
- 边缘函数:在边缘运行无服务器 TypeScript 函数
- 迁移:管理数据库架构变更
- 集成:连接 Next.js、React、Vue 或其他框架
Supabase 核心概念
1. 数据库(PostgreSQL)
Supabase 使用带扩展的 PostgreSQL:
- PostgREST:根据架构自动生成 REST API
- pg_graphql:可选的 GraphQL 支持
- 扩展:用于嵌入的 pgvector、用于计划任务的 pg_cron
2. 身份验证
内置身份验证,支持多种提供商:
- 电子邮件/密码,并进行确认
- 魔法链接(无密码)
- OAuth(Google、GitHub 等)
- 电话/SMS 身份验证
- SAML SSO(企业版)
3. 行级安全性(RLS)
在数据库层强制执行数据访问控制的 PostgreSQL 策略:
- 用户只能读取自己的数据
- 管理员可以读取所有数据
- 公开读取,经过身份验证后写入
4. 存储
兼容 S3 且带有 RLS 的对象存储:
- 公共和私有存储桶
- 文件大小和类型限制
- 即时图像转换
- CDN 集成
5. 实时功能
基于 WebSocket 的订阅:
- 数据库变更(INSERT、UPDATE、DELETE)
- 向频道广播消息
- 在线状态跟踪(谁在线)
6. 边缘函数
基于 Deno 的无服务器函数:
- 在全球边缘节点部署
- TypeScript/JavaScript 运行时
- 后台任务和 Webhook
- 自定义 API 端点
Supabase 实施的 6 个阶段
阶段 1:项目设置与配置
目标:初始化 Supabase 项目并将其连接到你的应用
1.1 创建 Supabase 项目
# 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
1.2 安装客户端库
# 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
1.3 环境配置
# .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!
1.4 初始化客户端
// 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 (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
}
}
}
)
}
阶段 2:身份验证实现
目标:通过会话管理保护用户身份验证
2.1 身份验证策略
电子邮件/密码身份验证:
// 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 身份验证:
// 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
魔法链接(无密码):
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
}
2.2 会话管理
// 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
}
})
2.3 受保护的路由(Next.js)
// 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*']
}
阶段 3:数据库设计与 RLS
目标:设计带有行级安全性的安全数据库架构
3.1 架构设计
-- 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);
3.2 行级安全性(RLS)策略
-- 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);
3.3 数据库函数
-- 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();
3.4 使用 TypeScript 查询
// 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'
})
阶段 4:存储实现
目标:通过访问控制管理文件上传
4.1 存储桶配置
-- 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);
4.2 存储 RLS 策略
-- 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]
);
4.3 文件上传/下载
// 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
}
4.4 图像转换
// 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
}
阶段 5:实时功能
目标:构建实时协作功能
5.1 数据库变更订阅
// 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()
5.2 广播消息
// 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()
5.3 在线状态跟踪
// 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()
阶段 6:边缘函数与高级功能
目标:实现无服务器函数和高级功能
6.1 边缘函数
// supabase/functions/hello/index.ts
import { serve } from 'https://deno.land/[email protected]/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 function
supabase functions deploy hello
# Invoke from client
const { data, error } = await supabase.functions.invoke('hello', {
body: { name: 'World' },
})
6.2 数据库 Webhook
-- 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();
6.3 向量搜索(pgvector)
-- 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
安全最佳实践
1. 切勿暴露服务角色密钥
// ❌ 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)
2. 始终使用 RLS
-- ❌ 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);
3. 验证输入
// ❌ 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)
4. 速率限制
// 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 })
}
性能优化
1. 使用索引
-- 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));
2. 只选择所需内容
// ❌ 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')
3. 使用分页
// 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)
4. 缓存静态数据
// 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')
})
常见模式
1. 乐观 UI 更新
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'] })
}
})
}
2. 软删除
-- 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;
3. 审计日志
-- 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();
故障排除
问题:RLS 策略不起作用
症状:即使策略正确,也无法查询数据
解决方案:
-- 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;
问题:出现 “JWT expired” 错误
解决方案:
// Auto-refresh tokens
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'TOKEN_REFRESHED') {
// Token refreshed automatically
}
})
// Manual refresh
const { data, error } = await supabase.auth.refreshSession()
问题:存储上传失败
解决方案:
// 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
问题:实时功能不起作用
解决方案:
-- 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()
集成示例
Next.js 13+ App Router
客户端/服务端配置模式请参见阶段 1。
React + Vite
// 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 }
}
总结
本技能涵盖完整的 Supabase 开发生命周期:
- ✅ 设置:项目初始化和客户端配置
- ✅ 身份验证:多种身份验证策略与会话管理
- ✅ 数据库:采用行级安全的 PostgreSQL 架构设计
- ✅ 存储:具备访问控制的文件管理
- ✅ 实时功能:实时订阅、广播和在线状态
- ✅ 边缘函数:无服务器 TypeScript 函数
- ✅ 安全:生产应用的最佳实践
- ✅ 性能:面向规模扩展的优化策略
- ✅ 测试:单元测试和集成测试模式
- ✅ 迁移:数据库变更管理
关键要点:Supabase 提供了一个以 PostgreSQL 为核心的完整后端平台。行级安全是首要的安全防护层——请谨慎设计 RLS 策略,确保数据默认安全。
对于复杂场景,可将此技能与以下工具结合使用:
- api-designer 用于自定义 API 端点
- security-engineer 用于高级安全审查
- performance-optimizer 用于扩展大型应用
- data-engineer 用于复杂数据管道