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
- Review the patterns and examples below
- Apply the relevant patterns to your implementation
- Follow the best practices outlined in this skill
Design and implement perfect tenant isolation with zero data leakage across all system layers.
Core Capabilities
- Tenant Isolation - Complete data separation between tenants
- Access Control - RBAC with tenant context
- Data Segregation - Row-level security, schema isolation
- Rate Limiting - Per-tenant quotas and throttling
- 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):
| Layer | Check | Status |
|---|---|---|
| Middleware | Tenant extracted from JWT, not client headers | ☐ |
| Middleware | AsyncLocalStorage propagates context | ☐ |
| Database | RLS enabled on ALL tenant tables | ☐ |
| Database | Triggers enforce tenant_id on INSERT | ☐ |
| Repository | All queries use tenant context | ☐ |
| API | Rate limits per tenant, not global | ☐ |
| API | Error messages don't leak tenant info | ☐ |
| Audit | All logs include tenant_id | ☐ |
| Testing | Cross-tenant access tests exist | ☐ |
| Testing | Tests 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-Pattern | Problem | Solution |
|---|---|---|
| Hardcoding tenant_id in application code | Brittle, error-prone, security risk | Use middleware + AsyncLocalStorage for context propagation |
| Trusting client-provided tenant_id | Client can spoof tenant to access other data | Extract tenant from validated JWT claims only |
| Applying tenant filter in application layer only | Database queries can bypass app logic | Use RLS policies at database level for defense in depth |
| Not validating tenant on writes | Malicious actor can inject tenant_id | Enforce tenant_id via triggers and CHECK constraints |
| Single connection pool for all tenants | Tenant context can leak between requests | Set session variable per connection or use tenant-scoped pools |
| Missing tenant in audit logs | Cannot trace cross-tenant access attempts | Always log tenant_id with every operation |
| No rate limiting per tenant | Single tenant can exhaust resources | Implement per-tenant quotas with Redis |
| Testing isolation manually | Incomplete coverage, human error | Automate with comprehensive isolation test suite |
| Forgetting RLS on new tables | New features create data leakage | Add 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