mcpField
S
supabase-mcp-integration

supabase-mcp-integration

github.com/manutej开发144 次安装48 周前更新
代码仓库

关于 supabase-mcp-integration

面向使用 Supabase 构建应用的开发者,涵盖认证、PostgreSQL 数据操作、实时订阅、存储和 TypeScript 集成。内容也包括 RLS 授权模式及常见实现示例。

主要功能

  1. 01认证与会话管理
  2. 02PostgreSQL 查询与变更
  3. 03实时订阅与广播
  4. 04文件存储操作
  5. 05TypeScript 类型集成
  6. 06RLS 授权模式

适用场景

  1. 01实现 OAuth、MFA 或无密码登录
  2. 02构建实时协作或仪表板
  3. 03为多租户应用配置行级授权

安装

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

$npx skills add https://github.com/manutej/crush-mcp-server --skill supabase-mcp-integration

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

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

EXAMPLES.md
README.md
REFERENCE.md

详细介绍

Supabase MCP 集成

一项全面的技能,助你使用 Supabase 构建可用于生产环境的应用。Supabase 是基于 PostgreSQL 构建的开源后端即服务平台。本技能涵盖身份验证、数据库操作、实时订阅、存储、TypeScript 集成和行级安全模式。

何时使用此技能

在以下情况下使用此技能:

  • 使用 PostgreSQL 后端构建全栈 Web 或移动应用
  • 实现身份验证(电子邮件、OAuth、魔法链接、MFA)和会话管理
  • 创建实时应用(聊天、协作、实时仪表板)
  • 管理文件存储,包括图像优化和 CDN 分发
  • 构建具有精细授权控制的多租户 SaaS 应用
  • 从 Firebase 迁移到基于 SQL 的后端
  • 需要使用 TypeScript 执行类型安全的数据库操作
  • 实现行级安全(RLS),用于数据库授权
  • 构建包含复杂查询、联接和关系的应用
  • 根据数据库架构即时创建 REST/GraphQL API

核心概念

Supabase 平台架构

Supabase 是一个集成平台,基于企业级开源组件构建:

主要组件:

  • PostgreSQL 数据库:完整的 Postgres,支持扩展(PostGIS、pg_vector)
  • GoTrue(Auth):基于 JWT 的身份验证,支持多个提供商
  • PostgREST:根据数据库架构自动生成 REST API
  • Realtime:用于数据库变更、广播和在线状态的 WebSocket 服务
  • Storage:兼容 S3 的文件存储,支持 CDN 和图像优化
  • Edge Functions:全球分布式无服务器函数(Deno 运行时)

统一客户端库:

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)

// All features through single client
await supabase.auth.signIn()           // Authentication
await supabase.from('users').select()  // Database
supabase.channel('room').subscribe()   // Realtime
await supabase.storage.from().upload() // Storage
行级安全(RLS)

使用 PostgreSQL 策略实现数据库级授权:

  • 直接在数据库中定义访问规则
  • 自动应用于所有查询
  • 与 JWT 身份验证集成
  • 在行和列级别进行精细控制
基于 JWT 的身份验证

Supabase Auth 使用 JSON Web Token:

  • 身份验证成功后签发
  • 自动包含在数据库查询中
  • 用于评估 RLS 策略
  • 使用刷新令牌流程支持长会话
类型安全

根据数据库架构自动生成 TypeScript 类型:

  • 从实时数据库生成类型
  • 类型安全的查询和变更操作
  • 编译时错误检测
  • 支持 IDE 自动补全

Supabase 客户端设置

安装
# npm
npm install @supabase/supabase-js

# yarn
yarn add @supabase/supabase-js

# pnpm
pnpm add @supabase/supabase-js

# bun
bun add @supabase/supabase-js
环境配置
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xyzcompany.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

# For server-side operations (keep secure!)
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

**安全提示:**切勿在客户端代码中公开 service_role 密钥。

