Skip to main content
skill CX Codex

multi-tenant-safety-checker — Claude Code skill: how to install

Enforces multi-tenant data isolation in PostgreSQL via Row Level Security, configures Prisma context, and runs security tests.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add patricio0312rev/skills --skill multi-tenant-safety-checker -a claude-code
Open on GitHub

What multi-tenant-safety-checker does

This skill provides patterns and scripts for multi-tenant PostgreSQL architectures. It sets up Row Level Security policies using current_setting('app.tenant_id'), provides a Prisma middleware wrapper that executes SET LOCAL app.tenant_id inside transactions, and includes a verification checklist.

For automated verification, the skill supplies test suites covering cross-tenant reads, updates, deletes, and transaction rollbacks. An included TypeScript audit script inspects pg_tables and information_schema.columns to find missing tenant_id columns and disabled RLS flags.

Without FORCE ROW LEVEL SECURITY, table owners bypass PostgreSQL isolation rules entirely. The skill explicitly enforces this flag across all protected tables to prevent accidental data leaks.

What it helps with

  • enforce PostgreSQL Row Level Security across tenant tables
  • bind tenant identifiers inside Prisma client transactions
  • audit PostgreSQL tables for missing tenant_id fields
  • write integration tests verifying cross-tenant read and delete blocks

Example prompts for the agent

  • Set up PostgreSQL Row Level Security policies and create a Prisma tenant context middleware
  • Audit my database schema for missing tenant_id columns and check RLS enforcement status
  • Add automated tests to verify that cross-tenant updates and deletes fail under RLS

How to install multi-tenant-safety-checker

1Skills CLI (Vercel)

npx skills add patricio0312rev/skills --skill multi-tenant-safety-checker -a claude-code

2Manually: put the files in place

~/.claude/skills/multi-tenant-safety-checker/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/multi-tenant-safety-checker/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about multi-tenant-safety-checker

How to install multi-tenant-safety-checker in Claude Code?

Run npx skills add patricio0312rev/skills --skill multi-tenant-safety-checker -a claude-code in your terminal.

What does multi-tenant-safety-checker do?

It prevents cross-tenant data leakage by setting up database RLS policies, setting session context in Prisma, and executing security audit tests.

Why does the skill enforce FORCE ROW LEVEL SECURITY?

PostgreSQL table owners bypass standard RLS policies during direct queries. Running ALTER TABLE ... FORCE ROW LEVEL SECURITY ensures the database restricts queries even when executed by table owners.

Source text

SKILL.md · MIT
---
name: multi-tenant-safety-checker
description: Ensures tenant isolation at query and policy level using Row Level Security, automated testing, and security audits. Prevents data leakage between tenants. Use for "multi-tenancy", "tenant isolation", "RLS", or "data security".
---

# Multi-tenant Safety Checker

Ensure complete tenant isolation and prevent data leakage.

## Row Level Security (RLS)

### PostgreSQL RLS Setup

```sql
-- Enable RLS on tables
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE products ENABLE ROW LEVEL SECURITY;

-- Create policy for users table
CREATE POLICY tenant_isolation_policy ON users
  USING (tenant_id = current_setting('app.tenant_id')::INTEGER);

-- Create policy for orders table
CREATE POLICY tenant_isolation_policy ON orders
  USING (tenant_id = current_setting('app.tenant_id')::INTEGER);

-- Create policy for products table
CREATE POLICY tenant_isolation_policy ON products
  USING (tenant_id = current_setting('app.tenant_id')::INTEGER);

-- Force RLS even for table owners
ALTER TABLE users FORCE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;
ALTER TABLE products FORCE ROW LEVEL SECURITY;
```

### Application-Level Tenant Context

```typescript
// middleware/tenant-context.ts
import { PrismaClient } from "@prisma/client";

export class TenantContext {
  constructor(private prisma: PrismaClient) {}

  async setTenant(tenantId: number): Promise<void> {
    await this.prisma.$executeRaw`
      SET LOCAL app.tenant_id = ${tenantId}
    `;
  }

  async withTenant<T>(
    tenantId: number,
    callback: () => Promise<T>
  ): Promise<T> {
    return this.prisma.$transaction(async (tx) => {
      // Set tenant context for this transaction
      await tx.$executeRaw`SET LOCAL app.tenant_id = ${tenantId}`;

      // Execute queries within tenant context
      return callback();
    });
  }
}

// Usage in API route
app.get("/api/orders", async (req, res) => {
  const tenantId = req.user.tenantId;

  const orders = await tenantContext.withTenant(tenantId, async () => {
    return prisma.order.findMany(); // Automatically filtered by RLS
  });

  res.json(orders);
});
```

## Tenant Isolation Checklist

