supabase

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Supabase Backend Platform Skill

Supabase后端平台技能指南


progressive_disclosure: entry_point: summary: "Open-source Firebase alternative with Postgres, authentication, storage, and realtime" when_to_use: - "When building full-stack applications" - "When auth, database, and storage are required" - "When realtime subscriptions are needed" - "When using Next.js, React, or Vue" quick_start: - "Create project on Supabase console" - "npm install @supabase/supabase-js" - "Initialize client with URL and anon key" - "Use auth, database, storage, realtime" token_estimate: entry: 80-95 full: 5000-6000


progressive_disclosure: entry_point: summary: "开源Firebase替代方案,内置Postgres、身份认证、存储及实时功能" when_to_use: - "构建全栈应用时" - "需要身份认证、数据库和存储服务时" - "需要实时订阅功能时" - "搭配Next.js、React或Vue使用时" quick_start: - "在Supabase控制台创建项目" - "执行npm install @supabase/supabase-js安装客户端" - "使用URL和匿名密钥初始化客户端" - "使用身份认证、数据库、存储及实时功能" token_estimate: entry: 80-95 full: 5000-6000

Supabase Fundamentals

Supabase基础

What is Supabase?

什么是Supabase?

Open-source Firebase alternative built on:
  • Postgres Database: Full SQL database with PostgREST API
  • Authentication: Built-in auth with multiple providers
  • Storage: File storage with image transformations
  • Realtime: WebSocket subscriptions to database changes
  • Edge Functions: Serverless functions on Deno runtime
  • Row Level Security: Postgres RLS for data access control
基于以下技术构建的开源Firebase替代方案:
  • Postgres数据库: 完整SQL数据库,搭配PostgREST API
  • 身份认证: 内置多提供商认证功能
  • 存储服务: 支持图片转换的文件存储
  • 实时功能: 数据库变更的WebSocket订阅
  • Edge Functions: 基于Deno运行时的无服务器函数
  • 行级安全(RLS): Postgres原生RLS实现数据访问控制

Project Setup

项目搭建

bash
undefined
bash
undefined

Install Supabase client

安装Supabase客户端

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

Install CLI for local development

安装本地开发CLI

npm install -D supabase
npm install -D supabase

TypeScript types

TypeScript类型定义

npm install -D @supabase/supabase-js
undefined
npm install -D @supabase/supabase-js
undefined

Client Initialization

客户端初始化

typescript
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!

export const supabase = createClient(supabaseUrl, supabaseAnonKey)

// With TypeScript types
import { Database } from '@/types/supabase'

export const supabase = createClient<Database>(
  supabaseUrl,
  supabaseAnonKey
)
typescript
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!

export const supabase = createClient(supabaseUrl, supabaseAnonKey)

// 搭配TypeScript类型
import { Database } from '@/types/supabase'

export const supabase = createClient<Database>(
  supabaseUrl,
  supabaseAnonKey
)

Database Operations

数据库操作

PostgREST API Basics

PostgREST API基础

Supabase auto-generates REST API from Postgres schema:
typescript
// SELECT * FROM posts
const { data, error } = await supabase
  .from('posts')
  .select('*')

// SELECT with filters
const { data } = await supabase
  .from('posts')
  .select('*')
  .eq('status', 'published')
  .order('created_at', { ascending: false })
  .limit(10)

// SELECT with joins
const { data } = await supabase
  .from('posts')
  .select(`
    *,
    author:profiles(name, avatar),
    comments(count)
  `)

// INSERT
const { data, error } = await supabase
  .from('posts')
  .insert({ title: 'Hello', content: 'World' })
  .select()
  .single()

// UPDATE
const { data } = await supabase
  .from('posts')
  .update({ status: 'published' })
  .eq('id', postId)
  .select()

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

// UPSERT
const { data } = await supabase
  .from('posts')
  .upsert({ id: 1, title: 'Updated' })
  .select()
Supabase从Postgres schema自动生成REST API:
typescript
// 查询所有posts
const { data, error } = await supabase
  .from('posts')
  .select('*')

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

// 关联查询
const { data } = await supabase
  .from('posts')
  .select(`
    *,
    author:profiles(name, avatar),
    comments(count)
  `)

// 插入数据
const { data, error } = await supabase
  .from('posts')
  .insert({ title: 'Hello', content: 'World' })
  .select()
  .single()

// 更新数据
const { data } = await supabase
  .from('posts')
  .update({ status: 'published' })
  .eq('id', postId)
  .select()

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

// 插入或更新数据
const { data } = await supabase
  .from('posts')
  .upsert({ id: 1, title: 'Updated' })
  .select()

Advanced Queries

高级查询

typescript
// Full-text search
const { data } = await supabase
  .from('posts')
  .select('*')
  .textSearch('title', 'postgresql', {
    type: 'websearch',
    config: 'english'
  })

// Range queries
const { data } = await supabase
  .from('posts')
  .select('*')
  .gte('created_at', '2024-01-01')
  .lte('created_at', '2024-12-31')

// Array contains
const { data } = await supabase
  .from('posts')
  .select('*')
  .contains('tags', ['postgres', 'supabase'])

// JSON operations
const { data } = await supabase
  .from('users')
  .select('*')
  .eq('metadata->theme', 'dark')

// Count without data
const { count } = await supabase
  .from('posts')
  .select('*', { count: 'exact', head: true })

// Pagination
const pageSize = 10
const page = 2
const { data } = await supabase
  .from('posts')
  .select('*')
  .range(page * pageSize, (page + 1) * pageSize - 1)
typescript
// 全文搜索
const { data } = await supabase
  .from('posts')
  .select('*')
  .textSearch('title', 'postgresql', {
    type: 'websearch',
    config: 'english'
  })

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

// 数组包含查询
const { data } = await supabase
  .from('posts')
  .select('*')
  .contains('tags', ['postgres', 'supabase'])

// JSON操作
const { data } = await supabase
  .from('users')
  .select('*')
  .eq('metadata->theme', 'dark')

// 仅统计数量不返回数据
const { count } = await supabase
  .from('posts')
  .select('*', { count: 'exact', head: true })

// 分页查询
const pageSize = 10
const page = 2
const { data } = await supabase
  .from('posts')
  .select('*')
  .range(page * pageSize, (page + 1) * pageSize - 1)

Database Functions and RPC

数据库函数与RPC

typescript
// Call Postgres function
const { data, error } = await supabase
  .rpc('get_trending_posts', {
    days: 7,
    min_score: 10
  })

