supabase
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSupabase Backend Platform Skill
Supabase后端平台技能指南
progressive_disclosure: entry_point: summary: "Open-source Firebase alternative with Postgres, authentication, storage, and realtime" when_to_use: - "When building full-stack applications" - "When auth, database, and storage are required" - "When realtime subscriptions are needed" - "When using Next.js, React, or Vue" quick_start: - "Create project on Supabase console" - "npm install @supabase/supabase-js" - "Initialize client with URL and anon key" - "Use auth, database, storage, realtime" token_estimate: entry: 80-95 full: 5000-6000
progressive_disclosure: entry_point: summary: "开源Firebase替代方案,内置Postgres、身份认证、存储及实时功能" when_to_use: - "构建全栈应用时" - "需要身份认证、数据库和存储服务时" - "需要实时订阅功能时" - "搭配Next.js、React或Vue使用时" quick_start: - "在Supabase控制台创建项目" - "执行npm install @supabase/supabase-js安装客户端" - "使用URL和匿名密钥初始化客户端" - "使用身份认证、数据库、存储及实时功能" token_estimate: entry: 80-95 full: 5000-6000
Supabase Fundamentals
Supabase基础
What is Supabase?
什么是Supabase?
Open-source Firebase alternative built on:
- Postgres Database: Full SQL database with PostgREST API
- Authentication: Built-in auth with multiple providers
- Storage: File storage with image transformations
- Realtime: WebSocket subscriptions to database changes
- Edge Functions: Serverless functions on Deno runtime
- Row Level Security: Postgres RLS for data access control
基于以下技术构建的开源Firebase替代方案:
- Postgres数据库: 完整SQL数据库,搭配PostgREST API
- 身份认证: 内置多提供商认证功能
- 存储服务: 支持图片转换的文件存储
- 实时功能: 数据库变更的WebSocket订阅
- Edge Functions: 基于Deno运行时的无服务器函数
- 行级安全(RLS): Postgres原生RLS实现数据访问控制
Project Setup
项目搭建
bash
undefinedbash
undefinedInstall Supabase client
安装Supabase客户端
npm install @supabase/supabase-js
npm install @supabase/supabase-js
Install CLI for local development
安装本地开发CLI
npm install -D supabase
npm install -D supabase
TypeScript types
TypeScript类型定义
npm install -D @supabase/supabase-js
undefinednpm install -D @supabase/supabase-js
undefinedClient Initialization
客户端初始化
typescript
// 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)
// With TypeScript types
import { Database } from '@/types/supabase'
export const supabase = createClient<Database>(
supabaseUrl,
supabaseAnonKey
)typescript
// 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)
// 搭配TypeScript类型
import { Database } from '@/types/supabase'
export const supabase = createClient<Database>(
supabaseUrl,
supabaseAnonKey
)Database Operations
数据库操作
PostgREST API Basics
PostgREST API基础
Supabase auto-generates REST API from Postgres schema:
typescript
// SELECT * FROM posts
const { data, error } = await supabase
.from('posts')
.select('*')
// SELECT with filters
const { data } = await supabase
.from('posts')
.select('*')
.eq('status', 'published')
.order('created_at', { ascending: false })
.limit(10)
// SELECT with joins
const { data } = await supabase
.from('posts')
.select(`
*,
author:profiles(name, avatar),
comments(count)
`)
// INSERT
const { data, error } = await supabase
.from('posts')
.insert({ title: 'Hello', content: 'World' })
.select()
.single()
// UPDATE
const { data } = await supabase
.from('posts')
.update({ status: 'published' })
.eq('id', postId)
.select()
// DELETE
const { error } = await supabase
.from('posts')
.delete()
.eq('id', postId)
// UPSERT
const { data } = await supabase
.from('posts')
.upsert({ id: 1, title: 'Updated' })
.select()Supabase从Postgres schema自动生成REST API:
typescript
// 查询所有posts
const { data, error } = await supabase
.from('posts')
.select('*')
// 带筛选条件的查询
const { data } = await supabase
.from('posts')
.select('*')
.eq('status', 'published')
.order('created_at', { ascending: false })
.limit(10)
// 关联查询
const { data } = await supabase
.from('posts')
.select(`
*,
author:profiles(name, avatar),
comments(count)
`)
// 插入数据
const { data, error } = await supabase
.from('posts')
.insert({ title: 'Hello', content: 'World' })
.select()
.single()
// 更新数据
const { data } = await supabase
.from('posts')
.update({ status: 'published' })
.eq('id', postId)
.select()
// 删除数据
const { error } = await supabase
.from('posts')
.delete()
.eq('id', postId)
// 插入或更新数据
const { data } = await supabase
.from('posts')
.upsert({ id: 1, title: 'Updated' })
.select()Advanced Queries
高级查询
typescript
// Full-text search
const { data } = await supabase
.from('posts')
.select('*')
.textSearch('title', 'postgresql', {
type: 'websearch',
config: 'english'
})
// Range queries
const { data } = await supabase
.from('posts')
.select('*')
.gte('created_at', '2024-01-01')
.lte('created_at', '2024-12-31')
// Array contains
const { data } = await supabase
.from('posts')
.select('*')
.contains('tags', ['postgres', 'supabase'])
// JSON operations
const { data } = await supabase
.from('users')
.select('*')
.eq('metadata->theme', 'dark')
// Count without data
const { count } = await supabase
.from('posts')
.select('*', { count: 'exact', head: true })
// Pagination
const pageSize = 10
const page = 2
const { data } = await supabase
.from('posts')
.select('*')
.range(page * pageSize, (page + 1) * pageSize - 1)typescript
// 全文搜索
const { data } = await supabase
.from('posts')
.select('*')
.textSearch('title', 'postgresql', {
type: 'websearch',
config: 'english'
})
// 范围查询
const { data } = await supabase
.from('posts')
.select('*')
.gte('created_at', '2024-01-01')
.lte('created_at', '2024-12-31')
// 数组包含查询
const { data } = await supabase
.from('posts')
.select('*')
.contains('tags', ['postgres', 'supabase'])
// JSON操作
const { data } = await supabase
.from('users')
.select('*')
.eq('metadata->theme', 'dark')
// 仅统计数量不返回数据
const { count } = await supabase
.from('posts')
.select('*', { count: 'exact', head: true })
// 分页查询
const pageSize = 10
const page = 2
const { data } = await supabase
.from('posts')
.select('*')
.range(page * pageSize, (page + 1) * pageSize - 1)Database Functions and RPC
数据库函数与RPC
typescript
// Call Postgres function
const { data, error } = await supabase
.rpc('get_trending_posts', {
days: 7,
min_score: 10
})
// Example function in SQL
/*
CREATE OR REPLACE FUNCTION get_trending_posts(
days INTEGER,
min_score INTEGER
)
RETURNS TABLE (
id UUID,
title TEXT,
score INTEGER
) AS $$
BEGIN
RETURN QUERY
SELECT p.id, p.title, COUNT(v.id)::INTEGER as score
FROM posts p
LEFT JOIN votes v ON p.id = v.post_id
WHERE p.created_at > NOW() - INTERVAL '1 day' * days
GROUP BY p.id
HAVING COUNT(v.id) >= min_score
ORDER BY score DESC;
END;
$$ LANGUAGE plpgsql;
*/typescript
// 调用Postgres函数
const { data, error } = await supabase
.rpc('get_trending_posts', {
days: 7,
min_score: 10
})
// SQL示例函数
/*
CREATE OR REPLACE FUNCTION get_trending_posts(
days INTEGER,
min_score INTEGER
)
RETURNS TABLE (
id UUID,
title TEXT,
score INTEGER
) AS $$
BEGIN
RETURN QUERY
SELECT p.id, p.title, COUNT(v.id)::INTEGER as score
FROM posts p
LEFT JOIN votes v ON p.id = v.post_id
WHERE p.created_at > NOW() - INTERVAL '1 day' * days
GROUP BY p.id
HAVING COUNT(v.id) >= min_score
ORDER BY score DESC;
END;
$$ LANGUAGE plpgsql;
*/Authentication
身份认证
Email/Password Authentication
邮箱/密码认证
typescript
// Sign up
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'secure-password',
options: {
data: {
name: 'John Doe',
avatar_url: 'https://...'
}
}
})
// Sign in
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'secure-password'
})
// Sign out
const { error } = await supabase.auth.signOut()
// Get current user
const { data: { user } } = await supabase.auth.getUser()
// Get session
const { data: { session } } = await supabase.auth.getSession()typescript
// 注册账号
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'secure-password',
options: {
data: {
name: 'John Doe',
avatar_url: 'https://...'
}
}
})
// 登录
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'secure-password'
})
// 登出
const { error } = await supabase.auth.signOut()
// 获取当前用户
const { data: { user } } = await supabase.auth.getUser()
// 获取会话
const { data: { session } } = await supabase.auth.getSession()OAuth Providers
OAuth认证提供商
typescript
// Sign in with OAuth
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: 'http://localhost:3000/auth/callback',
scopes: 'repo user'
}
})
// Available providers
// github, google, gitlab, bitbucket, azure, discord, facebook,
// linkedin, notion, slack, spotify, twitch, twitter, appletypescript
// 使用OAuth登录
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: 'http://localhost:3000/auth/callback',
scopes: 'repo user'
}
})
// 支持的提供商
// github, google, gitlab, bitbucket, azure, discord, facebook,
// linkedin, notion, slack, spotify, twitch, twitter, appleMagic Links
魔法链接
typescript
// Send magic link
const { data, error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
options: {
emailRedirectTo: 'http://localhost:3000/auth/callback'
}
})
// Verify OTP
const { data, error } = await supabase.auth.verifyOtp({
email: 'user@example.com',
token: '123456',
type: 'email'
})typescript
// 发送魔法链接
const { data, error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
options: {
emailRedirectTo: 'http://localhost:3000/auth/callback'
}
})
// 验证OTP
const { data, error } = await supabase.auth.verifyOtp({
email: 'user@example.com',
token: '123456',
type: 'email'
})Phone Authentication
手机号认证
typescript
// Sign in with phone
const { data, error } = await supabase.auth.signInWithOtp({
phone: '+1234567890'
})
// Verify phone OTP
const { data, error } = await supabase.auth.verifyOtp({
phone: '+1234567890',
token: '123456',
type: 'sms'
})typescript
// 使用手机号登录
const { data, error } = await supabase.auth.signInWithOtp({
phone: '+1234567890'
})
// 验证手机号OTP
const { data, error } = await supabase.auth.verifyOtp({
phone: '+1234567890',
token: '123456',
type: 'sms'
})Auth State Management
认证状态管理
typescript
// Listen to auth changes
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_IN') {
console.log('User signed in:', session?.user)
}
if (event === 'SIGNED_OUT') {
console.log('User signed out')
}
if (event === 'TOKEN_REFRESHED') {
console.log('Token refreshed')
}
})
// Update user metadata
const { data, error } = await supabase.auth.updateUser({
data: { theme: 'dark' }
})
// Change password
const { data, error } = await supabase.auth.updateUser({
password: 'new-password'
})typescript
// 监听认证状态变更
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_IN') {
console.log('用户已登录:', session?.user)
}
if (event === 'SIGNED_OUT') {
console.log('用户已登出')
}
if (event === 'TOKEN_REFRESHED') {
console.log('令牌已刷新')
}
})
// 更新用户元数据
const { data, error } = await supabase.auth.updateUser({
data: { theme: 'dark' }
})
// 修改密码
const { data, error } = await supabase.auth.updateUser({
password: 'new-password'
})Row Level Security (RLS)
行级安全(RLS)
RLS Fundamentals
RLS基础
Postgres Row Level Security controls data access at the database level:
sql
-- Enable RLS on table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Policy: Users can read all published posts
CREATE POLICY "Public posts are viewable by everyone"
ON posts FOR SELECT
USING (status = 'published');
-- Policy: Users can only update their own posts
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (auth.uid() = author_id);
-- Policy: Authenticated users can insert posts
CREATE POLICY "Authenticated users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = author_id);
-- Policy: Users can delete their own posts
CREATE POLICY "Users can delete own posts"
ON posts FOR DELETE
USING (auth.uid() = author_id);Postgres行级安全在数据库层面控制数据访问:
sql
-- 为表启用RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- 策略:所有用户可读取已发布的文章
CREATE POLICY "Public posts are viewable by everyone"
ON posts FOR SELECT
USING (status = 'published');
-- 策略:用户仅能更新自己的文章
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (auth.uid() = author_id);
-- 策略:已认证用户可插入文章
CREATE POLICY "Authenticated users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = author_id);
-- 策略:用户仅能删除自己的文章
CREATE POLICY "Users can delete own posts"
ON posts FOR DELETE
USING (auth.uid() = author_id);Common RLS Patterns
常见RLS模式
sql
-- Public read, authenticated write
CREATE POLICY "Anyone can view posts"
ON posts FOR SELECT
USING (true);
CREATE POLICY "Authenticated users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() IS NOT NULL);
-- Organization-based access
CREATE POLICY "Users can view org data"
ON documents FOR SELECT
USING (
organization_id IN (
SELECT organization_id
FROM memberships
WHERE user_id = auth.uid()
)
);
-- Role-based access
CREATE POLICY "Admins can do anything"
ON posts FOR ALL
USING (
EXISTS (
SELECT 1 FROM user_roles
WHERE user_id = auth.uid()
AND role = 'admin'
)
);
-- Time-based access
CREATE POLICY "View published or scheduled posts"
ON posts FOR SELECT
USING (
status = 'published'
OR (status = 'scheduled' AND publish_at <= NOW())
);sql
-- 公开读取,认证用户可写入
CREATE POLICY "Anyone can view posts"
ON posts FOR SELECT
USING (true);
CREATE POLICY "Authenticated users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() IS NOT NULL);
-- 基于组织的访问控制
CREATE POLICY "Users can view org data"
ON documents FOR SELECT
USING (
organization_id IN (
SELECT organization_id
FROM memberships
WHERE user_id = auth.uid()
)
);
-- 基于角色的访问控制
CREATE POLICY "Admins can do anything"
ON posts FOR ALL
USING (
EXISTS (
SELECT 1 FROM user_roles
WHERE user_id = auth.uid()
AND role = 'admin'
)
);
-- 基于时间的访问控制
CREATE POLICY "View published or scheduled posts"
ON posts FOR SELECT
USING (
status = 'published'
OR (status = 'scheduled' AND publish_at <= NOW())
);RLS Helper Functions
RLS辅助函数
sql
-- Get current user ID
SELECT auth.uid();
-- Get current user JWT
SELECT auth.jwt();
-- Get specific claim
SELECT auth.jwt()->>'email';
-- Custom claims
SELECT auth.jwt()->'app_metadata'->>'role';sql
-- 获取当前用户ID
SELECT auth.uid();
-- 获取当前用户JWT
SELECT auth.jwt();
-- 获取特定声明
SELECT auth.jwt()->>'email';
-- 自定义声明
SELECT auth.jwt()->'app_metadata'->>'role';Storage
存储服务
File Upload
文件上传
typescript
// Upload file
const { data, error } = await supabase.storage
.from('avatars')
.upload('public/avatar1.png', file, {
cacheControl: '3600',
upsert: false
})
// Upload with progress
const { data, error } = await supabase.storage
.from('avatars')
.upload('public/avatar1.png', file, {
onUploadProgress: (progress) => {
console.log(`${progress.loaded}/${progress.total}`)
}
})
// Upload from URL
const { data, error } = await supabase.storage
.from('avatars')
.uploadToSignedUrl('path', token, file)typescript
// 上传文件
const { data, error } = await supabase.storage
.from('avatars')
.upload('public/avatar1.png', file, {
cacheControl: '3600',
upsert: false
})
// 带进度的上传
const { data, error } = await supabase.storage
.from('avatars')
.upload('public/avatar1.png', file, {
onUploadProgress: (progress) => {
console.log(`${progress.loaded}/${progress.total}`)
}
})
// 通过URL上传
const { data, error } = await supabase.storage
.from('avatars')
.uploadToSignedUrl('path', token, file)File Operations
文件操作
typescript
// Download file
const { data, error } = await supabase.storage
.from('avatars')
.download('public/avatar1.png')
// Get public URL
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('public/avatar1.png')
// Create signed URL (temporary access)
const { data, error } = await supabase.storage
.from('avatars')
.createSignedUrl('private/document.pdf', 3600) // 1 hour
// List files
const { data, error } = await supabase.storage
.from('avatars')
.list('public', {
limit: 100,
offset: 0,
sortBy: { column: 'name', order: 'asc' }
})
// Delete file
const { data, error } = await supabase.storage
.from('avatars')
.remove(['public/avatar1.png'])
// Move file
const { data, error } = await supabase.storage
.from('avatars')
.move('public/avatar1.png', 'public/avatar2.png')typescript
// 下载文件
const { data, error } = await supabase.storage
.from('avatars')
.download('public/avatar1.png')
// 获取公开URL
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('public/avatar1.png')
// 创建签名URL(临时访问权限)
const { data, error } = await supabase.storage
.from('avatars')
.createSignedUrl('private/document.pdf', 3600) // 1小时
// 列出文件
const { data, error } = await supabase.storage
.from('avatars')
.list('public', {
limit: 100,
offset: 0,
sortBy: { column: 'name', order: 'asc' }
})
// 删除文件
const { data, error } = await supabase.storage
.from('avatars')
.remove(['public/avatar1.png'])
// 移动文件
const { data, error } = await supabase.storage
.from('avatars')
.move('public/avatar1.png', 'public/avatar2.png')Image Transformations
图片转换
typescript
// Transform image
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('avatar1.png', {
transform: {
width: 200,
height: 200,
resize: 'cover',
quality: 80
}
})
// Available transformations
// width, height, resize (cover|contain|fill),
// quality (1-100), format (origin|jpeg|png|webp)typescript
// 转换图片
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('avatar1.png', {
transform: {
width: 200,
height: 200,
resize: 'cover',
quality: 80
}
})
// 支持的转换参数
// width, height, resize (cover|contain|fill),
// quality (1-100), format (origin|jpeg|png|webp)Storage RLS
存储服务RLS
sql
-- Enable RLS on storage
CREATE POLICY "Avatar images are publicly accessible"
ON storage.objects FOR SELECT
USING (bucket_id = 'avatars' AND (storage.foldername(name))[1] = 'public');
CREATE POLICY "Users can upload their own avatar"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()::text
);
CREATE POLICY "Users can delete their own avatar"
ON storage.objects FOR DELETE
USING (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()::text
);sql
-- 为存储服务启用RLS
CREATE POLICY "Avatar images are publicly accessible"
ON storage.objects FOR SELECT
USING (bucket_id = 'avatars' AND (storage.foldername(name))[1] = 'public');
CREATE POLICY "Users can upload their own avatar"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()::text
);
CREATE POLICY "Users can delete their own avatar"
ON storage.objects FOR DELETE
USING (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()::text
);Realtime Subscriptions
实时订阅
Database Changes
数据库变更订阅
typescript
// Subscribe to inserts
const channel = supabase
.channel('posts-insert')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'posts'
},
(payload) => {
console.log('New post:', payload.new)
}
)
.subscribe()
// Subscribe to updates
const channel = supabase
.channel('posts-update')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'posts',
filter: 'id=eq.1'
},
(payload) => {
console.log('Updated:', payload.new)
console.log('Previous:', payload.old)
}
)
.subscribe()
// Subscribe to all changes
const channel = supabase
.channel('posts-all')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'posts'
},
(payload) => {
console.log('Change:', payload)
}
)
.subscribe()
// Unsubscribe
supabase.removeChannel(channel)typescript
// 订阅插入事件
const channel = supabase
.channel('posts-insert')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'posts'
},
(payload) => {
console.log('新文章:', payload.new)
}
)
.subscribe()
// 订阅更新事件
const channel = supabase
.channel('posts-update')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'posts',
filter: 'id=eq.1'
},
(payload) => {
console.log('已更新:', payload.new)
console.log('之前的数据:', payload.old)
}
)
.subscribe()
// 订阅所有变更事件
const channel = supabase
.channel('posts-all')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'posts'
},
(payload) => {
console.log('变更:', payload)
}
)
.subscribe()
// 取消订阅
supabase.removeChannel(channel)Presence (Track Online Users)
在线状态追踪(Presence)
typescript
// Track presence
const channel = supabase.channel('room-1')
// Track current user
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
console.log('Online users:', state)
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('User joined:', newPresences)
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('User left:', leftPresences)
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({
user_id: userId,
online_at: new Date().toISOString()
})
}
})
// Untrack
await channel.untrack()typescript
// 追踪在线状态
const channel = supabase.channel('room-1')
// 追踪当前用户
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
console.log('在线用户:', state)
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('用户加入:', newPresences)
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('用户离开:', leftPresences)
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({
user_id: userId,
online_at: new Date().toISOString()
})
}
})
// 停止追踪
await channel.untrack()Broadcast (Send Messages)
消息广播(Broadcast)
typescript
// Broadcast messages
const channel = supabase.channel('chat-room')
channel
.on('broadcast', { event: 'message' }, (payload) => {
console.log('Message:', payload)
})
.subscribe()
// Send message
await channel.send({
type: 'broadcast',
event: 'message',
payload: { text: 'Hello', user: 'John' }
})typescript
// 广播消息
const channel = supabase.channel('chat-room')
channel
.on('broadcast', { event: 'message' }, (payload) => {
console.log('消息:', payload)
})
.subscribe()
// 发送消息
await channel.send({
type: 'broadcast',
event: 'message',
payload: { text: 'Hello', user: 'John' }
})Edge Functions
Edge Functions
Edge Function Basics
Edge Functions基础
Serverless functions on Deno runtime:
bash
undefined基于Deno运行时的无服务器函数:
bash
undefinedCreate function
创建函数
supabase functions new my-function
supabase functions new my-function
Serve locally
本地运行
supabase functions serve
supabase functions serve
Deploy
部署函数
supabase functions deploy my-function
undefinedsupabase functions deploy my-function
undefinedEdge Function Example
Edge Functions示例
typescript
// supabase/functions/my-function/index.ts
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
serve(async (req) => {
try {
// Get auth header
const authHeader = req.headers.get('Authorization')!
// Create Supabase client
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{ global: { headers: { Authorization: authHeader } } }
)
// Verify user
const { data: { user }, error } = await supabase.auth.getUser()
if (error) throw error
// Process request
const { data } = await supabase
.from('posts')
.select('*')
.eq('author_id', user.id)
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' } }
)
}
})typescript
// supabase/functions/my-function/index.ts
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
serve(async (req) => {
try {
// 获取认证头
const authHeader = req.headers.get('Authorization')!
// 创建Supabase客户端
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{ global: { headers: { Authorization: authHeader } } }
)
// 验证用户
const { data: { user }, error } = await supabase.auth.getUser()
if (error) throw error
// 处理请求
const { data } = await supabase
.from('posts')
.select('*')
.eq('author_id', user.id)
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' } }
)
}
})Invoke Edge Function
调用Edge Functions
typescript
// From client
const { data, error } = await supabase.functions.invoke('my-function', {
body: { name: 'John' }
})
// With auth
const { data, error } = await supabase.functions.invoke('my-function', {
headers: {
Authorization: `Bearer ${session.access_token}`
},
body: { name: 'John' }
})typescript
// 客户端调用
const { data, error } = await supabase.functions.invoke('my-function', {
body: { name: 'John' }
})
// 带认证信息调用
const { data, error } = await supabase.functions.invoke('my-function', {
headers: {
Authorization: `Bearer ${session.access_token}`
},
body: { name: 'John' }
})Next.js Integration
Next.js集成
App Router Setup
App Router配置
typescript
// lib/supabase/client.ts (Client Component)
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 Component)
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await 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
},
set(name: string, value: string, options: CookieOptions) {
cookieStore.set({ name, value, ...options })
},
remove(name: string, options: CookieOptions) {
cookieStore.set({ name, value: '', ...options })
},
},
}
)
}
// lib/supabase/middleware.ts (Middleware)
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function updateSession(request: NextRequest) {
let response = NextResponse.next({
request: {
headers: request.headers,
},
})
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return request.cookies.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
request.cookies.set({ name, value, ...options })
response = NextResponse.next({
request: { headers: request.headers },
})
response.cookies.set({ name, value, ...options })
},
remove(name: string, options: CookieOptions) {
request.cookies.set({ name, value: '', ...options })
response = NextResponse.next({
request: { headers: request.headers },
})
response.cookies.set({ name, value: '', ...options })
},
},
}
)
await supabase.auth.getUser()
return response
}typescript
// 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 async function createClient() {
const cookieStore = await 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
},
set(name: string, value: string, options: CookieOptions) {
cookieStore.set({ name, value, ...options })
},
remove(name: string, options: CookieOptions) {
cookieStore.set({ name, value: '', ...options })
},
},
}
)
}
// lib/supabase/middleware.ts(中间件)
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function updateSession(request: NextRequest) {
let response = NextResponse.next({
request: {
headers: request.headers,
},
})
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return request.cookies.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
request.cookies.set({ name, value, ...options })
response = NextResponse.next({
request: { headers: request.headers },
})
response.cookies.set({ name, value, ...options })
},
remove(name: string, options: CookieOptions) {
request.cookies.set({ name, value: '', ...options })
response = NextResponse.next({
request: { headers: request.headers },
})
response.cookies.set({ name, value: '', ...options })
},
},
}
)
await supabase.auth.getUser()
return response
}Middleware
中间件
typescript
// middleware.ts
import { updateSession } from '@/lib/supabase/middleware'
export async function middleware(request: NextRequest) {
return await updateSession(request)
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}typescript
// middleware.ts
import { updateSession } from '@/lib/supabase/middleware'
export async function middleware(request: NextRequest) {
return await updateSession(request)
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}Server Component
服务端组件
typescript
// app/posts/page.tsx
import { createClient } from '@/lib/supabase/server'
export default async function PostsPage() {
const supabase = await createClient()
const { data: posts } = await supabase
.from('posts')
.select('*')
.order('created_at', { ascending: false })
return (
<div>
{posts?.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
</div>
)
}typescript
// app/posts/page.tsx
import { createClient } from '@/lib/supabase/server'
export default async function PostsPage() {
const supabase = await createClient()
const { data: posts } = await supabase
.from('posts')
.select('*')
.order('created_at', { ascending: false })
return (
<div>
{posts?.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
</div>
)
}Client Component
客户端组件
typescript
// app/components/new-post.tsx
'use client'
import { useState } from 'react'
import { createClient } from '@/lib/supabase/client'
export function NewPost() {
const [title, setTitle] = useState('')
const supabase = createClient()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return
const { error } = await supabase
.from('posts')
.insert({ title, author_id: user.id })
if (!error) {
setTitle('')
}
}
return (
<form onSubmit={handleSubmit}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Post title"
/>
<button>Create</button>
</form>
)
}typescript
// app/components/new-post.tsx
'use client'
import { useState } from 'react'
import { createClient } from '@/lib/supabase/client'
export function NewPost() {
const [title, setTitle] = useState('')
const supabase = createClient()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return
const { error } = await supabase
.from('posts')
.insert({ title, author_id: user.id })
if (!error) {
setTitle('')
}
}
return (
<form onSubmit={handleSubmit}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="文章标题"
/>
<button>创建</button>
</form>
)
}Server Actions
服务端操作
typescript
// app/actions/posts.ts
'use server'
import { revalidatePath } from 'next/cache'
import { createClient } from '@/lib/supabase/server'
export async function createPost(formData: FormData) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return { error: 'Not authenticated' }
}
const title = formData.get('title') as string
const { error } = await supabase
.from('posts')
.insert({ title, author_id: user.id })
if (error) {
return { error: error.message }
}
revalidatePath('/posts')
return { success: true }
}typescript
// app/actions/posts.ts
'use server'
import { revalidatePath } from 'next/cache'
import { createClient } from '@/lib/supabase/server'
export async function createPost(formData: FormData) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return { error: '未认证' }
}
const title = formData.get('title') as string
const { error } = await supabase
.from('posts')
.insert({ title, author_id: user.id })
if (error) {
return { error: error.message }
}
revalidatePath('/posts')
return { success: true }
}TypeScript Type Generation
TypeScript类型生成
Generate Types from Database
从数据库生成类型
bash
undefinedbash
undefinedInstall CLI
安装CLI
npm install -D supabase
npm install -D supabase
Login
登录
npx supabase login
npx supabase login
Link project
关联项目
npx supabase link --project-ref your-project-ref
npx supabase link --project-ref your-project-ref
Generate types
生成类型
npx supabase gen types typescript --project-id your-project-ref > types/supabase.ts
npx supabase gen types typescript --project-id your-project-ref > types/supabase.ts
Or from local database
从本地数据库生成
npx supabase gen types typescript --local > types/supabase.ts
undefinednpx supabase gen types typescript --local > types/supabase.ts
undefinedUse Generated Types
使用生成的类型
typescript
// types/supabase.ts (generated)
export type Database = {
public: {
Tables: {
posts: {
Row: {
id: string
title: string
content: string | null
author_id: string
created_at: string
}
Insert: {
id?: string
title: string
content?: string | null
author_id: string
created_at?: string
}
Update: {
id?: string
title?: string
content?: string | null
author_id?: string
created_at?: string
}
}
}
}
}
// Usage
import { createClient } from '@supabase/supabase-js'
import { Database } from '@/types/supabase'
const supabase = createClient<Database>(url, key)
// Type-safe queries
const { data } = await supabase
.from('posts') // TypeScript knows this table exists
.select('title, content') // Autocomplete for columns
.single()
// data is typed as { title: string; content: string | null }typescript
// types/supabase.ts(自动生成)
export type Database = {
public: {
Tables: {
posts: {
Row: {
id: string
title: string
content: string | null
author_id: string
created_at: string
}
Insert: {
id?: string
title: string
content?: string | null
author_id: string
created_at?: string
}
Update: {
id?: string
title?: string
content?: string | null
author_id?: string
created_at?: string
}
}
}
}
}
// 使用示例
import { createClient } from '@supabase/supabase-js'
import { Database } from '@/types/supabase'
const supabase = createClient<Database>(url, key)
// 类型安全查询
const { data } = await supabase
.from('posts') // TypeScript识别该表存在
.select('title, content') // 自动补全列名
.single()
// data类型为 { title: string; content: string | null }Supabase CLI and Local Development
Supabase CLI与本地开发
Setup Local Development
本地开发配置
bash
undefinedbash
undefinedInitialize Supabase
初始化Supabase
npx supabase init
npx supabase init
Start local Supabase (Postgres, Auth, Storage, etc.)
启动本地Supabase服务(Postgres、认证、存储等)
npx supabase start
npx supabase start
Stop
停止服务
npx supabase stop
npx supabase stop
Reset database
重置数据库
npx supabase db reset
npx supabase db reset
Status
查看服务状态
npx supabase status
undefinednpx supabase status
undefinedDatabase Migrations
数据库迁移
bash
undefinedbash
undefinedCreate migration
创建迁移文件
npx supabase migration new create_posts_table
npx supabase migration new create_posts_table
Edit migration file
编辑迁移文件
supabase/migrations/20240101000000_create_posts_table.sql
supabase/migrations/20240101000000_create_posts_table.sql
Apply migrations
应用迁移
npx supabase db push
npx supabase db push
Pull remote schema
拉取远程schema
npx supabase db pull
npx supabase db pull
Diff local vs remote
对比本地与远程差异
npx supabase db diff
undefinednpx supabase db diff
undefinedMigration Example
迁移示例
sql
-- supabase/migrations/20240101000000_create_posts_table.sql
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
content TEXT,
author_id UUID NOT NULL REFERENCES auth.users(id),
status TEXT NOT NULL DEFAULT 'draft',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Policies
CREATE POLICY "Anyone can view published posts"
ON posts FOR SELECT
USING (status = 'published');
CREATE POLICY "Users can create their own posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = author_id);
CREATE POLICY "Users can update their own posts"
ON posts FOR UPDATE
USING (auth.uid() = author_id);
-- Indexes
CREATE INDEX posts_author_id_idx ON posts(author_id);
CREATE INDEX posts_status_idx ON posts(status);
-- Trigger for updated_at
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION moddatetime(updated_at);sql
-- supabase/migrations/20240101000000_create_posts_table.sql
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
content TEXT,
author_id UUID NOT NULL REFERENCES auth.users(id),
status TEXT NOT NULL DEFAULT 'draft',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 启用RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- 策略配置
CREATE POLICY "Anyone can view published posts"
ON posts FOR SELECT
USING (status = 'published');
CREATE POLICY "Users can create their own posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = author_id);
CREATE POLICY "Users can update their own posts"
ON posts FOR UPDATE
USING (auth.uid() = author_id);
-- 创建索引
CREATE INDEX posts_author_id_idx ON posts(author_id);
CREATE INDEX posts_status_idx ON posts(status);
-- 更新时间触发器
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION moddatetime(updated_at);Security Best Practices
安全最佳实践
API Key Management
API密钥管理
typescript
// NEVER expose service_role key in client
// Use anon key for client-side
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! // Public
)
// Service role key only on server
const supabaseAdmin = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!, // Secret, bypasses RLS
{ auth: { persistSession: false } }
)typescript
// 切勿在客户端暴露service_role密钥
// 客户端使用匿名密钥
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! // 公开密钥
)
// 服务端仅使用service_role密钥
const supabaseAdmin = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!, // 私有密钥,绕过RLS
{ auth: { persistSession: false } }
)RLS Best Practices
RLS最佳实践
sql
-- Always enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Default deny (no policy = no access)
-- Explicitly grant access with policies
-- Test policies as different users
SET request.jwt.claims.sub = 'user-id';
SELECT * FROM posts; -- Test as this user
-- Disable RLS only for admin operations
-- Use service_role key from server, never clientsql
// 始终启用RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
// 默认拒绝访问(无策略则无法访问)
// 通过策略明确授予访问权限
// 以不同用户身份测试策略
SET request.jwt.claims.sub = 'user-id';
SELECT * FROM posts; -- 模拟该用户测试
// 仅在管理员操作时禁用RLS
// 仅在服务端使用service_role密钥,切勿在客户端使用Input Validation
输入验证
typescript
// Validate on client and server
function validatePost(data: unknown) {
const schema = z.object({
title: z.string().min(1).max(200),
content: z.string().max(10000).optional()
})
return schema.parse(data)
}
// Server-side validation in Edge Function
serve(async (req) => {
const body = await req.json()
try {
const validated = validatePost(body)
// Process validated data
} catch (error) {
return new Response(
JSON.stringify({ error: 'Invalid input' }),
{ status: 400 }
)
}
})typescript
// 在客户端和服务端都进行验证
function validatePost(data: unknown) {
const schema = z.object({
title: z.string().min(1).max(200),
content: z.string().max(10000).optional()
})
return schema.parse(data)
}
// 在Edge Functions中进行服务端验证
serve(async (req) => {
const body = await req.json()
try {
const validated = validatePost(body)
// 处理验证后的数据
} catch (error) {
return new Response(
JSON.stringify({ error: '输入无效' }),
{ status: 400 }
)
}
})Environment Variables
环境变量
bash
undefinedbash
undefined.env.local
.env.local
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... # Public
SUPABASE_SERVICE_ROLE_KEY=eyJ... # Secret, server-only
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... # 公开密钥
SUPABASE_SERVICE_ROLE_KEY=eyJ... # 私有密钥,仅服务端使用
Production: Use environment variables in hosting platform
生产环境:在托管平台配置环境变量
Never commit .env files to git
切勿将.env文件提交到git
undefinedundefinedProduction Deployment
生产环境部署
Database Optimization
数据库优化
sql
-- Add indexes for common queries
CREATE INDEX posts_created_at_idx ON posts(created_at DESC);
CREATE INDEX posts_author_status_idx ON posts(author_id, status);
-- Optimize full-text search
CREATE INDEX posts_title_search_idx ON posts
USING GIN (to_tsvector('english', title));
-- Analyze query performance
EXPLAIN ANALYZE
SELECT * FROM posts WHERE author_id = 'xxx';
-- Vacuum and analyze
VACUUM ANALYZE posts;sql
undefinedConnection Pooling
为常用查询添加索引
typescript
// Use connection pooling for serverless
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, key, {
db: {
schema: 'public',
},
auth: {
persistSession: true,
autoRefreshToken: true,
},
global: {
headers: { 'x-my-custom-header': 'my-value' },
},
})
// Configure pool in Supabase dashboard
// Settings > Database > Connection poolingCREATE INDEX posts_created_at_idx ON posts(created_at DESC);
CREATE INDEX posts_author_status_idx ON posts(author_id, status);
Monitoring
优化全文搜索
typescript
// Enable query logging
const supabase = createClient(url, key, {
global: {
fetch: async (url, options) => {
console.log('Query:', url)
return fetch(url, options)
}
}
})
// Monitor in Supabase Dashboard
// - Database performance
// - API usage
// - Storage usage
// - Auth activityCREATE INDEX posts_title_search_idx ON posts
USING GIN (to_tsvector('english', title));
Backup Strategy
分析查询性能
bash
undefinedEXPLAIN ANALYZE
SELECT * FROM posts WHERE author_id = 'xxx';
Automatic backups (Pro plan+)
清理和分析数据库
Daily backups with point-in-time recovery
—
Manual backup
—
pg_dump -h db.xxx.supabase.co -U postgres -d postgres > backup.sql
VACUUM ANALYZE posts;
undefinedRestore
连接池
psql -h db.xxx.supabase.co -U postgres -d postgres < backup.sql
undefinedtypescript
undefinedSupabase vs Firebase
为无服务器环境使用连接池
Similarities
—
- Backend-as-a-Service platform
- Authentication with multiple providers
- Realtime data synchronization
- File storage
- Serverless functions
- Generous free tier
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, key, {
db: {
schema: 'public',
},
auth: {
persistSession: true,
autoRefreshToken: true,
},
global: {
headers: { 'x-my-custom-header': 'my-value' },
},
})
Key Differences
在Supabase控制台配置连接池
—
设置 > 数据库 > 连接池
Database
- Supabase: PostgreSQL (SQL, full control)
- Firebase: Firestore (NoSQL, limited queries)
Queries
- Supabase: Full SQL, joins, aggregations
- Firebase: Limited filtering, no joins
Security
- Supabase: Row Level Security (Postgres native)
- Firebase: Security Rules (custom syntax)
Open Source
- Supabase: Fully open source, self-hostable
- Firebase: Proprietary, Google-hosted only
Pricing
- Supabase: Compute-based, predictable
- Firebase: Usage-based, can spike
Ecosystem
- Supabase: Postgres ecosystem (extensions, tools)
- Firebase: Google Cloud Platform integration
undefinedMigration Considerations
监控
typescript
// Firestore collection query
const snapshot = await db
.collection('posts')
.where('status', '==', 'published')
.orderBy('createdAt', 'desc')
.limit(10)
.get()
// Equivalent Supabase query
const { data } = await supabase
.from('posts')
.select('*')
.eq('status', 'published')
.order('created_at', { ascending: false })
.limit(10)
// Complex queries easier in Supabase
const { data } = await supabase
.from('posts')
.select(`
*,
author:profiles!inner(name),
comments(count)
`)
.gte('created_at', startDate)
.lte('created_at', endDate)
.order('created_at', { ascending: false })
// Firebase would require multiple queries + client-side joinstypescript
undefinedAdvanced Patterns
启用查询日志
Optimistic Updates
—
typescript
'use client'
import { useState, useOptimistic } from 'react'
import { createClient } from '@/lib/supabase/client'
export function PostList({ initialPosts }: { initialPosts: Post[] }) {
const [posts, setPosts] = useState(initialPosts)
const [optimisticPosts, addOptimisticPost] = useOptimistic(
posts,
(state, newPost: Post) => [...state, newPost]
)
const supabase = createClient()
const createPost = async (title: string) => {
const tempPost = {
id: crypto.randomUUID(),
title,
created_at: new Date().toISOString()
}
addOptimisticPost(tempPost)
const { data } = await supabase
.from('posts')
.insert({ title })
.select()
.single()
if (data) {
setPosts([...posts, data])
}
}
return (
<div>
{optimisticPosts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
)
}const supabase = createClient(url, key, {
global: {
fetch: async (url, options) => {
console.log('查询:', url)
return fetch(url, options)
}
}
})
Infinite Scroll
在Supabase控制台监控
—
- 数据库性能
—
- API使用情况
—
- 存储使用情况
—
- 认证活动
typescript
'use client'
import { useState, useEffect } from 'react'
import { createClient } from '@/lib/supabase/client'
const PAGE_SIZE = 20
export function InfinitePostList() {
const [posts, setPosts] = useState<Post[]>([])
const [page, setPage] = useState(0)
const [hasMore, setHasMore] = useState(true)
const supabase = createClient()
useEffect(() => {
const loadMore = async () => {
const { data } = await supabase
.from('posts')
.select('*')
.range(page * PAGE_SIZE, (page + 1) * PAGE_SIZE - 1)
.order('created_at', { ascending: false })
if (data) {
setPosts([...posts, ...data])
setHasMore(data.length === PAGE_SIZE)
}
}
loadMore()
}, [page])
return (
<div>
{posts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
{hasMore && (
<button onClick={() => setPage(page + 1)}>
Load More
</button>
)}
</div>
)
}undefinedDebounced Search
备份策略
typescript
'use client'
import { useState, useEffect } from 'react'
import { createClient } from '@/lib/supabase/client'
import { useDebounce } from '@/hooks/use-debounce'
export function SearchPosts() {
const [query, setQuery] = useState('')
const [results, setResults] = useState<Post[]>([])
const debouncedQuery = useDebounce(query, 300)
const supabase = createClient()
useEffect(() => {
if (!debouncedQuery) {
setResults([])
return
}
const search = async () => {
const { data } = await supabase
.from('posts')
.select('*')
.textSearch('title', debouncedQuery)
.limit(10)
if (data) setResults(data)
}
search()
}, [debouncedQuery])
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search posts..."
/>
{results.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
)
}bash
undefinedSummary
自动备份(专业版及以上)
—
每日备份,支持时间点恢复
—
手动备份
Supabase provides a complete backend platform with:
- Postgres Database with REST and GraphQL APIs
- Built-in Authentication with multiple providers
- Row Level Security for granular access control
- File Storage with image transformations
- Realtime Subscriptions for live updates
- Edge Functions for serverless compute
- Next.js Integration with Server and Client Components
- TypeScript Support with auto-generated types
- Local Development with Supabase CLI
- Production Ready with monitoring and backups
Use Supabase when a full-featured backend with the power of Postgres, built-in auth, and realtime capabilities is needed, all with excellent TypeScript and Next.js integration.
pg_dump -h db.xxx.supabase.co -U postgres -d postgres > backup.sql
—
恢复备份
—
psql -h db.xxx.supabase.co -U postgres -d postgres < backup.sql
undefined—
Supabase与Firebase对比
—
相似点
—
- 后端即服务(BaaS)平台
- 多提供商身份认证
- 实时数据同步
- 文件存储
- 无服务器函数
- 免费额度充足
—
核心差异
—
数据库
- Supabase: PostgreSQL(SQL,完全可控)
- Firebase: Firestore(NoSQL,查询能力有限)
查询能力
- Supabase: 完整SQL支持,关联查询、聚合操作
- Firebase: 过滤能力有限,无关联查询
安全机制
- Supabase: 行级安全(Postgres原生)
- Firebase: 安全规则(自定义语法)
开源属性
- Supabase: 完全开源,可自托管
- Firebase: 闭源,仅支持Google托管
定价模式
- Supabase: 基于计算资源,价格可预测
- Firebase: 基于使用量,成本可能激增
生态系统
- Supabase: Postgres生态(扩展、工具丰富)
- Firebase: 集成Google云平台
—
迁移注意事项
—
typescript
// Firestore集合查询
const snapshot = await db
.collection('posts')
.where('status', '==', 'published')
.orderBy('createdAt', 'desc')
.limit(10)
.get()
// 等效Supabase查询
const { data } = await supabase
.from('posts')
.select('*')
.eq('status', 'published')
.order('created_at', { ascending: false })
.limit(10)
// Supabase更易处理复杂查询
const { data } = await supabase
.from('posts')
.select(`
*,
author:profiles!inner(name),
comments(count)
`)
.gte('created_at', startDate)
.lte('created_at', endDate)
.order('created_at', { ascending: false })
// Firebase需要多次查询+客户端关联—
高级模式
—
乐观更新
—
typescript
'use client'
import { useState, useOptimistic } from 'react'
import { createClient } from '@/lib/supabase/client'
export function PostList({ initialPosts }: { initialPosts: Post[] }) {
const [posts, setPosts] = useState(initialPosts)
const [optimisticPosts, addOptimisticPost] = useOptimistic(
posts,
(state, newPost: Post) => [...state, newPost]
)
const supabase = createClient()
const createPost = async (title: string) => {
const tempPost = {
id: crypto.randomUUID(),
title,
created_at: new Date().toISOString()
}
addOptimisticPost(tempPost)
const { data } = await supabase
.from('posts')
.insert({ title })
.select()
.single()
if (data) {
setPosts([...posts, data])
}
}
return (
<div>
{optimisticPosts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
)
}—
无限滚动
—
typescript
'use client'
import { useState, useEffect } from 'react'
import { createClient } from '@/lib/supabase/client'
const PAGE_SIZE = 20
export function InfinitePostList() {
const [posts, setPosts] = useState<Post[]>([])
const [page, setPage] = useState(0)
const [hasMore, setHasMore] = useState(true)
const supabase = createClient()
useEffect(() => {
const loadMore = async () => {
const { data } = await supabase
.from('posts')
.select('*')
.range(page * PAGE_SIZE, (page + 1) * PAGE_SIZE - 1)
.order('created_at', { ascending: false })
if (data) {
setPosts([...posts, ...data])
setHasMore(data.length === PAGE_SIZE)
}
}
loadMore()
}, [page])
return (
<div>
{posts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
{hasMore && (
<button onClick={() => setPage(page + 1)}>
加载更多
</button>
)}
</div>
)
}—
防抖搜索
—
typescript
'use client'
import { useState, useEffect } from 'react'
import { createClient } from '@/lib/supabase/client'
import { useDebounce } from '@/hooks/use-debounce'
export function SearchPosts() {
const [query, setQuery] = useState('')
const [results, setResults] = useState<Post[]>([])
const debouncedQuery = useDebounce(query, 300)
const supabase = createClient()
useEffect(() => {
if (!debouncedQuery) {
setResults([])
return
}
const search = async () => {
const { data } = await supabase
.from('posts')
.select('*')
.textSearch('title', debouncedQuery)
.limit(10)
if (data) setResults(data)
}
search()
}, [debouncedQuery])
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜索文章..."
/>
{results.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
)
}—
总结
—
Supabase提供完整的后端平台,包含:
- Postgres数据库,支持REST和GraphQL API
- 内置身份认证,支持多提供商
- 行级安全,实现细粒度访问控制
- 文件存储,支持图片转换
- 实时订阅,实现实时更新
- Edge Functions,提供无服务器计算能力
- Next.js集成,支持服务端和客户端组件
- TypeScript支持,自动生成类型定义
- 本地开发,通过Supabase CLI实现
- 生产就绪,包含监控和备份功能
当你需要一个具备Postgres强大能力、内置认证和实时功能的全功能后端,且需要良好的TypeScript和Next.js集成时,选择Supabase是最佳方案。