关于 supabase-database-ops
supabase-database-ops 是面向 AIProDaily 开发的数据库操作安全护栏,适用于编写 Supabase 查询和数据库 API。它强制关注租户隔离、服务端访问、字段选择与错误检查,以降低跨租户数据泄漏风险。
主要功能
- 01租户表查询必须过滤 publication_id
- 02仅在服务端使用 supabaseAdmin
- 03避免无必要的 SELECT *
- 04检查数据库操作错误
- 05为插入、更新和删除提供模式
适用场景
- 01编写租户范围内的 Supabase 查询
- 02开发访问数据库的 API 路由
- 03修改文章、活动或 RSS 数据
- 04排查行级安全策略错误
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ venture-formations/ aiprodaily --skill supabase-database-ops安装后在 Claude Code 中输入/supabase-database-ops确认已加载。
详细介绍
Supabase 数据库操作——关键防护措施
目的
关键防护措施:防止 AIProDaily 平台发生多租户数据泄露,并确保遵循数据库最佳实践。
使用时机
在以下情况下,此技能会阻止数据库操作,直至完成验证:
- 编写 Supabase 查询(
supabaseAdmin.from()) - 访问按租户划分的表
- 创建可访问数据库的 API 路由
- 处理营销活动、文章或 RSS 数据
🚨 关键规则 🚨
规则 #1:始终按 publication_id 筛选
对按租户划分的表执行的每个查询都必须包含 publication_id 筛选条件。
// ✅ 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_id 筛选):
newsletter_campaignsarticlessecondary_articlesrss_postspost_ratingsrss_feedsapp_settingsadvertisementscampaign_advertisementsarchived_articlesarchived_rss_posts
非按租户划分的表(无需 publication_id):
newsletters(顶层租户表)- 全系统配置表
规则 #2:服务端操作使用 supabaseAdmin
切勿在客户端暴露服务角色密钥。
// ✅ 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()
}
适用场景:
- ✅ API 路由(
app/api/**/*.ts) - ✅ 服务端操作(
'use server'函数) - ✅ 服务端组件(不含
'use client') - ✅ 后台任务/定时任务
- ✅ 工作流步骤
不适用场景:
- ❌ 客户端组件(
'use client') - ❌ 在浏览器中执行的代码
- ❌ 面向公众的页面
规则 #3:避免使用 SELECT *
只选择所需字段。
// ✅ 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)
例外情况:确实需要所有列来执行数据操作时。
规则 #4:始终检查错误
切勿假定数据库操作会成功。
// ✅ 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
数据库查询模式
标准查询模式
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 data
插入模式
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 data
更新模式
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 data
删除模式
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')
}
联接模式(关系)
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()
常见错误
❌ 忘记添加 publication_id 筛选条件
// This query can access ANY campaign from ANY tenant!
const { data } = await supabaseAdmin
.from('newsletter_campaigns')
.select('*')
.eq('id', campaignId) // ❌ Missing publication_id
❌ 在客户端使用 supabaseAdmin
'use client'
// ❌ Exposes service role key to browser
export default function MyComponent() {
const { data } = await supabaseAdmin.from('...').select()
}
❌ 未处理错误
// ❌ No error check - will crash on failure
const { data } = await supabaseAdmin.from('...').select().single()
const status = data.status // Crashes if data is null
❌ 使用 SELECT *
// ❌ Fetches unnecessary data, impacts performance
const { data } = await supabaseAdmin
.from('articles')
.select('*')
快速参考
✅ 应做事项:
- 对按租户划分的表始终按
publication_id筛选 - 仅在服务端使用
supabaseAdmin - 选择特定字段
- 检查错误
- 查询单条记录时使用
.single() - 记录可能不存在时使用
.maybeSingle() - 使用
[DB]前缀记录错误
❌ 不应做事项:
- 跳过
publication_id筛选条件 - 在客户端组件中使用
supabaseAdmin - 无故使用
SELECT * - 忽略错误
- 假定数据存在
- 在客户端暴露服务密钥
错误恢复
如果看到“Row level security policy violated”:
- 检查是否按
publication_id筛选 - 确认使用的是
supabaseAdmin(而非客户端) - 确认代码运行在服务端(API 路由/服务端操作)
如果看到“column does not exist”:
- 确认列名拼写正确
- 检查数据库架构中是否存在该字段
- 确保查询的是正确的表
技能状态:防护措施已启用 ✅ 强制级别:阻止(关键) 行数:< 500 ✅ 目的:防止多租户数据泄露 ✅
评论
支持 Markdown