客户端初始化模式(推荐)
// lib/supabase.ts

import { createClient, SupabaseClient } from '@supabase/supabase-js'
import { Database } from './database.types'

function validateEnvironment() {
  const url = process.env.NEXT_PUBLIC_SUPABASE_URL
  const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY

  if (!url) {
    throw new Error('Missing environment variable: NEXT_PUBLIC_SUPABASE_URL')
  }

  if (!anonKey) {
    throw new Error('Missing environment variable: NEXT_PUBLIC_SUPABASE_ANON_KEY')
  }

  return { url, anonKey }
}

let supabaseInstance: SupabaseClient<Database> | null = null

export function getSupabaseClient(): SupabaseClient<Database> {
  if (!supabaseInstance) {
    const { url, anonKey } = validateEnvironment()

    supabaseInstance = createClient<Database>(url, anonKey, {
      auth: {
        autoRefreshToken: true,
        persistSession: true,
        detectSessionInUrl: true
      },
      global: {
        headers: {
          'X-Application-Name': 'MyApp'
        }
      }
    })
  }

  return supabaseInstance
}

// Export singleton instance
export const supabase = getSupabaseClient()
配置选项
const options = {
  // Database configuration
  db: {
    schema: 'public'  // Default schema
  },

  // Authentication configuration
  auth: {
    autoRefreshToken: true,     // Automatically refresh tokens
    persistSession: true,        // Persist session to localStorage
    detectSessionInUrl: true,    // Detect session from URL hash
    flowType: 'pkce',           // Use PKCE flow for OAuth
    storage: customStorage,      // Custom storage implementation
    storageKey: 'sb-auth-token' // Storage key for session
  },

  // Global configuration
  global: {
    headers: {
      'X-Application-Name': 'my-app',
      'apikey': SUPABASE_ANON_KEY
    },
    fetch: customFetch  // Custom fetch implementation
  },

  // Realtime configuration
  realtime: {
    params: {
      eventsPerSecond: 10
    },
    timeout: 10000,
    heartbeatInterval: 30000
  }
}

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, options)
特定平台的设置

使用 AsyncStorage 的 React Native:

import AsyncStorage from '@react-native-async-storage/async-storage'
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
  auth: {
    storage: AsyncStorage,
    autoRefreshToken: true,
    persistSession: true,
    detectSessionInUrl: false
  }
})

使用 Expo SecureStore 的 React Native:

import * as SecureStore from 'expo-secure-store'
import { createClient } from '@supabase/supabase-js'

const ExpoSecureStoreAdapter = {
  getItem: (key: string) => SecureStore.getItemAsync(key),
  setItem: (key: string, value: string) => SecureStore.setItemAsync(key, value),
  removeItem: (key: string) => SecureStore.deleteItemAsync(key)
}

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
  auth: {
    storage: ExpoSecureStoreAdapter,
    autoRefreshToken: true,
    persistSession: true
  }
})

身份验证与授权

电子邮件/密码身份验证

注册:

const { data, error } = await supabase.auth.signUp({
  email: '[email protected]',
  password: 'secure-password',
  options: {
    data: {
      // Additional user metadata
      display_name: 'John Doe',
      avatar_url: 'https://example.com/avatar.jpg'
    },
    emailRedirectTo: 'https://yourapp.com/welcome'
  }
})

if (error) {
  console.error('Signup failed:', error.message)
  return
}

console.log('User created:', data.user)
console.log('Session:', data.session)

登录:

const { data, error } = await supabase.auth.signInWithPassword({
  email: '[email protected]',
  password: 'secure-password'
})

if (error) {
  console.error('Login failed:', error.message)
  return
}

console.log('User:', data.user)
console.log('Session token:', data.session?.access_token)
魔法链接(无密码)
const { data, error } = await supabase.auth.signInWithOtp({
  email: '[email protected]',
  options: {
    emailRedirectTo: 'https://yourapp.com/login',
    shouldCreateUser: true
  }
})

if (error) {
  console.error('Failed to send magic link:', error.message)
  return
}

console.log('Magic link sent')
一次性密码(OTP)——电话
// Send OTP
const { data, error } = await supabase.auth.signInWithOtp({
  phone: '+1234567890',
  options: {
    channel: 'sms' // or 'whatsapp'
  }
})

// Verify OTP
const { data: verifyData, error: verifyError } = await supabase.auth.verifyOtp({
  phone: '+1234567890',
  token: '123456',
  type: 'sms'
})
OAuth(社交登录)
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'google',
  options: {
    redirectTo: 'https://yourapp.com/auth/callback',
    scopes: 'email profile',
    queryParams: {
      access_type: 'offline',
      prompt: 'consent'
    }
  }
})

// Supported providers:
// apple, google, github, gitlab, bitbucket, discord, facebook,
// twitter, microsoft, linkedin, notion, slack, spotify, twitch, etc.
会话管理
// Get current session
const { data: { session }, error } = await supabase.auth.getSession()

if (session) {
  console.log('Access token:', session.access_token)
  console.log('User:', session.user)
  console.log('Expires at:', session.expires_at)
}

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

// Refresh session
const { data, error } = await supabase.auth.refreshSession()

// Sign out
const { error } = await supabase.auth.signOut()
身份验证状态变更
const { data: { subscription } } = supabase.auth.onAuthStateChange(
  (event, session) => {
    console.log('Auth event:', event)

    switch (event) {
      case 'SIGNED_IN':
        console.log('User signed in:', session?.user)
        break

      case 'SIGNED_OUT':
        console.log('User signed out')
        break

      case 'TOKEN_REFRESHED':
        console.log('Token refreshed')
        break

      case 'USER_UPDATED':
        console.log('User updated:', session?.user)
        break

      case 'PASSWORD_RECOVERY':
        console.log('Password recovery initiated')
        break
    }
  }
)

// Cleanup
subscription.unsubscribe()
用户管理
// Update user
const { data, error } = await supabase.auth.updateUser({
  email: '[email protected]',
  password: 'new-password',
  data: {
    display_name: 'New Name',
    avatar_url: 'https://example.com/new-avatar.jpg'
  }
})

// Reset password
const { data, error } = await supabase.auth.resetPasswordForEmail(
  '[email protected]',
  {
    redirectTo: 'https://yourapp.com/reset-password'
  }
)

// Update password after reset
const { data: updateData, error: updateError } = await supabase.auth.updateUser({
  password: 'new-secure-password'
})
多因素身份验证(MFA)
// Enroll MFA
const { data: enrollData, error: enrollError } = await supabase.auth.mfa.enroll({
  factorType: 'totp',
  friendlyName: 'My Phone'
})

// Verify enrollment
const { data: verifyData, error: verifyError } = await supabase.auth.mfa.verify({
  factorId: enrollData.id,
  code: '123456'
})

// Challenge (during sign-in)
const { data: challengeData, error: challengeError } = await supabase.auth.mfa.challenge({
  factorId: 'factor-id'
})

// Verify challenge
const { data, error } = await supabase.auth.mfa.verify({
  factorId: 'factor-id',
  challengeId: challengeData.id,
  code: '123456'
})

数据库操作

SELECT 查询

基本查询:

// Select all columns
const { data, error } = await supabase
  .from('users')
  .select()

// Select specific columns
const { data, error } = await supabase
  .from('users')
  .select('id, email, created_at')

筛选:

// Equal
const { data } = await supabase
  .from('users')
  .select()
  .eq('status', 'active')

// Not equal
const { data } = await supabase
  .from('users')
  .select()
  .neq('role', 'admin')

// Greater than / Less than
const { data } = await supabase
  .from('products')
  .select()
  .gt('price', 100)
  .lte('stock', 10)

// In array
const { data } = await supabase
  .from('users')
  .select()
  .in('id', [1, 2, 3, 4, 5])