// Example function in SQL
/*
CREATE OR REPLACE FUNCTION get_trending_posts(
  days INTEGER,
  min_score INTEGER
)
RETURNS TABLE (
  id UUID,
  title TEXT,
  score INTEGER
) AS $$
BEGIN
  RETURN QUERY
  SELECT p.id, p.title, COUNT(v.id)::INTEGER as score
  FROM posts p
  LEFT JOIN votes v ON p.id = v.post_id
  WHERE p.created_at > NOW() - INTERVAL '1 day' * days
  GROUP BY p.id
  HAVING COUNT(v.id) >= min_score
  ORDER BY score DESC;
END;
$$ LANGUAGE plpgsql;
*/
typescript
// 调用Postgres函数
const { data, error } = await supabase
  .rpc('get_trending_posts', {
    days: 7,
    min_score: 10
  })

// SQL示例函数
/*
CREATE OR REPLACE FUNCTION get_trending_posts(
  days INTEGER,
  min_score INTEGER
)
RETURNS TABLE (
  id UUID,
  title TEXT,
  score INTEGER
) AS $$
BEGIN
  RETURN QUERY
  SELECT p.id, p.title, COUNT(v.id)::INTEGER as score
  FROM posts p
  LEFT JOIN votes v ON p.id = v.post_id
  WHERE p.created_at > NOW() - INTERVAL '1 day' * days
  GROUP BY p.id
  HAVING COUNT(v.id) >= min_score
  ORDER BY score DESC;
END;
$$ LANGUAGE plpgsql;
*/

Authentication

身份认证

Email/Password Authentication

邮箱/密码认证

typescript
// Sign up
const { data, error } = await supabase.auth.signUp({
  email: 'user@example.com',
  password: 'secure-password',
  options: {
    data: {
      name: 'John Doe',
      avatar_url: 'https://...'
    }
  }
})

// Sign in
const { data, error } = await supabase.auth.signInWithPassword({
  email: 'user@example.com',
  password: 'secure-password'
})

// Sign out
const { error } = await supabase.auth.signOut()

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

// Get session
const { data: { session } } = await supabase.auth.getSession()
typescript
// 注册账号
const { data, error } = await supabase.auth.signUp({
  email: 'user@example.com',
  password: 'secure-password',
  options: {
    data: {
      name: 'John Doe',
      avatar_url: 'https://...'
    }
  }
})

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

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

// 获取当前用户
const { data: { user } } = await supabase.auth.getUser()

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

OAuth Providers

OAuth认证提供商

typescript
// Sign in with OAuth
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'github',
  options: {
    redirectTo: 'http://localhost:3000/auth/callback',
    scopes: 'repo user'
  }
})

// Available providers
// github, google, gitlab, bitbucket, azure, discord, facebook,
// linkedin, notion, slack, spotify, twitch, twitter, apple
typescript
// 使用OAuth登录
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'github',
  options: {
    redirectTo: 'http://localhost:3000/auth/callback',
    scopes: 'repo user'
  }
})

// 支持的提供商
// github, google, gitlab, bitbucket, azure, discord, facebook,
// linkedin, notion, slack, spotify, twitch, twitter, apple

Magic Links

魔法链接

typescript
// Send magic link
const { data, error } = await supabase.auth.signInWithOtp({
  email: 'user@example.com',
  options: {
    emailRedirectTo: 'http://localhost:3000/auth/callback'
  }
})

// Verify OTP
const { data, error } = await supabase.auth.verifyOtp({
  email: 'user@example.com',
  token: '123456',
  type: 'email'
})
typescript
// 发送魔法链接
const { data, error } = await supabase.auth.signInWithOtp({
  email: 'user@example.com',
  options: {
    emailRedirectTo: 'http://localhost:3000/auth/callback'
  }
})

// 验证OTP
const { data, error } = await supabase.auth.verifyOtp({
  email: 'user@example.com',
  token: '123456',
  type: 'email'
})

Phone Authentication

手机号认证

typescript
// Sign in with phone
const { data, error } = await supabase.auth.signInWithOtp({
  phone: '+1234567890'
})

// Verify phone OTP
const { data, error } = await supabase.auth.verifyOtp({
  phone: '+1234567890',
  token: '123456',
  type: 'sms'
})
typescript
// 使用手机号登录
const { data, error } = await supabase.auth.signInWithOtp({
  phone: '+1234567890'
})

// 验证手机号OTP
const { data, error } = await supabase.auth.verifyOtp({
  phone: '+1234567890',
  token: '123456',
  type: 'sms'
})

Auth State Management

认证状态管理

typescript
// Listen to auth changes
supabase.auth.onAuthStateChange((event, session) => {
  if (event === 'SIGNED_IN') {
    console.log('User signed in:', session?.user)
  }
  if (event === 'SIGNED_OUT') {
    console.log('User signed out')
  }
  if (event === 'TOKEN_REFRESHED') {
    console.log('Token refreshed')
  }
})

// Update user metadata
const { data, error } = await supabase.auth.updateUser({
  data: { theme: 'dark' }
})

// Change password
const { data, error } = await supabase.auth.updateUser({
  password: 'new-password'
})
typescript
// 监听认证状态变更
supabase.auth.onAuthStateChange((event, session) => {
  if (event === 'SIGNED_IN') {
    console.log('用户已登录:', session?.user)
  }
  if (event === 'SIGNED_OUT') {
    console.log('用户已登出')
  }
  if (event === 'TOKEN_REFRESHED') {
    console.log('令牌已刷新')
  }
})

// 更新用户元数据
const { data, error } = await supabase.auth.updateUser({
  data: { theme: 'dark' }
})

// 修改密码
const { data, error } = await supabase.auth.updateUser({
  password: 'new-password'
})

Row Level Security (RLS)

行级安全(RLS)

RLS Fundamentals

RLS基础

Postgres Row Level Security controls data access at the database level:
sql
-- Enable RLS on table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Policy: Users can read all published posts
CREATE POLICY "Public posts are viewable by everyone"
ON posts FOR SELECT
USING (status = 'published');

-- Policy: Users can only update their own posts
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (auth.uid() = author_id);

-- Policy: Authenticated users can insert posts
CREATE POLICY "Authenticated users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = author_id);

-- Policy: Users can delete their own posts
CREATE POLICY "Users can delete own posts"
ON posts FOR DELETE
USING (auth.uid() = author_id);
Postgres行级安全在数据库层面控制数据访问:
sql
-- 为表启用RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- 策略:所有用户可读取已发布的文章
CREATE POLICY "Public posts are viewable by everyone"
ON posts FOR SELECT
USING (status = 'published');

-- 策略:用户仅能更新自己的文章
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (auth.uid() = author_id);

-- 策略:已认证用户可插入文章
CREATE POLICY "Authenticated users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = author_id);

-- 策略:用户仅能删除自己的文章
CREATE POLICY "Users can delete own posts"
ON posts FOR DELETE
USING (auth.uid() = author_id);

Common RLS Patterns

常见RLS模式

sql
-- Public read, authenticated write
CREATE POLICY "Anyone can view posts"
ON posts FOR SELECT
USING (true);

CREATE POLICY "Authenticated users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() IS NOT NULL);

-- Organization-based access
CREATE POLICY "Users can view org data"
ON documents FOR SELECT
USING (
  organization_id IN (
    SELECT organization_id
    FROM memberships
    WHERE user_id = auth.uid()
  )
);

-- Role-based access
CREATE POLICY "Admins can do anything"
ON posts FOR ALL
USING (
  EXISTS (
    SELECT 1 FROM user_roles
    WHERE user_id = auth.uid()
    AND role = 'admin'
  )
);

-- Time-based access
CREATE POLICY "View published or scheduled posts"
ON posts FOR SELECT
USING (
  status = 'published'
  OR (status = 'scheduled' AND publish_at <= NOW())
);
sql
-- 公开读取,认证用户可写入
CREATE POLICY "Anyone can view posts"
ON posts FOR SELECT
USING (true);

CREATE POLICY "Authenticated users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() IS NOT NULL);

-- 基于组织的访问控制
CREATE POLICY "Users can view org data"
ON documents FOR SELECT
USING (
  organization_id IN (
    SELECT organization_id
    FROM memberships
    WHERE user_id = auth.uid()
  )
);

-- 基于角色的访问控制
CREATE POLICY "Admins can do anything"
ON posts FOR ALL
USING (
  EXISTS (
    SELECT 1 FROM user_roles
    WHERE user_id = auth.uid()
    AND role = 'admin'
  )
);

-- 基于时间的访问控制
CREATE POLICY "View published or scheduled posts"
ON posts FOR SELECT
USING (
  status = 'published'
  OR (status = 'scheduled' AND publish_at <= NOW())
);

RLS Helper Functions

RLS辅助函数

sql
-- Get current user ID
SELECT auth.uid();

-- Get current user JWT
SELECT auth.jwt();

-- Get specific claim
SELECT auth.jwt()->>'email';

-- Custom claims
SELECT auth.jwt()->'app_metadata'->>'role';
sql
-- 获取当前用户ID
SELECT auth.uid();

-- 获取当前用户JWT
SELECT auth.jwt();

-- 获取特定声明
SELECT auth.jwt()->>'email';

-- 自定义声明
SELECT auth.jwt()->'app_metadata'->>'role';

Storage

存储服务

File Upload

文件上传

typescript
// Upload file
const { data, error } = await supabase.storage
  .from('avatars')
  .upload('public/avatar1.png', file, {
    cacheControl: '3600',
    upsert: false
  })

// Upload with progress
const { data, error } = await supabase.storage
  .from('avatars')
  .upload('public/avatar1.png', file, {
    onUploadProgress: (progress) => {
      console.log(`${progress.loaded}/${progress.total}`)
    }
  })

// Upload from URL
const { data, error } = await supabase.storage
  .from('avatars')
  .uploadToSignedUrl('path', token, file)
typescript
// 上传文件
const { data, error } = await supabase.storage
  .from('avatars')
  .upload('public/avatar1.png', file, {
    cacheControl: '3600',
    upsert: false
  })

// 带进度的上传
const { data, error } = await supabase.storage
  .from('avatars')
  .upload('public/avatar1.png', file, {
    onUploadProgress: (progress) => {
      console.log(`${progress.loaded}/${progress.total}`)
    }
  })

// 通过URL上传
const { data, error } = await supabase.storage
  .from('avatars')
  .uploadToSignedUrl('path', token, file)

File Operations

文件操作

typescript
// Download file
const { data, error } = await supabase.storage
  .from('avatars')
  .download('public/avatar1.png')

// Get public URL
const { data } = supabase.storage
  .from('avatars')
  .getPublicUrl('public/avatar1.png')

// Create signed URL (temporary access)
const { data, error } = await supabase.storage
  .from('avatars')
  .createSignedUrl('private/document.pdf', 3600) // 1 hour

// List files
const { data, error } = await supabase.storage
  .from('avatars')
  .list('public', {
    limit: 100,
    offset: 0,
    sortBy: { column: 'name', order: 'asc' }
  })

// Delete file
const { data, error } = await supabase.storage
  .from('avatars')
  .remove(['public/avatar1.png'])

// Move file
const { data, error } = await supabase.storage
  .from('avatars')
  .move('public/avatar1.png', 'public/avatar2.png')
typescript
// 下载文件
const { data, error } = await supabase.storage
  .from('avatars')
  .download('public/avatar1.png')

// 获取公开URL
const { data } = supabase.storage
  .from('avatars')
  .getPublicUrl('public/avatar1.png')

// 创建签名URL(临时访问权限)
const { data, error } = await supabase.storage
  .from('avatars')
  .createSignedUrl('private/document.pdf', 3600) // 1小时

// 列出文件
const { data, error } = await supabase.storage
  .from('avatars')
  .list('public', {
    limit: 100,
    offset: 0,
    sortBy: { column: 'name', order: 'asc' }
  })

// 删除文件
const { data, error } = await supabase.storage
  .from('avatars')
  .remove(['public/avatar1.png'])

// 移动文件
const { data, error } = await supabase.storage
  .from('avatars')
  .move('public/avatar1.png', 'public/avatar2.png')

Image Transformations

图片转换

typescript
// Transform image
const { data } = supabase.storage
  .from('avatars')
  .getPublicUrl('avatar1.png', {
    transform: {
      width: 200,
      height: 200,
      resize: 'cover',
      quality: 80
    }
  })

// Available transformations
// width, height, resize (cover|contain|fill),
// quality (1-100), format (origin|jpeg|png|webp)
typescript
// 转换图片
const { data } = supabase.storage
  .from('avatars')
  .getPublicUrl('avatar1.png', {
    transform: {
      width: 200,
      height: 200,
      resize: 'cover',
      quality: 80
    }
  })

// 支持的转换参数
// width, height, resize (cover|contain|fill),
// quality (1-100), format (origin|jpeg|png|webp)

Storage RLS

存储服务RLS

sql
-- Enable RLS on storage
CREATE POLICY "Avatar images are publicly accessible"
ON storage.objects FOR SELECT
USING (bucket_id = 'avatars' AND (storage.foldername(name))[1] = 'public');

CREATE POLICY "Users can upload their own avatar"
ON storage.objects FOR INSERT
WITH CHECK (
  bucket_id = 'avatars'
  AND (storage.foldername(name))[1] = auth.uid()::text
);

CREATE POLICY "Users can delete their own avatar"
ON storage.objects FOR DELETE
USING (
  bucket_id = 'avatars'
  AND (storage.foldername(name))[1] = auth.uid()::text
);
sql
-- 为存储服务启用RLS
CREATE POLICY "Avatar images are publicly accessible"
ON storage.objects FOR SELECT
USING (bucket_id = 'avatars' AND (storage.foldername(name))[1] = 'public');

CREATE POLICY "Users can upload their own avatar"
ON storage.objects FOR INSERT
WITH CHECK (
  bucket_id = 'avatars'
  AND (storage.foldername(name))[1] = auth.uid()::text
);

CREATE POLICY "Users can delete their own avatar"
ON storage.objects FOR DELETE
USING (
  bucket_id = 'avatars'
  AND (storage.foldername(name))[1] = auth.uid()::text
);

Realtime Subscriptions

实时订阅

Database Changes

数据库变更订阅

typescript
// Subscribe to inserts
const channel = supabase
  .channel('posts-insert')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'posts'
    },
    (payload) => {
      console.log('New post:', payload.new)
    }
  )
  .subscribe()

// Subscribe to updates
const channel = supabase
  .channel('posts-update')
  .on(
    'postgres_changes',
    {
      event: 'UPDATE',
      schema: 'public',
      table: 'posts',
      filter: 'id=eq.1'
    },
    (payload) => {
      console.log('Updated:', payload.new)
      console.log('Previous:', payload.old)
    }
  )
  .subscribe()

// Subscribe to all changes
const channel = supabase
  .channel('posts-all')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'posts'
    },
    (payload) => {
      console.log('Change:', payload)
    }
  )
  .subscribe()

// Unsubscribe
supabase.removeChannel(channel)
typescript
// 订阅插入事件
const channel = supabase
  .channel('posts-insert')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'posts'
    },
    (payload) => {
      console.log('新文章:', payload.new)
    }
  )
  .subscribe()

// 订阅更新事件
const channel = supabase
  .channel('posts-update')
  .on(
    'postgres_changes',
    {
      event: 'UPDATE',
      schema: 'public',
      table: 'posts',
      filter: 'id=eq.1'
    },
    (payload) => {
      console.log('已更新:', payload.new)
      console.log('之前的数据:', payload.old)
    }
  )
  .subscribe()

// 订阅所有变更事件
const channel = supabase
  .channel('posts-all')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'posts'
    },
    (payload) => {
      console.log('变更:', payload)
    }
  )
  .subscribe()

// 取消订阅
supabase.removeChannel(channel)

Presence (Track Online Users)

在线状态追踪(Presence)

typescript
// Track presence
const channel = supabase.channel('room-1')

// Track current user
channel
  .on('presence', { event: 'sync' }, () => {
    const state = channel.presenceState()
    console.log('Online users:', state)
  })
  .on('presence', { event: 'join' }, ({ key, newPresences }) => {
    console.log('User joined:', newPresences)
  })
  .on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
    console.log('User left:', leftPresences)
  })
  .subscribe(async (status) => {
    if (status === 'SUBSCRIBED') {
      await channel.track({
        user_id: userId,
        online_at: new Date().toISOString()
      })
    }
  })

// Untrack
await channel.untrack()
typescript
// 追踪在线状态
const channel = supabase.channel('room-1')

// 追踪当前用户
channel
  .on('presence', { event: 'sync' }, () => {
    const state = channel.presenceState()
    console.log('在线用户:', state)
  })
  .on('presence', { event: 'join' }, ({ key, newPresences }) => {
    console.log('用户加入:', newPresences)
  })
  .on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
    console.log('用户离开:', leftPresences)
  })
  .subscribe(async (status) => {
    if (status === 'SUBSCRIBED') {
      await channel.track({
        user_id: userId,
        online_at: new Date().toISOString()
      })
    }
  })

// 停止追踪
await channel.untrack()

Broadcast (Send Messages)

消息广播(Broadcast)

typescript
// Broadcast messages
const channel = supabase.channel('chat-room')

channel
  .on('broadcast', { event: 'message' }, (payload) => {
    console.log('Message:', payload)
  })
  .subscribe()

// Send message
await channel.send({
  type: 'broadcast',
  event: 'message',
  payload: { text: 'Hello', user: 'John' }
})
typescript
// 广播消息
const channel = supabase.channel('chat-room')

channel
  .on('broadcast', { event: 'message' }, (payload) => {
    console.log('消息:', payload)
  })
  .subscribe()

// 发送消息
await channel.send({
  type: 'broadcast',
  event: 'message',
  payload: { text: 'Hello', user: 'John' }
})

Edge Functions

Edge Functions

Edge Function Basics

Edge Functions基础

Serverless functions on Deno runtime:
bash
undefined
基于Deno运行时的无服务器函数:
bash
undefined

Create function

创建函数

supabase functions new my-function
supabase functions new my-function

Serve locally

本地运行

supabase functions serve
supabase functions serve

Deploy

部署函数

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

Edge Function Example

Edge Functions示例

typescript
// supabase/functions/my-function/index.ts
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

serve(async (req) => {
  try {
    // Get auth header
    const authHeader = req.headers.get('Authorization')!

    // Create Supabase client
    const supabase = createClient(
      Deno.env.get('SUPABASE_URL') ?? '',
      Deno.env.get('SUPABASE_ANON_KEY') ?? '',
      { global: { headers: { Authorization: authHeader } } }
    )

    // Verify user
    const { data: { user }, error } = await supabase.auth.getUser()
    if (error) throw error

    // Process request
    const { data } = await supabase
      .from('posts')
      .select('*')
      .eq('author_id', user.id)

    return new Response(
      JSON.stringify({ data }),
      { headers: { 'Content-Type': 'application/json' } }
    )
  } catch (error) {
    return new Response(
      JSON.stringify({ error: error.message }),
      { status: 400, headers: { 'Content-Type': 'application/json' } }
    )
  }
})
typescript
// supabase/functions/my-function/index.ts
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

