supabase-database-ops
π―Skillfrom venture-formations/aiprodaily
supabase-database-ops skill from venture-formations/aiprodaily
Installation
npx skills add https://github.com/venture-formations/aiprodaily --skill supabase-database-opsSkill Details
Critical guardrail for Supabase database operations ensuring multi-tenant isolation with publication_id filtering, proper use of supabaseAdmin, avoiding SELECT *, error handling patterns, and secure server-side database access. Use when writing database queries, working with supabase, accessing newsletter_campaigns, articles, rss_posts, or any tenant-scoped data.
Overview
# 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.
```typescript
// β 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.
```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 (
'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.
```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.
---
Rule #4: Always Check for Errors
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
```
---
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 data
```
Insert 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 data
```
Update 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 data
```
Delete 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')
}
```
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()
```
---
Common Mistakes
β Forgetting publication_id Filter
```typescript
// 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
```typescript
'use client'
// β Exposes service role key to browser
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 null
```
β Using SELECT *
```typescript
// β 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 β