// Pattern matching
const { data } = await supabase
  .from('users')
  .select()
  .like('email', '%@gmail.com')

// Case-insensitive pattern matching
const { data } = await supabase
  .from('products')
  .select()
  .ilike('name', '%laptop%')

// Full text search
const { data } = await supabase
  .from('articles')
  .select()
  .textSearch('title', 'postgres database')

// Null checks
const { data } = await supabase
  .from('users')
  .select()
  .is('deleted_at', null)

排序和分页:

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

// Multiple ordering
const { data } = await supabase
  .from('users')
  .select()
  .order('last_name', { ascending: true })
  .order('first_name', { ascending: true })

// Limit results
const { data } = await supabase
  .from('posts')
  .select()
  .limit(10)

// Pagination with range
const { data } = await supabase
  .from('posts')
  .select()
  .range(0, 9)  // First 10 items (0-indexed)

联接和嵌套查询:

// One-to-many relationship
const { data } = await supabase
  .from('users')
  .select(`
    id,
    email,
    posts (
      id,
      title,
      created_at
    )
  `)

// Many-to-many with junction table
const { data } = await supabase
  .from('users')
  .select(`
    id,
    email,
    user_roles (
      role:roles (
        id,
        name
      )
    )
  `)

// Nested filtering
const { data } = await supabase
  .from('users')
  .select(`
    id,
    email,
    posts!inner (
      id,
      title
    )
  `)
  .eq('posts.published', true)

聚合:

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

// Count with filtering
const { count } = await supabase
  .from('users')
  .select('*', { count: 'exact', head: true })
  .eq('status', 'active')
INSERT 操作
// Insert single row
const { data, error } = await supabase
  .from('users')
  .insert({
    email: '[email protected]',
    name: 'John Doe',
    age: 30
  })
  .select()  // Return inserted row

// Insert multiple rows
const { data, error } = await supabase
  .from('users')
  .insert([
    { email: '[email protected]', name: 'User One' },
    { email: '[email protected]', name: 'User Two' },
    { email: '[email protected]', name: 'User Three' }
  ])
  .select()

// Upsert (Insert or Update)
const { data, error } = await supabase
  .from('users')
  .upsert({
    id: 1,
    email: '[email protected]',
    name: 'Updated Name'
  }, {
    onConflict: 'id'  // Conflict column(s)
  })
  .select()
UPDATE 操作
// Update with filter
const { data, error } = await supabase
  .from('users')
  .update({ status: 'inactive' })
  .eq('last_login', null)
  .select()

// Update single row by ID
const { data, error } = await supabase
  .from('users')
  .update({ name: 'New Name' })
  .eq('id', userId)
  .select()
  .single()

// Increment value
const { data, error } = await supabase
  .from('profiles')
  .update({ login_count: supabase.raw('login_count + 1') })
  .eq('id', userId)
DELETE 操作
// Delete with filter
const { error } = await supabase
  .from('users')
  .delete()
  .eq('status', 'banned')

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

// Soft delete pattern
const { error } = await supabase
  .from('users')
  .update({ deleted_at: new Date().toISOString() })
  .eq('id', userId)
RPC(远程过程调用)
// Call function without parameters
const { data, error } = await supabase
  .rpc('get_user_count')

// Call function with parameters
const { data, error } = await supabase
  .rpc('calculate_discount', {
    product_id: 123,
    user_id: 456
  })

实时订阅

数据库变更订阅
// Listen to all changes
const channel = supabase
  .channel('db-changes')
  .on(
    'postgres_changes',
    {
      event: '*',          // All events: INSERT, UPDATE, DELETE
      schema: 'public',
      table: 'posts'
    },
    (payload) => {
      console.log('Change received:', payload)
      console.log('Event type:', payload.eventType)
      console.log('New data:', payload.new)
      console.log('Old data:', payload.old)
    }
  )
  .subscribe()