serve(async (req) => {
  try {
    // 获取认证头
    const authHeader = req.headers.get('Authorization')!

    // 创建Supabase客户端
    const supabase = createClient(
      Deno.env.get('SUPABASE_URL') ?? '',
      Deno.env.get('SUPABASE_ANON_KEY') ?? '',
      { global: { headers: { Authorization: authHeader } } }
    )

    // 验证用户
    const { data: { user }, error } = await supabase.auth.getUser()
    if (error) throw error

    // 处理请求
    const { data } = await supabase
      .from('posts')
      .select('*')
      .eq('author_id', user.id)

    return new Response(
      JSON.stringify({ data }),
      { headers: { 'Content-Type': 'application/json' } }
    )
  } catch (error) {
    return new Response(
      JSON.stringify({ error: error.message }),
      { status: 400, headers: { 'Content-Type': 'application/json' } }
    )
  }
})

Invoke Edge Function

调用Edge Functions

typescript
// From client
const { data, error } = await supabase.functions.invoke('my-function', {
  body: { name: 'John' }
})

// With auth
const { data, error } = await supabase.functions.invoke('my-function', {
  headers: {
    Authorization: `Bearer ${session.access_token}`
  },
  body: { name: 'John' }
})
typescript
// 客户端调用
const { data, error } = await supabase.functions.invoke('my-function', {
  body: { name: 'John' }
})

// 带认证信息调用
const { data, error } = await supabase.functions.invoke('my-function', {
  headers: {
    Authorization: `Bearer ${session.access_token}`
  },
  body: { name: 'John' }
})

Next.js Integration

Next.js集成

App Router Setup

App Router配置

typescript
// lib/supabase/client.ts (Client Component)
import { createBrowserClient } from '@supabase/ssr'

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}

// lib/supabase/server.ts (Server Component)
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'

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

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return cookieStore.get(name)?.value
        },
        set(name: string, value: string, options: CookieOptions) {
          cookieStore.set({ name, value, ...options })
        },
        remove(name: string, options: CookieOptions) {
          cookieStore.set({ name, value: '', ...options })
        },
      },
    }
  )
}

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

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

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

  await supabase.auth.getUser()
  return response
}
typescript
// lib/supabase/client.ts(客户端组件)
import { createBrowserClient } from '@supabase/ssr'

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}

// lib/supabase/server.ts(服务端组件)
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'

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

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return cookieStore.get(name)?.value
        },
        set(name: string, value: string, options: CookieOptions) {
          cookieStore.set({ name, value, ...options })
        },
        remove(name: string, options: CookieOptions) {
          cookieStore.set({ name, value: '', ...options })
        },
      },
    }
  )
}

// lib/supabase/middleware.ts(中间件)
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

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

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

  await supabase.auth.getUser()
  return response
}

Middleware

中间件

typescript
// middleware.ts
import { updateSession } from '@/lib/supabase/middleware'

export async function middleware(request: NextRequest) {
  return await updateSession(request)
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}
typescript
// middleware.ts
import { updateSession } from '@/lib/supabase/middleware'

export async function middleware(request: NextRequest) {
  return await updateSession(request)
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}

Server Component

服务端组件

typescript
// app/posts/page.tsx
import { createClient } from '@/lib/supabase/server'

export default async function PostsPage() {
  const supabase = await createClient()

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

  return (
    <div>
      {posts?.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
        </article>
      ))}
    </div>
  )
}
typescript
// app/posts/page.tsx
import { createClient } from '@/lib/supabase/server'

export default async function PostsPage() {
  const supabase = await createClient()

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

  return (
    <div>
      {posts?.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
        </article>
      ))}
    </div>
  )
}

Client Component

客户端组件

typescript
// app/components/new-post.tsx
'use client'

import { useState } from 'react'
import { createClient } from '@/lib/supabase/client'

export function NewPost() {
  const [title, setTitle] = useState('')
  const supabase = createClient()

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()

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

    const { error } = await supabase
      .from('posts')
      .insert({ title, author_id: user.id })

    if (!error) {
      setTitle('')
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="Post title"
      />
      <button>Create</button>
    </form>
  )
}
typescript
// app/components/new-post.tsx
'use client'

import { useState } from 'react'
import { createClient } from '@/lib/supabase/client'

export function NewPost() {
  const [title, setTitle] = useState('')
  const supabase = createClient()

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()

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

    const { error } = await supabase
      .from('posts')
      .insert({ title, author_id: user.id })

    if (!error) {
      setTitle('')
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="文章标题"
      />
      <button>创建</button>
    </form>
  )
}

Server Actions

服务端操作

typescript
// app/actions/posts.ts
'use server'

import { revalidatePath } from 'next/cache'
import { createClient } from '@/lib/supabase/server'

export async function createPost(formData: FormData) {
  const supabase = await createClient()

  const { data: { user } } = await supabase.auth.getUser()
  if (!user) {
    return { error: 'Not authenticated' }
  }

  const title = formData.get('title') as string

  const { error } = await supabase
    .from('posts')
    .insert({ title, author_id: user.id })

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

  revalidatePath('/posts')
  return { success: true }
}
typescript
// app/actions/posts.ts
'use server'

import { revalidatePath } from 'next/cache'
import { createClient } from '@/lib/supabase/server'

export async function createPost(formData: FormData) {
  const supabase = await createClient()

  const { data: { user } } = await supabase.auth.getUser()
  if (!user) {
    return { error: '未认证' }
  }

  const title = formData.get('title') as string

  const { error } = await supabase
    .from('posts')
    .insert({ title, author_id: user.id })

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

  revalidatePath('/posts')
  return { success: true }
}

TypeScript Type Generation

TypeScript类型生成

Generate Types from Database

从数据库生成类型

bash
undefined
bash
undefined

Install CLI

安装CLI

npm install -D supabase
npm install -D supabase

Login

登录

npx supabase login
npx supabase login

Link project

关联项目

npx supabase link --project-ref your-project-ref
npx supabase link --project-ref your-project-ref

Generate types

生成类型

npx supabase gen types typescript --project-id your-project-ref > types/supabase.ts
npx supabase gen types typescript --project-id your-project-ref > types/supabase.ts

Or from local database

从本地数据库生成

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

Use Generated Types

使用生成的类型

typescript
// types/supabase.ts (generated)
export type Database = {
  public: {
    Tables: {
      posts: {
        Row: {
          id: string
          title: string
          content: string | null
          author_id: string
          created_at: string
        }
        Insert: {
          id?: string
          title: string
          content?: string | null
          author_id: string
          created_at?: string
        }
        Update: {
          id?: string
          title?: string
          content?: string | null
          author_id?: string
          created_at?: string
        }
      }
    }
  }
}