```markdown
# Multi-tenant Security Checklist

## Database Level

- [ ] All tables have `tenant_id` column
- [ ] `tenant_id` is NOT NULL on all tables
- [ ] Foreign keys include tenant_id checks
- [ ] Row Level Security enabled on all tables
- [ ] RLS policies created for all tables
- [ ] RLS enforced even for table owners
- [ ] Composite indexes include tenant_id

## Application Level

- [ ] Tenant context set on every request
- [ ] Tenant ID validated from JWT/session
- [ ] No raw SQL without tenant filter
- [ ] All queries include tenant_id (if no RLS)
- [ ] API endpoints validate tenant access
- [ ] File uploads scoped to tenant
- [ ] Background jobs include tenant context

## Testing

- [ ] Cross-tenant query tests
- [ ] RLS bypass attempt tests
- [ ] SQL injection with tenant bypass tests
- [ ] Automated regression tests
- [ ] Regular security audits
```

## Automated Security Tests

```typescript
// tests/tenant-isolation.test.ts
import { PrismaClient } from "@prisma/client";

describe("Tenant Isolation", () => {
  let prisma: PrismaClient;
  let tenant1Id: number;
  let tenant2Id: number;

  beforeAll(async () => {
    prisma = new PrismaClient();

    // Create test tenants
    const tenant1 = await prisma.tenant.create({
      data: { name: "Tenant 1" },
    });
    const tenant2 = await prisma.tenant.create({
      data: { name: "Tenant 2" },
    });

    tenant1Id = tenant1.id;
    tenant2Id = tenant2.id;

    // Create test data
    await prisma.user.create({
      data: {
        email: "[email protected]",
        tenantId: tenant1Id,
      },
    });
    await prisma.user.create({
      data: {
        email: "[email protected]",
        tenantId: tenant2Id,
      },
    });
  });

  it("should not access data from other tenants", async () => {
    // Set tenant context to Tenant 1
    await prisma.$executeRaw`SET app.tenant_id = ${tenant1Id}`;

    // Query users
    const users = await prisma.user.findMany();

    // Should only see Tenant 1 users
    expect(users.length).toBe(1);
    expect(users[0].email).toBe("[email protected]");

    // Should NOT see Tenant 2 users
    expect(users.find((u) => u.email === "[email protected]")).toBeUndefined();
  });

  it("should prevent cross-tenant updates", async () => {
    await prisma.$executeRaw`SET app.tenant_id = ${tenant1Id}`;

    // Try to update Tenant 2 user (should fail silently with RLS)
    const tenant2User = await prisma.user.findFirst({
      where: { email: "[email protected]" },
    });

    // Should not find user from other tenant
    expect(tenant2User).toBeNull();
  });

  it("should prevent cross-tenant deletes", async () => {
    await prisma.$executeRaw`SET app.tenant_id = ${tenant1Id}`;

    // Try to delete Tenant 2 user
    const result = await prisma.user.deleteMany({
      where: { tenantId: tenant2Id },
    });

    // Should delete 0 rows (RLS prevents access)
    expect(result.count).toBe(0);

    // Verify user still exists
    await prisma.$executeRaw`SET app.tenant_id = ${tenant2Id}`;
    const user = await prisma.user.findFirst({
      where: { email: "[email protected]" },
    });
    expect(user).not.toBeNull();
  });

  it("should handle transaction rollback correctly", async () => {
    try {
      await prisma.$transaction(async (tx) => {
        await tx.$executeRaw`SET LOCAL app.tenant_id = ${tenant1Id}`;

        // Create user
        await tx.user.create({
          data: {
            email: "[email protected]",
            tenantId: tenant1Id,
          },
        });

        // Force error
        throw new Error("Rollback test");
      });
    } catch (error) {
      // Transaction rolled back
    }

    // User should not exist
    await prisma.$executeRaw`SET app.tenant_id = ${tenant1Id}`;
    const user = await prisma.user.findFirst({
      where: { email: "[email protected]" },
    });
    expect(user).toBeNull();
  });
});
```

## RLS Audit Script

```typescript
// scripts/audit-rls.ts
async function auditRLS() {
  const tables = await prisma.$queryRaw<any[]>`
    SELECT tablename
    FROM pg_tables
    WHERE schemaname = 'public'
    AND tablename != '_prisma_migrations'
  `;

  console.log("🔍 Auditing Row Level Security...\n");

  for (const { tablename } of tables) {
    // Check if table has tenant_id
    const columns = await prisma.$queryRaw<any[]>`
      SELECT column_name
      FROM information_schema.columns
      WHERE table_name = ${tablename}
      AND column_name = 'tenant_id'
    `;

    if (columns.length === 0) {
      console.log(`❌ ${tablename}: Missing tenant_id column`);
      continue;
    }

    // Check if RLS is enabled
    const rlsStatus = await prisma.$queryRaw<any[]>`
      SELECT relname, relrowsecurity, relforcerowsecurity
      FROM pg_class
      WHERE relname = ${tablename}
    `;

    if (!rlsStatus[0]?.relrowsecurity) {
      console.log(`❌ ${tablename}: RLS not enabled`);
      continue;
    }

    if (!rlsStatus[0]?.relforcerowsecurity) {
      console.log(`⚠️  ${tablename}: RLS not forced (owners can bypass)`);
    }

    // Check if policy exists
    const policies = await prisma.$queryRaw<any[]>`
      SELECT policyname, qual
      FROM pg_policies
      WHERE tablename = ${tablename}
    `;

    if (policies.length === 0) {
      console.log(`❌ ${tablename}: No RLS policies defined`);
    } else {
      console.log(
        `✅ ${tablename}: RLS configured (${policies.length} policies)`
      );
    }
  }
}
```

