// This skill should be used when working with Supabase database operations in the MyJKKN project, including creating modules, updating schemas, writing RLS policies, creating database functions, implementing Auth SSR, or developing Edge Functions. Automatically triggers when user mentions 'database', 'table', 'SQL', 'Supabase', 'migration', 'RLS', 'policy', or 'Edge Function'.
| name | supabase-expert |
| description | This skill should be used when working with Supabase database operations in the MyJKKN project, including creating modules, updating schemas, writing RLS policies, creating database functions, implementing Auth SSR, or developing Edge Functions. Automatically triggers when user mentions 'database', 'table', 'SQL', 'Supabase', 'migration', 'RLS', 'policy', or 'Edge Function'. |
This skill provides comprehensive guidance for working with Supabase in the MyJKKN education management system. It enforces critical file management rules, security patterns, and performance optimizations to maintain a clean, organized database structure.
mcp__supabase__list_tables to see current schemamcp__supabase__execute_sql to query structuresupabase/setup/ directory:
supabase/setup/01_tables.sqlsupabase/setup/02_functions.sqlsupabase/setup/03_policies.sqlsupabase/setup/04_triggers.sqlsupabase/setup/05_views.sqlNEVER USE (DEPRECATED - BREAKS APPLICATION):
get(), set(), remove()@supabase/auth-helpers-nextjsALWAYS USE:
@supabase/ssrgetAll() and setAll() ONLYgetUser() to refresh sessionsupabaseResponse object(SELECT auth.uid()) not auth.uid()TO authenticated or TO anonFOR ALL - create 4 separate policies (SELECT, INSERT, UPDATE, DELETE)SECURITY INVOKER (safer than DEFINER)search_path = '' for securitypublic.table_name)SECURITY DEFINER unless absolutely requiredDeno.serve (not old serve import)npm:/jsr:/node: prefix with version numbers_shared/ folder/tmp directoryUser mentions database/SQL work?
โโ> YES: Query real-time database with Supabase MCP FIRST
โ โโ> Creating new module?
โ โ โโ> Use: Module Creation Workflow
โ โโ> Updating existing table?
โ โ โโ> Use: Schema Update Workflow
โ โโ> Creating RLS policies?
โ โ โโ> Use: RLS Policy Workflow
โ โโ> Creating database function?
โ โ โโ> Use: Database Function Workflow
โ โโ> Creating Edge Function?
โ โ โโ> Use: Edge Function Workflow
โ โโ> Debugging database issue?
โ โโ> Use: Debug Workflow
โโ> NO: Skill not applicable
โ ๏ธ CRITICAL: Always use MCP to query real-time database state
SQL files may be outdated - MCP shows actual database reality
When to use: User asks to create a new module, add new tables, or build new database feature.
Process:
Query REAL-TIME database state with Supabase MCP (ALWAYS FIRST)
Use Supabase MCP to check current database schema:
mcp__supabase__list_tables
Verify table doesn't exist:
mcp__supabase__execute_sql
SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename LIKE '%keyword%';
Check related tables:
mcp__supabase__execute_sql
SELECT * FROM information_schema.tables WHERE table_schema = 'public';
Check SQL_FILE_INDEX.md (for documentation reference only)
Read supabase/SQL_FILE_INDEX.md
NOTE: This may be outdated - trust MCP query results over file contents
Design tables following MyJKKN conventions
Update ONLY supabase/setup/01_tables.sql
references/sql-templates.mdCreate RLS policies in supabase/setup/03_policies.sql
references/rls-policy-patterns.mdCreate TypeScript types in types/[module_name].ts
Create service layer in lib/services/[module_name]/
Create React Query hooks in hooks/[module_name]/
Update SQL_FILE_INDEX.md with new tables
โ ๏ธ IMPORTANT: Always verify with MCP that tables don't already exist before creating.
See references/module-creation-template.md for complete example.
When to use: User asks to add column, modify table, or update existing schema.
Process:
Query REAL-TIME table structure with Supabase MCP (ALWAYS FIRST)
Use Supabase MCP to get current schema:
mcp__supabase__execute_sql
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'your_table'
ORDER BY ordinal_position;
Check constraints:
mcp__supabase__execute_sql
SELECT constraint_name, constraint_type
FROM information_schema.table_constraints
WHERE table_schema = 'public' AND table_name = 'your_table';
Check indexes:
mcp__supabase__execute_sql
SELECT indexname, indexdef
FROM pg_indexes
WHERE schemaname = 'public' AND tablename = 'your_table';
Apply migration using Supabase MCP
Use Supabase MCP: mcp__supabase__apply_migration
Name: add_[column]_to_[table]
Query: ALTER TABLE public.table_name ADD COLUMN column_name TYPE;
Update supabase/setup/01_tables.sql to match database reality
-- Updated: YYYY-MM-DD - Added [column_name] for [reason]
ALTER TABLE public.table_name ADD COLUMN column_name TYPE;
NOTE: This file now documents what IS in the database (via MCP query)
Update TypeScript types in relevant type file
Update SQL_FILE_INDEX.md with changes
Verify with MCP that change was applied successfully
mcp__supabase__execute_sql
SELECT column_name FROM information_schema.columns
WHERE table_name = 'your_table' AND column_name = 'new_column';
โ ๏ธ IMPORTANT: Always query MCP first to see current state, then apply migration, then update SQL files.
When to use: User asks to create policies, secure table, or implement access control.
Critical Performance Rules:
Process:
Read references/rls-policy-patterns.md for templates
Choose correct pattern:
Update supabase/setup/03_policies.sql
-- =====================================================
-- [TABLE_NAME] RLS POLICIES
-- =====================================================
-- Created: YYYY-MM-DD
-- Performance: Indexed on [columns]
CREATE POLICY "policy_name"
ON public.table_name
FOR SELECT
TO authenticated
USING ((SELECT auth.has_institution_access(institution_id)));
Create required indexes
CREATE INDEX IF NOT EXISTS idx_[table]_[column]
ON public.table_name(column_name);
Test with different user roles
When to use: User asks to create stored procedure, trigger function, or database logic.
Process:
Read references/sql-templates.md for function templates
Choose security mode:
Choose volatility:
Update supabase/setup/02_functions.sql
-- =====================================================
-- FUNCTION: function_name
-- Purpose: [description]
-- Created: YYYY-MM-DD
-- Security: INVOKER (runs with caller permissions)
-- =====================================================
CREATE OR REPLACE FUNCTION public.function_name(
p_param1 TYPE
)
RETURNS return_type
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = ''
AS $$
BEGIN
-- Use fully qualified names
SELECT column_name
INTO v_result
FROM public.table_name
WHERE condition = p_param1;
RETURN v_result;
END;
$$;
Grant appropriate permissions
GRANT EXECUTE ON FUNCTION public.function_name TO authenticated;
See references/function-templates.md for complete examples.
When to use: User asks to create serverless function, API endpoint, or background task.
Process:
Read references/edge-function-templates.md for templates
Choose function type:
Create function directory
supabase/functions/[function-name]/index.ts
Use correct import format
import express from "npm:express@4.18.2"
import { createClient } from "npm:@supabase/supabase-js@2"
Use Deno.serve (not old serve)
Deno.serve(async (req: Request) => {
// Handler logic
})
Add CORS headers for browser requests
Deploy function
supabase functions deploy function-name
See references/edge-function-templates.md for complete examples.
When to use: User working with authentication, cookies, or middleware.
Browser Client (lib/supabase/client.ts):
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
Server Client (lib/supabase/server.ts):
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return cookieStore.getAll() },
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {
// Ignore if called from Server Component
}
},
},
}
)
}
Middleware (middleware.ts):
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
let supabaseResponse = NextResponse.next({ request })
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return request.cookies.getAll() },
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) =>
request.cookies.set(name, value)
)
supabaseResponse = NextResponse.next({ request })
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
)
},
},
}
)
// CRITICAL: Must call getUser() to refresh session
const { data: { user } } = await supabase.auth.getUser()
if (!user && !request.nextUrl.pathname.startsWith('/login')) {
const url = request.nextUrl.clone()
url.pathname = '/login'
return NextResponse.redirect(url)
}
return supabaseResponse // MUST return supabaseResponse
}
See references/auth-ssr-patterns.md for complete patterns.
When to use: User reports database error, performance issue, or unexpected behavior.
Process:
Query REAL-TIME database state with Supabase MCP (ALWAYS FIRST)
Get actual data:
mcp__supabase__execute_sql
SELECT * FROM public.table_name WHERE condition;
Check table structure:
mcp__supabase__execute_sql
\d public.table_name
Get table statistics:
mcp__supabase__execute_sql
SELECT COUNT(*), status FROM public.table_name GROUP BY status;
Check RLS policies using MCP
Query actual policies in database:
mcp__supabase__execute_sql
SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual
FROM pg_policies
WHERE tablename = 'your_table';
Check if RLS is enabled:
mcp__supabase__execute_sql
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public' AND tablename = 'your_table';
Verify user permissions using MCP
mcp__supabase__execute_sql
SELECT auth.jwt()->>'role' as user_role;
mcp__supabase__execute_sql
SELECT auth.has_institution_access('institution-id-here'::uuid);
Check foreign key constraints using MCP
mcp__supabase__execute_sql
SELECT
tc.constraint_name,
tc.table_name,
kcu.column_name,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage AS ccu
ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_name = 'your_table';
Check indexes using MCP
mcp__supabase__execute_sql
SELECT indexname, indexdef
FROM pg_indexes
WHERE schemaname = 'public' AND tablename = 'your_table';
Test query performance using MCP
mcp__supabase__execute_sql
EXPLAIN ANALYZE
SELECT * FROM public.table_name WHERE condition;
โ ๏ธ IMPORTANT: NEVER read SQL files for debugging - always query MCP for current database state.
Core Conventions:
Query Formatting:
-- Simple queries: compact
select * from public.users where is_active = true;
-- Complex queries: expanded
select
users.first_name,
users.last_name,
count(orders.id) as total_orders
from
public.users
left join
public.orders on users.id = orders.user_id
where
users.is_active = true
group by
users.id
order by
total_orders desc;
-- โ
CORRECT
CREATE TABLE public.students (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
institution_id UUID NOT NULL,
first_name TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- โ WRONG
CREATE TABLE Student ( -- Should be lowercase plural
ID INT, -- Should be UUID
FirstName VARCHAR -- Should be snake_case
);
idx_[table]_[column]trg_[table]_[action]verb_noun (get_student_attendance)All MyJKKN tables follow multi-tenant pattern:
CREATE TABLE public.module_table (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
institution_id UUID NOT NULL REFERENCES public.institutions(id),
-- other columns
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES public.profiles(id)
);
-- Always filter by institution
SELECT * FROM public.module_table
WHERE institution_id = (
SELECT auth.jwt() -> 'app_metadata' ->> 'institution_id'
)::uuid;
Before ANY Supabase work:
โ ๏ธ CRITICAL RULE: SQL files may be outdated. ALWAYS use MCP to query the actual database state first.
references/sql-templates.md - Complete SQL templates for all object typesreferences/rls-policy-patterns.md - Performance-optimized RLS policy templatesreferences/auth-ssr-patterns.md - Complete Auth SSR implementation patternsreferences/edge-function-templates.md - Edge function templates and patternsreferences/module-creation-template.md - Step-by-step module creation guidescripts/validate_sql_files.py - Check for duplicate SQL filesscripts/check_index.py - Verify SQL_FILE_INDEX.md is up to dateassets/table-template.sql - Base table creation templateassets/migration-template.sql - Migration file templateCreate [MODULE_NAME] module with [ENTITIES]. Follow supabase-expert skill:
FIRST query MCP for existing tables, then update setup/01_tables.sql only,
add RLS policies, create types/services/hooks, update index.
Update [TABLE]: add [COLUMNS]. Follow supabase-expert skill:
FIRST query MCP for current structure, apply migration via MCP,
then update SQL files to match database reality.
Create RLS policies for [TABLE]. Follow supabase-expert skill:
query MCP for existing policies, use performance-optimized patterns,
wrap functions in SELECT, create indexes.
Create Edge Function [NAME] for [PURPOSE]. Follow supabase-expert skill:
use Deno.serve, npm: imports with versions, proper CORS headers.
Debug [ISSUE]. Follow supabase-expert skill:
query MCP for real-time database state, check policies with MCP,
verify constraints and indexes via MCP queries.
๐ด MOST CRITICAL: Always use mcp__supabase__execute_sql or mcp__supabase__list_tables to query database BEFORE reading any SQL files.
These are the most useful MCP queries for checking real-time database state:
mcp__supabase__list_tables
mcp__supabase__execute_sql
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'your_table'
ORDER BY ordinal_position;
mcp__supabase__execute_sql
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'your_table'
);
mcp__supabase__execute_sql
SELECT indexname, indexdef
FROM pg_indexes
WHERE schemaname = 'public' AND tablename = 'your_table';
mcp__supabase__execute_sql
SELECT policyname, permissive, roles, cmd, qual, with_check
FROM pg_policies
WHERE schemaname = 'public' AND tablename = 'your_table';
mcp__supabase__execute_sql
SELECT
tc.constraint_name,
kcu.column_name,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage AS ccu
ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_name = 'your_table';
mcp__supabase__execute_sql
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public' AND tablename = 'your_table';
mcp__supabase__execute_sql
SELECT COUNT(*) FROM public.your_table;
mcp__supabase__execute_sql
SELECT tablename
FROM pg_tables
WHERE schemaname = 'public' AND tablename LIKE '%keyword%';
๐ก TIP: Save these queries for quick access during development.
With Memory Server:
Remember: ALWAYS query Supabase MCP for real-time database state FIRST
Remember: SQL files may be outdated - MCP shows reality
Remember: NEVER use CLI commands - only Supabase MCP tools
Remember: MyJKKN uses institution_id for multi-tenancy
Remember: RLS policies need (SELECT auth.uid()) wrapping
Remember: Update SQL files to match database reality (from MCP queries)
With Sequential Thinking:
Use sequential thinking to:
1. Plan complex module creation
2. Debug multi-table issues
3. Design RLS policy hierarchy
4. Optimize database performance
With Task Agents:
Use Task tool with general-purpose agent:
"Follow supabase-expert skill to create [MODULE] module.
FIRST query Supabase MCP for real-time database state.
NEVER create duplicate files. Update SQL files to match database reality."
Skill Version: 1.1.0 Last Updated: 2025-01-27 Tested On: MyJKKN v1.0 (Supabase, Next.js 15, TypeScript)
Version 1.1.0 Changes: