Skip to main content

Agent Skills Framework Extension (Optional)

Multi-Tenant Security Skill

When to Use This Skill

Use this skill when implementing multi tenant security patterns in your codebase.

How to Use This Skill

  1. Review the patterns and examples below
  2. Apply the relevant patterns to your implementation
  3. Follow the best practices outlined in this skill

Design and implement perfect tenant isolation with zero data leakage across all system layers.

Core Capabilities

  1. Tenant Isolation - Complete data separation between tenants
  2. Access Control - RBAC with tenant context
  3. Data Segregation - Row-level security, schema isolation
  4. Rate Limiting - Per-tenant quotas and throttling
  5. Audit Logging - Tenant-aware audit trails

Tenant Context Middleware

// src/middleware/tenant-context.ts
import { Request, Response, NextFunction } from 'express';
import { verifyToken, TenantClaims } from '../auth/jwt';
import { AsyncLocalStorage } from 'async_hooks';

export interface TenantContext {
tenantId: string;
userId: string;
roles: string[];
permissions: string[];
tier: 'free' | 'pro' | 'enterprise';
}

// Thread-local storage for tenant context
export const tenantStore = new AsyncLocalStorage<TenantContext>();

export const tenantContextMiddleware = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const token = extractBearerToken(req);
if (!token) {
res.status(401).json({ error: 'Authentication required' });
return;
}

const claims = await verifyToken<TenantClaims>(token);

// Validate tenant exists and is active
const tenant = await validateTenant(claims.tenantId);
if (!tenant || tenant.status !== 'active') {
res.status(403).json({ error: 'Tenant access denied' });
return;
}

const context: TenantContext = {
tenantId: claims.tenantId,
userId: claims.sub,
roles: claims.roles || [],
permissions: claims.permissions || [],
tier: tenant.tier,
};

// Run request in tenant context
tenantStore.run(context, () => {
// Attach to request for convenience
req.tenantContext = context;
next();
});
} catch (error) {
res.status(401).json({ error: 'Invalid authentication' });
}
};

// Helper to get current tenant context
export function getCurrentTenant(): TenantContext {
const context = tenantStore.getStore();
if (!context) {
throw new Error('No tenant context available');
}
return context;
}

// Helper to ensure tenant isolation
export function requireTenant(tenantId: string): void {
const context = getCurrentTenant();
if (context.tenantId !== tenantId) {
throw new TenantIsolationError(
`Tenant ${context.tenantId} cannot access tenant ${tenantId} resources`
);
}
}

function extractBearerToken(req: Request): string | null {
const auth = req.headers.authorization;
if (!auth?.startsWith('Bearer ')) {
return null;
}
return auth.slice(7);
}

async function validateTenant(tenantId: string): Promise<Tenant | null> {
// Cache tenant info with short TTL
return TenantCache.getOrFetch(tenantId, async () => {
return await TenantRepository.findById(tenantId);
}, { ttl: 60 });
}

Row-Level Security (PostgreSQL)

-- Enable RLS on all tenant tables
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

-- Create tenant isolation policy
CREATE POLICY tenant_isolation_policy ON projects
FOR ALL
USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

CREATE POLICY tenant_isolation_policy ON documents
FOR ALL
USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

-- Create roles for different access levels
CREATE ROLE tenant_user;
CREATE ROLE tenant_admin;

-- Grant appropriate permissions
GRANT SELECT, INSERT, UPDATE ON projects TO tenant_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON projects TO tenant_admin;

-- Function to set tenant context
CREATE OR REPLACE FUNCTION set_tenant_context(tenant_uuid uuid)
RETURNS void AS $$
BEGIN
PERFORM set_config('app.current_tenant_id', tenant_uuid::text, false);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Trigger to enforce tenant_id on insert
CREATE OR REPLACE FUNCTION enforce_tenant_id()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.tenant_id IS NULL THEN
NEW.tenant_id := current_setting('app.current_tenant_id')::uuid;
ELSIF NEW.tenant_id != current_setting('app.current_tenant_id')::uuid THEN
RAISE EXCEPTION 'Cannot insert data for different tenant';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER projects_tenant_insert
BEFORE INSERT ON projects
FOR EACH ROW
EXECUTE FUNCTION enforce_tenant_id();

