.claude/skills/glapi-rls-guide/SKILL.md
Row Level Security (RLS) implementation guide for GLAPI multi-tenant database isolation. Covers PostgreSQL RLS policies, session variables, contextual database connections, tRPC middleware, and common troubleshooting. Use when working with RLS, multi-tenancy, organization isolation, database security, or debugging RLS policy violations.
npx skillsauth add adteco/glapi glapi-rls-guideInstall 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.
This skill documents how Row Level Security is implemented in GLAPI to ensure multi-tenant data isolation. All database tables containing organization-specific data use RLS policies to prevent cross-tenant data access.
Request → tRPC Middleware → Set Session Variable → RLS Policy Check → Query Execution
current_setting('app.current_organization_id') against row's organization_idThe foundation of RLS is the PostgreSQL session variable:
-- Set on each connection before queries
SELECT set_config('app.current_organization_id', '<uuid>', false);
current_setting('app.current_organization_id', true)IMPORTANT: Policies must use current_setting() directly, NOT the get_current_organization_id() function for INSERT operations:
-- CORRECT: Use current_setting directly
CREATE POLICY "org_isolation_insert_tablename" ON tablename
FOR INSERT WITH CHECK (
organization_id::text = current_setting('app.current_organization_id', true)
);
-- For SELECT/UPDATE/DELETE, either works but direct is safer
CREATE POLICY "org_isolation_select_tablename" ON tablename
FOR SELECT USING (
organization_id::text = current_setting('app.current_organization_id', true)
);
Why cast to text? The organization_id column may be UUID type, but current_setting() returns text. Casting both to text ensures consistent comparison.
packages/database/src/context.ts)Primary functions for RLS context:
// Create a contextual DB with RLS set (used by tRPC)
const { db, release, client } = await createContextualDb({
organizationId: 'uuid-here',
userId: 'user-uuid',
});
// For one-off operations with automatic cleanup
await withOrganizationContext(
{ organizationId: 'uuid' },
async (db) => {
return db.select().from(table);
}
);
// For transactions
await withOrganizationContextTransaction(
{ organizationId: 'uuid' },
async (db) => {
await db.insert(table).values({...});
}
);
packages/trpc/src/trpc.ts)The authenticatedProcedure middleware:
ctx.db (RLS-protected) to proceduresCause: The session variable value doesn't match the organization_id being inserted.
Debug Steps:
// Add debug in repository
const check = await this.db.execute(
sql`SELECT current_setting('app.current_organization_id', true) as rls_org`
);
console.log('RLS org:', check.rows[0]?.rls_org);
console.log('Inserting org:', organizationId);
ctx.db not global db:// In service constructor - MUST pass options.db
this.repository = new SomeRepository(options.db);
// In router - MUST pass ctx.db
const service = new SomeService(ctx.serviceContext, { db: ctx.db });
Cause: Session variable not set (returns NULL).
Solution: Ensure repository receives contextual db:
// BaseRepository falls back to globalDb if no db passed
constructor(db?: ContextualDatabase) {
this.db = db ?? globalDb; // globalDb has NO RLS context!
}
Cause: get_current_organization_id() returns UUID, but comparison fails.
Solution: Use current_setting() directly in policies:
-- Instead of: get_current_organization_id()::text
-- Use: current_setting('app.current_organization_id', true)
ALTER TABLE new_table ENABLE ROW LEVEL SECURITY;
ALTER TABLE new_table FORCE ROW LEVEL SECURITY;
-- SELECT
CREATE POLICY "org_isolation_select_new_table" ON new_table
FOR SELECT USING (
organization_id::text = current_setting('app.current_organization_id', true)
);
-- INSERT
CREATE POLICY "org_isolation_insert_new_table" ON new_table
FOR INSERT WITH CHECK (
organization_id::text = current_setting('app.current_organization_id', true)
);
-- UPDATE
CREATE POLICY "org_isolation_update_new_table" ON new_table
FOR UPDATE
USING (organization_id::text = current_setting('app.current_organization_id', true))
WITH CHECK (organization_id::text = current_setting('app.current_organization_id', true));
-- DELETE
CREATE POLICY "org_isolation_delete_new_table" ON new_table
FOR DELETE USING (
organization_id::text = current_setting('app.current_organization_id', true)
);
Add to packages/database/drizzle/NNNN_tablename_rls.sql
Every repository that accesses RLS-protected tables MUST:
db in constructorthis.db for all queries (not imported global db)ctx.db from tRPC context// Repository
export class MyRepository extends BaseRepository {
constructor(db?: ContextualDatabase) {
super(db); // BaseRepository sets this.db
}
async findAll() {
return this.db.select().from(myTable); // Uses contextual db
}
}
// Service
export class MyService extends BaseService {
private repo: MyRepository;
constructor(context: ServiceContext, options: { db?: ContextualDatabase } = {}) {
super(context);
this.repo = new MyRepository(options.db); // Pass db through!
}
}
// Router
const service = new MyService(ctx.serviceContext, { db: ctx.db });
| Component | Location | Purpose |
|-----------|----------|---------|
| Context functions | packages/database/src/context.ts | Set session variables |
| tRPC middleware | packages/trpc/src/trpc.ts | Create RLS db per request |
| Base repository | packages/database/src/repositories/base-repository.ts | Fallback logic |
| Migration files | packages/database/drizzle/*.sql | RLS policy definitions |
| Variable | Purpose |
|----------|---------|
| app.current_organization_id | Organization UUID for RLS |
| app.current_user_id | User UUID for audit trails |
tools
Create and manage Claude Code skills following Anthropic best practices. Use when creating new skills, modifying skill-rules.json, understanding trigger patterns, working with hooks, debugging skill activation, or implementing progressive disclosure. Covers skill structure, YAML frontmatter, trigger types (keywords, intent patterns, file paths, content patterns), enforcement levels (block, suggest, warn), hook mechanisms (UserPromptSubmit, PreToolUse), session tracking, and the 500-line rule.
development
# Pre-Deployment Checklist Skill ## Purpose Run comprehensive quality checks before committing code and creating pull requests. This skill ensures code quality, documentation, API specs, and tests are all in order before deployment. ## When to Use This Skill - Before committing significant changes - Before creating a pull request - When user invokes `/ship`, `/pre-deploy`, or `/checklist` - When preparing code for production deployment ## Checklist Items ### 1. TypeScript Compilation **Com
development
Frontend development guidelines for Adteco's React 18/19 + ShadCN/Radix UI + Tailwind stack. Patterns for building accessible, performant components with type safety, TanStack Query data fetching, and modern styling.
development
GLAPI backend development guide for Next.js + TRPC + Drizzle ORM + PostgreSQL + Clerk. TRPC for internal type-safety, REST API exposure via OpenAPI. Covers layered architecture (routers → services → Drizzle), dual TRPC/REST endpoints, Clerk authentication, and testing strategies.