// Usage
import { createClient } from '@supabase/supabase-js'
import { Database } from '@/types/supabase'

const supabase = createClient<Database>(url, key)

// Type-safe queries
const { data } = await supabase
  .from('posts') // TypeScript knows this table exists
  .select('title, content') // Autocomplete for columns
  .single()

// data is typed as { title: string; content: string | null }
typescript
// types/supabase.ts(自动生成)
export type Database = {
  public: {
    Tables: {
      posts: {
        Row: {
          id: string
          title: string
          content: string | null
          author_id: string
          created_at: string
        }
        Insert: {
          id?: string
          title: string
          content?: string | null
          author_id: string
          created_at?: string
        }
        Update: {
          id?: string
          title?: string
          content?: string | null
          author_id?: string
          created_at?: string
        }
      }
    }
  }
}

// 使用示例
import { createClient } from '@supabase/supabase-js'
import { Database } from '@/types/supabase'

const supabase = createClient<Database>(url, key)

// 类型安全查询
const { data } = await supabase
  .from('posts') // TypeScript识别该表存在
  .select('title, content') // 自动补全列名
  .single()

// data类型为 { title: string; content: string | null }

Supabase CLI and Local Development

Supabase CLI与本地开发

Setup Local Development

本地开发配置

bash
undefined
bash
undefined

Initialize Supabase

初始化Supabase

npx supabase init
npx supabase init

Start local Supabase (Postgres, Auth, Storage, etc.)

启动本地Supabase服务(Postgres、认证、存储等)

npx supabase start
npx supabase start

Stop

停止服务

npx supabase stop
npx supabase stop

Reset database

重置数据库

npx supabase db reset
npx supabase db reset

Status

查看服务状态

npx supabase status
undefined
npx supabase status
undefined

Database Migrations

数据库迁移

bash
undefined
bash
undefined

Create migration

创建迁移文件

npx supabase migration new create_posts_table
npx supabase migration new create_posts_table

Edit migration file

编辑迁移文件

supabase/migrations/20240101000000_create_posts_table.sql

supabase/migrations/20240101000000_create_posts_table.sql

Apply migrations

应用迁移

npx supabase db push
npx supabase db push

Pull remote schema

拉取远程schema

npx supabase db pull
npx supabase db pull

Diff local vs remote

对比本地与远程差异

npx supabase db diff
undefined
npx supabase db diff
undefined

Migration Example

迁移示例

sql
-- supabase/migrations/20240101000000_create_posts_table.sql
CREATE TABLE posts (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  title TEXT NOT NULL,
  content TEXT,
  author_id UUID NOT NULL REFERENCES auth.users(id),
  status TEXT NOT NULL DEFAULT 'draft',
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Policies
CREATE POLICY "Anyone can view published posts"
ON posts FOR SELECT
USING (status = 'published');

CREATE POLICY "Users can create their own posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = author_id);

CREATE POLICY "Users can update their own posts"
ON posts FOR UPDATE
USING (auth.uid() = author_id);

-- Indexes
CREATE INDEX posts_author_id_idx ON posts(author_id);
CREATE INDEX posts_status_idx ON posts(status);

-- Trigger for updated_at
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION moddatetime(updated_at);
sql
-- supabase/migrations/20240101000000_create_posts_table.sql
CREATE TABLE posts (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  title TEXT NOT NULL,
  content TEXT,
  author_id UUID NOT NULL REFERENCES auth.users(id),
  status TEXT NOT NULL DEFAULT 'draft',
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- 启用RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- 策略配置
CREATE POLICY "Anyone can view published posts"
ON posts FOR SELECT
USING (status = 'published');

CREATE POLICY "Users can create their own posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = author_id);

CREATE POLICY "Users can update their own posts"
ON posts FOR UPDATE
USING (auth.uid() = author_id);

-- 创建索引
CREATE INDEX posts_author_id_idx ON posts(author_id);
CREATE INDEX posts_status_idx ON posts(status);

-- 更新时间触发器
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION moddatetime(updated_at);

Security Best Practices

安全最佳实践

API Key Management

API密钥管理

typescript
// NEVER expose service_role key in client
// Use anon key for client-side
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! // Public
)

// Service role key only on server
const supabaseAdmin = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!, // Secret, bypasses RLS
  { auth: { persistSession: false } }
)
typescript
// 切勿在客户端暴露service_role密钥
// 客户端使用匿名密钥
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! // 公开密钥
)

// 服务端仅使用service_role密钥
const supabaseAdmin = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!, // 私有密钥,绕过RLS
  { auth: { persistSession: false } }
)

RLS Best Practices

RLS最佳实践

sql
-- Always enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Default deny (no policy = no access)
-- Explicitly grant access with policies

-- Test policies as different users
SET request.jwt.claims.sub = 'user-id';
SELECT * FROM posts; -- Test as this user

-- Disable RLS only for admin operations
-- Use service_role key from server, never client
sql
// 始终启用RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

// 默认拒绝访问(无策略则无法访问)
// 通过策略明确授予访问权限

// 以不同用户身份测试策略
SET request.jwt.claims.sub = 'user-id';
SELECT * FROM posts; -- 模拟该用户测试

// 仅在管理员操作时禁用RLS
// 仅在服务端使用service_role密钥,切勿在客户端使用

Input Validation

输入验证

typescript
// Validate on client and server
function validatePost(data: unknown) {
  const schema = z.object({
    title: z.string().min(1).max(200),
    content: z.string().max(10000).optional()
  })

  return schema.parse(data)
}

// Server-side validation in Edge Function
serve(async (req) => {
  const body = await req.json()

  try {
    const validated = validatePost(body)
    // Process validated data
  } catch (error) {
    return new Response(
      JSON.stringify({ error: 'Invalid input' }),
      { status: 400 }
    )
  }
})
typescript
// 在客户端和服务端都进行验证
function validatePost(data: unknown) {
  const schema = z.object({
    title: z.string().min(1).max(200),
    content: z.string().max(10000).optional()
  })

  return schema.parse(data)
}

// 在Edge Functions中进行服务端验证
serve(async (req) => {
  const body = await req.json()

  try {
    const validated = validatePost(body)
    // 处理验证后的数据
  } catch (error) {
    return new Response(
      JSON.stringify({ error: '输入无效' }),
      { status: 400 }
    )
  }
})

Environment Variables

环境变量

bash
undefined
bash
undefined

.env.local

.env.local

NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... # Public SUPABASE_SERVICE_ROLE_KEY=eyJ... # Secret, server-only
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... # 公开密钥 SUPABASE_SERVICE_ROLE_KEY=eyJ... # 私有密钥,仅服务端使用

Production: Use environment variables in hosting platform

生产环境:在托管平台配置环境变量

Never commit .env files to git

切勿将.env文件提交到git

undefined
undefined

Production Deployment

生产环境部署

Database Optimization

数据库优化

sql
-- Add indexes for common queries
CREATE INDEX posts_created_at_idx ON posts(created_at DESC);
CREATE INDEX posts_author_status_idx ON posts(author_id, status);

-- Optimize full-text search
CREATE INDEX posts_title_search_idx ON posts
USING GIN (to_tsvector('english', title));

-- Analyze query performance
EXPLAIN ANALYZE
SELECT * FROM posts WHERE author_id = 'xxx';

-- Vacuum and analyze
VACUUM ANALYZE posts;
sql
undefined

Connection Pooling

为常用查询添加索引

typescript
// Use connection pooling for serverless
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(url, key, {
  db: {
    schema: 'public',
  },
  auth: {
    persistSession: true,
    autoRefreshToken: true,
  },
  global: {
    headers: { 'x-my-custom-header': 'my-value' },
  },
})

// Configure pool in Supabase dashboard
// Settings > Database > Connection pooling
CREATE INDEX posts_created_at_idx ON posts(created_at DESC); CREATE INDEX posts_author_status_idx ON posts(author_id, status);

Monitoring

优化全文搜索

typescript
// Enable query logging
const supabase = createClient(url, key, {
  global: {
    fetch: async (url, options) => {
      console.log('Query:', url)
      return fetch(url, options)
    }
  }
})

// Monitor in Supabase Dashboard
// - Database performance
// - API usage
// - Storage usage
// - Auth activity
CREATE INDEX posts_title_search_idx ON posts USING GIN (to_tsvector('english', title));

Backup Strategy

分析查询性能

bash
undefined
EXPLAIN ANALYZE SELECT * FROM posts WHERE author_id = 'xxx';

Automatic backups (Pro plan+)

清理和分析数据库

Daily backups with point-in-time recovery

Manual backup

pg_dump -h db.xxx.supabase.co -U postgres -d postgres > backup.sql
VACUUM ANALYZE posts;
undefined

Restore

连接池

psql -h db.xxx.supabase.co -U postgres -d postgres < backup.sql
undefined
typescript
undefined

Supabase vs Firebase

为无服务器环境使用连接池

Similarities

  • Backend-as-a-Service platform
  • Authentication with multiple providers
  • Realtime data synchronization
  • File storage
  • Serverless functions
  • Generous free tier
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, key, { db: { schema: 'public', }, auth: { persistSession: true, autoRefreshToken: true, }, global: { headers: { 'x-my-custom-header': 'my-value' }, }, })

Key Differences

在Supabase控制台配置连接池

设置 > 数据库 > 连接池

Database
  • Supabase: PostgreSQL (SQL, full control)
  • Firebase: Firestore (NoSQL, limited queries)
Queries
  • Supabase: Full SQL, joins, aggregations
  • Firebase: Limited filtering, no joins
Security
  • Supabase: Row Level Security (Postgres native)
  • Firebase: Security Rules (custom syntax)
Open Source
  • Supabase: Fully open source, self-hostable
  • Firebase: Proprietary, Google-hosted only
Pricing
  • Supabase: Compute-based, predictable
  • Firebase: Usage-based, can spike
Ecosystem
  • Supabase: Postgres ecosystem (extensions, tools)
  • Firebase: Google Cloud Platform integration
undefined

Migration Considerations

监控

typescript
// Firestore collection query
const snapshot = await db
  .collection('posts')
  .where('status', '==', 'published')
  .orderBy('createdAt', 'desc')
  .limit(10)
  .get()

// Equivalent Supabase query
const { data } = await supabase
  .from('posts')
  .select('*')
  .eq('status', 'published')
  .order('created_at', { ascending: false })
  .limit(10)

// Complex queries easier in Supabase
const { data } = await supabase
  .from('posts')
  .select(`
    *,
    author:profiles!inner(name),
    comments(count)
  `)
  .gte('created_at', startDate)
  .lte('created_at', endDate)
  .order('created_at', { ascending: false })
// Firebase would require multiple queries + client-side joins
typescript
undefined

Advanced Patterns

启用查询日志

Optimistic Updates

typescript
'use client'

import { useState, useOptimistic } from 'react'
import { createClient } from '@/lib/supabase/client'

