backend-database-specialist

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Supabase 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/docs
Major 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、database、tables、queries、migrations
  • 身份验证: authentication、login、signup、OAuth、SSO、multi-factor、magic links
  • 实时功能: real-time、subscriptions、websocket、live data、presence、broadcast
  • 存储: file upload、file storage、images、S3、CDN、buckets
  • 函数: edge functions、serverless、API、Deno、cloud functions
  • 安全: Row Level Security、RLS、policies、permissions、access control
  • AI/ML: vector search、embeddings、pgvector、semantic search、AI、RAG
  • 框架集成: Next.js、React、Supabase client、hooks

How to Use Documentation

文档使用方法

When answering questions:
  1. Search for specific topics:
    bash
    # Use Grep to find relevant docs
    grep -r "row level security" docs/supabase_com/ --include="*.md"
  2. Find guides:
    bash
    # Guides are organized by feature
    ls docs/supabase_com/guides_*
  3. Check reference docs:
    bash
    # Reference docs for client libraries
    ls docs/supabase_com/reference_*
回答问题时:
  1. 搜索特定主题:
    bash
    # 使用Grep查找相关文档
    grep -r "row level security" docs/supabase_com/ --include="*.md"
  2. 查找指南:
    bash
    # 指南按功能分类组织
    ls docs/supabase_com/guides_*
  3. 查看参考文档:
    bash
    # 客户端库参考文档
    ls docs/supabase_com/reference_*

Quick Start

快速开始

Installation

安装

bash
npm install @supabase/supabase-js
bash
npm install @supabase/supabase-js

Initialize 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:
  • NEXT_PUBLIC_SUPABASE_URL
    - Your project URL (safe for client)
  • NEXT_PUBLIC_SUPABASE_ANON_KEY
    - Anonymous/public key (safe for client)
  • SUPABASE_SERVICE_ROLE_KEY
    - Admin key (server-side only, bypasses RLS)
typescript
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)

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
  .from('posts')
  .insert({
    title: 'Hello World',
    content: 'My first post',
    user_id: user.id,
  })
  .select()
  .single();

// 查询(带筛选)
const { data: posts } = await supabase
  .from('posts')
  .select('*')
  .eq('published', true)
  .order('created_at', { ascending: false })
  .limit(10);

// 更新
const { data, error } = await supabase
  .from('posts')
  .update({ published: true })
  .eq('id', postId)
  .select()
  .single();

// 删除
const { error } = await supabase
  .from('posts')
  .delete()
  .eq('id', postId);

// 插入或更新(Upsert)
const { data, error } = await supabase
  .from('profiles')
  .upsert({
    id: user.id,
    name: 'John Doe',
    updated_at: new Date().toISOString(),
  })
  .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
  .from('posts')
  .select(`
    *,
    author:profiles(name, avatar),
    comments(count)
  `)
  .eq('published', true);

// 全文搜索
const { data } = await supabase
  .from('posts')
  .select('*')
  .textSearch('title', `'nextjs' & 'supabase'`);

// 范围查询
const { data } = await supabase
  .from('posts')
  .select('*')
  .gte('created_at', '2024-01-01')
  .lt('created_at', '2024-12-31');

// JSON查询
const { data } = await supabase
  .from('posts')
  .select('*')
  .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', {
    user_id: userId,
  });

// 带筛选条件调用
const { data } = await supabase
  .rpc('search_posts', { search_term: 'supabase' })
  .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({
  email: 'user@example.com',
  password: 'secure-password',
  options: {
    data: {
      first_name: 'John',
      last_name: 'Doe',
    },
  },
});

// 邮箱/密码登录
const { data, error } = await supabase.auth.signInWithPassword({
  email: 'user@example.com',
  password: 'secure-password',
});

// 魔法链接(无密码登录)
const { data, error } = await supabase.auth.signInWithOtp({
  email: 'user@example.com',
  options: {
    emailRedirectTo: 'https://example.com/auth/callback',
  },
});

// 手机号/SMS登录
const { data, error } = await supabase.auth.signInWithOtp({
  phone: '+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({
  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等

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 { data: { user } } = await supabase.auth.getUser();

// 获取会话
const { data: { session } } = await supabase.auth.getSession();

// 登出
const { error } = await supabase.auth.signOut();

// 监听身份验证状态变更
supabase.auth.onAuthStateChange((event, session) => {
  if (event === 'SIGNED_IN') {
    console.log('用户已登录:', session.user);
  }
  if (event === 'SIGNED_OUT') {
    console.log('用户已登出');
  }
  if (event === 'TOKEN_REFRESHED') {
    console.log('令牌已刷新');
  }
});

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({
  factorType: 'totp',
  friendlyName: 'My Authenticator App',
});

// 验证MFA
const { data, error } = await supabase.auth.mfa.challengeAndVerify({
  factorId: data.id,
  code: '123456',
});

// 列出已注册的验证方式
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')::text
sql
-- 获取当前用户ID
auth.uid()

-- 获取当前JWT
auth.jwt()

-- 访问JWT声明
(auth.jwt()->>'role')::text
(auth.jwt()->>'email')::text

Real-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
  .channel('posts-changes')
  .on(
    'postgres_changes',
    {
      event: '*', // 或 'INSERT', 'UPDATE', 'DELETE'
      schema: 'public',
      table: 'posts',
    },
    (payload) => {
      console.log('收到变更:', payload);
    }
  )
  .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
  .channel('my-posts')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'posts',
      filter: `user_id=eq.${userId}`,
    },
    (payload) => {
      console.log('新帖子:', payload.new);
    }
  )
  .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({
  type: 'broadcast',
  event: 'message',
  payload: { text: 'Hello!', user: 'John' },
});

// 接收消息
channel.on('broadcast', { event: 'message' }, (payload) => {
  console.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
  .on('presence', { event: 'sync' }, () => {
    const state = channel.presenceState();
    console.log('在线用户数:', Object.keys(state).length);
  })
  .on('presence', { event: 'join' }, ({ key, newPresences }) => {
    console.log('用户加入:', newPresences);
  })
  .on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
    console.log('用户离开:', leftPresences);
  })
  .subscribe(async (status) => {
    if (status === 'SUBSCRIBED') {
      await channel.track({
        user_id: userId,
        online_at: new Date().toISOString(),
      });
    }
  });

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
  .from('avatars')
  .upload(`public/${userId}/avatar.png`, file, {
    cacheControl: '3600',
    upsert: true,
  });

// 从base64上传
const { data, error } = await supabase.storage
  .from('avatars')
  .upload('file.png', decode(base64String), {
    contentType: 'image/png',
  });

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
  .from('avatars')
  .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
  .from('avatars')
  .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
  .from('private-files')
  .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', {
    transform: {
      width: 400,
      height: 400,
      resize: 'cover', // 'contain', 'cover', 'fill'
      quality: 80,
    },
  });

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', {
    limit: 100,
    offset: 0,
    sortBy: { column: 'created_at', order: 'desc' },
  });

Edge Functions

边缘函数

Create Function

创建函数

bash
undefined
bash
undefined

Install 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
undefined
supabase functions new my-function
undefined

Function Example

函数示例

typescript
// supabase/functions/my-function/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 {
    // 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 { 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 {
    // 初始化Supabase客户端
    const supabase = createClient(
      Deno.env.get('SUPABASE_URL')!,
      Deno.env.get('SUPABASE_ANON_KEY')!,
      {
        global: {
          headers: { Authorization: req.headers.get('Authorization')! },
        },
      }
    );

    // 获取已认证用户
    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' },
      });
    }

    // 查询数据库
    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 Function

部署函数

bash
undefined
bash
undefined

Deploy single function

部署单个函数

supabase functions deploy my-function
supabase functions deploy my-function

Deploy all functions

部署所有函数

supabase functions deploy
undefined
supabase functions deploy
undefined

