Back to list
nathanielcrowell12-spec

supabase

by nathanielcrowell12-spec

A collection point for various photography website

0🍴 0📅 Jan 21, 2026

SKILL.md


name: supabase description: > Implement Supabase database patterns for PhotoVault including RLS policies, TypeScript queries, and storage operations. Use when working with database schemas, Row Level Security, migrations, storage buckets, auth.uid() policies, or debugging empty query results. Includes PhotoVault table structure and client setup patterns.

⚠️ MANDATORY WORKFLOW - DO NOT SKIP

When this skill activates, you MUST follow the expert workflow before writing any code:

  1. Spawn Domain Expert using the Task tool with this prompt:

    Read the expert prompt at: C:\Users\natha\Stone-Fence-Brain\VENTURES\PhotoVault\claude\experts\supabase-expert.md
    
    Then research the codebase and write an implementation plan to: docs/claude/plans/supabase-[task-name]-plan.md
    
    Task: [describe the user's request]
    
  2. Spawn QA Critic after expert returns, using Task tool:

    Read the QA critic prompt at: C:\Users\natha\Stone-Fence-Brain\VENTURES\PhotoVault\claude\experts\qa-critic-expert.md
    
    Review the plan at: docs/claude/plans/supabase-[task-name]-plan.md
    Write critique to: docs/claude/plans/supabase-[task-name]-critique.md
    
  3. Present BOTH plan and critique to user - wait for approval before implementing

DO NOT read files and start coding. DO NOT rationalize that "this is simple." Follow the workflow.


Supabase Integration

Core Principles

RLS First, Always

Row Level Security is not optional. Every table in the public schema MUST have RLS enabled.

ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;

Key insight: Once RLS is enabled with no policies, the table is completely locked down. You then selectively grant access.

Type Safety is Non-Negotiable

npx supabase gen types typescript --project-id <project-id> > src/types/supabase.ts
import { createClient } from '@supabase/supabase-js'
import { Database } from '@/types/supabase'

const supabase = createClient<Database>(url, key)

Understand Your Three Clients

ClientWhen to UseRLS Behavior
Browser ClientClient components, user actionsRespects RLS based on auth.uid()
Server ClientServer components, API routesRespects RLS based on auth.uid()
Admin ClientWebhooks, cron jobs, migrationsBypasses RLS entirely

Never use the admin client for user-facing operations.

Anti-Patterns

RLS Mistakes

Not wrapping auth.uid() for performance

-- WRONG: Calls auth.uid() for every row
CREATE POLICY "slow_policy" ON posts
FOR SELECT USING (auth.uid() = user_id);

-- RIGHT: Caches auth.uid() per statement
CREATE POLICY "fast_policy" ON posts
FOR SELECT USING ((SELECT auth.uid()) = user_id);

Permissive policies without thinking

-- WRONG: ALL authenticated users see ALL rows
CREATE POLICY "bad_policy" ON posts
FOR SELECT TO authenticated
USING (true);

Query Mistakes

Not handling join result types correctly

// WRONG: Assuming joins always return arrays
const client = gallery.clients[0]

// RIGHT: Check the type
const clientData = gallery.clients
const client = Array.isArray(clientData) ? clientData[0] : clientData

Not using .single() or .maybeSingle() appropriately

// WRONG: Returns array when you expect one item
const { data } = await supabase.from('users').select().eq('id', id)

// RIGHT: Use .single() for guaranteed single row
const { data } = await supabase.from('users').select().eq('id', id).single()

// RIGHT: Use .maybeSingle() when row might not exist
const { data } = await supabase.from('users').select().eq('email', email).maybeSingle()

RLS Policy Patterns

Users own their data

CREATE POLICY "users_own_data" ON user_data
FOR ALL TO authenticated
USING ((SELECT auth.uid()) = user_id)
WITH CHECK ((SELECT auth.uid()) = user_id);

Photographer → Client relationship (PhotoVault pattern)

CREATE POLICY "photographer_sees_clients" ON clients
FOR SELECT TO authenticated
USING (
  photographer_id IN (
    SELECT id FROM photographers
    WHERE user_id = (SELECT auth.uid())
  )
);

CREATE POLICY "client_own_data" ON clients
FOR SELECT TO authenticated
USING (user_id = (SELECT auth.uid()));

Role-based access

CREATE POLICY "admin_only" ON admin_settings
FOR ALL TO authenticated
USING (
  EXISTS (
    SELECT 1 FROM user_profiles
    WHERE user_id = (SELECT auth.uid())
    AND role = 'admin'
  )
);

TypeScript Query Patterns

Fetch with joins

async function getGalleryWithDetails(galleryId: string) {
  const { data, error } = await supabase
    .from('photo_galleries')
    .select(`
      *,
      clients (id, name, email),
      gallery_photos (id, filename, thumbnail_url, original_url)
    `)
    .eq('id', galleryId)
    .single()

  if (error) throw error
  if (!data) return null

  const client = Array.isArray(data.clients)
    ? data.clients[0]
    : data.clients

  return { ...data, client, photos: data.gallery_photos ?? [] }
}

PhotoVault Configuration

Key Tables

TablePurposeKey Columns
user_profilesExtended user datauser_id, user_type, business_name
photo_galleriesGallery metadataphotographer_id, client_id, status
gallery_photosIndividual photosgallery_id, original_url, thumbnail_url
clientsClient recordsphotographer_id, user_id, email
commission_paymentsPayment trackingphotographer_id, amount, status

Client Setup

// Browser client (client components)
import { createBrowserClient } from '@supabase/ssr'
import { Database } from '@/types/supabase'

export function createClient() {
  return createBrowserClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}

// Server client (server components, API routes)
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createServerSupabaseClient() {
  const cookieStore = await cookies()
  return createServerClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() { return cookieStore.getAll() },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) =>
            cookieStore.set(name, value, options)
          )
        },
      },
    }
  )
}

// Admin client (webhooks, cron - BYPASSES RLS)
import { createClient } from '@supabase/supabase-js'

export function createAdminClient() {
  return createClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.SUPABASE_SERVICE_ROLE_KEY!
  )
}

Known PhotoVault Gotchas

  1. Primary gallery table is photo_galleries (not galleries)
  2. Join results: Single relations return objects, not arrays - always check Array.isArray()
  3. Photo URLs: Some photos may be missing original_url - fall back to thumbnail_url
  4. User types: Stored in user_profiles.user_type as 'photographer', 'client', or 'admin'

Debugging Checklist

  1. Is RLS enabled? Check in Supabase Dashboard → Table → RLS toggle
  2. Are there policies? Check Authentication → Policies
  3. Is the user authenticated? Check auth.uid() is not null
  4. Does the policy match? Test in SQL Editor with SET ROLE authenticated
  5. Are you using the right client? Browser vs Server vs Admin
  6. Check the Supabase logs for policy violations

Test RLS in SQL Editor:

SELECT set_config('request.jwt.claims', '{"sub": "user-uuid-here"}', true);
SET ROLE authenticated;
SELECT * FROM your_table;
RESET ROLE;

Score

Total Score

50/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon