🎯

supabase-database-ops

🎯Skill

from venture-formations/aiprodaily

VibeIndex|
What it does

supabase-database-ops skill from venture-formations/aiprodaily

supabase-database-ops

Installation

Install skill:
npx skills add https://github.com/venture-formations/aiprodaily --skill supabase-database-ops
1
AddedJan 29, 2026

Skill Details

SKILL.md

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_campaigns
  • articles
  • secondary_articles
  • rss_posts
  • post_ratings
  • rss_feeds
  • app_settings
  • advertisements
  • campaign_advertisements
  • archived_articles
  • archived_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_id on tenant-scoped tables
  • Use supabaseAdmin only 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_id filter
  • Use supabaseAdmin in 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":

  1. Check if you're filtering by publication_id
  2. Verify you're using supabaseAdmin (not client)
  3. Confirm you're on server-side (API route/Server Action)

If you see "column does not exist":

  1. Verify column name spelling
  2. Check if field exists in database schema
  3. Ensure you're querying the correct table

---

Skill Status: ACTIVE GUARDRAIL βœ…

Enforcement Level: BLOCK (Critical)

Line Count: < 500 βœ…

Purpose: Prevent multi-tenant data leakage βœ