Invoke 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', {
  body: { 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({
  model: 'text-embedding-ada-002',
  input: 'Supabase is awesome',
});

const embedding = response.data[0].embedding;

// 存储到数据库
const { data, error } = await supabase
  .from('documents')
  .insert({
    content: 'Supabase is awesome',
    embedding,
  });

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', {
  query_embedding: embedding,
  match_threshold: 0.78,
  match_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 { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';

export default async function PostsPage() {
  const supabase = createServerComponentClient({ cookies });

  const { data: posts } = await supabase
    .from('posts')
    .select('*')
    .order('created_at', { ascending: false });

  return (
    <div>
      {posts?.map((post) => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  );
}
typescript
// app/posts/page.tsx
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';

export default async function PostsPage() {
  const supabase = createServerComponentClient({ cookies });

  const { data: posts } = await supabase
    .from('posts')
    .select('*')
    .order('created_at', { ascending: false });

  return (
    <div>
      {posts?.map((post) => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  );
}

Client Components

客户端组件

typescript
// app/new-post/page.tsx
'use client';

import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
import { useState } from 'react';

export default function NewPostPage() {
  const supabase = createClientComponentClient();
  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 { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
import { useState } from 'react';

export default function NewPostPage() {
  const supabase = createClientComponentClient();
  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 { createMiddlewareClient } from '@supabase/auth-helpers-nextjs';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export async function middleware(req: NextRequest) {
  const res = NextResponse.next();
  const supabase = createMiddlewareClient({ req, res });

  const { data: { session } } = await supabase.auth.getSession();

  // Redirect to login if not authenticated
  if (!session && req.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', req.url));
  }

  return res;
}

export const config = {
  matcher: ['/dashboard/:path*'],
};
typescript
// middleware.ts
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export async function middleware(req: NextRequest) {
  const res = NextResponse.next();
  const supabase = createMiddlewareClient({ req, res });

  const { data: { session } } = await supabase.auth.getSession();

  // 若未认证则重定向到登录页
  if (!session && req.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', req.url));
  }

  return res;
}

export const config = {
  matcher: ['/dashboard/:path*'],
};

Route Handlers

路由处理器

typescript
// app/api/posts/route.ts
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';

export async function GET() {
  const supabase = createRouteHandlerClient({ cookies });

  const { data: posts } = await supabase
    .from('posts')
    .select('*');

  return NextResponse.json({ posts });
}

export async function POST(request: Request) {
  const supabase = createRouteHandlerClient({ cookies });
  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 { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';

export async function GET() {
  const supabase = createRouteHandlerClient({ cookies });

  const { data: posts } = await supabase
    .from('posts')
    .select('*');

  return NextResponse.json({ posts });
}

export async function POST(request: Request) {
  const supabase = createRouteHandlerClient({ cookies });
  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 });
}

Database Migrations

数据库迁移

Create Migration

创建迁移

bash
supabase migration new create_posts_table
bash
supabase migration new create_posts_table

Migration 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
undefined
bash
undefined

Apply migrations locally

本地应用迁移

supabase db reset
supabase db reset

Push to remote (production)

推送到远程(生产环境)

supabase db push
undefined
supabase db push
undefined

TypeScript Integration

TypeScript集成

Database Type Generation

数据库类型生成

bash
undefined
bash
undefined

Generate 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
undefined
supabase gen types typescript --local > types/supabase.ts
undefined

Type-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 =
  | 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!
)

// 现在你拥有完整的类型安全!
const { data } = await supabase
  .from('posts')  // ✅ TypeScript知晓该表存在
  .select('title, content, profiles(name)')  // ✅ TypeScript验证列
  .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, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { Database } from './types'

export function createClient() {
  const cookieStore = cookies()

  return createServerClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return cookieStore.get(name)?.value
        },
        set(name: string, value: string, options: CookieOptions) {
          try {
            cookieStore.set({ name, value, ...options })
          } catch (error) {
            // Called from Server Component - ignore
          }
        },
        remove(name: string, options: CookieOptions) {
          try {
            cookieStore.set({ name, value: '', ...options })
          } catch (error) {
            // 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() {
  return createBrowserClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}

// lib/supabase/server.ts - 服务器端(Next.js App Router)
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { Database } from './types'

export function createClient() {
  const cookieStore = cookies()

  return createServerClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return cookieStore.get(name)?.value
        },
        set(name: string, value: string, options: CookieOptions) {
          try {
            cookieStore.set({ name, value, ...options })
          } catch (error) {
            // 从服务器组件调用时忽略
          }
        },
        remove(name: string, options: CookieOptions) {
          try {
            cookieStore.set({ name, value: '', ...options })
          } catch (error) {
            // 从服务器组件调用时忽略
          }
        },
      },
    }
  )
}

// lib/supabase/admin.ts - 管理员客户端(绕过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!,  // ⚠️ 仅服务器端使用!
  {
    auth: {
      autoRefreshToken: false,
      persistSession: false
    }
  }
)

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>作者:{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) {
  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('未授权')

  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)  // 确保用户拥有该帖子

  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('未授权')

  const { error } = await supabase
    .from('posts')
    .delete()
    .eq('id', id)
    .eq('user_id', user.id)

  if (error) throw new Error(error.message)

  revalidatePath('/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) {
  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: '未授权' }, { 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)
}

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) {
  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,  // 存储在auth.users.raw_user_meta_data中
      },
      emailRedirectTo: `${process.env.NEXT_PUBLIC_URL}/auth/callback`,
    },
  })

  if (error) {
    return { error: error.message }
  }

  return { success: true, message: '请检查邮箱以确认您的账户' }
}

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, 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() {
  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)
  }
}

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) {
  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: '请检查邮箱获取登录链接' }
}

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) {
  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')
}

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() {
  const supabase = createClient()

  const { data, error } = await supabase.auth.mfa.enroll({
    factorType: 'totp',
    friendlyName: 'Authenticator App',
  })

  if (error) throw error

  // data.totp.qr_code - 需扫描的二维码
  // data.totp.secret - 手动输入的密钥
  return data
}

