[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Use Cases/Build Tenant Data Isolation for Multi-Tenant SaaS

Build Tenant Data Isolation for Multi-Tenant SaaS

Build bulletproof tenant data isolation using PostgreSQL Row-Level Security, tenant-scoped middleware, cross-tenant query prevention, and audit logging — ensuring no customer ever sees another customer's data.

Development#typescript#tsconfig#type-checking#type-safety#javascript-migration
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 96/100·All SAFE
>

typescript

v1.0.0

Sets up and repairs TypeScript projects: picks tsconfig.json settings for a Node.js service, an npm library or a bundled web app, runs type-checking in CI, and replaces `any` and unsafe casts with types the compiler can verify. Use when someone asks to "set up TypeScript", "fix this tsconfig", "fix a TypeScript error", "upgrade to TypeScript 6 or 7", "migrate JavaScript to TypeScript", "publish types for a package", "run .ts files in Node" or "make this type-safe". Covers the changed defaults in TypeScript 6.0 and 7.0, module and moduleResolution, strict mode, project references, declaration output, narrowing, satisfies and common compiler errors.

93/100 quality
7.30× impact
SAFE· stale score
View skill
>

postgresql

v1.1.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

100/100 quality
5.15× impact
SAFE· stale score
View skill
>

hono

v1.1.0

Hono is a small web framework built on Web Standards (Request/Response) that runs unchanged on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda and Vercel. Use it when the user asks to build a Hono API, add Zod validation, JWT auth, CORS or middleware, use the type-safe RPC client (hc), render JSX, stream responses, or deploy one codebase to several runtimes.

100/100 quality
6.20× impact
SAFE· stale score
View skill
>

zod

v1.1.0

Zod is a TypeScript-first schema library that validates data at runtime and infers static types from the same schema. Use when validating API request bodies, form input, environment variables or config files, parsing untrusted JSON, or migrating from Zod 3 to Zod 4. Covers z.object, safeParse, z.infer, coercion, discriminated unions, refinements, JSON Schema output and the v4 API changes.

93/100 quality
3.87× impact
SAFE· stale score
View skill
>

redis

v1.1.0

Redis is an in-memory data store used as a cache, session store, message broker and counter service. Use when adding a cache layer, storing sessions, rate limiting an API, running pub/sub or Streams with consumer groups, taking a distributed lock, building a leaderboard, or configuring Redis for production (persistence, maxmemory, eviction).

93/100 quality
1.79× impact
SAFE· stale score
View skill
$

The Problem

Chen leads engineering at a 35-person B2B SaaS. All customers share one database. Last month, a query bug exposed Company A's revenue data to Company B. The CEO got a call from the client's legal team. The bug was a missing WHERE tenant_id = ? in one query — a simple oversight that could have killed the company. Every new query, every new feature is a potential data leak. They need isolation that works at the database level, not just application code, so a missing WHERE clause can't leak data.

Step 1: Build Database-Level Isolation with RLS

typescript
// src/tenant/isolation.ts — PostgreSQL RLS + middleware for bulletproof tenant isolation
import { pool } from "../db";
import { Redis } from "ioredis";

const redis = new Redis(process.env.REDIS_URL!);

// Database setup: Run once to enable RLS on all tenant tables
export async function setupTenantIsolation(): Promise<void> {
  const tenantTables = ["projects", "documents", "invoices", "contacts", "tasks", "comments"];

  for (const table of tenantTables) {
    await pool.query(`
      -- Add tenant_id column if missing
      ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS tenant_id UUID NOT NULL;
      
      -- Create index for performance
      CREATE INDEX IF NOT EXISTS idx_${table}_tenant ON ${table} (tenant_id);
      
      -- Enable Row-Level Security
      ALTER TABLE ${table} ENABLE ROW LEVEL SECURITY;
      
      -- Force RLS even for table owners
      ALTER TABLE ${table} FORCE ROW LEVEL SECURITY;
      
      -- Policy: users can only see their tenant's rows
      DROP POLICY IF EXISTS tenant_isolation ON ${table};
      CREATE POLICY tenant_isolation ON ${table}
        USING (tenant_id = current_setting('app.current_tenant_id')::uuid)
        WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::uuid);
    `);
  }

  console.log(`[Tenant] RLS enabled on ${tenantTables.length} tables`);
}

// Middleware: Set tenant context on every request
export async function tenantMiddleware(c: any, next: any): Promise<void> {
  const tenantId = c.get("tenantId"); // extracted from JWT or session
  if (!tenantId) {
    return c.json({ error: "Tenant context required" }, 401);
  }

  // Validate tenant exists and is active
  const tenantValid = await validateTenant(tenantId);
  if (!tenantValid) {
    return c.json({ error: "Invalid or suspended tenant" }, 403);
  }

  // Set tenant context for this database connection
  // Every query through this connection will be filtered by RLS
  const client = await pool.connect();
  try {
    await client.query(`SET app.current_tenant_id = '${tenantId}'`);
    c.set("db", client);
    await next();
  } finally {
    // Reset context and return connection to pool
    await client.query("RESET app.current_tenant_id");
    client.release();
  }
}

// Tenant-scoped query helper (defense in depth)
export function tenantQuery(tenantId: string) {
  return {
    async query(sql: string, params: any[] = []): Promise<any> {
      const client = await pool.connect();
      try {
        await client.query(`SET app.current_tenant_id = '${tenantId}'`);
        const result = await client.query(sql, params);

        // Audit: log cross-tenant query attempts
        if (result.rows?.some((r: any) => r.tenant_id && r.tenant_id !== tenantId)) {
          console.error(`[SECURITY] Cross-tenant data detected! tenant=${tenantId}`);
          await logSecurityEvent(tenantId, "cross_tenant_leak_prevented", { sql });
          return { rows: result.rows.filter((r: any) => r.tenant_id === tenantId) };
        }

        return result;
      } finally {
        await client.query("RESET app.current_tenant_id");
        client.release();
      }
    },
  };
}

// Tenant-aware data export (ensures complete isolation)
export async function exportTenantData(tenantId: string): Promise<Record<string, any[]>> {
  const tables = ["projects", "documents", "invoices", "contacts", "tasks"];
  const data: Record<string, any[]> = {};

  const tq = tenantQuery(tenantId);

  for (const table of tables) {
    const result = await tq.query(`SELECT * FROM ${table}`);
    data[table] = result.rows;
  }

  await logSecurityEvent(tenantId, "data_export", {
    tables,
    rowCounts: Object.fromEntries(Object.entries(data).map(([k, v]) => [k, v.length])),
  });

  return data;
}

// Tenant deletion (GDPR compliance)
export async function deleteTenantData(tenantId: string): Promise<{ tablesCleared: number; rowsDeleted: number }> {
  const tables = ["comments", "tasks", "contacts", "invoices", "documents", "projects"]; // order matters (FK)
  let totalDeleted = 0;

  for (const table of tables) {
    const result = await pool.query(
      `DELETE FROM ${table} WHERE tenant_id = $1`,
      [tenantId]
    );
    totalDeleted += result.rowCount || 0;
  }

  await pool.query("UPDATE tenants SET status = 'deleted', deleted_at = NOW() WHERE id = $1", [tenantId]);

  return { tablesCleared: tables.length, rowsDeleted: totalDeleted };
}

async function validateTenant(tenantId: string): Promise<boolean> {
  const cached = await redis.get(`tenant:valid:${tenantId}`);
  if (cached !== null) return cached === "1";

  const { rows } = await pool.query(
    "SELECT 1 FROM tenants WHERE id = $1 AND status = 'active'",
    [tenantId]
  );

  const valid = rows.length > 0;
  await redis.setex(`tenant:valid:${tenantId}`, 300, valid ? "1" : "0");
  return valid;
}

async function logSecurityEvent(tenantId: string, event: string, details: any): Promise<void> {
  await pool.query(
    "INSERT INTO security_audit_log (tenant_id, event, details, created_at) VALUES ($1, $2, $3, NOW())",
    [tenantId, event, JSON.stringify(details)]
  );
}

Results

  • Cross-tenant data leak: impossible — PostgreSQL RLS enforces isolation at the database level; even if application code forgets WHERE tenant_id = ?, the database returns only the current tenant's rows
  • Legal crisis avoided permanently — RLS policies are checked on every query automatically; a missing filter clause can't expose another customer's data
  • New features ship faster — developers write normal queries without worrying about tenant filters; RLS handles it; code reviews focus on logic instead of "did you add the tenant check?"
  • GDPR data export/deletion in minutes — exportTenantData and deleteTenantData are one-click operations; complete compliance with data portability and right to erasure
  • Defense in depth — RLS at database level + tenant middleware + application-layer audit logging; three independent layers of protection

Related use cases