// Cleanup
channel.unsubscribe()

监听特定事件:

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

// UPDATE only
const updateChannel = supabase
  .channel('post-updates')
  .on(
    'postgres_changes',
    {
      event: 'UPDATE',
      schema: 'public',
      table: 'posts'
    },
    (payload) => {
      console.log('Post updated:', payload.new)
    }
  )
  .subscribe()

// Filter changes for specific rows
const channel = supabase
  .channel('user-posts')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'posts',
      filter: `user_id=eq.${userId}`
    },
    (payload) => {
      console.log('User post changed:', payload)
    }
  )
  .subscribe()
广播消息
// Send broadcast
const channel = supabase.channel('room-1')

channel.subscribe((status) => {
  if (status === 'SUBSCRIBED') {
    channel.send({
      type: 'broadcast',
      event: 'cursor-move',
      payload: { x: 100, y: 200, user: 'Alice' }
    })
  }
})

// Receive broadcast
const channel = supabase
  .channel('room-1')
  .on('broadcast', { event: 'cursor-move' }, (payload) => {
    console.log('Cursor moved:', payload)
  })
  .subscribe()
在线状态跟踪
// Track user presence
const channel = supabase.channel('online-users')

// Set initial state
channel.subscribe(async (status) => {
  if (status === 'SUBSCRIBED') {
    await channel.track({
      user: 'user-1',
      online_at: new Date().toISOString(),
      status: 'online'
    })
  }
})

// Listen to presence changes
channel.on('presence', { event: 'sync' }, () => {
  const state = channel.presenceState()
  console.log('Online users:', state)
})

channel.on('presence', { event: 'join' }, ({ newPresences }) => {
  console.log('Users joined:', newPresences)
})

channel.on('presence', { event: 'leave' }, ({ leftPresences }) => {
  console.log('Users left:', leftPresences)
})

// Cleanup
channel.unsubscribe()

存储操作

Bucket 管理
// List buckets
const { data: buckets, error } = await supabase
  .storage
  .listBuckets()

// Create bucket
const { data, error } = await supabase
  .storage
  .createBucket('avatars', {
    public: false,           // Private bucket
    fileSizeLimit: 1048576,  // 1MB limit
    allowedMimeTypes: ['image/png', 'image/jpeg']
  })

// Update bucket
const { data, error } = await supabase
  .storage
  .updateBucket('avatars', {
    public: true
  })

// Delete bucket
const { data, error } = await supabase
  .storage
  .deleteBucket('avatars')
文件上传
// Standard upload
const file = event.target.files[0]
const filePath = `${userId}/${Date.now()}-${file.name}`

const { data, error } = await supabase
  .storage
  .from('avatars')
  .upload(filePath, file, {
    cacheControl: '3600',
    upsert: false
  })

// Upload with progress tracking
const { data, error } = await supabase
  .storage
  .from('videos')
  .upload(filePath, file, {
    onUploadProgress: (progress) => {
      const percent = (progress.loaded / progress.total) * 100
      console.log(`Upload progress: ${percent.toFixed(2)}%`)
    }
  })
文件下载和 URL
// Download file
const { data, error } = await supabase
  .storage
  .from('avatars')
  .download('path/to/file.jpg')

// Get public URL (for public buckets)
const { data } = supabase
  .storage
  .from('avatars')
  .getPublicUrl('path/to/file.jpg')

// Create signed URL (for private buckets)
const { data, error } = await supabase
  .storage
  .from('private-files')
  .createSignedUrl('path/to/file.pdf', 60) // Expires in 60 seconds
图像转换
const { data } = supabase
  .storage
  .from('avatars')
  .getPublicUrl('user-avatar.jpg', {
    transform: {
      width: 200,
      height: 200,
      resize: 'cover',  // or 'contain', 'fill'
      quality: 80,
      format: 'webp'
    }
  })
