skills/stack/supabase-postgrest-patterns/SKILL.md
# Supabase PostgREST Patterns Patterns for querying Supabase effectively through the PostgREST API. Use resource embedding to avoid N+1 queries, type all responses, handle errors explicitly, and paginate everything. --- ## Select With Specific Columns Never use `select('*')` in production. Select only the columns you need. ```typescript // YES — specific columns const { data, error } = await supabase .from("invoices") .select("id, status, created_at, customer:customers(id, name)") .eq
npx skillsauth add 33prime/rtg-forge skills/stack/supabase-postgrest-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Patterns for querying Supabase effectively through the PostgREST API. Use resource embedding to avoid N+1 queries, type all responses, handle errors explicitly, and paginate everything.
Never use select('*') in production. Select only the columns you need.
// YES — specific columns
const { data, error } = await supabase
.from("invoices")
.select("id, status, created_at, customer:customers(id, name)")
.eq("tenant_id", tenantId);
// NO — fetches everything, including large JSONB columns
const { data, error } = await supabase
.from("invoices")
.select("*");
Use PostgREST resource embedding to fetch related data in a single query. This avoids N+1 problems.
// ONE query fetches invoices with their customer and line items
const { data, error } = await supabase
.from("invoices")
.select(`
id,
status,
created_at,
customer:customers (
id,
name,
email
),
line_items (
id,
description,
quantity,
unit_price
)
`)
.eq("tenant_id", tenantId)
.order("created_at", { ascending: false });
alias:table_name(columns) syntax!inner for INNER JOIN behavior: customer:customers!inner(name)// Equality
.eq("status", "paid")
// Not equal
.neq("status", "cancelled")
// Greater than / less than
.gt("total", 1000)
.lt("created_at", "2026-01-01")
// In a set
.in("status", ["draft", "sent"])
// Pattern matching
.ilike("customer_name", "%acme%")
// JSONB containment
.contains("metadata", { source: "import" })
// NULL checks
.is("deleted_at", null)
.not("assigned_to", "is", null)
// Combining filters (AND)
.eq("tenant_id", tenantId)
.eq("status", "paid")
.gte("created_at", startDate)
// OR conditions
.or("status.eq.draft,status.eq.sent")
Always paginate list queries. Never return unbounded result sets.
const PAGE_SIZE = 50;
interface PaginatedResult<T> {
items: T[];
total: number;
page: number;
pageSize: number;
hasMore: boolean;
}
async function getInvoices(
tenantId: string,
page: number = 1,
pageSize: number = PAGE_SIZE,
): Promise<PaginatedResult<Invoice>> {
const from = (page - 1) * pageSize;
const to = from + pageSize - 1;
const { data, error, count } = await supabase
.from("invoices")
.select("id, status, created_at, customer:customers(name)", { count: "exact" })
.eq("tenant_id", tenantId)
.order("created_at", { ascending: false })
.range(from, to);
if (error) throw new SupabaseQueryError("Failed to fetch invoices", error);
return {
items: data as Invoice[],
total: count ?? 0,
page,
pageSize,
hasMore: (count ?? 0) > page * pageSize,
};
}
range(from, to) for offset pagination{ count: "exact" } to select() to get total countALWAYS check the error property on Supabase responses. Never assume success.
// Custom error class for Supabase query errors
class SupabaseQueryError extends Error {
constructor(
message: string,
public readonly pgError: { message: string; code: string; details: string | null },
) {
super(`${message}: ${pgError.message} (code: ${pgError.code})`);
this.name = "SupabaseQueryError";
}
}
// Helper function that throws on error
async function queryOrThrow<T>(
query: PromiseLike<{ data: T | null; error: any }>,
): Promise<T> {
const { data, error } = await query;
if (error) throw new SupabaseQueryError("Query failed", error);
if (data === null) throw new Error("Query returned null data without error");
return data;
}
// Usage
const invoices = await queryOrThrow(
supabase
.from("invoices")
.select("id, status")
.eq("tenant_id", tenantId),
);
Use Postgres functions (RPC) for complex operations that don't fit the REST model.
-- Define the function in a migration
CREATE OR REPLACE FUNCTION calculate_invoice_total(p_invoice_id UUID)
RETURNS TABLE(subtotal NUMERIC, tax NUMERIC, total NUMERIC)
LANGUAGE sql
STABLE
SECURITY INVOKER -- Respects RLS
AS $$
SELECT
SUM(quantity * unit_price) AS subtotal,
SUM(quantity * unit_price) * 0.08 AS tax,
SUM(quantity * unit_price) * 1.08 AS total
FROM line_items
WHERE invoice_id = p_invoice_id;
$$;
// Call from TypeScript
const { data, error } = await supabase
.rpc("calculate_invoice_total", { p_invoice_id: invoiceId });
if (error) throw new SupabaseQueryError("Failed to calculate total", error);
// data is { subtotal: number, tax: number, total: number }
SECURITY INVOKER to respect RLScalculate_invoice_total, get_dashboard_statsSTABLE or IMMUTABLE volatility when possibleGenerate types from your database schema and use them for all queries.
// Generated types (from supabase gen types typescript)
import type { Database } from "./database.types";
type Invoice = Database["public"]["Tables"]["invoices"]["Row"];
type InvoiceInsert = Database["public"]["Tables"]["invoices"]["Insert"];
type InvoiceUpdate = Database["public"]["Tables"]["invoices"]["Update"];
// Typed client
const supabase = createClient<Database>(url, key);
// Queries are now typed
const { data } = await supabase
.from("invoices") // TypeScript knows this table exists
.select("id, status") // TypeScript knows these columns exist
.eq("status", "paid"); // TypeScript validates the column name and value type
supabase gen types typescript after every migrationInsert and Update types for mutationsUse Supabase Realtime for live data updates:
const channel = supabase
.channel("invoice-changes")
.on<Invoice>(
"postgres_changes",
{
event: "*",
schema: "public",
table: "invoices",
filter: `tenant_id=eq.${tenantId}`,
},
(payload) => {
switch (payload.eventType) {
case "INSERT":
handleNewInvoice(payload.new);
break;
case "UPDATE":
handleUpdatedInvoice(payload.new);
break;
case "DELETE":
handleDeletedInvoice(payload.old);
break;
}
},
)
.subscribe();
// Cleanup
channel.unsubscribe();
filter)development
# Parallel Execution > This skill is under development. Workflow patterns for running independent tasks in parallel to improve performance and throughput. ## Topics to Cover - Identifying independent tasks suitable for parallel execution - `asyncio.gather()` with `return_exceptions=True` - `asyncio.TaskGroup` for structured concurrency (Python 3.11+) - Semaphores for bounded concurrency - `Promise.all()` and `Promise.allSettled()` in TypeScript - Handling partial failures (some tasks succeed
development
# Module Extraction > This skill is under development. Workflow for identifying and extracting reusable modules from existing codebases. Extract when a pattern is used in 3+ places and has stabilized. ## Topics to Cover - Identifying extraction candidates (rule of three) - Defining module boundaries and public interface - Dependency analysis: what does the module need? - Interface design: protocols, abstract base classes - Step-by-step extraction process - Testing strategy: tests before, dur
development
# Forge Orchestrate — Intelligent Build Orchestration You are a build planner, not a build executor. Your job is to look at a project, figure out what's left to build, decompose the work into parallel streams, assign the right intelligence level to each stream, estimate cost, and hand the user a set of terminal commands they can run. You plan. They execute. --- ## Stream Decomposition The unit of parallelism is a **stream** — a self-contained bundle of tasks that one Claude session handles e
development
# Code Review > This skill is under development. Workflow for conducting effective code reviews that catch real issues and improve code quality. ## Topics to Cover - Review priorities: correctness > design > performance > style - What to check in every review (checklist) - How to give constructive feedback - Automated checks that should run before human review - Review scope: how big is too big? - Patterns for reviewing database migrations - Patterns for reviewing API changes - When to reque