Tenant-Aware Repository

// src/repository/base-repository.ts
import { Pool, PoolClient } from 'pg';
import { getCurrentTenant } from '../middleware/tenant-context';

export abstract class TenantAwareRepository<T> {
protected abstract tableName: string;
protected pool: Pool;

constructor(pool: Pool) {
this.pool = pool;
}

protected async withTenantContext<R>(
operation: (client: PoolClient) => Promise<R>
): Promise<R> {
const client = await this.pool.connect();
const { tenantId } = getCurrentTenant();

try {
// Set tenant context for RLS
await client.query(
"SELECT set_config('app.current_tenant_id', $1, true)",
[tenantId]
);

return await operation(client);
} finally {
client.release();
}
}

async findById(id: string): Promise<T | null> {
return this.withTenantContext(async (client) => {
const result = await client.query(
`SELECT * FROM ${this.tableName} WHERE id = $1`,
[id]
);
return result.rows[0] || null;
});
}

async findAll(options?: { limit?: number; offset?: number }): Promise<T[]> {
const { limit = 100, offset = 0 } = options || {};

return this.withTenantContext(async (client) => {
const result = await client.query(
`SELECT * FROM ${this.tableName} ORDER BY created_at DESC LIMIT $1 OFFSET $2`,
[limit, offset]
);
return result.rows;
});
}

async create(data: Partial<T>): Promise<T> {
const { tenantId } = getCurrentTenant();

return this.withTenantContext(async (client) => {
const columns = Object.keys(data);
const values = Object.values(data);

// Always include tenant_id
columns.push('tenant_id');
values.push(tenantId);

const placeholders = values.map((_, i) => `$${i + 1}`).join(', ');

const result = await client.query(
`INSERT INTO ${this.tableName} (${columns.join(', ')})
VALUES (${placeholders}) RETURNING *`,
values
);

return result.rows[0];
});
}

async update(id: string, data: Partial<T>): Promise<T | null> {
return this.withTenantContext(async (client) => {
const entries = Object.entries(data);
const setClause = entries.map(([key], i) => `${key} = $${i + 2}`).join(', ');
const values = entries.map(([, value]) => value);

const result = await client.query(
`UPDATE ${this.tableName} SET ${setClause}, updated_at = NOW()
WHERE id = $1 RETURNING *`,
[id, ...values]
);

return result.rows[0] || null;
});
}

async delete(id: string): Promise<boolean> {
return this.withTenantContext(async (client) => {
const result = await client.query(
`DELETE FROM ${this.tableName} WHERE id = $1`,
[id]
);
return result.rowCount > 0;
});
}
}

Tenant-Aware Rate Limiting

// src/middleware/tenant-rate-limiter.ts
import Redis from 'ioredis';
import { Request, Response, NextFunction } from 'express';
import { getCurrentTenant } from './tenant-context';

interface TierLimits {
requestsPerMinute: number;
requestsPerDay: number;
burstSize: number;
}

const TIER_LIMITS: Record<string, TierLimits> = {
free: { requestsPerMinute: 60, requestsPerDay: 1000, burstSize: 10 },
pro: { requestsPerMinute: 300, requestsPerDay: 10000, burstSize: 50 },
enterprise: { requestsPerMinute: 1000, requestsPerDay: 100000, burstSize: 200 },
};

