mcpField
S
supabase-expert

supabase-expert

github.com/timelessco数据与数据库79 次安装
代码仓库

关于 supabase-expert

面向使用 Supabase 构建应用的开发者的文档型技能,涵盖数据库、身份验证、实时功能、存储、Edge Functions、向量搜索及平台集成。

主要功能

  1. 01覆盖 Supabase 多个产品领域
  2. 02数据库与 Row Level Security 模式
  3. 03身份验证和实时功能示例
  4. 04存储与 Edge Functions 指引
  5. 05向量搜索与框架集成

适用场景

  1. 01为应用添加 Supabase 身份验证
  2. 02设计数据库访问策略
  3. 03实现实时数据和文件存储
  4. 04使用 pgvector 构建向量搜索

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/timelessco/recollect --skill supabase-expert

安装后在 Claude Code 中输入/supabase-expert确认已加载。

详细介绍

Supabase 集成专家

目的

基于 2,616 多个官方文档文件,为使用 Supabase 构建应用提供全面、准确的指导。涵盖数据库操作、身份验证、实时功能、文件存储、边缘函数、向量搜索和平台集成的所有方面。

文档覆盖范围

可完整访问官方 Supabase 文档(如有):

  • 位置: docs/supabase_com/
  • 文件: 2,616 个 Markdown 文件
  • 覆盖范围: 完整指南、API 参考、客户端库和平台文档

注意: 必须单独拉取文档:

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

何时使用

用户提及以下内容时调用:

  • 数据库: PostgreSQL、Postgres、SQL、数据库、表、查询、迁移
  • 身份验证: 身份验证、登录、注册、OAuth、SSO、多因素、魔法链接
  • 实时功能: 实时、订阅、websocket、实时数据、在线状态、广播
  • 存储: 文件上传、文件存储、图像、S3、CDN、存储桶
  • 函数: 边缘函数、无服务器、API、Deno、云函数
  • 安全性: 行级安全性、RLS、策略、权限、访问控制
  • AI/ML: 向量搜索、嵌入、pgvector、语义搜索、AI、RAG
  • 框架集成: Next.js、React、Supabase 客户端、钩子

如何使用文档

回答问题时:

  1. 搜索特定主题:

    # Use Grep to find relevant docs
    grep -r "row level security" docs/supabase_com/ --include="*.md"
    
  2. 查找指南:

    # Guides are organized by feature
    ls docs/supabase_com/guides_*
    
  3. 查看参考文档:

    # Reference docs for client libraries
    ls docs/supabase_com/reference_*
    

快速入门

安装
npm install @supabase/supabase-js
初始化客户端
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
	process.env.NEXT_PUBLIC_SUPABASE_URL!,
	process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);

环境变量:

  • NEXT_PUBLIC_SUPABASE_URL - 项目 URL(可安全用于客户端)
  • NEXT_PUBLIC_SUPABASE_ANON_KEY - 匿名/公开密钥(可安全用于客户端)
  • SUPABASE_SERVICE_ROLE_KEY - 管理员密钥(仅限服务器端使用,可绕过 RLS)

数据库操作

CRUD 操作
// 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();
高级查询
// 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"] });
数据库函数
// 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);

身份验证

注册 / 登录
// Email/password signup
const { data, error } = await supabase.auth.signUp({
	email: "[email protected]",
	password: "secure-password",
	options: {
		data: {
			first_name: "John",
			last_name: "Doe",
		},
	},
});

// Email/password sign in
const { data, error } = await supabase.auth.signInWithPassword({
	email: "[email protected]",
	password: "secure-password",
});

// Magic link (passwordless)
const { data, error } = await supabase.auth.signInWithOtp({
	email: "[email protected]",
	options: {
		emailRedirectTo: "https://example.com/auth/callback",
	},
});

// Phone/SMS
const { data, error } = await supabase.auth.signInWithOtp({
	phone: "+1234567890",
});
OAuth 提供商
// Google sign in
const { data, error } = await supabase.auth.signInWithOAuth({
	provider: "google",
	options: {
		redirectTo: "http://localhost:3000/auth/callback",
		scopes: "profile email",
	},
});