// 验证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
}

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) {
  const requestUrl = new URL(request.url)
  const code = requestUrl.searchParams.get('code')

  if (code) {
    const supabase = createClient()
    await supabase.auth.exchangeCodeForSession(code)
  }

  // 重定向到仪表板或其他页面
  return NextResponse.redirect(new URL('/dashboard', request.url))
}

Protected Routes

受保护路由

typescript
// middleware.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
  let response = NextResponse.next({
    request: {
      headers: request.headers,
    },
  })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return request.cookies.get(name)?.value
        },
        set(name: string, value: string, options: CookieOptions) {
          request.cookies.set({
            name,
            value,
            ...options,
          })
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          })
          response.cookies.set({
            name,
            value,
            ...options,
          })
        },
        remove(name: string, options: CookieOptions) {
          request.cookies.set({
            name,
            value: '',
            ...options,
          })
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          })
          response.cookies.set({
            name,
            value: '',
            ...options,
          })
        },
      },
    }
  )

  const { data: { user } } = await supabase.auth.getUser()

  // 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, type CookieOptions } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
  let response = NextResponse.next({
    request: {
      headers: request.headers,
    },
  })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return request.cookies.get(name)?.value
        },
        set(name: string, value: string, options: CookieOptions) {
          request.cookies.set({
            name,
            value,
            ...options,
          })
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          })
          response.cookies.set({
            name,
            value,
            ...options,
          })
        },
        remove(name: string, options: CookieOptions) {
          request.cookies.set({
            name,
            value: '',
            ...options,
          })
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          })
          response.cookies.set({
            name,
            value: '',
            ...options,
          })
        },
      },
    }
  )

  const { data: { user } } = await supabase.auth.getUser()

  // 保护仪表板路由
  if (request.nextUrl.pathname.startsWith('/dashboard') && !user) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  // 已认证用户重定向出认证页面
  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'],
}

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)

在线状态追踪(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
  .channel('post-changes')
  .on(
    'postgres_changes',
    {
      event: 'UPDATE',
      schema: 'public',
      table: 'posts',
      filter: 'id=eq.123',  // 特定行
    },
    (payload) => {
      console.log('帖子已更新:', payload)
    }
  )
  .subscribe()

// 监听多个表
const channel = supabase
  .channel('changes')
  .on(
    'postgres_changes',
    { event: '*', schema: 'public', table: 'posts' },
    handlePostChange
  )
  .on(
    'postgres_changes',
    { event: '*', schema: 'public', table: 'comments' },
    handleCommentChange
  )
  .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) {
  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

  // 获取转换后的图片URL
  const { data } = supabase.storage
    .from('avatars')
    .getPublicUrl(filePath, {
      transform: {
        width: 200,
        height: 200,
        resize: 'cover',
        quality: 80,
      },
    })

  return 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) {
  const supabase = createClient()

  const { data, error } = await supabase.storage
    .from('private-files')
    .createSignedUrl(path, 3600)  // 1小时

  if (error) throw error

  return data.signedUrl
}