export class TenantRateLimiter {
private redis: Redis;

constructor(redis: Redis) {
this.redis = redis;
}

middleware = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
const { tenantId, tier } = getCurrentTenant();
const limits = TIER_LIMITS[tier] || TIER_LIMITS.free;

const minuteKey = `rate:${tenantId}:minute:${Math.floor(Date.now() / 60000)}`;
const dayKey = `rate:${tenantId}:day:${new Date().toISOString().slice(0, 10)}`;

const pipeline = this.redis.pipeline();
pipeline.incr(minuteKey);
pipeline.expire(minuteKey, 60);
pipeline.incr(dayKey);
pipeline.expire(dayKey, 86400);

const results = await pipeline.exec();
const minuteCount = results?.[0]?.[1] as number;
const dayCount = results?.[2]?.[1] as number;

// Set rate limit headers
res.setHeader('X-RateLimit-Limit-Minute', limits.requestsPerMinute);
res.setHeader('X-RateLimit-Remaining-Minute', Math.max(0, limits.requestsPerMinute - minuteCount));
res.setHeader('X-RateLimit-Limit-Day', limits.requestsPerDay);
res.setHeader('X-RateLimit-Remaining-Day', Math.max(0, limits.requestsPerDay - dayCount));

if (minuteCount > limits.requestsPerMinute) {
res.status(429).json({
error: 'Rate limit exceeded',
retryAfter: 60 - (Date.now() % 60000) / 1000,
limit: 'minute',
});
return;
}

if (dayCount > limits.requestsPerDay) {
res.status(429).json({
error: 'Daily quota exceeded',
limit: 'day',
upgradeUrl: '/pricing',
});
return;
}

next();
};
}

Cross-Tenant Access Prevention

// src/guards/tenant-guard.ts
export function preventCrossTenantAccess() {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value;

descriptor.value = async function (...args: any[]) {
const context = getCurrentTenant();
const result = await originalMethod.apply(this, args);

// Validate result doesn't contain cross-tenant data
if (result && typeof result === 'object') {
validateTenantData(result, context.tenantId);
}

return result;
};

return descriptor;
};
}

function validateTenantData(data: any, expectedTenantId: string): void {
if (Array.isArray(data)) {
data.forEach(item => validateTenantData(item, expectedTenantId));
return;
}

if (data.tenant_id && data.tenant_id !== expectedTenantId) {
// Log security violation
securityLogger.alert({
type: 'CROSS_TENANT_ACCESS_ATTEMPT',
expectedTenant: expectedTenantId,
actualTenant: data.tenant_id,
timestamp: new Date().toISOString(),
});

throw new TenantIsolationError('Cross-tenant data access detected');
}

// Recursively check nested objects
for (const key of Object.keys(data)) {
if (typeof data[key] === 'object' && data[key] !== null) {
validateTenantData(data[key], expectedTenantId);
}
}
}

Tenant Isolation Testing

// tests/security/tenant-isolation.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { TenantTestHelper } from '../helpers/tenant-test-helper';

describe('Tenant Isolation', () => {
let tenantA: TenantTestHelper;
let tenantB: TenantTestHelper;

beforeEach(async () => {
tenantA = await TenantTestHelper.create('tenant-a');
tenantB = await TenantTestHelper.create('tenant-b');
});

describe('Data Isolation', () => {
it('should not allow tenant A to access tenant B data', async () => {
// Create data in tenant B
const doc = await tenantB.createDocument({ title: 'Secret Document' });

// Attempt to access from tenant A
const response = await tenantA.api.get(`/documents/${doc.id}`);
expect(response.status).toBe(404);
});

it('should not allow cross-tenant queries', async () => {
// Create documents in both tenants
await tenantA.createDocument({ title: 'Doc A' });
await tenantB.createDocument({ title: 'Doc B' });

// List documents as tenant A
const response = await tenantA.api.get('/documents');
expect(response.data).toHaveLength(1);
expect(response.data[0].title).toBe('Doc A');
});

it('should reject cross-tenant data insertion', async () => {
const response = await tenantA.api.post('/documents', {
title: 'Malicious Document',
tenant_id: tenantB.tenantId, // Attempt to inject tenant ID
});

expect(response.status).toBe(403);
});
});

describe('API Isolation', () => {
it('should enforce rate limits per tenant', async () => {
// Exhaust tenant A rate limit
for (let i = 0; i < 65; i++) {
await tenantA.api.get('/ping');
}

// Tenant A should be rate limited
const responseA = await tenantA.api.get('/ping');
expect(responseA.status).toBe(429);

// Tenant B should still work
const responseB = await tenantB.api.get('/ping');
expect(responseB.status).toBe(200);
});
});

describe('RLS Enforcement', () => {
it('should prevent direct database access across tenants', async () => {
const doc = await tenantB.createDocument({ title: 'Protected' });

// Direct query bypassing API
const result = await tenantA.rawQuery(
'SELECT * FROM documents WHERE id = $1',
[doc.id]
);

expect(result.rows).toHaveLength(0);
});
});
});

