supabase-expert
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSupabase Integration Expert
Supabase集成专家
Purpose
目的
Provide comprehensive, accurate guidance for building applications with Supabase based on 2,616+ official documentation files. Cover all aspects of database operations, authentication, real-time features, file storage, edge functions, vector search, and platform integrations.
基于2616+份官方文档,为使用Supabase构建应用提供全面、准确的指导。涵盖数据库操作、身份认证、实时功能、文件存储、边缘函数、向量搜索和平台集成的所有方面。
Documentation Coverage
文档覆盖范围
Full access to official Supabase documentation (when available):
- Location:
docs/supabase_com/ - Files: 2,616 markdown files
- Coverage: Complete guides, API references, client libraries, and platform docs
Note: Documentation must be pulled separately:
bash
pipx install docpull
docpull https://supabase.com/docs -o .claude/skills/supabase/docsMajor Areas:
- Database: PostgreSQL, Row Level Security (RLS), migrations, functions, triggers
- Authentication: Email/password, OAuth, magic links, SSO, MFA, phone auth
- Real-time: Database changes, broadcast, presence, channels
- Storage: File uploads, image transformations, CDN, buckets
- Edge Functions: Deno runtime, serverless, global deployment
- Vector/AI: pgvector, embeddings, semantic search, RAG
- Client Libraries: JavaScript, Python, Dart (Flutter), Swift, Kotlin
- Platform: CLI, local development, branching, observability
- Integrations: Next.js, React, Vue, Svelte, React Native, Expo
完全访问官方Supabase文档(如有):
- 位置:
docs/supabase_com/ - 文件: 2616个markdown文件
- 覆盖范围: 完整指南、API参考、客户端库和平台文档
注意: 文档需要单独拉取:
bash
pipx install docpull
docpull https://supabase.com/docs -o .claude/skills/supabase/docs主要领域:
- 数据库: PostgreSQL、行级安全(RLS)、迁移、函数、触发器
- 身份认证: 邮箱/密码、OAuth、魔法链接、SSO、MFA、手机认证
- 实时功能: 数据库变更、广播、在线状态、频道
- 存储: 文件上传、图片转换、CDN、存储桶
- 边缘函数: Deno运行时、无服务器、全局部署
- 向量/AI: pgvector、向量嵌入、语义搜索、RAG
- 客户端库: JavaScript、Python、Dart(Flutter)、Swift、Kotlin
- 平台: CLI、本地开发、分支、可观测性
- 集成: Next.js、React、Vue、Svelte、React Native、Expo
When to Use
使用场景
Invoke when user mentions:
- Database: PostgreSQL, Postgres, SQL, database, tables, queries, migrations
- Auth: authentication, login, signup, OAuth, SSO, multi-factor, magic links
- Real-time: real-time, subscriptions, websocket, live data, presence, broadcast
- Storage: file upload, file storage, images, S3, CDN, buckets
- Functions: edge functions, serverless, API, Deno, cloud functions
- Security: Row Level Security, RLS, policies, permissions, access control
- AI/ML: vector search, embeddings, pgvector, semantic search, AI, RAG
- Framework Integration: Next.js, React, Supabase client, hooks
当用户提及以下内容时使用本指南:
- 数据库: PostgreSQL、Postgres、SQL、数据库、表、查询、迁移
- 认证: authentication、登录、注册、OAuth、SSO、多因素认证、魔法链接
- 实时功能: real-time、订阅、websocket、实时数据、在线状态、广播
- 存储: 文件上传、文件存储、图片、S3、CDN、存储桶
- 函数: edge functions、无服务器、API、Deno、云函数
- 安全: Row Level Security、RLS、策略、权限、访问控制
- AI/ML: 向量搜索、向量嵌入、pgvector、语义搜索、AI、RAG
- 框架集成: Next.js、React、Supabase客户端、hooks
How to Use Documentation
文档使用方法
When answering questions:
-
Search for specific topics:bash
# Use Grep to find relevant docs grep -r "row level security" docs/supabase_com/ --include="*.md" -
Find guides:bash
# Guides are organized by feature ls docs/supabase_com/guides_* -
Check reference docs:bash
# Reference docs for client libraries ls docs/supabase_com/reference_*
回答问题时:
-
搜索特定主题:bash
# 使用Grep查找相关文档 grep -r "row level security" docs/supabase_com/ --include="*.md" -
查找指南:bash
# 指南按功能组织 ls docs/supabase_com/guides_* -
查看参考文档:bash
# 客户端库参考文档 ls docs/supabase_com/reference_*
Quick Start
快速开始
Installation
安装
bash
npm install @supabase/supabase-jsbash
npm install @supabase/supabase-jsInitialize Client
初始化客户端
typescript
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);Environment Variables:
- - Your project URL (safe for client)
NEXT_PUBLIC_SUPABASE_URL - - Anonymous/public key (safe for client)
NEXT_PUBLIC_SUPABASE_ANON_KEY - - Admin key (server-side only, bypasses RLS)
SUPABASE_SERVICE_ROLE_KEY
typescript
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
\tprocess.env.NEXT_PUBLIC_SUPABASE_URL!,
\tprocess.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);环境变量:
- - 你的项目URL(客户端安全可用)
NEXT_PUBLIC_SUPABASE_URL - - 匿名/公钥(客户端安全可用)
NEXT_PUBLIC_SUPABASE_ANON_KEY - - 管理员密钥(仅服务器端使用,绕过RLS)
SUPABASE_SERVICE_ROLE_KEY
Database Operations
数据库操作
CRUD Operations
CRUD操作
typescript
// Insert
const { data, error } = await supabase
.from("posts")
.insert({
title: "Hello World",
content: "My first post",
user_id: user.id,
})
.select()
.single();
// Read (with filters)
const { data: posts } = await supabase
.from("posts")
.select("*")
.eq("published", true)
.order("created_at", { ascending: false })
.limit(10);
// Update
const { data, error } = await supabase
.from("posts")
.update({ published: true })
.eq("id", postId)
.select()
.single();
// Delete
const { error } = await supabase.from("posts").delete().eq("id", postId);
// Upsert (insert or update)
const { data, error } = await supabase
.from("profiles")
.upsert({
id: user.id,
name: "John Doe",
updated_at: new Date().toISOString(),
})
.select();typescript
// 插入
const { data, error } = await supabase
\t.from("posts")
\t.insert({
\t\ttitle: "Hello World",
\t\tcontent: "My first post",
\t\tuser_id: user.id,
\t})
\t.select()
\t.single();
// 查询(带筛选)
const { data: posts } = await supabase
\t.from("posts")
\t.select("*")
\t.eq("published", true)
\t.order("created_at", { ascending: false })
\t.limit(10);
// 更新
const { data, error } = await supabase
\t.from("posts")
\t.update({ published: true })
\t.eq("id", postId)
\t.select()
\t.single();
// 删除
const { error } = await supabase.from("posts").delete().eq("id", postId);
// 插入或更新(Upsert)
const { data, error } = await supabase
\t.from("profiles")
\t.upsert({
\t\tid: user.id,
\t\tname: "John Doe",
\t\tupdated_at: new Date().toISOString(),
\t})
\t.select();Advanced Queries
高级查询
typescript
// Joins
const { data } = await supabase
.from("posts")
.select(
`
*,
author:profiles(name, avatar),
comments(count)
`,
)
.eq("published", true);
// Full-text search
const { data } = await supabase
.from("posts")
.select("*")
.textSearch("title", `'nextjs' & 'supabase'`);
// Range queries
const { data } = await supabase
.from("posts")
.select("*")
.gte("created_at", "2024-01-01")
.lt("created_at", "2024-12-31");
// JSON queries
const { data } = await supabase
.from("posts")
.select("*")
.contains("metadata", { tags: ["tutorial"] });typescript
// 关联查询
const { data } = await supabase
\t.from("posts")
\t.select(
\t\t`
*,
author:profiles(name, avatar),
comments(count)
`,
\t)
\t.eq("published", true);
// 全文搜索
const { data } = await supabase
\t.from("posts")
\t.select("*")
\t.textSearch("title", `'nextjs' & 'supabase'`);
// 范围查询
const { data } = await supabase
\t.from("posts")
\t.select("*")
\t.gte("created_at", "2024-01-01")
\t.lt("created_at", "2024-12-31");
// JSON查询
const { data } = await supabase
\t.from("posts")
\t.select("*")
\t.contains("metadata", { tags: ["tutorial"] });Database Functions
数据库函数
typescript
// Call stored procedure
const { data, error } = await supabase.rpc("get_user_stats", {
user_id: userId,
});
// Call with filters
const { data } = await supabase
.rpc("search_posts", { search_term: "supabase" })
.limit(10);typescript
// 调用存储过程
const { data, error } = await supabase.rpc("get_user_stats", {
\tuser_id: userId,
});
// 带筛选的调用
const { data } = await supabase
\t.rpc("search_posts", { search_term: "supabase" })
\t.limit(10);Authentication
身份认证
Sign Up / Sign In
注册/登录
typescript
// Email/password signup
const { data, error } = await supabase.auth.signUp({
email: "user@example.com",
password: "secure-password",
options: {
data: {
first_name: "John",
last_name: "Doe",
},
},
});
// Email/password sign in
const { data, error } = await supabase.auth.signInWithPassword({
email: "user@example.com",
password: "secure-password",
});
// Magic link (passwordless)
const { data, error } = await supabase.auth.signInWithOtp({
email: "user@example.com",
options: {
emailRedirectTo: "https://example.com/auth/callback",
},
});
// Phone/SMS
const { data, error } = await supabase.auth.signInWithOtp({
phone: "+1234567890",
});typescript
// 邮箱/密码注册
const { data, error } = await supabase.auth.signUp({
\temail: "user@example.com",
\tpassword: "secure-password",
\toptions: {
\t\tdata: {
\t\t\tfirst_name: "John",
\t\t\tlast_name: "Doe",
\t\t},
\t},
});
// 邮箱/密码登录
const { data, error } = await supabase.auth.signInWithPassword({
\temail: "user@example.com",
\tpassword: "secure-password",
});
// 魔法链接(无密码)
const { data, error } = await supabase.auth.signInWithOtp({
\temail: "user@example.com",
\toptions: {
\t\temailRedirectTo: "https://example.com/auth/callback",
\t},
});
// 手机/SMS
const { data, error } = await supabase.auth.signInWithOtp({
\tphone: "+1234567890",
});OAuth Providers
OAuth提供商
typescript
// Google sign in
const { data, error } = await supabase.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: "http://localhost:3000/auth/callback",
scopes: "profile email",
},
});Supported providers:
- Google, GitHub, GitLab, Bitbucket
- Azure, Apple, Discord, Facebook
- Slack, Spotify, Twitch, Twitter/X
- Linear, Notion, Figma, and more
typescript
// Google登录
const { data, error } = await supabase.auth.signInWithOAuth({
\tprovider: "google",
\toptions: {
\t\tredirectTo: "http://localhost:3000/auth/callback",
\t\tscopes: "profile email",
\t},
});支持的提供商:
- Google、GitHub、GitLab、Bitbucket
- Azure、Apple、Discord、Facebook
- Slack、Spotify、Twitch、Twitter/X
- Linear、Notion、Figma等
User Session Management
用户会话管理
typescript
// Get current user
const {
data: { user },
} = await supabase.auth.getUser();
// Get session
const {
data: { session },
} = await supabase.auth.getSession();
// Sign out
const { error } = await supabase.auth.signOut();
// 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");
}
});typescript
// 获取当前用户
const {
\tdata: { user },
} = await supabase.auth.getUser();
// 获取会话
const {
\tdata: { session },
} = await supabase.auth.getSession();
// 登出
const { error } = await supabase.auth.signOut();
// 监听认证状态变化
supabase.auth.onAuthStateChange((event, session) => {
\tif (event === "SIGNED_IN") {
\t\tconsole.log("用户已登录:", session.user);
\t}
\tif (event === "SIGNED_OUT") {
\t\tconsole.log("用户已登出");
\t}
\tif (event === "TOKEN_REFRESHED") {
\t\tconsole.log("令牌已刷新");
\t}
});Multi-Factor Authentication (MFA)
多因素认证(MFA)
typescript
// Enroll MFA
const { data, error } = await supabase.auth.mfa.enroll({
factorType: "totp",
friendlyName: "My Authenticator App",
});
// Verify MFA
const { data, error } = await supabase.auth.mfa.challengeAndVerify({
factorId: data.id,
code: "123456",
});
// List factors
const { data: factors } = await supabase.auth.mfa.listFactors();typescript
// 注册MFA
const { data, error } = await supabase.auth.mfa.enroll({
\tfactorType: "totp",
\tfriendlyName: "My Authenticator App",
});
// 验证MFA
const { data, error } = await supabase.auth.mfa.challengeAndVerify({
\tfactorId: data.id,
\tcode: "123456",
});
// 列出已注册的MFA方式
const { data: factors } = await supabase.auth.mfa.listFactors();Row Level Security (RLS)
行级安全(RLS)
Enable RLS
启用RLS
sql
-- Enable RLS on table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;sql
-- 在表上启用RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;Create Policies
创建策略
sql
-- Public read access
CREATE POLICY "Posts are viewable by everyone"
ON posts FOR SELECT
USING (true);
-- Users can insert their own posts
CREATE POLICY "Users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Users can update only their posts
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (auth.uid() = user_id);
-- Users can delete only their posts
CREATE POLICY "Users can delete own posts"
ON posts FOR DELETE
USING (auth.uid() = user_id);
-- Conditional access (e.g., premium users)
CREATE POLICY "Premium content for premium users"
ON posts FOR SELECT
USING (
NOT premium OR
(auth.uid() IN (
SELECT user_id FROM subscriptions
WHERE status = 'active'
))
);sql
-- 公开读取权限
CREATE POLICY "Posts are viewable by everyone"
ON posts FOR SELECT
USING (true);
-- 用户只能插入自己的帖子
CREATE POLICY "Users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- 用户只能更新自己的帖子
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (auth.uid() = user_id);
-- 用户只能删除自己的帖子
CREATE POLICY "Users can delete own posts"
ON posts FOR DELETE
USING (auth.uid() = user_id);
-- 条件访问(如付费用户)
CREATE POLICY "Premium content for premium users"
ON posts FOR SELECT
USING (
NOT premium OR
(auth.uid() IN (
SELECT user_id FROM subscriptions
WHERE status = 'active'
))
);Helper Functions
辅助函数
sql
-- Get current user ID
auth.uid()
-- Get current JWT
auth.jwt()
-- Access JWT claims
(auth.jwt()->>'role')::text
(auth.jwt()->>'email')::textsql
-- 获取当前用户ID
auth.uid()
-- 获取当前JWT
auth.jwt()
-- 访问JWT声明
(auth.jwt()->>'role')::text
(auth.jwt()->>'email')::textReal-time Subscriptions
实时订阅
Listen to Database Changes
监听数据库变更
typescript
const channel = supabase
.channel("posts-changes")
.on(
"postgres_changes",
{
event: "*", // or 'INSERT', 'UPDATE', 'DELETE'
schema: "public",
table: "posts",
},
(payload) => {
console.log("Change received:", payload);
},
)
.subscribe();
// Cleanup
channel.unsubscribe();typescript
const channel = supabase
\t.channel("posts-changes")
\t.on(
\t\t"postgres_changes",
\t\t{
\t\t\tevent: "*", // 或 'INSERT', 'UPDATE', 'DELETE'
\t\t\tschema: "public",
\t\t\ttable: "posts",
\t\t},
\t\t(payload) => {
\t\t\tconsole.log("收到变更:", payload);
\t\t},
\t)
\t.subscribe();
// 清理资源
channel.unsubscribe();Filter Real-time Events
筛选实时事件
typescript
// Only listen to specific user's posts
const channel = supabase
.channel("my-posts")
.on(
"postgres_changes",
{
event: "INSERT",
schema: "public",
table: "posts",
filter: `user_id=eq.${userId}`,
},
(payload) => {
console.log("New post:", payload.new);
},
)
.subscribe();typescript
// 仅监听特定用户的帖子
const channel = supabase
\t.channel("my-posts")
\t.on(
\t\t"postgres_changes",
\t\t{
\t\t\tevent: "INSERT",
\t\t\tschema: "public",
\t\t\ttable: "posts",
\t\t\tfilter: `user_id=eq.${userId}`,
\t\t},
\t\t(payload) => {
\t\t\tconsole.log("新帖子:", payload.new);
\t\t},
\t)
\t.subscribe();Broadcast (Ephemeral Messages)
广播(临时消息)
typescript
const channel = supabase.channel("chat-room");
// Send message
await channel.send({
type: "broadcast",
event: "message",
payload: { text: "Hello!", user: "John" },
});
// Receive messages
channel.on("broadcast", { event: "message" }, (payload) => {
console.log("Message:", payload.payload);
});
await channel.subscribe();typescript
const channel = supabase.channel("chat-room");
// 发送消息
await channel.send({
\ttype: "broadcast",
\tevent: "message",
\tpayload: { text: "Hello!", user: "John" },
});
// 接收消息
channel.on("broadcast", { event: "message" }, (payload) => {
\tconsole.log("消息:", payload.payload);
});
await channel.subscribe();Presence Tracking
在线状态追踪
typescript
const channel = supabase.channel("room-1");
// Track presence
channel
.on("presence", { event: "sync" }, () => {
const state = channel.presenceState();
console.log("Online users:", Object.keys(state).length);
})
.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(),
});
}
});typescript
const channel = supabase.channel("room-1");
// 追踪在线状态
channel
\t.on("presence", { event: "sync" }, () => {
\t\tconst state = channel.presenceState();
\t\tconsole.log("在线用户数:", Object.keys(state).length);
\t})
\t.on("presence", { event: "join" }, ({ key, newPresences }) => {
\t\tconsole.log("用户加入:", newPresences);
\t})
\t.on("presence", { event: "leave" }, ({ key, leftPresences }) => {
\t\tconsole.log("用户离开:", leftPresences);
\t})
\t.subscribe(async (status) => {
\t\tif (status === "SUBSCRIBED") {
\t\t\tawait channel.track({
\t\t\t\tuser_id: userId,
\t\t\t\tonline_at: new Date().toISOString(),
\t\t\t});
\t\t}
\t});Storage
存储
Upload Files
上传文件
typescript
const file = event.target.files[0];
const { data, error } = await supabase.storage
.from("avatars")
.upload(`public/${userId}/avatar.png`, file, {
cacheControl: "3600",
upsert: true,
});
// Upload from base64
const { data, error } = await supabase.storage
.from("avatars")
.upload("file.png", decode(base64String), {
contentType: "image/png",
});typescript
const file = event.target.files[0];
const { data, error } = await supabase.storage
\t.from("avatars")
\t.upload(`public/${userId}/avatar.png`, file, {
\t\tcacheControl: "3600",
\t\tupsert: true,
\t});
// 从base64上传
const { data, error } = await supabase.storage
\t.from("avatars")
\t.upload("file.png", decode(base64String), {
\t\tcontentType: "image/png",
\t});Download Files
下载文件
typescript
// Download as blob
const { data, error } = await supabase.storage
.from("avatars")
.download("public/avatar.png");
const url = URL.createObjectURL(data);typescript
// 以blob形式下载
const { data, error } = await supabase.storage
\t.from("avatars")
\t.download("public/avatar.png");
const url = URL.createObjectURL(data);Public URLs
公共URL
typescript
// Get public URL (for public buckets)
const { data } = supabase.storage
.from("avatars")
.getPublicUrl("public/avatar.png");
console.log(data.publicUrl);typescript
// 获取公共URL(适用于公共存储桶)
const { data } = supabase.storage
\t.from("avatars")
\t.getPublicUrl("public/avatar.png");
console.log(data.publicUrl);Signed URLs (Private Files)
签名URL(私有文件)
typescript
// Create temporary access URL
const { data, error } = await supabase.storage
.from("private-files")
.createSignedUrl("document.pdf", 3600); // 1 hour
console.log(data.signedUrl);typescript
// 创建临时访问URL
const { data, error } = await supabase.storage
\t.from("private-files")
\t.createSignedUrl("document.pdf", 3600); // 1小时
console.log(data.signedUrl);Image Transformations
图片转换
typescript
const { data } = supabase.storage.from("avatars").getPublicUrl("avatar.png", {
transform: {
width: 400,
height: 400,
resize: "cover", // 'contain', 'cover', 'fill'
quality: 80,
},
});typescript
const { data } = supabase.storage.from("avatars").getPublicUrl("avatar.png", {
\ttransform: {
\t\twidth: 400,
\t\theight: 400,
\t\tresize: "cover", // 'contain', 'cover', 'fill'
\t\tquality: 80,
\t},
});List Files
列出文件
typescript
const { data, error } = await supabase.storage.from("avatars").list("public", {
limit: 100,
offset: 0,
sortBy: { column: "created_at", order: "desc" },
});typescript
const { data, error } = await supabase.storage.from("avatars").list("public", {
\tlimit: 100,
\toffset: 0,
\tsortBy: { column: "created_at", order: "desc" },
});Edge Functions
边缘函数
Create Function
创建函数
bash
undefinedbash
undefinedInstall Supabase CLI
安装Supabase CLI
npm install -g supabase
npm install -g supabase
Initialize project
初始化项目
supabase init
supabase init
Create function
创建函数
supabase functions new my-function
undefinedsupabase functions new my-function
undefinedFunction Example
函数示例
typescript
// supabase/functions/my-function/index.ts
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
Deno.serve(async (req) => {
try {
// Initialize Supabase client
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_ANON_KEY")!,
{
global: {
headers: { Authorization: req.headers.get("Authorization")! },
},
},
);
// Get authenticated user
const {
data: { user },
error: authError,
} = await supabase.auth.getUser();
if (authError || !user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
// Query database
const { data: posts, error } = await supabase
.from("posts")
.select("*")
.eq("user_id", user.id);
if (error) {
throw error;
}
return new Response(JSON.stringify({ posts }), {
headers: { "Content-Type": "application/json" },
});
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
});typescript
// supabase/functions/my-function/index.ts
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
Deno.serve(async (req) => {
\ttry {
\t\t// 初始化Supabase客户端
\t\tconst supabase = createClient(
\t\t\tDeno.env.get("SUPABASE_URL")!,
\t\t\tDeno.env.get("SUPABASE_ANON_KEY")!,
\t\t\t{
\t\t\t\tglobal: {
\t\t\t\t\theaders: { Authorization: req.headers.get("Authorization")! },
\t\t\t\t},
\t\t\t},
\t\t);
\t\t// 获取已认证用户
\t\tconst {
\t\t\tdata: { user },
\t\t\terror: authError,
\t\t} = await supabase.auth.getUser();
\t\tif (authError || !user) {
\t\t\treturn new Response(JSON.stringify({ error: "Unauthorized" }), {
\t\t\t\tstatus: 401,
\t\t\t\theaders: { "Content-Type": "application/json" },
\t\t\t});
\t\t}
\t\t// 查询数据库
\t\tconst { data: posts, error } = await supabase
\t\t\t.from("posts")
\t\t\t.select("*")
\t\t\t.eq("user_id", user.id);
\t\tif (error) {
\t\t\tthrow error;
\t\t}
\t\treturn new Response(JSON.stringify({ posts }), {
\t\t\theaders: { "Content-Type": "application/json" },
\t\t});
\t} catch (error) {
\t\treturn new Response(JSON.stringify({ error: error.message }), {
\t\t\tstatus: 500,
\t\t\theaders: { "Content-Type": "application/json" },
\t\t});
\t}
});Deploy Function
部署函数
bash
undefinedbash
undefinedDeploy single function
部署单个函数
supabase functions deploy my-function
supabase functions deploy my-function
Deploy all functions
部署所有函数
supabase functions deploy
undefinedsupabase functions deploy
undefinedInvoke Function
调用函数
typescript
const { data, error } = await supabase.functions.invoke("my-function", {
body: { name: "World" },
});
console.log(data);typescript
const { data, error } = await supabase.functions.invoke("my-function", {
\tbody: { name: "World" },
});
console.log(data);Vector Search (AI/ML)
向量搜索(AI/ML)
Enable pgvector
启用pgvector
sql
-- Enable extension
CREATE EXTENSION IF NOT EXISTS vector;
-- Create table with vector column
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
content TEXT,
embedding VECTOR(1536) -- OpenAI ada-002 dimensions
);
-- Create HNSW index for fast similarity search
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);sql
-- 启用扩展
CREATE EXTENSION IF NOT EXISTS vector;
-- 创建带向量列的表
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
content TEXT,
embedding VECTOR(1536) -- OpenAI ada-002的维度
);
-- 创建HNSW索引以加速相似度搜索
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);Store Embeddings
存储向量嵌入
typescript
import OpenAI from "openai";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// Generate embedding
const response = await openai.embeddings.create({
model: "text-embedding-ada-002",
input: "Supabase is awesome",
});
const embedding = response.data[0].embedding;
// Store in database
const { data, error } = await supabase.from("documents").insert({
content: "Supabase is awesome",
embedding,
});typescript
import OpenAI from "openai";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// 生成向量嵌入
const response = await openai.embeddings.create({
\tmodel: "text-embedding-ada-002",
\tinput: "Supabase is awesome",
});
const embedding = response.data[0].embedding;
// 存储到数据库
const { data, error } = await supabase.from("documents").insert({
\tcontent: "Supabase is awesome",
\tembedding,
});Similarity Search
相似度搜索
typescript
// Find similar documents
const { data, error } = await supabase.rpc("match_documents", {
query_embedding: embedding,
match_threshold: 0.78,
match_count: 10,
});Similarity search function:
sql
CREATE FUNCTION match_documents (
query_embedding VECTOR(1536),
match_threshold FLOAT,
match_count INT
)
RETURNS TABLE (
id BIGINT,
content TEXT,
similarity FLOAT
)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT
documents.id,
documents.content,
1 - (documents.embedding <=> query_embedding) AS similarity
FROM documents
WHERE 1 - (documents.embedding <=> query_embedding) > match_threshold
ORDER BY documents.embedding <=> query_embedding
LIMIT match_count;
END;
$$;typescript
// 查找相似文档
const { data, error } = await supabase.rpc("match_documents", {
\tquery_embedding: embedding,
\tmatch_threshold: 0.78,
\tmatch_count: 10,
});相似度搜索函数:
sql
CREATE FUNCTION match_documents (
query_embedding VECTOR(1536),
match_threshold FLOAT,
match_count INT
)
RETURNS TABLE (
id BIGINT,
content TEXT,
similarity FLOAT
)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT
documents.id,
documents.content,
1 - (documents.embedding <=> query_embedding) AS similarity
FROM documents
WHERE 1 - (documents.embedding <=> query_embedding) > match_threshold
ORDER BY documents.embedding <=> query_embedding
LIMIT match_count;
END;
$$;Next.js Integration
Next.js集成
Server Components
服务器组件
typescript
// app/posts/page.tsx
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export default async function PostsPage() {
const cookieStore = cookies();
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options),
);
} catch {
// Called from Server Component - ignore
}
},
},
},
);
}typescript
// app/posts/page.tsx
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export default async function PostsPage() {
\tconst cookieStore = cookies();
\tconst supabase = createServerClient(
\t\tprocess.env.NEXT_PUBLIC_SUPABASE_URL!,
\t\tprocess.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
\t\t{
\t\t\tcookies: {
\t\t\t\tgetAll() {
\t\t\t\t\treturn cookieStore.getAll();
\t\t\t\t},
\t\t\t\tsetAll(cookiesToSet) {
\t\t\t\t\ttry {
\t\t\t\t\t\tcookiesToSet.forEach(({ name, value, options }) =>
\t\t\t\t\t\t\tcookieStore.set(name, value, options),
\t\t\t\t\t\t);
\t\t\t\t\t} catch {
\t\t\t\t\t\t// 从服务器组件调用 - 忽略
\t\t\t\t\t}
\t\t\t\t},
\t\t\t},
\t\t},
\t);
}Client Components
客户端组件
typescript
// app/new-post/page.tsx
'use client';
import { createBrowserClient } from '@supabase/ssr';
import { useState } from 'react';
export default function NewPostPage() {
const supabase = createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
const [title, setTitle] = useState('');
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
const { error } = await supabase
.from('posts')
.insert({ title });
if (error) console.error(error);
};
return <form onSubmit={handleSubmit}>...</form>;
}typescript
// app/new-post/page.tsx
'use client';
import { createBrowserClient } from '@supabase/ssr';
import { useState } from 'react';
export default function NewPostPage() {
const supabase = createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
const [title, setTitle] = useState('');
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
const { error } = await supabase
.from('posts')
.insert({ title });
if (error) console.error(error);
};
return <form onSubmit={handleSubmit}>...</form>;
}Middleware (Auth Protection)
中间件(认证保护)
typescript
// middleware.ts
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
export async function middleware(request: NextRequest) {
let response = NextResponse.next({
request,
});
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
for (const { name, value } of cookiesToSet) {
request.cookies.set(name, value);
}
response = NextResponse.next({
request,
});
for (const { name, value, options } of cookiesToSet) {
response.cookies.set(name, value, options);
}
},
},
},
);
const { data, error } = await supabase.auth.getClaims();
if (error) {
throw error;
}
const user = data?.claims;
// Redirect to login if not authenticated
if (!user && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}
return response;
}
export const config = {
matcher: ["/dashboard/:path*"],
};typescript
// middleware.ts
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
export async function middleware(request: NextRequest) {
\tlet response = NextResponse.next({
\t\trequest,
\t});
\tconst supabase = createServerClient(
\t\tprocess.env.NEXT_PUBLIC_SUPABASE_URL!,
\t\tprocess.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
\t\t{
\t\t\tcookies: {
\t\t\t\tgetAll() {
\t\t\t\t\treturn request.cookies.getAll();
\t\t\t\t},
\t\t\t\tsetAll(cookiesToSet) {
\t\t\t\t\tfor (const { name, value } of cookiesToSet) {
\t\t\t\t\t\trequest.cookies.set(name, value);
\t\t\t\t\t}
\t\t\t\t\tresponse = NextResponse.next({
\t\t\t\t\t\trequest,
\t\t\t\t\t});
\t\t\t\t\tfor (const { name, value, options } of cookiesToSet) {
\t\t\t\t\t\tresponse.cookies.set(name, value, options);
\t\t\t\t\t}
\t\t\t\t},
\t\t\t},
\t\t},
\t);
\tconst { data, error } = await supabase.auth.getClaims();
\tif (error) {
\t\tthrow error;
\t}
\tconst user = data?.claims;
\t// 未认证则重定向到登录页
\tif (!user && request.nextUrl.pathname.startsWith("/dashboard")) {
\t\treturn NextResponse.redirect(new URL("/login", request.url));
\t}
\treturn response;
}
export const config = {
\tmatcher: ["/dashboard/:path*"],
};Route Handlers
路由处理器
typescript
// app/api/posts/route.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
async function createClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
for (const { name, value, options } of cookiesToSet) {
cookieStore.set(name, value, options);
}
} catch {
// Called from Server Component - ignore
}
},
},
},
);
}
export async function GET() {
const supabase = await createClient();
const { data: posts } = await supabase.from("posts").select("*");
return NextResponse.json({ posts });
}
export async function POST(request: Request) {
const supabase = await createClient();
const body = await request.json();
const { data, error } = await supabase
.from("posts")
.insert(body)
.select()
.single();
if (error) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
return NextResponse.json({ post: data });
}typescript
// app/api/posts/route.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
async function createClient() {
\tconst cookieStore = await cookies();
\treturn createServerClient(
\t\tprocess.env.NEXT_PUBLIC_SUPABASE_URL!,
\t\tprocess.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
\t\t{
\t\t\tcookies: {
\t\t\t\tgetAll() {
\t\t\t\t\treturn cookieStore.getAll();
\t\t\t\t},
\t\t\t\tsetAll(cookiesToSet) {
\t\t\t\t\ttry {
\t\t\t\t\t\tfor (const { name, value, options } of cookiesToSet) {
\t\t\t\t\t\t\tcookieStore.set(name, value, options);
\t\t\t\t\t\t}
\t\t\t\t\t} catch {
\t\t\t\t\t\t// 从服务器组件调用 - 忽略
\t\t\t\t\t}
\t\t\t\t},
\t\t\t},
\t\t},
\t);
}
export async function GET() {
\tconst supabase = await createClient();
\tconst { data: posts } = await supabase.from("posts").select("*");
\treturn NextResponse.json({ posts });
}
export async function POST(request: Request) {
\tconst supabase = await createClient();
\tconst body = await request.json();
\tconst { data, error } = await supabase
\t\t.from("posts")
\t\t.insert(body)
\t\t.select()
\t\t.single();
\tif (error) {
\t\treturn NextResponse.json({ error: error.message }, { status: 400 });
\t}
\treturn NextResponse.json({ post: data });
}Database Migrations
数据库迁移
Create Migration
创建迁移
bash
supabase migration new create_posts_tablebash
supabase migration new create_posts_tableMigration File Example
迁移文件示例
sql
-- supabase/migrations/20241116000000_create_posts_table.sql
-- Create table
CREATE TABLE posts (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID REFERENCES auth.users NOT NULL,
title TEXT NOT NULL,
content TEXT,
published BOOLEAN DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
);
-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Create policies
CREATE POLICY "Public posts are viewable by everyone"
ON posts FOR SELECT
USING (published = true);
CREATE POLICY "Users can view their own posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY "Users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (auth.uid() = user_id);
-- Create indexes
CREATE INDEX posts_user_id_idx ON posts(user_id);
CREATE INDEX posts_created_at_idx ON posts(created_at DESC);
CREATE INDEX posts_published_idx ON posts(published) WHERE published = true;
-- Create updated_at trigger
CREATE OR REPLACE FUNCTION handle_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION handle_updated_at();sql
-- supabase/migrations/20241116000000_create_posts_table.sql
-- 创建表
CREATE TABLE posts (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID REFERENCES auth.users NOT NULL,
title TEXT NOT NULL,
content TEXT,
published BOOLEAN DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
);
-- 启用RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- 创建策略
CREATE POLICY "Public posts are viewable by everyone"
ON posts FOR SELECT
USING (published = true);
CREATE POLICY "Users can view their own posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY "Users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (auth.uid() = user_id);
-- 创建索引
CREATE INDEX posts_user_id_idx ON posts(user_id);
CREATE INDEX posts_created_at_idx ON posts(created_at DESC);
CREATE INDEX posts_published_idx ON posts(published) WHERE published = true;
-- 创建updated_at触发器
CREATE OR REPLACE FUNCTION handle_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION handle_updated_at();Run Migrations
运行迁移
bash
undefinedbash
undefinedApply migrations locally
本地应用迁移
supabase db reset
supabase db reset
Push to remote (production)
推送到远程(生产环境)
supabase db push
undefinedsupabase db push
undefinedTypeScript Integration
TypeScript集成
Database Type Generation
数据库类型生成
bash
undefinedbash
undefinedGenerate types from your database schema
从数据库架构生成类型
supabase gen types typescript --project-id YOUR_PROJECT_ID > types/supabase.ts
supabase gen types typescript --project-id YOUR_PROJECT_ID > types/supabase.ts
Or from local development
或从本地开发环境生成
supabase gen types typescript --local > types/supabase.ts
undefinedsupabase gen types typescript --local > types/supabase.ts
undefinedType-Safe Client
类型安全客户端
typescript
// lib/supabase/types.ts
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json | undefined }
| Json[];
export interface Database {
public: {
Tables: {
posts: {
Row: {
id: string;
created_at: string;
title: string;
content: string | null;
user_id: string;
published: boolean;
};
Insert: {
id?: string;
created_at?: string;
title: string;
content?: string | null;
user_id: string;
published?: boolean;
};
Update: {
id?: string;
created_at?: string;
title?: string;
content?: string | null;
user_id?: string;
published?: boolean;
};
};
profiles: {
Row: {
id: string;
name: string | null;
avatar_url: string | null;
created_at: string;
};
Insert: {
id: string;
name?: string | null;
avatar_url?: string | null;
created_at?: string;
};
Update: {
id?: string;
name?: string | null;
avatar_url?: string | null;
created_at?: string;
};
};
};
Views: {
[_ in never]: never;
};
Functions: {
[_ in never]: never;
};
Enums: {
[_ in never]: never;
};
};
}
// lib/supabase/client.ts
import { createClient } from "@supabase/supabase-js";
import { Database } from "./types";
export const supabase = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
// Now you get full type safety!
const { data } = await supabase
.from("posts") // ✅ TypeScript knows this table exists
.select("title, content, profiles(name)") // ✅ TypeScript validates columns
.eq("published", true); // ✅ TypeScript validates types
// data is typed as:
// Array<{ title: string; content: string | null; profiles: { name: string | null } }>typescript
// lib/supabase/types.ts
export type Json =
\t| string
\t| number
\t| boolean
\t| null
\t| { [key: string]: Json | undefined }
\t| Json[];
export interface Database {
\tpublic: {
\t\tTables: {
\t\t\tposts: {
\t\t\t\tRow: {
\t\t\t\t\tid: string;
\t\t\t\t\tcreated_at: string;
\t\t\t\t\ttitle: string;
\t\t\t\t\tcontent: string | null;
\t\t\t\t\tuser_id: string;
\t\t\t\t\tpublished: boolean;
\t\t\t\t};
\t\t\t\tInsert: {
\t\t\t\t\tid?: string;
\t\t\t\t\tcreated_at?: string;
\t\t\t\t\ttitle: string;
\t\t\t\t\tcontent?: string | null;
\t\t\t\t\tuser_id: string;
\t\t\t\t\tpublished?: boolean;
\t\t\t\t};
\t\t\t\tUpdate: {
\t\t\t\t\tid?: string;
\t\t\t\t\tcreated_at?: string;
\t\t\t\t\ttitle?: string;
\t\t\t\t\tcontent?: string | null;
\t\t\t\t\tuser_id?: string;
\t\t\t\t\tpublished?: boolean;
\t\t\t\t};
\t\t\t};
\t\t\tprofiles: {
\t\t\t\tRow: {
\t\t\t\t\tid: string;
\t\t\t\t\tname: string | null;
\t\t\t\t\tavatar_url: string | null;
\t\t\t\t\tcreated_at: string;
\t\t\t\t};
\t\t\t\tInsert: {
\t\t\t\t\tid: string;
\t\t\t\t\tname?: string | null;
\t\t\t\t\tavatar_url?: string | null;
\t\t\t\t\tcreated_at?: string;
\t\t\t\t};
\t\t\t\tUpdate: {
\t\t\t\t\tid?: string;
\t\t\t\t\tname?: string | null;
\t\t\t\t\tavatar_url?: string | null;
\t\t\t\t\tcreated_at?: string;
\t\t\t\t};
\t\t\t};
\t\t};
\t\tViews: {
\t\t\t[_ in never]: never;
\t\t};
\t\tFunctions: {
\t\t\t[_ in never]: never;
\t\t};
\t\tEnums: {
\t\t\t[_ in never]: never;
\t\t};
\t};
}
// lib/supabase/client.ts
import { createClient } from "@supabase/supabase-js";
import { Database } from "./types";
export const supabase = createClient<Database>(
\tprocess.env.NEXT_PUBLIC_SUPABASE_URL!,
\tprocess.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
// 现在你获得了完整的类型安全!
const { data } = await supabase
\t.from("posts") // ✅ TypeScript知道这个表存在
\t.select("title, content, profiles(name)") // ✅ TypeScript验证列
\t.eq("published", true); // ✅ TypeScript验证类型
// data的类型是:
// Array<{ title: string; content: string | null; profiles: { name: string | null } }>Server vs Client Supabase
服务器端与客户端Supabase
typescript
// lib/supabase/client.ts - Client-side (respects RLS)
import { createBrowserClient } from "@supabase/ssr";
import { Database } from "./types";
export function createClient() {
return createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
}
// lib/supabase/server.ts - Server-side (Next.js App Router)
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import { Database } from "./types";
export async function createClient() {
const cookieStore = await cookies();
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
for (const { name, value, options } of cookiesToSet) {
cookieStore.set(name, value, options);
}
} catch {
// Called from Server Component - ignore
}
},
},
},
);
}
// lib/supabase/admin.ts - Admin client (bypasses RLS)
import { createClient } from "@supabase/supabase-js";
import { Database } from "./types";
export const supabaseAdmin = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!, // ⚠️ Server-side only!
{
auth: {
autoRefreshToken: false,
persistSession: false,
},
},
);typescript
// lib/supabase/client.ts - 客户端(遵循RLS)
import { createBrowserClient } from "@supabase/ssr";
import { Database } from "./types";
export function createClient() {
\treturn createBrowserClient<Database>(
\t\tprocess.env.NEXT_PUBLIC_SUPABASE_URL!,
\t\tprocess.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
\t);
}
// lib/supabase/server.ts - 服务器端(Next.js App Router)
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import { Database } from "./types";
export async function createClient() {
\tconst cookieStore = await cookies();
\treturn createServerClient<Database>(
\t\tprocess.env.NEXT_PUBLIC_SUPABASE_URL!,
\t\tprocess.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
\t\t{
\t\t\tcookies: {
\t\t\t\tgetAll() {
\t\t\t\t\treturn cookieStore.getAll();
\t\t\t\t},
\t\t\t\tsetAll(cookiesToSet) {
\t\t\t\t\ttry {
\t\t\t\t\t\tfor (const { name, value, options } of cookiesToSet) {
\t\t\t\t\t\t\tcookieStore.set(name, value, options);
\t\t\t\t\t\t}
\t\t\t\t\t} catch {
\t\t\t\t\t\t// 从服务器组件调用 - 忽略
\t\t\t\t\t}
\t\t\t\t},
\t\t\t},
\t\t},
\t);
}
// lib/supabase/admin.ts - 管理员客户端(绕过RLS)
import { createClient } from "@supabase/supabase-js";
import { Database } from "./types";
export const supabaseAdmin = createClient<Database>(
\tprocess.env.NEXT_PUBLIC_SUPABASE_URL!,
\tprocess.env.SUPABASE_SERVICE_ROLE_KEY!, // ⚠️ 仅服务器端使用!
\t{
\t\tauth: {
\t\t\tautoRefreshToken: false,
\t\t\tpersistSession: false,
\t\t},
\t},
);Next.js App Router Patterns
Next.js App Router模式
Server Components (Recommended)
服务器组件(推荐)
typescript
// app/posts/page.tsx
import { createClient } from '@/lib/supabase/server'
export default async function PostsPage() {
const supabase = createClient()
// Fetch data on server (no loading state needed!)
const { data: posts } = await supabase
.from('posts')
.select('*, profiles(*)')
.eq('published', true)
.order('created_at', { ascending: false })
return (
<div>
{posts?.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>By {post.profiles?.name}</p>
<div>{post.content}</div>
</article>
))}
</div>
)
}typescript
// app/posts/page.tsx
import { createClient } from '@/lib/supabase/server'
export default async function PostsPage() {
const supabase = createClient()
// 在服务器端获取数据(无需加载状态!)
const { data: posts } = await supabase
.from('posts')
.select('*, profiles(*)')
.eq('published', true)
.order('created_at', { ascending: false })
return (
<div>
{posts?.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>By {post.profiles?.name}</p>
<div>{post.content}</div>
</article>
))}
</div>
)
}Server Actions for Mutations
服务器操作用于变更
typescript
// app/actions/posts.ts
"use server";
import { createClient } from "@/lib/supabase/server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
export async function createPost(formData: FormData) {
const supabase = createClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
redirect("/login");
}
const title = formData.get("title") as string;
const content = formData.get("content") as string;
const { error } = await supabase.from("posts").insert({
title,
content,
user_id: user.id,
});
if (error) {
throw new Error(error.message);
}
revalidatePath("/posts");
redirect("/posts");
}
export async function updatePost(id: string, formData: FormData) {
const supabase = createClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
throw new Error("Unauthorized");
}
const { error } = await supabase
.from("posts")
.update({
title: formData.get("title") as string,
content: formData.get("content") as string,
})
.eq("id", id)
.eq("user_id", user.id); // Ensure user owns the post
if (error) {
throw new Error(error.message);
}
revalidatePath("/posts");
}
export async function deletePost(id: string) {
const supabase = createClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
throw new Error("Unauthorized");
}
const { error } = await supabase
.from("posts")
.delete()
.eq("id", id)
.eq("user_id", user.id);
if (error) {
throw new Error(error.message);
}
revalidatePath("/posts");
}typescript
// app/actions/posts.ts
"use server";
import { createClient } from "@/lib/supabase/server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
export async function createPost(formData: FormData) {
\tconst supabase = createClient();
\tconst {
\t\tdata: { user },
\t} = await supabase.auth.getUser();
\tif (!user) {
\t\tredirect("/login");
\t}
\tconst title = formData.get("title") as string;
\tconst content = formData.get("content") as string;
\tconst { error } = await supabase.from("posts").insert({
\t\ttitle,
\t\tcontent,
\t\tuser_id: user.id,
\t});
\tif (error) {
\t\tthrow new Error(error.message);
\t}
\trevalidatePath("/posts");
\tredirect("/posts");
}
export async function updatePost(id: string, formData: FormData) {
\tconst supabase = createClient();
\tconst {
\t\tdata: { user },
\t} = await supabase.auth.getUser();
\tif (!user) {
\t\tthrow new Error("Unauthorized");
\t}
\tconst { error } = await supabase
\t\t.from("posts")
\t\t.update({
\t\t\ttitle: formData.get("title") as string,
\t\t\tcontent: formData.get("content") as string,
\t\t})
\t\t.eq("id", id)
\t\t.eq("user_id", user.id); // 确保用户拥有该帖子
\tif (error) {
\t\tthrow new Error(error.message);
\t}
\trevalidatePath("/posts");
}
export async function deletePost(id: string) {
\tconst supabase = createClient();
\tconst {
\t\tdata: { user },
\t} = await supabase.auth.getUser();
\tif (!user) {
\t\tthrow new Error("Unauthorized");
\t}
\tconst { error } = await supabase
\t\t.from("posts")
\t\t.delete()
\t\t.eq("id", id)
\t\t.eq("user_id", user.id);
\tif (error) {
\t\tthrow new Error(error.message);
\t}
\trevalidatePath("/posts");
}Client Component with Real-time
带实时功能的客户端组件
typescript
// app/components/PostsList.tsx
'use client'
import { useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'
import { Database } from '@/lib/supabase/types'
type Post = Database['public']['Tables']['posts']['Row']
export function PostsList({ initialPosts }: { initialPosts: Post[] }) {
const [posts, setPosts] = useState(initialPosts)
const supabase = createClient()
useEffect(() => {
const channel = supabase
.channel('posts-changes')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'posts',
filter: 'published=eq.true',
},
(payload) => {
if (payload.eventType === 'INSERT') {
setPosts(prev => [payload.new as Post, ...prev])
} else if (payload.eventType === 'UPDATE') {
setPosts(prev =>
prev.map(post =>
post.id === payload.new.id ? (payload.new as Post) : post
)
)
} else if (payload.eventType === 'DELETE') {
setPosts(prev => prev.filter(post => post.id !== payload.old.id))
}
}
)
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [supabase])
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
</div>
)
}typescript
// app/components/PostsList.tsx
'use client'
import { useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'
import { Database } from '@/lib/supabase/types'
type Post = Database['public']['Tables']['posts']['Row']
export function PostsList({ initialPosts }: { initialPosts: Post[] }) {
const [posts, setPosts] = useState(initialPosts)
const supabase = createClient()
useEffect(() => {
const channel = supabase
.channel('posts-changes')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'posts',
filter: 'published=eq.true',
},
(payload) => {
if (payload.eventType === 'INSERT') {
setPosts(prev => [payload.new as Post, ...prev])
} else if (payload.eventType === 'UPDATE') {
setPosts(prev =>
prev.map(post =>
post.id === payload.new.id ? (payload.new as Post) : post
)
)
} else if (payload.eventType === 'DELETE') {
setPosts(prev => prev.filter(post => post.id !== payload.old.id))
}
}
)
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [supabase])
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
</div>
)
}Route Handlers
路由处理器
typescript
// app/api/posts/route.ts
import { createClient } from "@/lib/supabase/server";
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const supabase = createClient();
const { searchParams } = new URL(request.url);
const limit = parseInt(searchParams.get("limit") || "10");
const { data, error } = await supabase
.from("posts")
.select("*")
.eq("published", true)
.order("created_at", { ascending: false })
.limit(limit);
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json(data);
}
export async function POST(request: NextRequest) {
const supabase = createClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body = await request.json();
const { data, error } = await supabase
.from("posts")
.insert({
...body,
user_id: user.id,
})
.select()
.single();
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json(data);
}typescript
// app/api/posts/route.ts
import { createClient } from "@/lib/supabase/server";
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
\tconst supabase = createClient();
\tconst { searchParams } = new URL(request.url);
\tconst limit = parseInt(searchParams.get("limit") || "10");
\tconst { data, error } = await supabase
\t\t.from("posts")
\t\t.select("*")
\t\t.eq("published", true)
\t\t.order("created_at", { ascending: false })
\t\t.limit(limit);
\tif (error) {
\t\treturn NextResponse.json({ error: error.message }, { status: 500 });
\t}
\treturn NextResponse.json(data);
}
export async function POST(request: NextRequest) {
\tconst supabase = createClient();
\tconst {
\t\tdata: { user },
\t} = await supabase.auth.getUser();
\tif (!user) {
\t\treturn NextResponse.json({ error: "Unauthorized" }, { status: 401 });
\t}
\tconst body = await request.json();
\tconst { data, error } = await supabase
\t\t.from("posts")
\t\t.insert({
\t\t\t...body,
\t\t\tuser_id: user.id,
\t\t})
\t\t.select()
\t\t.single();
\tif (error) {
\t\treturn NextResponse.json({ error: error.message }, { status: 500 });
\t}
\treturn NextResponse.json(data);
}Advanced Authentication
高级身份认证
Email/Password with Email Confirmation
邮箱/密码带邮箱确认
typescript
// app/actions/auth.ts
"use server";
import { createClient } from "@/lib/supabase/server";
import { redirect } from "next/navigation";
export async function signUp(formData: FormData) {
const supabase = createClient();
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const name = formData.get("name") as string;
const { error } = await supabase.auth.signUp({
email,
password,
options: {
data: {
name, // Stored in auth.users.raw_user_meta_data
},
emailRedirectTo: `${process.env.NEXT_PUBLIC_URL}/auth/callback`,
},
});
if (error) {
return { error: error.message };
}
return { success: true, message: "Check your email to confirm your account" };
}
export async function signIn(formData: FormData) {
const supabase = createClient();
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
return { error: error.message };
}
redirect("/dashboard");
}
export async function signOut() {
const supabase = createClient();
await supabase.auth.signOut();
redirect("/");
}typescript
// app/actions/auth.ts
"use server";
import { createClient } from "@/lib/supabase/server";
import { redirect } from "next/navigation";
export async function signUp(formData: FormData) {
\tconst supabase = createClient();
\tconst email = formData.get("email") as string;
\tconst password = formData.get("password") as string;
\tconst name = formData.get("name") as string;
\tconst { error } = await supabase.auth.signUp({
\t\temail,
\t\tpassword,
\t\toptions: {
\t\t\tdata: {
\t\t\t\tname, // 存储在auth.users.raw_user_meta_data中
\t\t\t},
\t\t\temailRedirectTo: `${process.env.NEXT_PUBLIC_URL}/auth/callback`,
\t\t},
\t});
\tif (error) {
\t\treturn { error: error.message };
\t}
\treturn { success: true, message: "检查你的邮箱以确认账户" };
}
export async function signIn(formData: FormData) {
\tconst supabase = createClient();
\tconst email = formData.get("email") as string;
\tconst password = formData.get("password") as string;
\tconst { error } = await supabase.auth.signInWithPassword({
\t\temail,
\t\tpassword,
\t});
\tif (error) {
\t\treturn { error: error.message };
\t}
\tredirect("/dashboard");
}
export async function signOut() {
\tconst supabase = createClient();
\tawait supabase.auth.signOut();
\tredirect("/");
}OAuth (Google, GitHub, etc.)
OAuth(Google、GitHub等)
typescript
// app/actions/auth.ts
export async function signInWithGoogle() {
const supabase = createClient();
const { data, error } = await supabase.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: `${process.env.NEXT_PUBLIC_URL}/auth/callback`,
queryParams: {
access_type: "offline",
prompt: "consent",
},
},
});
if (data?.url) {
redirect(data.url);
}
}
export async function signInWithGitHub() {
const supabase = createClient();
const { data } = await supabase.auth.signInWithOAuth({
provider: "github",
options: {
redirectTo: `${process.env.NEXT_PUBLIC_URL}/auth/callback`,
scopes: "read:user user:email",
},
});
if (data?.url) {
redirect(data.url);
}
}typescript
// app/actions/auth.ts
export async function signInWithGoogle() {
\tconst supabase = createClient();
\tconst { data, error } = await supabase.auth.signInWithOAuth({
\t\tprovider: "google",
\t\toptions: {
\t\t\tredirectTo: `${process.env.NEXT_PUBLIC_URL}/auth/callback`,
\t\t\tqueryParams: {
\t\t\t\taccess_type: "offline",
\t\t\t\tprompt: "consent",
\t\t\t},
\t\t},
\t});
\tif (data?.url) {
\t\tredirect(data.url);
\t}
}
export async function signInWithGitHub() {
\tconst supabase = createClient();
\tconst { data } = await supabase.auth.signInWithOAuth({
\t\tprovider: "github",
\t\toptions: {
\t\t\tredirectTo: `${process.env.NEXT_PUBLIC_URL}/auth/callback`,
\t\t\tscopes: "read:user user:email",
\t\t},
\t});
\tif (data?.url) {
\t\tredirect(data.url);
\t}
}Magic Links
魔法链接
typescript
export async function sendMagicLink(email: string) {
const supabase = createClient();
const { error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: `${process.env.NEXT_PUBLIC_URL}/auth/callback`,
},
});
if (error) {
return { error: error.message };
}
return { success: true, message: "Check your email for the login link" };
}typescript
export async function sendMagicLink(email: string) {
\tconst supabase = createClient();
\tconst { error } = await supabase.auth.signInWithOtp({
\t\temail,
\t\toptions: {
\t\t\temailRedirectTo: `${process.env.NEXT_PUBLIC_URL}/auth/callback`,
\t\t},
\t});
\tif (error) {
\t\treturn { error: error.message };
\t}
\treturn { success: true, message: "检查你的邮箱获取登录链接" };
}Phone Auth (SMS)
手机认证(SMS)
typescript
export async function sendPhoneOTP(phone: string) {
const supabase = createClient();
const { error } = await supabase.auth.signInWithOtp({
phone,
});
if (error) {
return { error: error.message };
}
return { success: true };
}
export async function verifyPhoneOTP(phone: string, token: string) {
const supabase = createClient();
const { error } = await supabase.auth.verifyOtp({
phone,
token,
type: "sms",
});
if (error) {
return { error: error.message };
}
redirect("/dashboard");
}typescript
export async function sendPhoneOTP(phone: string) {
\tconst supabase = createClient();
\tconst { error } = await supabase.auth.signInWithOtp({
\t\tphone,
\t});
\tif (error) {
\t\treturn { error: error.message };
\t}
\treturn { success: true };
}
export async function verifyPhoneOTP(phone: string, token: string) {
\tconst supabase = createClient();
\tconst { error } = await supabase.auth.verifyOtp({
\t\tphone,
\t\ttoken,
\t\ttype: "sms",
\t});
\tif (error) {
\t\treturn { error: error.message };
\t}
\tredirect("/dashboard");
}Multi-Factor Authentication (MFA)
多因素认证(MFA)
typescript
// Enable MFA for user
export async function enableMFA() {
const supabase = createClient();
const { data, error } = await supabase.auth.mfa.enroll({
factorType: "totp",
friendlyName: "Authenticator App",
});
if (error) {
throw error;
}
// data.totp.qr_code - QR code to scan
// data.totp.secret - Secret to enter manually
return data;
}
// Verify MFA
export async function verifyMFA(factorId: string, code: string) {
const supabase = createClient();
const { data, error } = await supabase.auth.mfa.challengeAndVerify({
factorId,
code,
});
if (error) {
throw error;
}
return data;
}typescript
// 为用户启用MFA
export async function enableMFA() {
\tconst supabase = createClient();
\tconst { data, error } = await supabase.auth.mfa.enroll({
\t\tfactorType: "totp",
\t\tfriendlyName: "Authenticator App",
\t});
\tif (error) {
\t\tthrow error;
\t}
\t// data.totp.qr_code - 要扫描的二维码
\t// data.totp.secret - 手动输入的密钥
\treturn data;
}
// 验证MFA
export async function verifyMFA(factorId: string, code: string) {
\tconst supabase = createClient();
\tconst { data, error } = await supabase.auth.mfa.challengeAndVerify({
\t\tfactorId,
\t\tcode,
\t});
\tif (error) {
\t\tthrow error;
\t}
\treturn data;
}Auth Callback Handler
认证回调处理器
typescript
// app/auth/callback/route.ts
import { createClient } from "@/lib/supabase/server";
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const requestUrl = new URL(request.url);
const code = requestUrl.searchParams.get("code");
if (code) {
const supabase = createClient();
await supabase.auth.exchangeCodeForSession(code);
}
// Redirect to dashboard or wherever
return NextResponse.redirect(new URL("/dashboard", request.url));
}typescript
// app/auth/callback/route.ts
import { createClient } from "@/lib/supabase/server";
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
\tconst requestUrl = new URL(request.url);
\tconst code = requestUrl.searchParams.get("code");
\tif (code) {
\t\tconst supabase = createClient();
\t\tawait supabase.auth.exchangeCodeForSession(code);
\t}
\t// 重定向到仪表板或其他页面
\treturn NextResponse.redirect(new URL("/dashboard", request.url));
}Protected Routes
受保护路由
typescript
// middleware.ts
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
export async function middleware(request: NextRequest) {
let response = NextResponse.next({
request,
});
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
for (const { name, value } of cookiesToSet) {
request.cookies.set(name, value);
}
response = NextResponse.next({
request,
});
for (const { name, value, options } of cookiesToSet) {
response.cookies.set(name, value, options);
}
},
},
},
);
const { data, error } = await supabase.auth.getClaims();
if (error) {
throw error;
}
const user = data?.claims;
// Protect dashboard routes
if (request.nextUrl.pathname.startsWith("/dashboard") && !user) {
return NextResponse.redirect(new URL("/login", request.url));
}
// Redirect authenticated users away from auth pages
if (request.nextUrl.pathname.startsWith("/login") && user) {
return NextResponse.redirect(new URL("/dashboard", request.url));
}
return response;
}
export const config = {
matcher: ["/dashboard/:path*", "/login", "/signup"],
};typescript
// middleware.ts
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
export async function middleware(request: NextRequest) {
\tlet response = NextResponse.next({
\t\trequest,
\t});
\tconst supabase = createServerClient(
\t\tprocess.env.NEXT_PUBLIC_SUPABASE_URL!,
\t\tprocess.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
\t\t{
\t\t\tcookies: {
\t\t\t\tgetAll() {
\t\t\t\t\treturn request.cookies.getAll();
\t\t\t\t},
\t\t\t\tsetAll(cookiesToSet) {
\t\t\t\t\tfor (const { name, value } of cookiesToSet) {
\t\t\t\t\t\trequest.cookies.set(name, value);
\t\t\t\t\t}
\t\t\t\t\tresponse = NextResponse.next({
\t\t\t\t\t\trequest,
\t\t\t\t\t});
\t\t\t\t\tfor (const { name, value, options } of cookiesToSet) {
\t\t\t\t\t\tresponse.cookies.set(name, value, options);
\t\t\t\t\t}
\t\t\t\t},
\t\t\t},
\t\t},
\t);
\tconst { data, error } = await supabase.auth.getClaims();
\tif (error) {
\t\tthrow error;
\t}
\tconst user = data?.claims;
\t// 保护仪表板路由
\tif (request.nextUrl.pathname.startsWith("/dashboard") && !user) {
\t\treturn NextResponse.redirect(new URL("/login", request.url));
\t}
\t// 已认证用户重定向离开认证页面
\tif (request.nextUrl.pathname.startsWith("/login") && user) {
\t\treturn NextResponse.redirect(new URL("/dashboard", request.url));
\t}
\treturn response;
}
export const config = {
\tmatcher: ["/dashboard/:path*", "/login", "/signup"],
};Advanced Row Level Security
高级行级安全
Complex RLS Policies
复杂RLS策略
sql
-- Users can only see published posts or their own drafts
CREATE POLICY "Users can read appropriate posts"
ON posts FOR SELECT
USING (
published = true
OR
auth.uid() = user_id
);
-- Users can update only their own posts
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- Team-based access
CREATE TABLE teams (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL
);
CREATE TABLE team_members (
team_id UUID REFERENCES teams,
user_id UUID REFERENCES auth.users,
role TEXT CHECK (role IN ('owner', 'admin', 'member')),
PRIMARY KEY (team_id, user_id)
);
CREATE TABLE team_documents (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
team_id UUID REFERENCES teams,
title TEXT,
content TEXT
);
-- Only team members can see team documents
CREATE POLICY "Team members can view documents"
ON team_documents FOR SELECT
USING (
team_id IN (
SELECT team_id
FROM team_members
WHERE user_id = auth.uid()
)
);
-- Only team owners/admins can delete
CREATE POLICY "Team admins can delete documents"
ON team_documents FOR DELETE
USING (
team_id IN (
SELECT team_id
FROM team_members
WHERE user_id = auth.uid()
AND role IN ('owner', 'admin')
)
);sql
// 用户只能查看已发布的帖子或自己的草稿
CREATE POLICY "Users can read appropriate posts"
ON posts FOR SELECT
USING (
published = true
OR
auth.uid() = user_id
);
// 用户只能更新自己的帖子
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
// 团队级访问
CREATE TABLE teams (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL
);
CREATE TABLE team_members (
team_id UUID REFERENCES teams,
user_id UUID REFERENCES auth.users,
role TEXT CHECK (role IN ('owner', 'admin', 'member')),
PRIMARY KEY (team_id, user_id)
);
CREATE TABLE team_documents (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
team_id UUID REFERENCES teams,
title TEXT,
content TEXT
);
// 只有团队成员可以查看团队文档
CREATE POLICY "Team members can view documents"
ON team_documents FOR SELECT
USING (
team_id IN (
SELECT team_id
FROM team_members
WHERE user_id = auth.uid()
)
);
// 只有团队所有者/管理员可以删除
CREATE POLICY "Team admins can delete documents"
ON team_documents FOR DELETE
USING (
team_id IN (
SELECT team_id
FROM team_members
WHERE user_id = auth.uid()
AND role IN ('owner', 'admin')
)
);Function-Based RLS
基于函数的RLS
sql
-- Create helper function
CREATE OR REPLACE FUNCTION is_team_admin(team_id UUID)
RETURNS BOOLEAN AS $$
BEGIN
RETURN EXISTS (
SELECT 1
FROM team_members
WHERE team_members.team_id = is_team_admin.team_id
AND team_members.user_id = auth.uid()
AND team_members.role IN ('owner', 'admin')
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Use in policy
CREATE POLICY "Admins can update team settings"
ON teams FOR UPDATE
USING (is_team_admin(id))
WITH CHECK (is_team_admin(id));sql
// 创建辅助函数
CREATE OR REPLACE FUNCTION is_team_admin(team_id UUID)
RETURNS BOOLEAN AS $$
BEGIN
RETURN EXISTS (
SELECT 1
FROM team_members
WHERE team_members.team_id = is_team_admin.team_id
AND team_members.user_id = auth.uid()
AND team_members.role IN ('owner', 'admin')
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
// 在策略中使用
CREATE POLICY "Admins can update team settings"
ON teams FOR UPDATE
USING (is_team_admin(id))
WITH CHECK (is_team_admin(id));RLS with JWT Claims
带JWT声明的RLS
sql
-- Access custom JWT claims
CREATE POLICY "Premium users can view premium content"
ON premium_content FOR SELECT
USING (
(auth.jwt() -> 'user_metadata' ->> 'subscription_tier') = 'premium'
);
-- Role-based access
CREATE POLICY "Admins have full access"
ON sensitive_data FOR ALL
USING (
(auth.jwt() -> 'user_metadata' ->> 'role') = 'admin'
);sql
// 访问自定义JWT声明
CREATE POLICY "Premium users can view premium content"
ON premium_content FOR SELECT
USING (
(auth.jwt() -> 'user_metadata' ->> 'subscription_tier') = 'premium'
);
// 基于角色的访问
CREATE POLICY "Admins have full access"
ON sensitive_data FOR ALL
USING (
(auth.jwt() -> 'user_metadata' ->> 'role') = 'admin'
);Advanced Real-time Features
高级实时功能
Presence (Who's Online)
在线状态(谁在线)
typescript
'use client'
import { useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'
export function OnlineUsers() {
const [onlineUsers, setOnlineUsers] = useState<any[]>([])
const supabase = createClient()
useEffect(() => {
const channel = supabase.channel('online-users')
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
const users = Object.values(state).flat()
setOnlineUsers(users)
})
.on('presence', { event: 'join' }, ({ newPresences }) => {
console.log('Users joined:', newPresences)
})
.on('presence', { event: 'leave' }, ({ leftPresences }) => {
console.log('Users left:', leftPresences)
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
// Track this user
const { data: { user } } = await supabase.auth.getUser()
if (user) {
await channel.track({
user_id: user.id,
email: user.email,
online_at: new Date().toISOString(),
})
}
}
})
return () => {
supabase.removeChannel(channel)
}
}, [])
return (
<div>
<h3>{onlineUsers.length} users online</h3>
<ul>
{onlineUsers.map((user, i) => (
<li key={i}>{user.email}</li>
))}
</ul>
</div>
)
}typescript
'use client'
import { useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'
export function OnlineUsers() {
const [onlineUsers, setOnlineUsers] = useState<any[]>([])
const supabase = createClient()
useEffect(() => {
const channel = supabase.channel('online-users')
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
const users = Object.values(state).flat()
setOnlineUsers(users)
})
.on('presence', { event: 'join' }, ({ newPresences }) => {
console.log('用户加入:', newPresences)
})
.on('presence', { event: 'leave' }, ({ leftPresences }) => {
console.log('用户离开:', leftPresences)
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
// 追踪当前用户
const { data: { user } } = await supabase.auth.getUser()
if (user) {
await channel.track({
user_id: user.id,
email: user.email,
online_at: new Date().toISOString(),
})
}
}
})
return () => {
supabase.removeChannel(channel)
}
}, [])
return (
<div>
<h3>{onlineUsers.length} 用户在线</h3>
<ul>
{onlineUsers.map((user, i) => (
<li key={i}>{user.email}</li>
))}
</ul>
</div>
)
}Broadcast (Send Messages)
广播(发送消息)
typescript
// Cursor tracking
export function CollaborativeCanvas() {
const supabase = createClient()
useEffect(() => {
const channel = supabase.channel('canvas')
channel
.on('broadcast', { event: 'cursor' }, (payload) => {
// Update cursor position
updateCursor(payload.payload)
})
.subscribe()
// Send cursor position
const handleMouseMove = (e: MouseEvent) => {
channel.send({
type: 'broadcast',
event: 'cursor',
payload: { x: e.clientX, y: e.clientY },
})
}
window.addEventListener('mousemove', handleMouseMove)
return () => {
window.removeEventListener('mousemove', handleMouseMove)
supabase.removeChannel(channel)
}
}, [])
return <canvas />
}typescript
// 光标追踪
export function CollaborativeCanvas() {
const supabase = createClient()
useEffect(() => {
const channel = supabase.channel('canvas')
channel
.on('broadcast', { event: 'cursor' }, (payload) => {
// 更新光标位置
updateCursor(payload.payload)
})
.subscribe()
// 发送光标位置
const handleMouseMove = (e: MouseEvent) => {
channel.send({
type: 'broadcast',
event: 'cursor',
payload: { x: e.clientX, y: e.clientY },
})
}
window.addEventListener('mousemove', handleMouseMove)
return () => {
window.removeEventListener('mousemove', handleMouseMove)
supabase.removeChannel(channel)
}
}, [])
return <canvas />
}Postgres Changes (Database Events)
Postgres变更(数据库事件)
typescript
// Listen to specific columns
const channel = supabase
.channel("post-changes")
.on(
"postgres_changes",
{
event: "UPDATE",
schema: "public",
table: "posts",
filter: "id=eq.123", // Specific row
},
(payload) => {
console.log("Post updated:", payload);
},
)
.subscribe();
// Listen to multiple tables
const channel = supabase
.channel("changes")
.on(
"postgres_changes",
{ event: "*", schema: "public", table: "posts" },
handlePostChange,
)
.on(
"postgres_changes",
{ event: "*", schema: "public", table: "comments" },
handleCommentChange,
)
.subscribe();typescript
// 监听特定列
const channel = supabase
\t.channel("post-changes")
\t.on(
\t\t"postgres_changes",
\t\t{
\t\t\tevent: "UPDATE",
\t\t\tschema: "public",
\t\t\ttable: "posts",
\t\t\tfilter: "id=eq.123", // 特定行
\t\t},
\t\t(payload) => {
\t\t\tconsole.log("帖子已更新:", payload);
\t\t},
\t)
\t.subscribe();
// 监听多个表
const channel = supabase
\t.channel("changes")
\t.on(
\t\t"postgres_changes",
\t\t{ event: "*", schema: "public", table: "posts" },
\t\thandlePostChange,
\t)
\t.on(
\t\t"postgres_changes",
\t\t{ event: "*", schema: "public", table: "comments" },
\t\thandleCommentChange,
\t)
\t.subscribe();Advanced Storage
高级存储
Image Transformations
图片转换
typescript
// Upload with transformation
export async function uploadAvatar(file: File, userId: string) {
const supabase = createClient();
const fileExt = file.name.split(".").pop();
const fileName = `${userId}-${Date.now()}.${fileExt}`;
const filePath = `avatars/${fileName}`;
const { error: uploadError } = await supabase.storage
.from("avatars")
.upload(filePath, file, {
cacheControl: "3600",
upsert: false,
});
if (uploadError) {
throw uploadError;
}
// Get transformed image URL
const { data } = supabase.storage.from("avatars").getPublicUrl(filePath, {
transform: {
width: 200,
height: 200,
resize: "cover",
quality: 80,
},
});
return data.publicUrl;
}typescript
// 上传带转换
export async function uploadAvatar(file: File, userId: string) {
\tconst supabase = createClient();
\tconst fileExt = file.name.split(".").pop();
\tconst fileName = `${userId}-${Date.now()}.${fileExt}`;
\tconst filePath = `avatars/${fileName}`;
\tconst { error: uploadError } = await supabase.storage
\t\t.from("avatars")
\t\t.upload(filePath, file, {
\t\t\tcacheControl: "3600",
\t\t\tupsert: false,
\t\t});
\tif (uploadError) {
\t\tthrow uploadError;
\t}
\t// 获取转换后的图片URL
\tconst { data } = supabase.storage.from("avatars").getPublicUrl(filePath, {
\t\ttransform: {
\t\t\twidth: 200,
\t\t\theight: 200,
\t\t\tresize: "cover",
\t\t\tquality: 80,
\t\t},
\t});
\treturn data.publicUrl;
}Signed URLs (Private Files)
签名URL(私有文件)
typescript
// Generate signed URL (expires after 1 hour)
export async function getPrivateFileUrl(path: string) {
const supabase = createClient();
const { data, error } = await supabase.storage
.from("private-files")
.createSignedUrl(path, 3600); // 1 hour
if (error) {
throw error;
}
return data.signedUrl;
}
// Upload to private bucket
export async function uploadPrivateFile(file: File, userId: string) {
const supabase = createClient();
const filePath = `${userId}/${file.name}`;
const { error } = await supabase.storage
.from("private-files")
.upload(filePath, file);
if (error) {
throw error;
}
return filePath;
}typescript
// 生成签名URL(1小时后过期)
export async function getPrivateFileUrl(path: string) {
\tconst supabase = createClient();
\tconst { data, error } = await supabase.storage
\t\t.from("private-files")
\t\t.createSignedUrl(path, 3600); // 1小时
\tif (error) {
\t\tthrow error;
\t}
\treturn data.signedUrl;
}
// 上传到私有存储桶
export async function uploadPrivateFile(file: File, userId: string) {
\tconst supabase = createClient();
\tconst filePath = `${userId}/${file.name}`;
\tconst { error } = await supabase.storage
\t\t.from("private-files")
\t\t.upload(filePath, file);
\tif (error) {
\t\tthrow error;
\t}
\treturn filePath;
}Storage RLS
存储RLS
sql
-- Enable RLS on storage.objects
CREATE POLICY "Users can upload to their own folder"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'avatars' AND
(storage.foldername(name))[1] = auth.uid()::text
);
CREATE POLICY "Users can view their own files"
ON storage.objects FOR SELECT
USING (
bucket_id = 'avatars' AND
(storage.foldername(name))[1] = auth.uid()::text
);
CREATE POLICY "Users can update their own files"
ON storage.objects FOR UPDATE
USING (
bucket_id = 'avatars' AND
(storage.foldername(name))[1] = auth.uid()::text
);
CREATE POLICY "Users can delete their own files"
ON storage.objects FOR DELETE
USING (
bucket_id = 'avatars' AND
(storage.foldername(name))[1] = auth.uid()::text
);sql
// 在storage.objects上启用RLS
CREATE POLICY "Users can upload to their own folder"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'avatars' AND
(storage.foldername(name))[1] = auth.uid()::text
);
CREATE POLICY "Users can view their own files"
ON storage.objects FOR SELECT
USING (
bucket_id = 'avatars' AND
(storage.foldername(name))[1] = auth.uid()::text
);
CREATE POLICY "Users can update their own files"
ON storage.objects FOR UPDATE
USING (
bucket_id = 'avatars' AND
(storage.foldername(name))[1] = auth.uid()::text
);
CREATE POLICY "Users can delete their own files"
ON storage.objects FOR DELETE
USING (
bucket_id = 'avatars' AND
(storage.foldername(name))[1] = auth.uid()::text
);Edge Functions
边缘函数
Basic Edge Function
基础边缘函数
typescript
// supabase/functions/hello/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
serve(async (req) => {
const { name } = await req.json();
return new Response(JSON.stringify({ message: `Hello ${name}!` }), {
headers: { "Content-Type": "application/json" },
});
});typescript
// supabase/functions/hello/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
serve(async (req) => {
\tconst { name } = await req.json();
\treturn new Response(JSON.stringify({ message: `Hello ${name}!` }), {
\t\theaders: { "Content-Type": "application/json" },
\t});
});Edge Function with Supabase Client
带Supabase客户端的边缘函数
typescript
// supabase/functions/create-post/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
serve(async (req) => {
try {
const supabaseClient = createClient(
Deno.env.get("SUPABASE_URL") ?? "",
Deno.env.get("SUPABASE_ANON_KEY") ?? "",
{
global: {
headers: { Authorization: req.headers.get("Authorization")! },
},
},
);
// Get authenticated user
const {
data: { user },
error: userError,
} = await supabaseClient.auth.getUser();
if (userError || !user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
});
}
const { title, content } = await req.json();
const { data, error } = await supabaseClient
.from("posts")
.insert({
title,
content,
user_id: user.id,
})
.select()
.single();
if (error) {
throw error;
}
return new Response(JSON.stringify(data), {
headers: { "Content-Type": "application/json" },
});
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
});
}
});typescript
// supabase/functions/create-post/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
serve(async (req) => {
\ttry {
\t\tconst supabaseClient = createClient(
\t\t\tDeno.env.get("SUPABASE_URL") ?? "",
\t\t\tDeno.env.get("SUPABASE_ANON_KEY") ?? "",
\t\t\t{
\t\t\t\tglobal: {
\t\t\t\t\theaders: { Authorization: req.headers.get("Authorization")! },
\t\t\t\t},
\t\t\t},
\t\t);
\t\t// 获取已认证用户
\t\tconst {
\t\t\tdata: { user },
\t\t\terror: userError,
\t\t} = await supabaseClient.auth.getUser();
\t\tif (userError || !user) {
\t\t\treturn new Response(JSON.stringify({ error: "Unauthorized" }), {
\t\t\t\tstatus: 401,
\t\t\t});
\t\t}
\t\tconst { title, content } = await req.json();
\t\tconst { data, error } = await supabaseClient
\t\t\t.from("posts")
\t\t\t.insert({
\t\t\t\ttitle,
\t\t\t\tcontent,
\t\t\t\tuser_id: user.id,
\t\t\t})
\t\t\t.select()
\t\t\t.single();
\t\tif (error) {
\t\t\tthrow error;
\t\t}
\t\treturn new Response(JSON.stringify(data), {
\t\t\theaders: { "Content-Type": "application/json" },
\t\t});
\t} catch (error) {
\t\treturn new Response(JSON.stringify({ error: error.message }), {
\t\t\tstatus: 500,
\t\t\theaders: { "Content-Type": "application/json" },
\t\t});
\t}
});Scheduled Edge Function (Cron)
定时边缘函数(Cron)
typescript
// supabase/functions/cleanup-old-data/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
serve(async (req) => {
// Verify request is from Supabase Cron
const authHeader = req.headers.get("Authorization");
if (authHeader !== `Bearer ${Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")}`) {
return new Response("Unauthorized", { status: 401 });
}
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
);
// Delete old data
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const { error } = await supabase
.from("temporary_data")
.delete()
.lt("created_at", thirtyDaysAgo.toISOString());
if (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
});
}
return new Response(JSON.stringify({ success: true }));
});
// Configure in Dashboard: Database > Cron Jobs
// Schedule: 0 2 * * * (2am daily)
// HTTP Request: https://your-project.supabase.co/functions/v1/cleanup-old-datatypescript
// supabase/functions/cleanup-old-data/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
serve(async (req) => {
\t// 验证请求来自Supabase Cron
\tconst authHeader = req.headers.get("Authorization");
\tif (authHeader !== `Bearer ${Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")}`) {
\t\treturn new Response("Unauthorized", { status: 401 });
\t}
\tconst supabase = createClient(
\t\tDeno.env.get("SUPABASE_URL")!,
\t\tDeno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
\t);
\t// 删除旧数据
\tconst thirtyDaysAgo = new Date();
\tthirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
\tconst { error } = await supabase
\t\t.from("temporary_data")
\t\t.delete()
\t\t.lt("created_at", thirtyDaysAgo.toISOString());
\tif (error) {
\t\treturn new Response(JSON.stringify({ error: error.message }), {
\t\t\tstatus: 500,
\t\t});
\t}
\treturn new Response(JSON.stringify({ success: true }));
});
// 在仪表板配置: Database > Cron Jobs
// 调度: 0 2 * * *(每天凌晨2点)
// HTTP请求: https://your-project.supabase.co/functions/v1/cleanup-old-dataInvoke Edge Function from Client
从客户端调用边缘函数
typescript
// Client-side
const { data, error } = await supabase.functions.invoke("hello", {
body: { name: "World" },
});
// With auth headers automatically included
const {
data: { session },
} = await supabase.auth.getSession();
const { data, error } = await supabase.functions.invoke("create-post", {
body: {
title: "My Post",
content: "Content here",
},
headers: {
Authorization: `Bearer ${session?.access_token}`,
},
});typescript
// 客户端
const { data, error } = await supabase.functions.invoke("hello", {
\tbody: { name: "World" },
});
// 自动包含认证头
const {
\tdata: { session },
} = await supabase.auth.getSession();
const { data, error } = await supabase.functions.invoke("create-post", {
\tbody: {
\t\ttitle: "My Post",
\t\tcontent: "Content here",
\t},
\theaders: {
\t\tAuthorization: `Bearer ${session?.access_token}`,
\t},
});Vector Search (AI/RAG)
向量搜索(AI/RAG)
Enable pgvector
启用pgvector
sql
-- Enable vector extension
CREATE EXTENSION IF NOT EXISTS vector;
-- Create table with embedding column
CREATE TABLE documents (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
content TEXT NOT NULL,
metadata JSONB,
embedding vector(1536), -- For OpenAI ada-002 (1536 dimensions)
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create index for fast similarity search
CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- Or use HNSW for better performance (Postgres 15+)
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);sql
// 启用向量扩展
CREATE EXTENSION IF NOT EXISTS vector;
// 创建带向量嵌入列的表
CREATE TABLE documents (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
content TEXT NOT NULL,
metadata JSONB,
embedding vector(1536), // 适用于OpenAI ada-002(1536维度)
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
// 创建ivfflat索引以加速相似性搜索
CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
// 或使用HNSW以获得更好的性能(Postgres 15+)
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);Generate and Store Embeddings
生成并存储向量嵌入
typescript
// lib/embeddings.ts
import { OpenAI } from "openai";
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY!,
});
export async function generateEmbedding(text: string): Promise<number[]> {
const response = await openai.embeddings.create({
model: "text-embedding-ada-002",
input: text,
});
return response.data[0].embedding;
}
// Store document with embedding
export async function storeDocument(content: string, metadata: any) {
const supabase = createClient();
const embedding = await generateEmbedding(content);
const { data, error } = await supabase
.from("documents")
.insert({
content,
metadata,
embedding,
})
.select()
.single();
if (error) {
throw error;
}
return data;
}typescript
// lib/embeddings.ts
import { OpenAI } from "openai";
const openai = new OpenAI({
\tapiKey: process.env.OPENAI_API_KEY!,
});
export async function generateEmbedding(text: string): Promise<number[]> {
\tconst response = await openai.embeddings.create({
\t\tmodel: "text-embedding-ada-002",
\t\tinput: text,
\t});
\treturn response.data[0].embedding;
}
// 存储带向量嵌入的文档
export async function storeDocument(content: string, metadata: any) {
\tconst supabase = createClient();
\tconst embedding = await generateEmbedding(content);
\tconst { data, error } = await supabase
\t\t.from("documents")
\t\t.insert({
\t\t\tcontent,
\t\t\tmetadata,
\t\t\tembedding,
\t\t})
\t\t.select()
\t\t.single();
\tif (error) {
\t\tthrow error;
\t}
\treturn data;
}Semantic Search
语义搜索
typescript
// Search similar documents
export async function searchSimilarDocuments(query: string, limit = 5) {
const supabase = createClient();
// Generate embedding for query
const queryEmbedding = await generateEmbedding(query);
// Search with RPC function
const { data, error } = await supabase.rpc("match_documents", {
query_embedding: queryEmbedding,
match_threshold: 0.78, // Minimum similarity
match_count: limit,
});
if (error) {
throw error;
}
return data;
}
// Create the RPC functionsql
CREATE OR REPLACE FUNCTION match_documents (
query_embedding vector(1536),
match_threshold float,
match_count int
)
RETURNS TABLE (
id UUID,
content TEXT,
metadata JSONB,
similarity float
)
LANGUAGE SQL STABLE
AS $$
SELECT
documents.id,
documents.content,
documents.metadata,
1 - (documents.embedding <=> query_embedding) AS similarity
FROM documents
WHERE 1 - (documents.embedding <=> query_embedding) > match_threshold
ORDER BY documents.embedding <=> query_embedding
LIMIT match_count;
$$;typescript
// 搜索相似文档
export async function searchSimilarDocuments(query: string, limit = 5) {
\tconst supabase = createClient();
\t// 为查询生成向量嵌入
\tconst queryEmbedding = await generateEmbedding(query);
\t// 使用RPC函数搜索
\tconst { data, error } = await supabase.rpc("match_documents", {
\t\tquery_embedding: queryEmbedding,
\t\tmatch_threshold: 0.78, // 最小相似度
\t\tmatch_count: limit,
\t});
\tif (error) {
\t\tthrow error;
\t}
\treturn data;
}
// 创建RPC函数sql
CREATE OR REPLACE FUNCTION match_documents (
query_embedding vector(1536),
match_threshold float,
match_count int
)
RETURNS TABLE (
id UUID,
content TEXT,
metadata JSONB,
similarity float
)
LANGUAGE SQL STABLE
AS $$
SELECT
documents.id,
documents.content,
documents.metadata,
1 - (documents.embedding <=> query_embedding) AS similarity
FROM documents
WHERE 1 - (documents.embedding <=> query_embedding) > match_threshold
ORDER BY documents.embedding <=> query_embedding
LIMIT match_count;
$$;RAG (Retrieval Augmented Generation)
RAG(检索增强生成)
typescript
export async function ragQuery(question: string) {
// 1. Search for relevant documents
const relevantDocs = await searchSimilarDocuments(question, 5);
// 2. Build context from relevant documents
const context = relevantDocs.map((doc) => doc.content).join("\n\n");
// 3. Generate answer with GPT
const completion = await openai.chat.completions.create({
model: "gpt-4",
messages: [
{
role: "system",
content:
"You are a helpful assistant. Answer questions based on the provided context.",
},
{
role: "user",
content: `Context:\n${context}\n\nQuestion: ${question}`,
},
],
});
return {
answer: completion.choices[0].message.content,
sources: relevantDocs,
};
}typescript
export async function ragQuery(question: string) {
\t// 1. 搜索相关文档
\tconst relevantDocs = await searchSimilarDocuments(question, 5);
\t// 2. 从相关文档构建上下文
\tconst context = relevantDocs.map((doc) => doc.content).join("\
\
");
\t// 3. 使用GPT生成答案
\tconst completion = await openai.chat.completions.create({
\t\tmodel: "gpt-4",
\t\tmessages: [
\t\t\t{
\t\t\t\trole: "system",
\t\t\t\tcontent:
\t\t\t\t\t"你是一个乐于助人的助手。根据提供的上下文回答问题。",
\t\t\t},
\t\t\t{
\t\t\t\trole: "user",
\t\t\t\tcontent: `上下文:\
${context}\
\
问题: ${question}`,
\t\t\t},
\t\t],
\t});
\treturn {
\t\tanswer: completion.choices[0].message.content,
\t\tsources: relevantDocs,
\t};
}Database Functions & Triggers
数据库函数与触发器
Custom Functions
自定义函数
sql
-- Get user's post count
CREATE OR REPLACE FUNCTION get_user_post_count(user_id UUID)
RETURNS INTEGER AS $$
SELECT COUNT(*)::INTEGER
FROM posts
WHERE posts.user_id = get_user_post_count.user_id;
$$ LANGUAGE SQL STABLE;
-- Call from TypeScript
const { data, error } = await supabase.rpc('get_user_post_count', {
user_id: userId,
})sql
// 获取用户的帖子数量
CREATE OR REPLACE FUNCTION get_user_post_count(user_id UUID)
RETURNS INTEGER AS $$
SELECT COUNT(*)::INTEGER
FROM posts
WHERE posts.user_id = get_user_post_count.user_id;
$$ LANGUAGE SQL STABLE;
// 从TypeScript调用
const { data, error } = await supabase.rpc('get_user_post_count', {
user_id: userId,
})Triggers
触发器
sql
-- Auto-update updated_at column
CREATE OR REPLACE FUNCTION handle_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION handle_updated_at();
-- Update post count when post is created/deleted
CREATE OR REPLACE FUNCTION update_post_count()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
UPDATE profiles
SET post_count = post_count + 1
WHERE id = NEW.user_id;
RETURN NEW;
ELSIF TG_OP = 'DELETE' THEN
UPDATE profiles
SET post_count = post_count - 1
WHERE id = OLD.user_id;
RETURN OLD;
END IF;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_post_count_trigger
AFTER INSERT OR DELETE ON posts
FOR EACH ROW
EXECUTE FUNCTION update_post_count();sql
// 自动更新updated_at列
CREATE OR REPLACE FUNCTION handle_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION handle_updated_at();
// 当帖子创建/删除时更新帖子计数
CREATE OR REPLACE FUNCTION update_post_count()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
UPDATE profiles
SET post_count = post_count + 1
WHERE id = NEW.user_id;
RETURN NEW;
ELSIF TG_OP = 'DELETE' THEN
UPDATE profiles
SET post_count = post_count - 1
WHERE id = OLD.user_id;
RETURN OLD;
END IF;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_post_count_trigger
AFTER INSERT OR DELETE ON posts
FOR EACH ROW
EXECUTE FUNCTION update_post_count();Performance Optimization
性能优化
Query Optimization
查询优化
typescript
// Bad: N+1 query problem
const { data: posts } = await supabase.from("posts").select("*");
for (const post of posts) {
const { data: author } = await supabase
.from("profiles")
.select("*")
.eq("id", post.user_id)
.single();
}
// Good: Join in single query
const { data: posts } = await supabase.from("posts").select(`
*,
profiles (
id,
name,
avatar_url
)
`);
// Good: Use specific columns
const { data: posts } = await supabase
.from("posts")
.select("id, title, created_at, profiles(name)") // Only what you need
.eq("published", true)
.order("created_at", { ascending: false })
.limit(20);typescript
// 糟糕:N+1查询问题
const { data: posts } = await supabase.from("posts").select("*");
for (const post of posts) {
\tconst { data: author } = await supabase
\t\t.from("profiles")
\t\t.select("*")
\t\t.eq("id", post.user_id)
\t\t.single();
}
// 良好:在单个查询中关联
const { data: posts } = await supabase.from("posts").select(`
*,
profiles (
id,
name,
avatar_url
)
`);
// 良好:使用特定列
const { data: posts } = await supabase
\t.from("posts")
\t.select("id, title, created_at, profiles(name)") // 只选择需要的列
\t.eq("published", true)
\t.order("created_at", { ascending: false })
\t.limit(20);Indexes
索引
sql
-- Index on frequently filtered columns
CREATE INDEX posts_user_id_idx ON posts(user_id);
CREATE INDEX posts_created_at_idx ON posts(created_at DESC);
-- Partial index (filtered)
CREATE INDEX posts_published_idx ON posts(published)
WHERE published = true;
-- Composite index
CREATE INDEX posts_user_published_idx ON posts(user_id, published, created_at DESC);
-- Full-text search index
CREATE INDEX posts_content_idx ON posts
USING GIN (to_tsvector('english', content));sql
// 对频繁筛选的列创建索引
CREATE INDEX posts_user_id_idx ON posts(user_id);
CREATE INDEX posts_created_at_idx ON posts(created_at DESC);
// 部分索引(筛选后)
CREATE INDEX posts_published_idx ON posts(published)
WHERE published = true;
// 复合索引
CREATE INDEX posts_user_published_idx ON posts(user_id, published, created_at DESC);
// 全文搜索索引
CREATE INDEX posts_content_idx ON posts
USING GIN (to_tsvector('english', content));Connection Pooling
连接池
typescript
// Use connection pooler for serverless (Supavisor)
// Connection string: postgresql://postgres.[project-ref]:[password]@aws-0-us-east-1.pooler.supabase.com:6543/postgres
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{
db: {
schema: "public",
},
global: {
headers: { "x-my-custom-header": "my-app-name" },
},
},
);typescript
// 为无服务器使用连接池(Supavisor)
// 连接字符串: postgresql://postgres.[project-ref]:[password]@aws-0-us-east-1.pooler.supabase.com:6543/postgres
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
\tprocess.env.SUPABASE_URL!,
\tprocess.env.SUPABASE_ANON_KEY!,
\t{
\t\tdb: {
\t\t\tschema: "public",
\t\t},
\t\tglobal: {
\t\t\theaders: { "x-my-custom-header": "my-app-name" },
\t\t},
\t},
);Caching
缓存
typescript
// Next.js cache with revalidation
import { unstable_cache } from "next/cache";
import { createClient } from "@/lib/supabase/server";
export const getCachedPosts = unstable_cache(
async () => {
const supabase = createClient();
const { data } = await supabase
.from("posts")
.select("*")
.eq("published", true);
return data;
},
["posts"],
{
revalidate: 300, // 5 minutes
tags: ["posts"],
},
);
// Revalidate on mutation
import { revalidateTag } from "next/cache";
export async function createPost(data: any) {
const supabase = createClient();
await supabase.from("posts").insert(data);
revalidateTag("posts");
}typescript
// Next.js缓存带重新验证
import { unstable_cache } from "next/cache";
import { createClient } from "@/lib/supabase/server";
export const getCachedPosts = unstable_cache(
\tasync () => {
\t\tconst supabase = createClient();
\t\tconst { data } = await supabase
\t\t\t.from("posts")
\t\t\t.select("*")
\t\t\t.eq("published", true);
\t\treturn data;
\t},
\t["posts"],
\t{
\t\trevalidate: 300, // 5分钟
\t\ttags: ["posts"],
\t},
);
// 变更时重新验证
import { revalidateTag } from "next/cache";
export async function createPost(data: any) {
\tconst supabase = createClient();
\tawait supabase.from("posts").insert(data);
\trevalidateTag("posts");
}Local Development
本地开发
Setup Local Supabase
设置本地Supabase
bash
undefinedbash
undefinedInstall Supabase CLI
安装Supabase CLI
brew install supabase/tap/supabase
brew install supabase/tap/supabase
Initialize project
初始化项目
supabase init
supabase init
Start local Supabase (Docker required)
启动本地Supabase(需要Docker)
supabase start
supabase start
This starts:
启动的服务:
- PostgreSQL
- PostgreSQL
- GoTrue (Auth)
- GoTrue(认证)
- Realtime
- Realtime
- Storage
- Storage
- Kong (API Gateway)
- Kong(API网关)
- Studio (Dashboard)
- Studio(仪表板)
undefinedundefinedLocal Development URLs
本地开发URL
bash
undefinedbash
undefinedAfter supabase start:
supabase start后:
API URL: http://localhost:54321
Studio URL: http://localhost:54323
Inbucket URL: http://localhost:54324 # Email testing
undefinedAPI URL: http://localhost:54321
Studio URL: http://localhost:54323
Inbucket URL: http://localhost:54324 # 邮箱测试
undefinedMigration Workflow
迁移工作流
bash
undefinedbash
undefinedCreate migration
创建迁移
supabase migration new add_posts_table
supabase migration new add_posts_table
Edit migration file in supabase/migrations/
编辑supabase/migrations/中的迁移文件
Apply migration locally
本地应用迁移
supabase db reset
supabase db reset
Push to production
推送到生产环境
supabase db push
supabase db push
Pull remote schema
拉取远程架构
supabase db pull
undefinedsupabase db pull
undefinedGenerate Types from Local DB
从本地数据库生成类型
bash
undefinedbash
undefinedGenerate TypeScript types
生成TypeScript类型
supabase gen types typescript --local > types/database.ts
undefinedsupabase gen types typescript --local > types/database.ts
undefinedTesting
测试
Testing RLS Policies
测试RLS策略
sql
-- Test as specific user
SET LOCAL ROLE authenticated;
SET LOCAL "request.jwt.claims" TO '{"sub": "user-uuid-here"}';
-- Test query
SELECT * FROM posts;
-- Reset
RESET ROLE;sql
undefinedTesting with Supabase Test Helpers
以特定用户身份测试
typescript
// tests/posts.test.ts
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!, // For testing
);
describe("Posts", () => {
beforeEach(async () => {
// Clean up
await supabase
.from("posts")
.delete()
.neq("id", "00000000-0000-0000-0000-000000000000");
});
it("should create post", async () => {
const { data, error } = await supabase
.from("posts")
.insert({ title: "Test", content: "Test" })
.select()
.single();
expect(error).toBeNull();
expect(data.title).toBe("Test");
});
});SET LOCAL ROLE authenticated;
SET LOCAL "request.jwt.claims" TO '{"sub": "user-uuid-here"}';
Error Handling
测试查询
Comprehensive Error Handler
—
typescript
import { PostgrestError } from "@supabase/supabase-js";
export function handleSupabaseError(error: PostgrestError | null) {
if (!error) {
return null;
}
// Common error codes
const errorMessages: Record<string, string> = {
"23505": "This record already exists", // Unique violation
"23503": "Related record not found", // Foreign key violation
"42P01": "Table does not exist",
"42501": "Permission denied",
PGRST116: "No rows found",
};
const userMessage = errorMessages[error.code] || error.message;
console.error("Supabase error:", {
code: error.code,
message: error.message,
details: error.details,
hint: error.hint,
});
return userMessage;
}
// Usage
const { data, error } = await supabase.from("posts").insert(postData);
if (error) {
const message = handleSupabaseError(error);
toast.error(message);
return;
}SELECT * FROM posts;
Best Practices
重置
-
Row Level Security:
- Enable RLS on ALL tables
- Never rely on client-side checks alone
- Test policies thoroughly
- Use service role key sparingly (server-side only)
-
Query Optimization:
- Use to specify needed columns
.select() - Add database indexes for filtered/sorted columns
- Use to cap results
.limit() - Consider pagination for large datasets
- Use
-
Real-time Subscriptions:
- Always unsubscribe when component unmounts
- Use RLS policies to filter events
- Use broadcast for ephemeral data
- Limit number of simultaneous subscriptions
-
Authentication:
- Store JWT in httpOnly cookies when possible
- Refresh tokens before expiry
- Handle auth state changes
- Validate user on server-side
-
Storage:
- Set appropriate bucket policies
- Use image transformations for optimization
- Consider storage limits
- Clean up unused files
-
Error Handling:
- Always check object
error - Provide user-friendly error messages
- Log errors for debugging
- Handle network failures gracefully
- Always check
RESET ROLE;
undefinedCommon Patterns
使用Supabase测试助手测试
Auto-create Profile on Signup
—
sql
-- Create profiles table
CREATE TABLE profiles (
id UUID REFERENCES auth.users PRIMARY KEY,
name TEXT,
avatar_url TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Trigger function
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.profiles (id, name, avatar_url)
VALUES (
NEW.id,
NEW.raw_user_meta_data->>'name',
NEW.raw_user_meta_data->>'avatar_url'
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Trigger
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();typescript
// tests/posts.test.ts
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
\tprocess.env.SUPABASE_URL!,
\tprocess.env.SUPABASE_SERVICE_ROLE_KEY!, // 用于测试
);
describe("Posts", () => {
\tbeforeEach(async () => {
\t\t// 清理
\t\tawait supabase
\t\t\t.from("posts")
\t\t\t.delete()
\t\t\t.neq("id", "00000000-0000-0000-0000-000000000000");
\t});
\tit("should create post", async () => {
\t\tconst { data, error } = await supabase
\t\t\t.from("posts")
\t\t\t.insert({ title: "Test", content: "Test" })
\t\t\t.select()
\t\t\t.single();
\t\texpect(error).toBeNull();
\t\texpect(data.title).toBe("Test");
\t});
});Documentation Quick Reference
错误处理
—
全面错误处理器
Need to find something specific?
Search the 2,616 documentation files:
bash
undefinedtypescript
import { PostgrestError } from "@supabase/supabase-js";
export function handleSupabaseError(error: PostgrestError | null) {
\tif (!error) {
\t\treturn null;
\t}
\t// 常见错误代码
\tconst errorMessages: Record<string, string> = {
\t\t"23505": "该记录已存在", // 唯一约束冲突
\t\t"23503": "关联记录不存在", // 外键约束冲突
\t\t"42P01": "表不存在",
\t\t"42501": "权限被拒绝",
\t\tPGRST116: "未找到行",
\t};
\tconst userMessage = errorMessages[error.code] || error.message;
\tconsole.error("Supabase错误:", {
\t\tcode: error.code,
\t\tmessage: error.message,
\t\tdetails: error.details,
\t\thint: error.hint,
\t});
\treturn userMessage;
}
// 使用示例
const { data, error } = await supabase.from("posts").insert(postData);
if (error) {
\tconst message = handleSupabaseError(error);
\ttoast.error(message);
\treturn;
}Search all docs
最佳实践
grep -r "search term" docs/supabase_com/
-
行级安全:
- 对所有表启用RLS
- 永远不要仅依赖客户端检查
- 彻底测试策略
- 谨慎使用服务角色密钥(仅服务器端)
-
查询优化:
- 使用指定需要的列
.select() - 为筛选/排序的列添加数据库索引
- 使用限制结果
.limit() - 考虑对大型数据集使用分页
- 使用
-
实时订阅:
- 组件卸载时始终取消订阅
- 使用RLS策略筛选事件
- 对临时数据使用广播
- 限制同时订阅的数量
-
身份认证:
- 尽可能将JWT存储在httpOnly cookie中
- 过期前刷新令牌
- 处理认证状态变化
- 在服务器端验证用户
-
存储:
- 设置合适的存储桶策略
- 使用图片转换进行优化
- 考虑存储限制
- 清理未使用的文件
-
错误处理:
- 始终检查对象
error - 提供用户友好的错误消息
- 记录错误用于调试
- 优雅处理网络故障
- 始终检查
Find guides
常见模式
—
注册时自动创建用户资料
ls docs/supabase_com/guides_*
sql
undefinedFind API reference
创建资料表
ls docs/supabase_com/reference_*
**Common doc locations:**
- Guides: `docs/supabase_com/guides_*`
- JavaScript Reference: `docs/supabase_com/reference_javascript_*`
- Database: `docs/supabase_com/guides_database_*`
- Auth: `docs/supabase_com/guides_auth_*`
- Storage: `docs/supabase_com/guides_storage_*`CREATE TABLE profiles (
id UUID REFERENCES auth.users PRIMARY KEY,
name TEXT,
avatar_url TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
Resources
触发器函数
- Dashboard: https://supabase.com/dashboard
- Docs: https://supabase.com/docs
- Status: https://status.supabase.com
- CLI Docs: https://supabase.com/docs/guides/cli
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.profiles (id, name, avatar_url)
VALUES (
NEW.id,
NEW.raw_user_meta_data->>'name',
NEW.raw_user_meta_data->>'avatar_url'
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
Implementation Checklist
触发器
- Create Supabase project
- Install:
npm install @supabase/supabase-js - Set environment variables
- Design database schema
- Create migrations
- Enable RLS and create policies
- Set up authentication
- Implement auth state management
- Create CRUD operations
- Add real-time subscriptions (if needed)
- Configure storage buckets (if needed)
- Test RLS policies
- Add database indexes
- Deploy edge functions (if needed)
- Test in production
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
undefined—
文档快速参考
—
需要查找特定内容?
搜索2616份文档:
bash
undefined—
搜索所有文档
—
grep -r "search term" docs/supabase_com/
—
查找指南
—
ls docs/supabase_com/guides_*
—
查找API参考
—
ls docs/supabase_com/reference_*
**常见文档位置:**
- 指南: `docs/supabase_com/guides_*`
- JavaScript参考: `docs/supabase_com/reference_javascript_*`
- 数据库: `docs/supabase_com/guides_database_*`
- 认证: `docs/supabase_com/guides_auth_*`
- 存储: `docs/supabase_com/guides_storage_*`—
资源
—
—
实施检查清单
—
- 创建Supabase项目
- 安装:
npm install @supabase/supabase-js - 设置环境变量
- 设计数据库架构
- 创建迁移
- 启用RLS并创建策略
- 设置身份认证
- 实现认证状态管理
- 创建CRUD操作
- 添加实时订阅(如果需要)
- 配置存储桶(如果需要)
- 测试RLS策略
- 添加数据库索引
- 部署边缘函数(如果需要)
- 在生产环境测试 ",