Manage Apple Reminders via the `remindctl` CLI on macOS (list, add, edit, complete, delete)....
npx skills add modh-labs/ai-software-os --skill "repository-pattern"
Install specific skill from multi-skill repository
# Description
Ensure all database access follows the your repository pattern. Use when writing database queries, creating repositories, adding CRUD operations, select queries, or data access code. Enforces select *, SupabaseClient parameter, generated types, and RLS reliance.
# SKILL.md
name: repository-pattern
description: Ensure all database access follows the your repository pattern. Use when writing database queries, creating repositories, adding CRUD operations, select queries, or data access code. Enforces select *, SupabaseClient parameter, generated types, and RLS reliance.
allowed-tools: Read, Grep, Glob
Repository Pattern Skill
When This Skill Activates
This skill automatically activates when you:
- Write or modify repository files (*.repository.ts)
- Create database queries using Supabase
- Discuss data access patterns
- Need to add CRUD operations for an entity
Core Rules (MUST Follow)
1. Always Use select *
NEVER pick specific columns. Always use select * for the main table and all relations.
// β WRONG - Column picking
.select("id, title, scheduled_at")
.select(`
id, title,
lead:leads(id, full_name, email)
`)
// β
CORRECT - Always select *
.select("*")
.select(`
*,
lead:leads!calls_lead_id_fkey(*),
closer:users!calls_closer_id_fkey(*)
`)
Why: Types automatically stay in sync with schema. No maintenance burden.
2. Accept SupabaseClient as First Parameter
Every repository function MUST accept the Supabase client as its first parameter:
// β WRONG - Creating client inside function
export async function getCalls() {
const supabase = await createClient();
// ...
}
// β
CORRECT - Accept client as parameter
export async function getCalls(
supabase: Awaited<ReturnType<typeof createClient>>,
filters?: CallFilters
) {
// ...
}
Why: Allows both authenticated and service role clients. Enables testing with mocks.
3. Use Generated Types from database.types.ts
NEVER create custom interfaces for database entities:
// β WRONG - Custom interface
interface Call {
id: string;
title: string;
scheduled_at: string;
// ...manually defining fields
}
// β
CORRECT - Use generated types
import type { Database } from "@/app/_shared/lib/supabase/database.types";
type Call = Database["public"]["Tables"]["calls"]["Row"];
type CallInsert = Database["public"]["Tables"]["calls"]["Insert"];
type CallUpdate = Database["public"]["Tables"]["calls"]["Update"];
4. Let RLS Handle organization_id
NEVER pass organization_id in queries. RLS policies read it from the JWT automatically:
// β WRONG - Passing org_id manually
.eq("organization_id", orgId)
// β
CORRECT - RLS handles it automatically
// Just query the table, RLS filters by org_id from JWT
const { data } = await supabase.from("calls").select("*");
5. Use Module Logger
Always use Pino logger, never console.log:
import { createModuleLogger } from "@/app/_shared/lib/logger";
const logger = createModuleLogger("calls-repository");
// β
Use logger
logger.info({ callId }, "Fetching call details");
logger.error({ error }, "Failed to fetch call");
// β Never use console
console.log("Fetching call...");
Repository File Template
"use server";
import type { QueryData } from "@supabase/supabase-js";
import { createModuleLogger } from "@/app/_shared/lib/logger";
import type { Database } from "@/app/_shared/lib/supabase/database.types";
import { createClient } from "@/app/_shared/lib/supabase/server";
const logger = createModuleLogger("entity-repository");
// Type aliases from generated types
type Entity = Database["public"]["Tables"]["entities"]["Row"];
type EntityInsert = Database["public"]["Tables"]["entities"]["Insert"];
type EntityUpdate = Database["public"]["Tables"]["entities"]["Update"];
/**
* Query builder with standard relations
*/
function buildEntityQueryBuilder(
supabase: Awaited<ReturnType<typeof createClient>>
) {
return supabase.from("entities").select(`
*,
related:related_table!entities_related_id_fkey(*)
`);
}
// Type inference from query builder
export type EntityWithRelations = QueryData<
ReturnType<typeof buildEntityQueryBuilder>
>[number];
/**
* List all entities (RLS filters by org)
*/
export async function getEntities(
supabase: Awaited<ReturnType<typeof createClient>>,
filters?: { status?: string }
): Promise<EntityWithRelations[]> {
let query = buildEntityQueryBuilder(supabase)
.order("created_at", { ascending: false });
if (filters?.status) {
query = query.eq("status", filters.status);
}
const { data, error } = await query;
if (error) {
logger.error({ error }, "Failed to fetch entities");
throw error;
}
return data || [];
}
/**
* Get single entity by ID
*/
export async function getEntityById(
supabase: Awaited<ReturnType<typeof createClient>>,
id: string
): Promise<EntityWithRelations | null> {
const { data, error } = await buildEntityQueryBuilder(supabase)
.eq("id", id)
.single();
if (error) {
logger.error({ error, id }, "Failed to fetch entity");
throw error;
}
return data;
}
/**
* Create new entity
*/
export async function createEntity(
supabase: Awaited<ReturnType<typeof createClient>>,
input: EntityInsert
): Promise<Entity> {
const { data, error } = await supabase
.from("entities")
.insert(input)
.select("*")
.single();
if (error) {
logger.error({ error }, "Failed to create entity");
throw error;
}
return data;
}
/**
* Update existing entity
*/
export async function updateEntity(
supabase: Awaited<ReturnType<typeof createClient>>,
id: string,
updates: EntityUpdate
): Promise<Entity> {
const { data, error } = await supabase
.from("entities")
.update(updates)
.eq("id", id)
.select("*")
.single();
if (error) {
logger.error({ error, id }, "Failed to update entity");
throw error;
}
return data;
}
/**
* Delete entity
*/
export async function deleteEntity(
supabase: Awaited<ReturnType<typeof createClient>>,
id: string
): Promise<void> {
const { error } = await supabase
.from("entities")
.delete()
.eq("id", id);
if (error) {
logger.error({ error, id }, "Failed to delete entity");
throw error;
}
}
Reference Implementation
See the canonical example at:
- app/_shared/repositories/calls.repository.ts - Full repository with query builder
- app/_shared/repositories/CLAUDE.md - Complete pattern documentation
Common Mistakes to Avoid
- Column picking - Always use
select * - Creating client inside function - Accept as parameter
- Manual type definitions - Use generated types
- Passing organization_id - Let RLS handle it
- Using console.log - Use Pino logger
- Forgetting
"use server"- Required at top of file
Quick Reference
| Pattern | Correct | Wrong |
|---|---|---|
| Select | select(*) |
select("id, name") |
| Client | Parameter | createClient() inside |
| Types | Database["public"]["Tables"]["x"]["Row"] |
interface X {} |
| Org ID | (let RLS handle) | .eq("organization_id", x) |
| Logging | logger.info() |
console.log() |
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.