支持的提供商:

  • Google、GitHub、GitLab、Bitbucket
  • Azure、Apple、Discord、Facebook
  • Slack、Spotify、Twitch、Twitter/X
  • Linear、Notion、Figma 等
用户会话管理
// 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");
	}
});
多因素身份验证(MFA)
// 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();

行级安全性(RLS)

启用 RLS
-- Enable RLS on table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
创建策略
-- 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'
    ))
  );
辅助函数
-- Get current user ID
auth.uid()

-- Get current JWT
auth.jwt()

-- Access JWT claims
(auth.jwt()->>'role')::text
(auth.jwt()->>'email')::text

实时订阅

监听数据库变更
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();
筛选实时事件
// 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();
广播(临时消息)
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();
在线状态跟踪
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(),
			});
		}
	});

存储

上传文件
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",
	});
下载文件
// Download as blob
const { data, error } = await supabase.storage
	.from("avatars")
	.download("public/avatar.png");

const url = URL.createObjectURL(data);
公开 URL
// Get public URL (for public buckets)
const { data } = supabase.storage
	.from("avatars")
	.getPublicUrl("public/avatar.png");

console.log(data.publicUrl);
签名 URL(私有文件)
// Create temporary access URL
const { data, error } = await supabase.storage
	.from("private-files")
	.createSignedUrl("document.pdf", 3600); // 1 hour

console.log(data.signedUrl);
图像转换
const { data } = supabase.storage.from("avatars").getPublicUrl("avatar.png", {
	transform: {
		width: 400,
		height: 400,
		resize: "cover", // 'contain', 'cover', 'fill'
		quality: 80,
	},
});
列出文件
const { data, error } = await supabase.storage.from("avatars").list("public", {
	limit: 100,
	offset: 0,
	sortBy: { column: "created_at", order: "desc" },
});

边缘函数

创建函数
# Install Supabase CLI
npm install -g supabase

# Initialize project
supabase init

# Create function
supabase functions new my-function
函数示例
// 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" },
		});
	}
});
部署函数
# Deploy single function
supabase functions deploy my-function

# Deploy all functions
supabase functions deploy
调用函数
const { data, error } = await supabase.functions.invoke("my-function", {
	body: { name: "World" },
});

console.log(data);

向量搜索(AI/ML)

启用 pgvector
-- 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);
存储嵌入
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,
});
相似度搜索
// Find similar documents
const { data, error } = await supabase.rpc("match_documents", {
	query_embedding: embedding,
	match_threshold: 0.78,
	match_count: 10,
});

相似度搜索函数:

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 集成

服务端组件
// 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
					}
				},
			},
		},
	);
}
客户端组件
// 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.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*"],
};
路由处理程序
// 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 });
}

数据库迁移

创建迁移
supabase migration new create_posts_table
迁移文件示例
-- 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();
运行迁移
# Apply migrations locally
supabase db reset

# Push to remote (production)
supabase db push

TypeScript 集成

数据库类型生成
# Generate types from your database schema
supabase gen types typescript --project-id YOUR_PROJECT_ID > types/supabase.ts

# Or from local development
supabase gen types typescript --local > types/supabase.ts
类型安全的客户端
// 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 } }>
服务端 Supabase 与客户端 Supabase
// 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,
		},
	},
);

Next.js App Router 模式

服务端组件(推荐)
// 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>
  )
}
用于变更的服务端操作
// 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");
}
带实时功能的客户端组件
// 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>
  )
}
路由处理程序
// 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);
}

高级身份验证

使用电子邮件确认的电子邮件/密码验证
// 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("/");
}
OAuth(Google、GitHub 等)
// 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);
	}
}
魔法链接
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" };
}
手机验证(SMS)
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");
}
多因素身份验证(MFA)
// 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;
}
身份验证回调处理程序
// 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));
}
受保护的路由
// 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"],
};

高级行级安全性

复杂的 RLS 策略
-- 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')
    )
  );
基于函数的 RLS
-- 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));
使用 JWT 声明的 RLS
-- 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'
  );

