supabase-database-ops
Supabase Database Operations - Critical Guardrail
Purpose
CRITICAL GUARDRAIL to prevent multi-tenant data leakage and enforce database best practices in the AIProDaily platform.
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
🚨 CRITICAL RULES 🚨
Rule #1: ALWAYS Filter by publication_id
EVERY query on tenant-scoped tables MUST include publication_id filter.
// ✅ 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()
Tenant-Scoped Tables (MUST filter by publication_id):
newsletter_campaignsarticlessecondary_articlesrss_postspost_ratingsrss_feedsapp_settingsadvertisementscampaign_advertisementsarchived_articlesarchived_rss_posts
Non-Scoped Tables (publication_id not needed):
newsletters(top-level tenant table)- System-wide configuration tables
Rule #2: Use supabaseAdmin for Server-Side Operations
NEVER expose service role key client-side.
// ✅ 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 (
'use server'functions) - ✅ Server Components (without
'use client') - ✅ Background jobs/cron
- ✅ Workflow steps
Where NOT to use:
- ❌ Client Components (
'use client') - ❌ Browser-executed code
- ❌ Public-facing pages
Rule #3: Avoid SELECT *
Only select the fields you need.
// ✅ 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.
Rule #4: Always Check for Errors
Never assume database operations succeed.
// ✅ 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
Database Query Patterns
Standard Query Pattern
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
Insert Pattern
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
Update Pattern
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
Delete Pattern
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')
}
Join Pattern (Relationships)
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()
Common Mistakes
❌ Forgetting publication_id Filter
// This query can access ANY campaign from ANY tenant!
const { data } = await supabaseAdmin
.from('newsletter_campaigns')
.select('*')
.eq('id', campaignId) // ❌ Missing publication_id
❌ Using supabaseAdmin Client-Side
'use client'
// ❌ Exposes service role key to browser
export default function MyComponent() {
const { data } = await supabaseAdmin.from('...').select()
}
❌ No Error Handling
// ❌ No error check - will crash on failure
const { data } = await supabaseAdmin.from('...').select().single()
const status = data.status // Crashes if data is null
❌ Using SELECT *
// ❌ Fetches unnecessary data, impacts performance
const { data } = await supabaseAdmin
.from('articles')
.select('*')
Quick Reference
✅ DO:
- Always filter by
publication_idon tenant-scoped tables - Use
supabaseAdminonly server-side - Select specific fields
- Check for errors
- Use
.single()for single records - Use
.maybeSingle()if record might not exist - Log errors with
[DB]prefix
❌ DON'T:
- Skip
publication_idfilter - Use
supabaseAdminin client components - Use
SELECT *without reason - Ignore errors
- Assume data exists
- Expose service keys client-side
Error Recovery
If you see "Row level security policy violated":
- Check if you're filtering by
publication_id - Verify you're using
supabaseAdmin(not client) - 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 ✅
More from venture-formations/aiprodaily
nextjs-api-routes
Next.js 15 API route patterns, NextRequest, NextResponse, error handling, maxDuration configuration, authentication, request validation, server-side operations, route handlers, and API endpoint best practices. Use when creating API routes, handling requests, configuring timeouts, or building server-side endpoints.
47ai-content-generation
AI content generation with OpenAI and Claude, callAIWithPrompt usage, prompt storage in app_settings, structured outputs, response format validation, multi-criteria scoring, rate limiting, JSON schema, and AI API best practices. Use when generating content, creating prompts, scoring articles, or working with OpenAI/Claude APIs.
33newsletter-campaign-workflow
Guide for working with newsletter campaigns, RSS processing workflows, article generation, MailerLite integration, multi-tenant isolation, campaign status management, Vercel workflows, advertorial rotation, and publication automation. Use when creating campaigns, processing RSS feeds, generating articles, managing workflow steps, handling campaign statuses, or working with newsletter-related database operations.
29skill-developer
Create and manage Claude Code skills following Anthropic best practices. Use when creating new skills, modifying skill-rules.json, understanding trigger patterns, working with hooks, debugging skill activation, or implementing progressive disclosure. Covers skill structure, YAML frontmatter, trigger types (keywords, intent patterns, file paths, content patterns), enforcement levels (block, suggest, warn), hook mechanisms (UserPromptSubmit, PreToolUse), session tracking, and the 500-line rule.
24