supabase-backend-platform
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 Database: 完整的SQL数据库,搭配PostgREST API
- Authentication: 内置多提供商身份验证功能
- Storage: 支持图片转换的文件存储服务
- Realtime: 数据库变更的WebSocket订阅服务
- Edge Functions: 运行在Deno runtime上的无服务器函数
- Row Level Security: 基于Postgres的行级数据访问控制
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)
// 插入或更新数据(UPSERT)
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函数
Edge Function Basics
Edge函数基础
Serverless functions on Deno runtime:
bash
undefined运行在Deno runtime上的无服务器函数:
bash
// 创建函数
supabase functions new my-function
// 本地运行函数
supabase functions serve
// 部署函数
supabase functions deploy my-functionCreate function
Edge函数示例
supabase functions new my-function
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' } }
)
}
})Serve locally
调用Edge函数
supabase functions serve
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' }
})Deploy
Next.js集成
—
App Router配置
supabase functions deploy my-function
undefinedtypescript
// 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
}Edge Function Example
中间件配置
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
// 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)$).*)',
],
}Invoke Edge Function
服务端组件示例
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
// 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>
)
}Next.js Integration
客户端组件示例
App Router Setup
—
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
// 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>
)
}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
// 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 }
}Server Component
TypeScript类型生成
—
从数据库生成类型
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>
)
}bash
undefinedClient Component
安装CLI
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>
)
}npm install -D supabase
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 }
}npx supabase login
TypeScript Type Generation
关联项目
Generate Types from Database
—
bash
undefinednpx supabase link --project-ref your-project-ref
Install CLI
生成类型
npm install -D supabase
npx supabase gen types typescript --project-id your-project-ref > types/supabase.ts
Login
或从本地数据库生成
npx supabase login
npx supabase gen types typescript --local > types/supabase.ts
undefinedLink project
使用生成的类型
npx supabase link --project-ref your-project-ref
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 }Generate types
Supabase CLI与本地开发
—
本地开发环境搭建
npx supabase gen types typescript --project-id your-project-ref > types/supabase.ts
bash
undefinedOr from local database
初始化Supabase
npx supabase gen types typescript --local > types/supabase.ts
undefinednpx supabase init
Use Generated Types
启动本地Supabase服务(包含Postgres、Auth、Storage等)
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 }npx supabase start
Supabase CLI and Local Development
停止服务
Setup Local Development
—
bash
undefinednpx supabase stop
Initialize Supabase
重置数据库
npx supabase init
npx supabase db reset
Start local Supabase (Postgres, Auth, Storage, etc.)
查看服务状态
npx supabase start
npx supabase status
undefinedStop
数据库迁移
npx supabase stop
bash
undefinedReset database
创建迁移文件
npx supabase db reset
npx supabase migration new create_posts_table
Status
编辑迁移文件
—
supabase/migrations/20240101000000_create_posts_table.sql
—
应用迁移
npx supabase status
undefinednpx supabase db push
Database Migrations
拉取远程数据库schema
bash
undefinednpx supabase db pull
Create migration
对比本地与远程数据库差异
npx supabase migration new create_posts_table
npx supabase db diff
undefinedEdit migration file
迁移文件示例
supabase/migrations/20240101000000_create_posts_table.sql
—
Apply migrations
—
npx supabase db push
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);
-- 为updated_at字段创建触发器
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION moddatetime(updated_at);Pull remote schema
安全最佳实践
—
API密钥管理
npx supabase db pull
typescript
// 切勿在客户端暴露service_role密钥
// 客户端仅使用anon密钥
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 } }
)Diff local vs remote
RLS最佳实践
npx supabase db diff
undefinedsql
// 始终为表启用RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
// 默认拒绝访问(无策略则无法访问)
// 通过策略明确授予访问权限
// 以不同用户身份测试策略
SET request.jwt.claims.sub = 'user-id';
SELECT * FROM posts; // 模拟该用户测试访问权限
// 仅在管理员操作时临时禁用RLS
// 仅在服务端使用service_role密钥,切勿在客户端使用Migration 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);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函数中进行服务端验证
serve(async (req) => {
const body = await req.json()
try {
const validated = validatePost(body)
// 处理验证后的数据
} catch (error) {
return new Response(
JSON.stringify({ error: '输入无效' }),
{ status: 400 }
)
}
})Security Best Practices
环境变量管理
API Key Management
—
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 } }
)bash
undefinedRLS Best Practices
.env.local
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 clientNEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... # 公开密钥
SUPABASE_SERVICE_ROLE_KEY=eyJ... # 私密密钥,仅服务端使用
Input Validation
生产环境:在托管平台配置环境变量
—
切勿将.env文件提交到git仓库
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 }
)
}
})undefinedEnvironment Variables
生产环境部署
—
数据库优化
bash
undefinedsql
// 为常用查询添加索引
CREATE INDEX posts_created_at_idx ON posts(created_at DESC);
CREATE INDEX posts_author_status_idx ON posts(author_id, status);
// 优化全文搜索
CREATE INDEX posts_title_search_idx ON posts
USING GIN (to_tsvector('english', title));
// 分析查询性能
EXPLAIN ANALYZE
SELECT * FROM posts WHERE author_id = 'xxx';
// 清理和分析数据库
VACUUM ANALYZE posts;.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
typescript
// 为无服务器环境使用连接池
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' },
},
})
// 在Supabase控制台配置连接池
// 设置 > 数据库 > 连接池Production: Use environment variables in hosting platform
监控
Never commit .env files to git
—
undefinedtypescript
// 启用查询日志
const supabase = createClient(url, key, {
global: {
fetch: async (url, options) => {
console.log('查询:', url)
return fetch(url, options)
}
}
})
// 在Supabase控制台监控
// - 数据库性能
// - API使用情况
// - 存储使用情况
// - 认证活动Production 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;bash
// 自动备份(专业版及以上)
// 每日备份,支持时间点恢复
// 手动备份
pg_dump -h db.xxx.supabase.co -U postgres -d postgres > backup.sql
// 恢复备份
psql -h db.xxx.supabase.co -U postgres -d postgres < backup.sqlConnection Pooling
Supabase vs Firebase
—
相似点
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 pooling- 后端即服务平台
- 多提供商身份验证
- 实时数据同步
- 文件存储
- 无服务器函数
- 免费额度充足
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 activity数据库
- Supabase: PostgreSQL(SQL,完全可控)
- Firebase: Firestore(NoSQL,查询能力有限)
查询能力
- Supabase: 完整SQL支持,支持关联查询、聚合操作
- Firebase: 过滤能力有限,不支持关联查询
安全机制
- Supabase: 行级安全(Postgres原生功能)
- Firebase: 安全规则(自定义语法)
开源属性
- Supabase: 完全开源,可自行部署
- Firebase: 闭源,仅支持Google托管
定价模式
- Supabase: 基于计算资源,价格可预测
- Firebase: 基于使用量,可能出现费用突增
生态系统
- Supabase: 兼容Postgres生态(扩展、工具)
- Firebase: 深度集成Google云平台
Backup Strategy
迁移注意事项
bash
undefinedtypescript
// 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需要多次查询+客户端关联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
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>
)
}Restore
无限滚动
psql -h db.xxx.supabase.co -U postgres -d postgres < backup.sql
undefinedtypescript
'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>
)
}Supabase vs Firebase
防抖搜索
Similarities
—
- Backend-as-a-Service platform
- Authentication with multiple providers
- Realtime data synchronization
- File storage
- Serverless functions
- Generous free tier
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>
)
}Key Differences
总结
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
Supabase提供完整的后端平台,包含:
- Postgres数据库,支持REST和GraphQL API
- 内置身份验证,支持多提供商
- 行级安全,实现细粒度访问控制
- 文件存储,支持图片转换
- 实时订阅,实现数据实时更新
- Edge函数,提供无服务器计算能力
- Next.js集成,支持服务端与客户端组件
- TypeScript支持,自动生成类型定义
- 本地开发,通过Supabase CLI实现
- 生产就绪,包含监控与备份功能
当你需要一个功能完整、基于Postgres、内置身份验证与实时能力的后端平台,且需要良好的TypeScript和Next.js集成时,Supabase是理想选择。
Migration 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 joins—
Advanced 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>
)
}—
Infinite Scroll
—
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>
)
}—
Debounced 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>
)
}—
Summary
—
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.
—