高级实时功能

Presence(谁在线)
'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>
  )
}
Broadcast(发送消息)
// 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 />
}
Postgres 更改(数据库事件)
// 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();

高级存储

图像转换
// 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;
}
签名 URL(私有文件)
// 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;
}
存储 RLS
-- 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
  );

Edge Functions

基础 Edge Function
// supabase/functions/hello/index.ts
import { serve } from "https://deno.land/[email protected]/http/server.ts";

serve(async (req) => {
	const { name } = await req.json();

	return new Response(JSON.stringify({ message: `Hello ${name}!` }), {
		headers: { "Content-Type": "application/json" },
	});
});
使用 Supabase 客户端的 Edge Function
// supabase/functions/create-post/index.ts
import { serve } from "https://deno.land/[email protected]/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";

serve(async (req) => {
	try {
		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,
		});
	}
});
定时 Edge Function(Cron)
// supabase/functions/cleanup-old-data/index.ts
import { serve } from "https://deno.land/[email protected]/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";

serve(async (req) => {
	// 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-data
从客户端调用 Edge Function
// 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}`,
	},
});

向量搜索(AI/RAG)

启用 pgvector
-- 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);
生成并存储嵌入向量
// 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;
}
语义搜索
// 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 function
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(检索增强生成)
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,
	};
}

数据库函数和触发器

自定义函数
-- 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,
})
触发器
-- 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();

性能优化

查询优化
// 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);
索引
-- 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));
连接池
// 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" },
		},
	},
);
缓存
// 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");
}

本地开发

设置本地 Supabase
# Install Supabase CLI
brew install supabase/tap/supabase

# Initialize project
supabase init

# Start local Supabase (Docker required)
supabase start

# This starts:
# - PostgreSQL
# - GoTrue (Auth)
# - Realtime
# - Storage
# - Kong (API Gateway)
# - Studio (Dashboard)
本地开发 URL
# After supabase start:
API URL: http://localhost:54321
Studio URL: http://localhost:54323
Inbucket URL: http://localhost:54324 # Email testing
迁移工作流
# Create migration
supabase migration new add_posts_table

# Edit migration file in supabase/migrations/

# Apply migration locally
supabase db reset

# Push to production
supabase db push

# Pull remote schema
supabase db pull
从本地数据库生成类型
# Generate TypeScript types
supabase gen types typescript --local > types/database.ts

测试

测试 RLS 策略
-- 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;
使用 Supabase 测试辅助工具进行测试
// 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");
	});
});

错误处理

全面的错误处理程序
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;
}

最佳实践

  1. 行级安全性:

    • 在所有表上启用 RLS
    • 切勿仅依赖客户端检查
    • 彻底测试策略
    • 谨慎使用服务角色密钥(仅限服务端)
  2. 查询优化:

    • 使用 .select() 指定所需列
    • 为筛选或排序列添加数据库索引
    • 使用 .limit() 限制结果数量
    • 对大型数据集考虑使用分页
  3. 实时订阅:

    • 组件卸载时务必取消订阅
    • 使用 RLS 策略筛选事件
    • 对临时数据使用 broadcast
    • 限制同时订阅的数量
  4. 身份验证:

    • 尽可能将 JWT 存储在 httpOnly Cookie 中
    • 在令牌过期前刷新令牌
    • 处理身份验证状态更改
    • 在服务端验证用户
  5. 存储:

    • 设置适当的存储桶策略
    • 使用图像转换进行优化
    • 考虑存储限制
    • 清理未使用的文件
  6. 错误处理:

    • 务必检查 error 对象
    • 提供对用户友好的错误消息
    • 记录错误以便调试
    • 妥善处理网络故障

常见模式

注册时自动创建个人资料
-- 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();

文档快速参考

需要查找特定内容?

搜索 2,616 个文档文件:

# Search all docs
grep -r "search term" docs/supabase_com/

# Find guides
ls docs/supabase_com/guides_*

# Find API reference
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 策略
  • 添加数据库索引
  • 部署边缘函数(如有需要)
  • 在生产环境中测试

评论

支持 Markdown