## Composite Indexes for Performance

```sql
-- Composite indexes with tenant_id first
CREATE INDEX idx_orders_tenant_user ON orders(tenant_id, user_id);
CREATE INDEX idx_orders_tenant_created ON orders(tenant_id, created_at DESC);
CREATE INDEX idx_products_tenant_category ON products(tenant_id, category);

-- This ensures queries filtered by tenant_id are fast
-- SELECT * FROM orders WHERE tenant_id = 1 AND user_id = 123; -- Uses index
```

## Middleware for Automatic Tenant Injection

```typescript
// prisma/middleware.ts
import { Prisma } from "@prisma/client";

export function tenantMiddleware(tenantId: number) {
  return async (
    params: Prisma.MiddlewareParams,
    next: (params: Prisma.MiddlewareParams) => Promise<any>
  ) => {
    // Inject tenant_id into all queries
    if (params.action === "findMany" || params.action === "findFirst") {
      params.args.where = {
        ...params.args.where,
        tenantId,
      };
    }

    if (params.action === "create") {
      params.args.data = {
        ...params.args.data,
        tenantId,
      };
    }

    if (params.action === "createMany") {
      if (Array.isArray(params.args.data)) {
        params.args.data = params.args.data.map((item) => ({
          ...item,
          tenantId,
        }));
      }
    }

    return next(params);
  };
}

// Usage:
const prisma = new PrismaClient();
prisma.$use(tenantMiddleware(req.user.tenantId));
```

## Security Regression Tests

```typescript
// tests/security-regression.test.ts
describe("Security Regression Tests", () => {
  it("should not allow SQL injection to bypass tenant", async () => {
    const maliciousInput = "1 OR 1=1 --";

    // This should be safely parameterized
    const users = await prisma.user.findMany({
      where: {
        tenantId: parseInt(maliciousInput), // Will be NaN, safe
      },
    });

    expect(users).toEqual([]);
  });

  it("should not expose tenant data via API error messages", async () => {
    try {
      await prisma.user.findUniqueOrThrow({
        where: { id: 9999 }, // Non-existent
      });
    } catch (error) {
      // Error should not leak tenant information
      expect(error.message).not.toContain("tenant_id");
      expect(error.message).not.toContain("tenantId");
    }
  });
});
```

## Best Practices

1. **Always use RLS**: Don't rely on application logic alone
2. **Force RLS**: Even for table owners (FORCE ROW LEVEL SECURITY)
3. **Test thoroughly**: Automated tests for cross-tenant access
4. **Audit regularly**: Monthly RLS configuration audits
5. **Composite indexes**: tenant_id first in all indexes
6. **Tenant validation**: Verify user belongs to tenant
7. **Monitor**: Log cross-tenant access attempts

## Output Checklist

- [ ] RLS enabled on all tables
- [ ] RLS policies created
- [ ] Tenant context middleware
- [ ] Automated security tests
- [ ] RLS audit script
- [ ] Composite indexes created
- [ ] Cross-tenant access prevention tested
- [ ] Security regression test suite

© patricio0312rev · MIT · patricio0312rev/skills

Similar skills

All skills for Security →

From the same set36

animation-micro-interaction-pack A set of reusable motion patterns for interfaces: hover effects, entrances and exits, gesture feedback. Useful when a UI feels static and you want it to move without hand-tuning timings. CX Codex +1 499installs explaining-code Walks through unfamiliar code in steps: an everyday analogy, an ASCII diagram of the flow, then a line-by-line breakdown. Handy when you inherit a repo. CX Codex +1 473installs conventional-commits The skill writes commit messages to the Conventional Commits spec: type, scope, body and footers. Useful when you want automated versioning and changelogs. CX Codex +1 469installs nginx-config-optimizer A skill that writes Nginx configs: reverse proxy, load balancing, caching, SSL and rate limits. Useful when you need to set up or rewrite a server config. CX Codex +1 433installs api-docs-generator Generates OpenAPI specifications and interactive docs from your codebase. It structures endpoints, schemas, realistic examples, and helps generate client SDKs. CX Codex +1 396installs codebase-summarizer Scans your repository and writes ARCHITECTURE.md: entry points, module relationships, Mermaid diagrams and where-to-edit guides. CX Codex +1 394installs unit-test-generator Generates unit test suites using the Arrange-Act-Assert pattern for functions, classes, and React components. Useful when you need test coverage for edge cases and errors. CX Codex +1 363installs redis-patterns A skill that steers your coding agent through Redis work: caching, session storage, rate limiting, pub/sub and distributed locks. CX Codex +1 361installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor