supabase-database-ops
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSupabase Database Operations - Critical Guardrail
Supabase数据库操作 - 关键防护准则
Purpose
目的
CRITICAL GUARDRAIL to prevent multi-tenant data leakage and enforce database best practices in the AIProDaily platform.
关键防护准则,用于在AIProDaily平台中防止多租户数据泄露并执行数据库最佳实践。
When to Use
适用场景
This skill BLOCKS database operations until verified when:
- Writing Supabase queries ()
supabaseAdmin.from() - Accessing tenant-scoped tables
- Creating API routes with database access
- Working with campaign, article, or RSS data
当出现以下情况时,该准则会阻止数据库操作,直到验证通过:
- 编写Supabase查询()
supabaseAdmin.from() - 访问租户范围的表
- 创建带有数据库访问权限的API路由
- 处理营销活动、文章或RSS数据
🚨 CRITICAL RULES 🚨
🚨 核心规则 🚨
Rule #1: ALWAYS Filter by publication_id
规则1:始终通过publication_id进行过滤
EVERY query on tenant-scoped tables MUST include filter.
publication_idtypescript
// ✅ CORRECT - publication_id filter present
const { data, error } = await supabaseAdmin
.from('newsletter_campaigns')
.select('id, status, date')
.eq('publication_id', newsletterId) // ✅ REQUIRED
.eq('id', campaignId)
.single()
// ❌ WRONG - Missing publication_id filter (DATA LEAKAGE!)
const { data, error } = await supabaseAdmin
.from('newsletter_campaigns')
.select('id, status, date')
.eq('id', campaignId) // ❌ Can access other tenants' data!
.single()所有针对租户范围表的查询必须包含过滤条件。
publication_idtypescript
// ✅ 正确示例 - 包含publication_id过滤条件
const { data, error } = await supabaseAdmin
.from('newsletter_campaigns')
.select('id, status, date')
.eq('publication_id', newsletterId) // ✅ 必须包含
.eq('id', campaignId)
.single()
// ❌ 错误示例 - 缺少publication_id过滤条件(存在数据泄露风险!)
const { data, error } = await supabaseAdmin
.from('newsletter_campaigns')
.select('id, status, date')
.eq('id', campaignId) // ❌ 可访问其他租户的数据!
.single()Tenant-Scoped Tables (MUST filter by publication_id):
需按租户隔离的表(必须通过publication_id过滤):
newsletter_campaignsarticlessecondary_articlesrss_postspost_ratingsrss_feedsapp_settingsadvertisementscampaign_advertisementsarchived_articlesarchived_rss_posts
newsletter_campaignsarticlessecondary_articlesrss_postspost_ratingsrss_feedsapp_settingsadvertisementscampaign_advertisementsarchived_articlesarchived_rss_posts
Non-Scoped Tables (publication_id not needed):
无需租户隔离的表(不需要publication_id):
- (top-level tenant table)
newsletters - System-wide configuration tables
- (顶层租户表)
newsletters - 系统级配置表
Rule #2: Use supabaseAdmin for Server-Side Operations
规则2:仅在服务器端使用supabaseAdmin
NEVER expose service role key client-side.
typescript
// ✅ CORRECT - Server-side API route or Server Action
import { supabaseAdmin } from '@/lib/supabase'
export async function POST(request: NextRequest) {
const { data } = await supabaseAdmin
.from('newsletter_campaigns')
.select('*')
.eq('publication_id', newsletterId)
return NextResponse.json({ data })
}
// ❌ WRONG - Never in client components
'use client'
import { supabaseAdmin } from '@/lib/supabase' // ❌ Security risk!
export default function ClientComponent() {
// This exposes service role key to browser
const { data } = await supabaseAdmin.from('...').select()
}Where to use supabaseAdmin:
- ✅ API routes ()
app/api/**/*.ts - ✅ Server Actions (functions)
'use server' - ✅ Server Components (without )
'use client' - ✅ Background jobs/cron
- ✅ Workflow steps
Where NOT to use:
- ❌ Client Components ()
'use client' - ❌ Browser-executed code
- ❌ Public-facing pages
绝对不要在客户端暴露服务角色密钥。
typescript
// ✅ 正确示例 - 服务器端API路由或Server Action
import { supabaseAdmin } from '@/lib/supabase'
export async function POST(request: NextRequest) {
const { data } = await supabaseAdmin
.from('newsletter_campaigns')
.select('*')
.eq('publication_id', newsletterId)
return NextResponse.json({ data })
}
// ❌ 错误示例 - 绝不能在客户端组件中使用
'use client'
import { supabaseAdmin } from '@/lib/supabase' // ❌ 存在安全风险!
export default function ClientComponent() {
// 会将服务角色密钥暴露给浏览器
const { data } = await supabaseAdmin.from('...').select()
}supabaseAdmin的适用场景:
- ✅ API路由()
app/api/**/*.ts - ✅ Server Action(函数)
'use server' - ✅ 服务器组件(无标识)
'use client' - ✅ 后台任务/定时任务
- ✅ 工作流步骤
supabaseAdmin的禁用场景:
- ❌ 客户端组件(标识)
'use client' - ❌ 浏览器执行的代码
- ❌ 面向公众的页面
Rule #3: Avoid SELECT *
规则3:避免使用SELECT *
Only select the fields you need.
typescript
// ✅ CORRECT - Specific fields
const { data } = await supabaseAdmin
.from('articles')
.select('id, headline, article_text, is_active')
.eq('publication_id', newsletterId)
.eq('campaign_id', campaignId)
// ❌ WRONG - Fetches all columns (performance impact)
const { data } = await supabaseAdmin
.from('articles')
.select('*')
.eq('publication_id', newsletterId)
.eq('campaign_id', campaignId)Exception: When you genuinely need all columns for data operations.
仅选择你需要的字段。
typescript
// ✅ 正确示例 - 指定字段
const { data } = await supabaseAdmin
.from('articles')
.select('id, headline, article_text, is_active')
.eq('publication_id', newsletterId)
.eq('campaign_id', campaignId)
// ❌ 错误示例 - 获取所有列(影响性能)
const { data } = await supabaseAdmin
.from('articles')
.select('*')
.eq('publication_id', newsletterId)
.eq('campaign_id', campaignId)例外情况:当你确实需要所有列进行数据操作时。
Rule #4: Always Check for Errors
规则4:始终检查错误
Never assume database operations succeed.
typescript
// ✅ CORRECT - Check for errors
const { data, error } = await supabaseAdmin
.from('newsletter_campaigns')
.select('id, status')
.eq('publication_id', newsletterId)
.eq('id', campaignId)
.single()
if (error) {
console.error('[DB] Query failed:', error.message)
throw new Error('Failed to fetch campaign')
}
if (!data) {
console.log('[DB] No campaign found')
return null
}
// Now safe to use data
return data
// ❌ WRONG - No error handling
const { data } = await supabaseAdmin
.from('newsletter_campaigns')
.select('id, status')
.eq('id', campaignId)
.single()
return data.status // ❌ Crashes if error or data is null永远不要假设数据库操作一定会成功。
typescript
// ✅ 正确示例 - 检查错误
const { data, error } = await supabaseAdmin
.from('newsletter_campaigns')
.select('id, status')
.eq('publication_id', newsletterId)
.eq('id', campaignId)
.single()
if (error) {
console.error('[DB] 查询失败:', error.message)
throw new Error('获取营销活动失败')
}
if (!data) {
console.log('[DB] 未找到营销活动')
return null
}
// 现在可以安全使用data
return data
// ❌ 错误示例 - 未处理错误
const { data } = await supabaseAdmin
.from('newsletter_campaigns')
.select('id, status')
.eq('id', campaignId)
.single()
return data.status // ❌ 若出现错误或data为null会崩溃Database Query Patterns
数据库查询模式
Standard Query Pattern
标准查询模式
typescript
const { data, error } = await supabaseAdmin
.from('table_name')
.select('field1, field2, field3')
.eq('publication_id', newsletterId) // ✅ ALWAYS for tenant tables
.eq('other_field', value)
.single() // or .maybeSingle() if record might not exist
if (error) {
console.error('[DB] Query error:', error.message)
throw new Error(`Database query failed: ${error.message}`)
}
if (!data) {
console.log('[DB] No record found')
return null
}
return datatypescript
const { data, error } = await supabaseAdmin
.from('table_name')
.select('field1, field2, field3')
.eq('publication_id', newsletterId) // ✅ 租户表必须包含
.eq('other_field', value)
.single() // 若记录可能不存在,使用.maybeSingle()
if (error) {
console.error('[DB] 查询错误:', error.message)
throw new Error(`数据库查询失败: ${error.message}`)
}
if (!data) {
console.log('[DB] 未找到记录')
return null
}
return dataInsert Pattern
插入模式
typescript
const { data, error } = await supabaseAdmin
.from('articles')
.insert({
publication_id: newsletterId, // ✅ REQUIRED
campaign_id: campaignId,
headline: 'Article headline',
article_text: 'Content here',
is_active: false
})
.select()
.single()
if (error) {
console.error('[DB] Insert failed:', error.message)
throw new Error('Failed to create article')
}
return datatypescript
const { data, error } = await supabaseAdmin
.from('articles')
.insert({
publication_id: newsletterId, // ✅ 必须包含
campaign_id: campaignId,
headline: '文章标题',
article_text: '文章内容',
is_active: false
})
.select()
.single()
if (error) {
console.error('[DB] 插入失败:', error.message)
throw new Error('创建文章失败')
}
return dataUpdate Pattern
更新模式
typescript
const { data, error } = await supabaseAdmin
.from('articles')
.update({
is_active: true,
updated_at: new Date().toISOString()
})
.eq('id', articleId)
.eq('publication_id', newsletterId) // ✅ REQUIRED - prevents updating other tenants
.select()
.single()
if (error) {
console.error('[DB] Update failed:', error.message)
throw new Error('Failed to update article')
}
return datatypescript
const { data, error } = await supabaseAdmin
.from('articles')
.update({
is_active: true,
updated_at: new Date().toISOString()
})
.eq('id', articleId)
.eq('publication_id', newsletterId) // ✅ 必须包含 - 防止修改其他租户的数据
.select()
.single()
if (error) {
console.error('[DB] 更新失败:', error.message)
throw new Error('更新文章失败')
}
return dataDelete Pattern
删除模式
typescript
const { error } = await supabaseAdmin
.from('rss_posts')
.delete()
.eq('id', postId)
.eq('publication_id', newsletterId) // ✅ REQUIRED - prevents deleting other tenants' data
if (error) {
console.error('[DB] Delete failed:', error.message)
throw new Error('Failed to delete post')
}typescript
const { error } = await supabaseAdmin
.from('rss_posts')
.delete()
.eq('id', postId)
.eq('publication_id', newsletterId) // ✅ 必须包含 - 防止删除其他租户的数据
if (error) {
console.error('[DB] 删除失败:', error.message)
throw new Error('删除帖子失败')
}Join Pattern (Relationships)
关联查询模式
typescript
const { data, error } = await supabaseAdmin
.from('newsletter_campaigns')
.select(`
id,
status,
date,
articles (
id,
headline,
is_active
),
secondary_articles (
id,
headline,
is_active
)
`)
.eq('publication_id', newsletterId) // ✅ REQUIRED on parent table
.eq('id', campaignId)
.single()typescript
const { data, error } = await supabaseAdmin
.from('newsletter_campaigns')
.select(`
id,
status,
date,
articles (
id,
headline,
is_active
),
secondary_articles (
id,
headline,
is_active
)
`)
.eq('publication_id', newsletterId) // ✅ 必须在父表中添加
.eq('id', campaignId)
.single()Common Mistakes
常见错误
❌ Forgetting publication_id Filter
❌ 忘记添加publication_id过滤条件
typescript
// This query can access ANY campaign from ANY tenant!
const { data } = await supabaseAdmin
.from('newsletter_campaigns')
.select('*')
.eq('id', campaignId) // ❌ Missing publication_idtypescript
// 该查询可以访问任意租户的任意营销活动!
const { data } = await supabaseAdmin
.from('newsletter_campaigns')
.select('*')
.eq('id', campaignId) // ❌ 缺少publication_id❌ Using supabaseAdmin Client-Side
❌ 在客户端使用supabaseAdmin
typescript
'use client'
// ❌ Exposes service role key to browser
export default function MyComponent() {
const { data } = await supabaseAdmin.from('...').select()
}typescript
'use client'
// ❌ 会将服务角色密钥暴露给浏览器
export default function MyComponent() {
const { data } = await supabaseAdmin.from('...').select()
}❌ No Error Handling
❌ 未处理错误
typescript
// ❌ No error check - will crash on failure
const { data } = await supabaseAdmin.from('...').select().single()
const status = data.status // Crashes if data is nulltypescript
// ❌ 未检查错误 - 失败时会崩溃
const { data } = await supabaseAdmin.from('...').select().single()
const status = data.status // 若data为null会崩溃❌ Using SELECT *
❌ 使用SELECT *
typescript
// ❌ Fetches unnecessary data, impacts performance
const { data } = await supabaseAdmin
.from('articles')
.select('*')typescript
// ❌ 获取不必要的数据,影响性能
const { data } = await supabaseAdmin
.from('articles')
.select('*')Quick Reference
快速参考
✅ DO:
- Always filter by on tenant-scoped tables
publication_id - Use only server-side
supabaseAdmin - Select specific fields
- Check for errors
- Use for single records
.single() - Use if record might not exist
.maybeSingle() - Log errors with prefix
[DB]
❌ DON'T:
- Skip filter
publication_id - Use in client components
supabaseAdmin - Use without reason
SELECT * - Ignore errors
- Assume data exists
- Expose service keys client-side
✅ 需要做:
- 租户范围表始终通过过滤
publication_id - 仅在服务器端使用
supabaseAdmin - 选择特定字段
- 检查错误
- 单条记录使用
.single() - 记录可能不存在时使用
.maybeSingle() - 错误日志添加前缀
[DB]
❌ 禁止做:
- 省略过滤条件
publication_id - 在客户端组件中使用
supabaseAdmin - 无理由使用
SELECT * - 忽略错误
- 假设数据一定存在
- 在客户端暴露服务密钥
Error Recovery
错误排查
If you see "Row level security policy violated":
- Check if you're filtering by
publication_id - Verify you're using (not client)
supabaseAdmin - Confirm you're on server-side (API route/Server Action)
If you see "column does not exist":
- Verify column name spelling
- Check if field exists in database schema
- Ensure you're querying the correct table
Skill Status: ACTIVE GUARDRAIL ✅
Enforcement Level: BLOCK (Critical)
Line Count: < 500 ✅
Purpose: Prevent multi-tenant data leakage ✅
如果出现“Row level security policy violated”错误:
- 检查是否添加了过滤条件
publication_id - 确认使用的是(而非客户端实例)
supabaseAdmin - 确认代码运行在服务器端(API路由/Server Action)
如果出现“column does not exist”错误:
- 验证列名拼写
- 检查字段是否存在于数据库架构中
- 确保查询的是正确的表
准则状态: 启用防护 ✅
执行级别: 阻止操作(关键)
代码行数: < 500 ✅
目的: 防止多租户数据泄露 ✅