文件管理
// List files
const { data, error } = await supabase
  .storage
  .from('avatars')
  .list('user-123', {
    limit: 100,
    offset: 0,
    sortBy: { column: 'name', order: 'asc' }
  })

// Delete files
const { data, error } = await supabase
  .storage
  .from('avatars')
  .remove(['path/to/file1.jpg', 'path/to/file2.jpg'])

// Move file
const { data, error } = await supabase
  .storage
  .from('avatars')
  .move('old/path/file.jpg', 'new/path/file.jpg')

// Copy file
const { data, error } = await supabase
  .storage
  .from('avatars')
  .copy('source/file.jpg', 'destination/file.jpg')

TypeScript 集成

生成数据库类型
# Install Supabase CLI
npm install -g supabase

# Login
supabase login

# Generate types
supabase gen types typescript --project-id YOUR_PROJECT_ID > database.types.ts
使用生成的类型
import { createClient } from '@supabase/supabase-js'
import { Database } from './database.types'

// Create typed client
const supabase = createClient<Database>(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_ANON_KEY!
)

// Type-safe queries
const { data, error } = await supabase
  .from('users')
  .select('id, email, created_at')
  .eq('id', userId)

// data is typed as:
// Array<{ id: string; email: string; created_at: string }> | null

// Type-safe inserts
const { data, error } = await supabase
  .from('posts')
  .insert({
    title: 'My Post',
    content: 'Content here',
    user_id: userId,
    published: true
  })
  .select()
辅助类型
import { Database } from './database.types'

// Get table row type
type User = Database['public']['Tables']['users']['Row']

// Get insert type
type NewUser = Database['public']['Tables']['users']['Insert']

// Get update type
type UserUpdate = Database['public']['Tables']['users']['Update']

// Get enum type
type UserRole = Database['public']['Enums']['user_role']

// Use in functions
function createUser(user: NewUser): Promise<User> {
  // Implementation
}

行级安全性(RLS)

启用 RLS
-- Enable RLS on a table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
常见 RLS 模式

公开读取访问权限:

CREATE POLICY "Public profiles are visible to everyone"
ON profiles
FOR SELECT
TO anon, authenticated
USING (true);

用户只能查看自己的数据:

CREATE POLICY "Users can only see own data"
ON posts
FOR SELECT
TO authenticated
USING (auth.uid() = user_id);

用户只能修改自己的数据:

CREATE POLICY "Users can insert own posts"
ON posts
FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);

CREATE POLICY "Users can update own posts"
ON posts
FOR UPDATE
TO authenticated
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);

CREATE POLICY "Users can delete own posts"
ON posts
FOR DELETE
TO authenticated
USING (auth.uid() = user_id);

多租户模式:

CREATE POLICY "Users can only see data from own organization"
ON documents
FOR SELECT
TO authenticated
USING (
  organization_id IN (
    SELECT organization_id
    FROM user_organizations
    WHERE user_id = auth.uid()
  )
);

基于角色的访问控制:

CREATE POLICY "Admin can see all users"
ON users
FOR SELECT
TO authenticated
USING (
  auth.jwt() ->> 'role' = 'admin'
  OR auth.uid() = id
);

最佳实践

客户端初始化

使用单例模式:

  • 创建单个客户端实例,并在整个应用中重复使用
  • 避免在每次请求时都创建新的客户端
  • 将其存储在模块级变量或上下文中
错误处理

始终检查错误:

const { data, error } = await supabase
  .from('users')
  .select()

if (error) {
  console.error('Error fetching users:', error.message)
  // Handle error appropriately
  return
}

// Use data safely
console.log(data)

使用 throwOnError() 拒绝 Promise:

try {
  const { data } = await supabase
    .from('users')
    .insert({ name: 'John' })
    .throwOnError()

  console.log('User created:', data)
} catch (error) {
  console.error('Failed to create user:', error)
}
安全性

绝不公开服务角色密钥:

  • 在客户端代码中使用 anon 密钥
  • 仅在服务端代码中使用 service_role 密钥
  • 将服务角色密钥保存在服务端环境变量中

始终启用 RLS:

  • 在所有表上启用 RLS
  • 为每个表创建适当的策略
  • 彻底测试策略

验证用户输入:

  • 绝不信任客户端数据
  • 使用数据库约束和验证
  • 同时在客户端和数据库中进行验证
性能

合理使用 Select:

// Bad: Fetch all columns
const { data } = await supabase.from('users').select()

// Good: Only fetch needed columns
const { data } = await supabase.from('users').select('id, email')

使用分页:

// Bad: Fetch all rows
const { data } = await supabase.from('posts').select()

// Good: Paginate results
const { data } = await supabase
  .from('posts')
  .select()
  .range(0, 9)
  .order('created_at', { ascending: false })

为数据库列添加索引:

  • 为经常查询的列添加索引
  • 为外键添加索引
  • 对多列查询使用复合索引
连接管理

重复使用客户端实例:

  • 不要为每个请求都创建新客户端
  • 使用单例模式初始化客户端
  • 对于服务端,考虑使用连接池

清理订阅:

useEffect(() => {
  const channel = supabase.channel('room-1')

  channel.subscribe(/* ... */)

  return () => {
    channel.unsubscribe()
  }
}, [])

常见模式和工作流

用户身份验证流程
// 1. Sign up
const { data: signUpData, error: signUpError } = await supabase.auth.signUp({
  email: '[email protected]',
  password: 'secure-password'
})

// 2. Listen to auth state
supabase.auth.onAuthStateChange((event, session) => {
  if (event === 'SIGNED_IN') {
    // Redirect to dashboard
  }
})

// 3. Protected route check
const { data: { session } } = await supabase.auth.getSession()
if (!session) {
  // Redirect to login
}

// 4. Sign out
await supabase.auth.signOut()
使用 RLS 执行 CRUD
// Enable RLS on table
/*
ALTER TABLE todos ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can CRUD own todos"
ON todos
FOR ALL
TO authenticated
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
*/

// Create
const { data, error } = await supabase
  .from('todos')
  .insert({
    user_id: session.user.id,
    title: 'My Todo',
    completed: false
  })
  .select()

// Read (only user's own todos due to RLS)
const { data, error } = await supabase
  .from('todos')
  .select()

// Update
const { data, error } = await supabase
  .from('todos')
  .update({ completed: true })
  .eq('id', todoId)

// Delete
const { error } = await supabase
  .from('todos')
  .delete()
  .eq('id', todoId)
实时聊天实现
import { useEffect, useState } from 'react'
import { supabase } from '@/lib/supabase'

function Chat({ roomId, userId }) {
  const [messages, setMessages] = useState([])

  useEffect(() => {
    // Fetch existing messages
    const fetchMessages = async () => {
      const { data } = await supabase
        .from('messages')
        .select()
        .eq('room_id', roomId)
        .order('created_at', { ascending: true })

      if (data) setMessages(data)
    }

    fetchMessages()

    // Subscribe to new messages
    const channel = supabase
      .channel(`room-${roomId}`)
      .on(
        'postgres_changes',
        {
          event: 'INSERT',
          schema: 'public',
          table: 'messages',
          filter: `room_id=eq.${roomId}`
        },
        (payload) => {
          setMessages((prev) => [...prev, payload.new])
        }
      )
      .subscribe()

    return () => {
      channel.unsubscribe()
    }
  }, [roomId])

  const sendMessage = async (content: string) => {
    await supabase.from('messages').insert({
      room_id: roomId,
      user_id: userId,
      content
    })
  }

  return (
    <div>
      {messages.map((msg) => (
        <div key={msg.id}>{msg.content}</div>
      ))}
    </div>
  )
}
显示进度的文件上传
async function uploadAvatar(file: File, userId: string) {
  const filePath = `${userId}/${Date.now()}-${file.name}`

  const { data, error } = await supabase.storage
    .from('avatars')
    .upload(filePath, file, {
      cacheControl: '3600',
      upsert: false,
      onUploadProgress: (progress) => {
        const percent = (progress.loaded / progress.total) * 100
        console.log(`Upload: ${percent.toFixed(2)}%`)
      }
    })

  if (error) {
    console.error('Upload failed:', error.message)
    return null
  }

  // Get public URL
  const { data: urlData } = supabase.storage
    .from('avatars')
    .getPublicUrl(filePath)

  // Update user profile with avatar URL
  await supabase
    .from('profiles')
    .update({ avatar_url: urlData.publicUrl })
    .eq('id', userId)

  return urlData.publicUrl
}