export function PostList({ initialPosts }: { initialPosts: Post[] }) {
  const [posts, setPosts] = useState(initialPosts)
  const [optimisticPosts, addOptimisticPost] = useOptimistic(
    posts,
    (state, newPost: Post) => [...state, newPost]
  )

  const supabase = createClient()

  const createPost = async (title: string) => {
    const tempPost = {
      id: crypto.randomUUID(),
      title,
      created_at: new Date().toISOString()
    }

    addOptimisticPost(tempPost)

    const { data } = await supabase
      .from('posts')
      .insert({ title })
      .select()
      .single()

    if (data) {
      setPosts([...posts, data])
    }
  }

  return (
    <div>
      {optimisticPosts.map((post) => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  )
}
const supabase = createClient(url, key, { global: { fetch: async (url, options) => { console.log('查询:', url) return fetch(url, options) } } })

Infinite Scroll

在Supabase控制台监控

- 数据库性能

- API使用情况

- 存储使用情况

- 认证活动

typescript
'use client'

import { useState, useEffect } from 'react'
import { createClient } from '@/lib/supabase/client'

const PAGE_SIZE = 20

export function InfinitePostList() {
  const [posts, setPosts] = useState<Post[]>([])
  const [page, setPage] = useState(0)
  const [hasMore, setHasMore] = useState(true)

  const supabase = createClient()

  useEffect(() => {
    const loadMore = async () => {
      const { data } = await supabase
        .from('posts')
        .select('*')
        .range(page * PAGE_SIZE, (page + 1) * PAGE_SIZE - 1)
        .order('created_at', { ascending: false })

      if (data) {
        setPosts([...posts, ...data])
        setHasMore(data.length === PAGE_SIZE)
      }
    }

    loadMore()
  }, [page])

  return (
    <div>
      {posts.map((post) => (
        <div key={post.id}>{post.title}</div>
      ))}
      {hasMore && (
        <button onClick={() => setPage(page + 1)}>
          Load More
        </button>
      )}
    </div>
  )
}
undefined

Debounced Search

备份策略

typescript
'use client'

import { useState, useEffect } from 'react'
import { createClient } from '@/lib/supabase/client'
import { useDebounce } from '@/hooks/use-debounce'

export function SearchPosts() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState<Post[]>([])
  const debouncedQuery = useDebounce(query, 300)

  const supabase = createClient()

  useEffect(() => {
    if (!debouncedQuery) {
      setResults([])
      return
    }

    const search = async () => {
      const { data } = await supabase
        .from('posts')
        .select('*')
        .textSearch('title', debouncedQuery)
        .limit(10)

      if (data) setResults(data)
    }

    search()
  }, [debouncedQuery])

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search posts..."
      />
      {results.map((post) => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  )
}

bash
undefined

Summary

自动备份(专业版及以上)

每日备份,支持时间点恢复

手动备份

Supabase provides a complete backend platform with:
  • Postgres Database with REST and GraphQL APIs
  • Built-in Authentication with multiple providers
  • Row Level Security for granular access control
  • File Storage with image transformations
  • Realtime Subscriptions for live updates
  • Edge Functions for serverless compute
  • Next.js Integration with Server and Client Components
  • TypeScript Support with auto-generated types
  • Local Development with Supabase CLI
  • Production Ready with monitoring and backups
Use Supabase when a full-featured backend with the power of Postgres, built-in auth, and realtime capabilities is needed, all with excellent TypeScript and Next.js integration.
pg_dump -h db.xxx.supabase.co -U postgres -d postgres > backup.sql

恢复备份

psql -h db.xxx.supabase.co -U postgres -d postgres < backup.sql
undefined

Supabase与Firebase对比

相似点

  • 后端即服务(BaaS)平台
  • 多提供商身份认证
  • 实时数据同步
  • 文件存储
  • 无服务器函数
  • 免费额度充足

核心差异

数据库
  • Supabase: PostgreSQL(SQL,完全可控)
  • Firebase: Firestore(NoSQL,查询能力有限)
查询能力
  • Supabase: 完整SQL支持,关联查询、聚合操作
  • Firebase: 过滤能力有限,无关联查询
安全机制
  • Supabase: 行级安全(Postgres原生)
  • Firebase: 安全规则(自定义语法)
开源属性
  • Supabase: 完全开源,可自托管
  • Firebase: 闭源,仅支持Google托管
定价模式
  • Supabase: 基于计算资源,价格可预测
  • Firebase: 基于使用量,成本可能激增
生态系统
  • Supabase: Postgres生态(扩展、工具丰富)
  • Firebase: 集成Google云平台

迁移注意事项

typescript
// Firestore集合查询
const snapshot = await db
  .collection('posts')
  .where('status', '==', 'published')
  .orderBy('createdAt', 'desc')
  .limit(10)
  .get()

// 等效Supabase查询
const { data } = await supabase
  .from('posts')
  .select('*')
  .eq('status', 'published')
  .order('created_at', { ascending: false })
  .limit(10)

// Supabase更易处理复杂查询
const { data } = await supabase
  .from('posts')
  .select(`
    *,
    author:profiles!inner(name),
    comments(count)
  `)
  .gte('created_at', startDate)
  .lte('created_at', endDate)
  .order('created_at', { ascending: false })
// Firebase需要多次查询+客户端关联

高级模式

乐观更新

typescript
'use client'

import { useState, useOptimistic } from 'react'
import { createClient } from '@/lib/supabase/client'

export function PostList({ initialPosts }: { initialPosts: Post[] }) {
  const [posts, setPosts] = useState(initialPosts)
  const [optimisticPosts, addOptimisticPost] = useOptimistic(
    posts,
    (state, newPost: Post) => [...state, newPost]
  )

  const supabase = createClient()

  const createPost = async (title: string) => {
    const tempPost = {
      id: crypto.randomUUID(),
      title,
      created_at: new Date().toISOString()
    }

    addOptimisticPost(tempPost)

    const { data } = await supabase
      .from('posts')
      .insert({ title })
      .select()
      .single()

    if (data) {
      setPosts([...posts, data])
    }
  }

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

无限滚动

typescript
'use client'

import { useState, useEffect } from 'react'
import { createClient } from '@/lib/supabase/client'

const PAGE_SIZE = 20

export function InfinitePostList() {
  const [posts, setPosts] = useState<Post[]>([])
  const [page, setPage] = useState(0)
  const [hasMore, setHasMore] = useState(true)

  const supabase = createClient()

  useEffect(() => {
    const loadMore = async () => {
      const { data } = await supabase
        .from('posts')
        .select('*')
        .range(page * PAGE_SIZE, (page + 1) * PAGE_SIZE - 1)
        .order('created_at', { ascending: false })

      if (data) {
        setPosts([...posts, ...data])
        setHasMore(data.length === PAGE_SIZE)
      }
    }

    loadMore()
  }, [page])

  return (
    <div>
      {posts.map((post) => (
        <div key={post.id}>{post.title}</div>
      ))}
      {hasMore && (
        <button onClick={() => setPage(page + 1)}>
          加载更多
        </button>
      )}
    </div>
  )
}

防抖搜索

typescript
'use client'

import { useState, useEffect } from 'react'
import { createClient } from '@/lib/supabase/client'
import { useDebounce } from '@/hooks/use-debounce'

export function SearchPosts() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState<Post[]>([])
  const debouncedQuery = useDebounce(query, 300)

  const supabase = createClient()

  useEffect(() => {
    if (!debouncedQuery) {
      setResults([])
      return
    }

    const search = async () => {
      const { data } = await supabase
        .from('posts')
        .select('*')
        .textSearch('title', debouncedQuery)
        .limit(10)

      if (data) setResults(data)
    }

    search()
  }, [debouncedQuery])

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="搜索文章..."
      />
      {results.map((post) => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  )
}

总结

Supabase提供完整的后端平台,包含:
  • Postgres数据库,支持REST和GraphQL API
  • 内置身份认证,支持多提供商
  • 行级安全,实现细粒度访问控制
  • 文件存储,支持图片转换
  • 实时订阅,实现实时更新
  • Edge Functions,提供无服务器计算能力
  • Next.js集成,支持服务端和客户端组件
  • TypeScript支持,自动生成类型定义
  • 本地开发,通过Supabase CLI实现
  • 生产就绪,包含监控和备份功能
当你需要一个具备Postgres强大能力、内置认证和实时功能的全功能后端,且需要良好的TypeScript和Next.js集成时,选择Supabase是最佳方案。