// 上传到私有存储桶
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
}

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) => {
  const { name } = await req.json()

  return new Response(
    JSON.stringify({ message: `Hello ${name}!` }),
    {
      headers: { 'Content-Type': 'application/json' },
    },
  )
})

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) => {
  try {
    const supabaseClient = createClient(
      Deno.env.get('SUPABASE_URL') ?? '',
      Deno.env.get('SUPABASE_ANON_KEY') ?? '',
      {
        global: {
          headers: { Authorization: req.headers.get('Authorization')! },
        },
      }
    )

    // 获取已认证用户
    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,
    })
  }
})

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-data
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) => {
  // 验证请求来自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')!
  )

  // 删除旧数据
  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 }))
})

// 在仪表板配置:Database > Cron Jobs
// 调度:0 2 * * *(每天凌晨2点)
// HTTP请求:https://your-project.supabase.co/functions/v1/cleanup-old-data

Invoke 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', {
  body: { name: 'World' },
})

// 自动包含身份验证头
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}`,
  },
})

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()
);

-- 创建索引以加速相似性搜索
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({
  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
}

// 存储带嵌入向量的文档
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
}

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 function
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;
$$;
typescript
// 搜索相似文档
export async function searchSimilarDocuments(query: string, limit = 5) {
  const supabase = createClient()

  // 为查询生成嵌入向量
  const queryEmbedding = await generateEmbedding(query)

  // 使用RPC函数搜索
  const { data, error } = await supabase.rpc('match_documents', {
    query_embedding: queryEmbedding,
    match_threshold: 0.78,  // 最小相似度
    match_count: limit,
  })

  if (error) throw error

  return 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) {
  // 1. 搜索相关文档
  const relevantDocs = await searchSimilarDocuments(question, 5)

  // 2. 从相关文档构建上下文
  const context = relevantDocs
    .map(doc => doc.content)
    .join('\n\n')

  // 3. 使用GPT生成答案
  const completion = await openai.chat.completions.create({
    model: 'gpt-4',
    messages: [
      {
        role: 'system',
        content: '你是一个乐于助人的助手。请根据提供的上下文回答问题。',
      },
      {
        role: 'user',
        content: `上下文:\n${context}\n\n问题: ${question}`,
      },
    ],
  })

  return {
    answer: completion.choices[0].message.content,
    sources: relevantDocs,
  }
}

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) {
  const { data: author } = await supabase
    .from('profiles')
    .select('*')
    .eq('id', post.user_id)
    .single()
}

// 良好:单次查询关联
const { data: posts } = await supabase
  .from('posts')
  .select(`
    *,
    profiles (
      id,
      name,
      avatar_url
    )
  `)

// 良好:使用特定列
const { data: posts } = await supabase
  .from('posts')
  .select('id, title, created_at, profiles(name)')  // 仅选择需要的列
  .eq('published', true)
  .order('created_at', { ascending: false })
  .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(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_ANON_KEY!,
  {
    db: {
      schema: 'public',
    },
    global: {
      headers: { 'x-my-custom-header': 'my-app-name' },
    },
  }
)

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(
  async () => {
    const supabase = createClient()
    const { data } = await supabase
      .from('posts')
      .select('*')
      .eq('published', true)
    return data
  },
  ['posts'],
  {
    revalidate: 300,  // 5分钟
    tags: ['posts'],
  }
)

// 变更时重新验证
import { revalidateTag } from 'next/cache'

export async function createPost(data: any) {
  const supabase = createClient()
  await supabase.from('posts').insert(data)
  revalidateTag('posts')
}

Local Development

本地开发

Setup Local Supabase

设置本地Supabase

bash
undefined
bash
undefined

Install 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(仪表板)

undefined
undefined

Local Development URLs

本地开发URL

bash
undefined
bash
undefined

After supabase start:

启动supabase start后:

API URL: http://localhost:54321 Studio URL: http://localhost:54323 Inbucket URL: http://localhost:54324 # Email testing
undefined
API URL: http://localhost:54321 Studio URL: http://localhost:54323 Inbucket URL: http://localhost:54324 # 邮箱测试
undefined

Migration Workflow

迁移工作流

bash
undefined
bash
undefined

Create 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
undefined
supabase db pull
undefined

Generate Types from Local DB

从本地数据库生成类型

bash
undefined
bash
undefined

Generate TypeScript types

生成TypeScript类型

supabase gen types typescript --local > types/database.ts
undefined
supabase gen types typescript --local > types/database.ts
undefined

Testing

测试

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
undefined

Testing 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

重置

  1. 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)
  2. Query Optimization:
    • Use
      .select()
      to specify needed columns
    • Add database indexes for filtered/sorted columns
    • Use
      .limit()
      to cap results
    • Consider pagination for large datasets
  3. Real-time Subscriptions:
    • Always unsubscribe when component unmounts
    • Use RLS policies to filter events
    • Use broadcast for ephemeral data
    • Limit number of simultaneous subscriptions
  4. Authentication:
    • Store JWT in httpOnly cookies when possible
    • Refresh tokens before expiry
    • Handle auth state changes
    • Validate user on server-side
  5. Storage:
    • Set appropriate bucket policies
    • Use image transformations for optimization
    • Consider storage limits
    • Clean up unused files
  6. Error Handling:
    • Always check
      error
      object
    • Provide user-friendly error messages
    • Log errors for debugging
    • Handle network failures gracefully
RESET ROLE;
undefined

Common 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(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!  // 用于测试
)

describe('Posts', () => {
  beforeEach(async () => {
    // 清理
    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')
  })
})

Documentation Quick Reference

错误处理

全面错误处理器

Need to find something specific?
Search the 2,616 documentation files:
bash
undefined
typescript
import { PostgrestError } from '@supabase/supabase-js'

export function handleSupabaseError(error: PostgrestError | null) {
  if (!error) return null

  // 常见错误码
  const errorMessages: Record<string, string> = {
    '23505': '该记录已存在',  // 唯一约束冲突
    '23503': '关联记录不存在',    // 外键约束冲突
    '42P01': '表不存在',
    '42501': '权限不足',
    'PGRST116': '未找到行',
  }

  const userMessage = errorMessages[error.code] || error.message

  console.error('Supabase错误:', {
    code: error.code,
    message: error.message,
    details: error.details,
    hint: error.hint,
  })

  return userMessage
}

// 使用示例
const { data, error } = await supabase.from('posts').insert(postData)

if (error) {
  const message = handleSupabaseError(error)
  toast.error(message)
  return
}

Search all docs

最佳实践

grep -r "search term" docs/supabase_com/
  1. 行级安全:
    • 为所有表启用RLS
    • 绝不单独依赖客户端检查
    • 彻底测试策略
    • 谨慎使用服务角色密钥(仅服务器端)
  2. 查询优化:
    • 使用
      .select()
      指定需要的列
    • 为筛选/排序的列添加数据库索引
    • 使用
      .limit()
      限制结果数量
    • 大型数据集考虑分页
  3. 实时订阅:
    • 组件卸载时始终取消订阅
    • 使用RLS策略筛选事件
    • 使用广播处理临时数据
    • 限制同时订阅的数量
  4. 身份验证:
    • 尽可能将JWT存储在httpOnly cookie中
    • 令牌过期前刷新
    • 处理身份验证状态变更
    • 服务器端验证用户
  5. 存储:
    • 设置合适的存储桶策略
    • 使用图片转换进行优化
    • 考虑存储限制
    • 清理未使用的文件
  6. 错误处理:
    • 始终检查
      error
      对象
    • 提供用户友好的错误消息
    • 记录错误用于调试
    • 优雅处理网络故障

Find guides

常见模式

注册时自动创建用户资料

ls docs/supabase_com/guides_*
sql
-- 创建profiles表
CREATE TABLE profiles (
  id UUID REFERENCES auth.users PRIMARY KEY,
  name TEXT,
  avatar_url TEXT,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- 触发器函数
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;

-- 触发器
CREATE TRIGGER on_auth_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();

Find 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_*`
需要查找特定内容?
搜索2616份文档:
bash
undefined

Resources

搜索所有文档

grep -r "search term" docs/supabase_com/

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