故障排除

常见问题

RLS 阻止查询:

  • 检查是否已启用 RLS:ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;
  • 确认策略存在且符合使用场景
  • 在不同用户上下文中测试策略
  • 对 SELECT/UPDATE/DELETE 使用 USING 子句
  • 对 INSERT/UPDATE 使用 WITH CHECK 子句

身份验证会话未持久化:

  • 确保配置中设置了 persistSession: true
  • 检查存储(localStorage)是否可用
  • 确认 Cookie 未被阻止
  • 检查是否启用了第三方 Cookie(用于 OAuth)

实时功能无法使用:

  • 在 Supabase 控制台中为表启用实时功能
  • 检查 RLS 策略是否允许订阅
  • 确认频道订阅成功
  • 检查网络或防火墙是否阻止 WebSocket

类型生成错误:

  • 确保已安装并更新 Supabase CLI
  • 确认项目 ID 正确
  • 检查与 Supabase 的网络连接
  • 尝试使用 --debug 标志重新生成类型
调试 RLS 策略
-- Test policy as specific user
SET request.jwt.claims.sub = 'user-uuid-here';

-- Run query to see what's visible
SELECT * FROM posts;

-- Reset to admin
RESET request.jwt.claims.sub;
性能问题

查询缓慢:

  • 为经常查询的列添加索引
  • 使用 EXPLAIN ANALYZE 分析查询计划
  • 避免获取不必要的列
  • 对大型数据集使用分页

连接过多:

  • 使用连接池
  • 重复使用客户端实例
  • 关闭未使用的订阅
  • 考虑使用 Edge Functions 处理服务端逻辑

生产环境部署

环境配置
# Production .env
NEXT_PUBLIC_SUPABASE_URL=https://prod-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=prod-anon-key
SUPABASE_SERVICE_ROLE_KEY=prod-service-role-key

# Staging .env
NEXT_PUBLIC_SUPABASE_URL=https://staging-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=staging-anon-key
SUPABASE_SERVICE_ROLE_KEY=staging-service-role-key
数据库迁移

使用 Supabase CLI 进行架构迁移:

# Initialize migrations
supabase migration new create_posts_table

# Apply migrations
supabase db push

# Generate migration from changes
supabase db diff -f create_users_table
监控
  • 启用 Supabase 控制台监控
  • 为错误和性能问题设置警报
  • 监控数据库连接
  • 跟踪 API 使用情况和配额
  • 为身份验证事件设置日志记录
备份策略
  • 在 Supabase 控制台中启用自动备份
  • 配置时间点恢复
  • 测试恢复流程
  • 定期导出架构和数据
  • 将备份存储在单独的位置
扩展性考量
  • 升级 Supabase 套餐以获得更高的限制
  • 使用数据库连接池
  • 实施缓存(Redis 等)
  • 对于大量读取负载,考虑使用只读副本
  • 使用 Edge Functions 处理繁重的计算任务
  • 优化数据库索引
  • 监控并优化慢查询

技能版本:1.0.0 最后更新:2025 年 10 月 技能类别:后端即服务、数据库集成、身份验证、实时功能 兼容对象:React、Next.js、Vue、Angular、React Native、Flutter、Node.js、Deno

评论

支持 Markdown