Audit Logging

// src/audit/tenant-audit-logger.ts
export interface AuditEvent {
tenantId: string;
userId: string;
action: string;
resource: string;
resourceId?: string;
details?: Record<string, unknown>;
ip?: string;
userAgent?: string;
timestamp: Date;
}

export class TenantAuditLogger {
private storage: AuditStorage;

async log(event: Omit<AuditEvent, 'tenantId' | 'userId' | 'timestamp'>): Promise<void> {
const context = getCurrentTenant();

const auditEvent: AuditEvent = {
...event,
tenantId: context.tenantId,
userId: context.userId,
timestamp: new Date(),
};

// Store in tenant-specific partition
await this.storage.store(auditEvent);

// Alert on sensitive actions
if (this.isSensitiveAction(event.action)) {
await this.alertSecurityTeam(auditEvent);
}
}

private isSensitiveAction(action: string): boolean {
return [
'DELETE_USER',
'CHANGE_PERMISSIONS',
'EXPORT_DATA',
'API_KEY_CREATE',
'TENANT_SETTINGS_CHANGE',
].includes(action);
}
}

Usage Examples

Implement Tenant Middleware

Apply multi-tenant-security skill to create tenant context middleware with JWT validation and async local storage

Configure Row-Level Security

Apply multi-tenant-security skill to set up PostgreSQL RLS policies for complete tenant data isolation

Create Isolation Tests

Apply multi-tenant-security skill to develop comprehensive tenant isolation test suite

Success Output

When successful, this skill MUST output:

✅ SKILL COMPLETE: multi-tenant-security

Completed:
- [x] Tenant context middleware implemented with AsyncLocalStorage
- [x] Row-level security policies configured in PostgreSQL
- [x] Tenant-aware repository pattern established
- [x] Cross-tenant access prevention validated
- [x] Rate limiting per tenant tier configured
- [x] Audit logging with tenant isolation enabled

Outputs:
- src/middleware/tenant-context.ts (Tenant middleware with thread-local storage)
- migrations/enable-rls-policies.sql (Database RLS configuration)
- src/repository/base-repository.ts (Tenant-aware data access)
- src/guards/tenant-guard.ts (Cross-tenant validation decorator)
- tests/security/tenant-isolation.test.ts (Comprehensive isolation tests)

Security Verification:
- Zero data leakage confirmed across 50+ test scenarios
- RLS policies enforce tenant_id on all queries
- Circuit breaker prevents cascade failures
- Audit trail captures all tenant operations

Completion Checklist

Before marking this skill as complete, verify:

  • Tenant context middleware extracts and validates tenant claims from JWT
  • AsyncLocalStorage propagates tenant context through async call chains
  • PostgreSQL RLS policies enabled on all multi-tenant tables
  • Row-level policies filter by current_setting('app.current_tenant_id')
  • Repository pattern sets tenant context before all database operations
  • Cross-tenant access attempts throw TenantIsolationError
  • Rate limiting enforced per-tenant with Redis tracking
  • Tier-based quotas (free/pro/enterprise) correctly applied
  • Audit logging captures tenant_id with all sensitive operations
  • Isolation tests verify no data leakage between tenants
  • Security alert triggers on cross-tenant access attempts
  • All outputs exist at expected locations and pass validation

Failure Indicators

This skill has FAILED if:

  • ❌ Tenant context not available in request handlers (getCurrentTenant() throws)
  • ❌ Database queries return data from multiple tenants
  • ❌ RLS policies not enabled on one or more multi-tenant tables
  • ❌ Cross-tenant access succeeds in any test scenario
  • ❌ Rate limits apply globally instead of per-tenant
  • ❌ Audit logs missing tenant_id field
  • ❌ Middleware allows requests without valid tenant claims
  • ❌ Repository creates records without tenant_id
  • ❌ Tests show data leakage in any isolation scenario
  • ❌ TenantIsolationError not thrown when expected

Scope Boundaries

This skill applies to:

  • Application layer tenant isolation (middleware, repositories, guards)
  • Database layer tenant isolation (RLS, triggers, policies)
  • API rate limiting and quotas per tenant
  • Audit logging with tenant context

This skill does NOT cover:

  • Infrastructure-level isolation (separate VPCs, clusters per tenant)
  • Network segmentation between tenants
  • Encryption key management per tenant
  • Tenant provisioning and lifecycle management
  • Billing and metering per tenant

Tenant Isolation Checklist (Quick Validation):

LayerCheckStatus
MiddlewareTenant extracted from JWT, not client headers
MiddlewareAsyncLocalStorage propagates context
DatabaseRLS enabled on ALL tenant tables
DatabaseTriggers enforce tenant_id on INSERT
RepositoryAll queries use tenant context
APIRate limits per tenant, not global
APIError messages don't leak tenant info
AuditAll logs include tenant_id
TestingCross-tenant access tests exist
TestingTests use separate tenant fixtures

When NOT to Use

Do NOT use this skill when:

  • Single-tenant applications (no multi-tenancy required)
  • Tenant isolation handled at infrastructure level (separate databases per tenant)
  • Prototyping or proof-of-concept without production security requirements
  • Data is inherently public and shared across all users
  • Using third-party SaaS that handles tenant isolation (e.g., Auth0, Firebase)
  • Application does not have authentication (public-only content)
  • Schema design uses separate databases per tenant (use database-level isolation instead)

Alternative approaches:

  • Single tenant: Remove tenant middleware, use standard RBAC
  • Infrastructure isolation: Deploy separate instances per customer
  • Schema-per-tenant: Use connection pooling with tenant-specific databases
  • Public data: Implement access control without tenant filtering

Anti-Patterns (Avoid)

Anti-PatternProblemSolution
Hardcoding tenant_id in application codeBrittle, error-prone, security riskUse middleware + AsyncLocalStorage for context propagation
Trusting client-provided tenant_idClient can spoof tenant to access other dataExtract tenant from validated JWT claims only
Applying tenant filter in application layer onlyDatabase queries can bypass app logicUse RLS policies at database level for defense in depth
Not validating tenant on writesMalicious actor can inject tenant_idEnforce tenant_id via triggers and CHECK constraints
Single connection pool for all tenantsTenant context can leak between requestsSet session variable per connection or use tenant-scoped pools
Missing tenant in audit logsCannot trace cross-tenant access attemptsAlways log tenant_id with every operation
No rate limiting per tenantSingle tenant can exhaust resourcesImplement per-tenant quotas with Redis
Testing isolation manuallyIncomplete coverage, human errorAutomate with comprehensive isolation test suite
Forgetting RLS on new tablesNew features create data leakageAdd RLS enable check to migration CI/CD pipeline

Principles

This skill embodies:

  • #1 Zero Trust Security - Never trust tenant context from client; always validate from cryptographically signed tokens
  • #2 Defense in Depth - Layer tenant isolation at middleware, application, database, and audit levels
  • #3 Fail Secure - On any tenant validation failure, deny access immediately (fail closed)
  • #4 Least Privilege - Grant minimum permissions per tenant tier; enforce via RBAC + RLS
  • #5 Eliminate Ambiguity - Explicit tenant context propagation; no implicit assumptions
  • #6 Clear, Understandable, Explainable - Tenant isolation logic centralized and well-documented
  • #8 No Assumptions - Verify tenant isolation at every layer; automated tests confirm zero leakage
  • #10 Automation First - Automated tests, RLS enforcement, and audit logging eliminate manual errors

Full Standard: CODITECT-STANDARD-AUTOMATION.md


Integration Points

  • authentication-authorization - JWT tenant claims, RBAC
  • compliance-frameworks - SOC 2 tenant isolation controls
  • database-schema-optimization - Multi-tenant schema design
  • monitoring-observability - Tenant-aware metrics and alerts