🎯

multi-tenancy

🎯Skill

from dadbodgeoff/drift

VibeIndex|
What it does

Enables secure multi-tenant architecture with tenant isolation, data separation, and configurable per-tenant settings across shared or dedicated database models.

πŸ“¦

Part of

dadbodgeoff/drift(69 items)

multi-tenancy

Installation

npm installInstall npm package
npm install -g driftdetect
npm installInstall npm package
npm install -g driftdetect@latest
npm installInstall npm package
npm install -g driftdetect-mcp
Claude Desktop ConfigurationAdd this to your claude_desktop_config.json
{ "mcpServers": { "drift": { "command": "driftdetect-mcp" } } ...
πŸ“– Extracted from docs: dadbodgeoff/drift
5Installs
-
AddedFeb 4, 2026

Skill Details

SKILL.md

Implement multi-tenant architecture with tenant isolation, data separation, and per-tenant configuration. Supports shared database and schema-per-tenant models.

Overview

# Multi-Tenancy

Build SaaS apps that serve multiple organizations securely.

When to Use This Skill

  • B2B SaaS applications
  • White-label platforms
  • Enterprise software
  • Any app serving multiple organizations

Isolation Models

1. Shared Database, Shared Schema (Recommended for most)

```

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”

β”‚ Database β”‚

β”‚ β”‚

β”‚ users: id, tenant_id, email, ... β”‚

β”‚ orders: id, tenant_id, user_id, ... β”‚

β”‚ products: id, tenant_id, name, ... β”‚

β”‚ β”‚

β”‚ All tables have tenant_id column β”‚

β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

```

2. Shared Database, Schema per Tenant

```

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”

β”‚ Database β”‚

β”‚ β”‚

β”‚ tenant_acme.users β”‚

β”‚ tenant_acme.orders β”‚

β”‚ tenant_globex.users β”‚

β”‚ tenant_globex.orders β”‚

β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

```

3. Database per Tenant (Enterprise)

```

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”

β”‚ acme_db β”‚ β”‚ globex_db β”‚ β”‚ initech_db β”‚

β”‚ β”‚ β”‚ β”‚ β”‚ β”‚

β”‚ users β”‚ β”‚ users β”‚ β”‚ users β”‚

β”‚ orders β”‚ β”‚ orders β”‚ β”‚ orders β”‚

β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

```

TypeScript Implementation

Tenant Context

```typescript

// tenant-context.ts

import { AsyncLocalStorage } from 'async_hooks';

interface TenantContext {

tenantId: string;

tenantSlug: string;

plan: 'free' | 'pro' | 'enterprise';

features: string[];

}

const tenantStorage = new AsyncLocalStorage();

export function getTenant(): TenantContext {

const tenant = tenantStorage.getStore();

if (!tenant) {

throw new Error('No tenant context available');

}

return tenant;

}

export function runWithTenant(tenant: TenantContext, fn: () => T): T {

return tenantStorage.run(tenant, fn);

}

export { tenantStorage, TenantContext };

```

Tenant Middleware

```typescript

// tenant-middleware.ts

import { Request, Response, NextFunction } from 'express';

import { runWithTenant, TenantContext } from './tenant-context';

interface TenantMiddlewareOptions {

headerName?: string;

subdomainExtract?: boolean;

pathExtract?: boolean;

}

export function tenantMiddleware(options: TenantMiddlewareOptions = {}) {

const { headerName = 'x-tenant-id', subdomainExtract = true } = options;

return async (req: Request, res: Response, next: NextFunction) => {

let tenantId: string | undefined;

// Strategy 1: Header

tenantId = req.headers[headerName.toLowerCase()] as string;

// Strategy 2: Subdomain (acme.yourapp.com)

if (!tenantId && subdomainExtract) {

const host = req.hostname;

const subdomain = host.split('.')[0];

if (subdomain && subdomain !== 'www' && subdomain !== 'app') {

tenantId = subdomain;

}

}

// Strategy 3: Path (/t/acme/dashboard)

if (!tenantId && options.pathExtract) {

const match = req.path.match(/^\/t\/([^/]+)/);

if (match) {

tenantId = match[1];

}

}

// Strategy 4: User's default tenant (from JWT)

if (!tenantId && req.user?.defaultTenantId) {

tenantId = req.user.defaultTenantId;

}

if (!tenantId) {

return res.status(400).json({ error: 'Tenant not specified' });

}

// Load tenant from database

const tenant = await db.tenants.findUnique({

where: { id: tenantId },

select: { id: true, slug: true, plan: true, features: true },

});

if (!tenant) {

return res.status(404).json({ error: 'Tenant not found' });

}

// Check user has access to tenant

if (req.user) {

const membership = await db.tenantMemberships.findFirst({

where: { userId: req.user.id, tenantId: tenant.id },

});

if (!membership) {

return res.status(403).json({ error: 'Access denied to tenant' });

}

req.userRole = membership.role;

}

// Run request with tenant context

runWithTenant(

{

tenantId: tenant.id,

tenantSlug: tenant.slug,

plan: tenant.plan,

features: tenant.features,

},

() => next()

);

};

}

```

Tenant-Scoped Queries

```typescript

// tenant-prisma.ts

import { PrismaClient } from '@prisma/client';

import { getTenant } from './tenant-context';

// Extend Prisma with automatic tenant filtering

export function createTenantPrisma(prisma: PrismaClient) {

return prisma.$extends({

query: {

$allModels: {

async findMany({ model, operation, args, query }) {

// Auto-add tenant filter

args.where = { ...args.where, tenantId: getTenant().tenantId };

return query(args);

},

async findFirst({ model, operation, args, query }) {

args.where = { ...args.where, tenantId: getTenant().tenantId };

return query(args);

},

async findUnique({ model, operation, args, query }) {

// For unique queries, verify tenant after fetch

const result = await query(args);

if (result && result.tenantId !== getTenant().tenantId) {

return null; // Hide cross-tenant data

}

return result;

},

async create({ model, operation, args, query }) {

// Auto-set tenant on create

args.data = { ...args.data, tenantId: getTenant().tenantId };

return query(args);

},

async update({ model, operation, args, query }) {

// Ensure update is scoped to tenant

args.where = { ...args.where, tenantId: getTenant().tenantId };

return query(args);

},

async delete({ model, operation, args, query }) {

args.where = { ...args.where, tenantId: getTenant().tenantId };

return query(args);

},

},

},

});

}

// Usage

const tenantDb = createTenantPrisma(prisma);

// These are automatically scoped to current tenant

const users = await tenantDb.user.findMany();

const order = await tenantDb.order.create({ data: { ... } });

```

Per-Tenant Configuration

```typescript

// tenant-config.ts

interface TenantConfig {

branding: {

logo?: string;

primaryColor?: string;

companyName?: string;

};

features: {

maxUsers: number;

maxStorage: number;

apiAccess: boolean;

sso: boolean;

};

integrations: {

slack?: { webhookUrl: string };

stripe?: { customerId: string };

};

}

class TenantConfigService {

private cache = new Map();

async getConfig(tenantId: string): Promise {

if (this.cache.has(tenantId)) {

return this.cache.get(tenantId)!;

}

const tenant = await db.tenants.findUnique({

where: { id: tenantId },

include: { config: true },

});

const config = this.buildConfig(tenant);

this.cache.set(tenantId, config);

return config;

}

private buildConfig(tenant: Tenant): TenantConfig {

// Merge plan defaults with tenant overrides

const planDefaults = PLAN_CONFIGS[tenant.plan];

return {

branding: { ...tenant.config?.branding },

features: { ...planDefaults.features, ...tenant.config?.features },

integrations: { ...tenant.config?.integrations },

};

}

invalidateCache(tenantId: string) {

this.cache.delete(tenantId);

}

}

```

Database Schema

```sql

-- Tenants table

CREATE TABLE tenants (

id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

slug VARCHAR(50) UNIQUE NOT NULL,

name VARCHAR(255) NOT NULL,

plan VARCHAR(50) DEFAULT 'free',

features TEXT[] DEFAULT '{}',

config JSONB DEFAULT '{}',

created_at TIMESTAMP DEFAULT NOW()

);

-- Users belong to tenants via memberships

CREATE TABLE tenant_memberships (

id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

user_id UUID REFERENCES users(id) ON DELETE CASCADE,

tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,

role VARCHAR(50) DEFAULT 'member',

created_at TIMESTAMP DEFAULT NOW(),

UNIQUE(user_id, tenant_id)

);

-- All data tables have tenant_id

CREATE TABLE orders (

id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

tenant_id UUID NOT NULL REFERENCES tenants(id),

user_id UUID REFERENCES users(id),

-- ... other columns

created_at TIMESTAMP DEFAULT NOW()

);

-- Index for tenant queries

CREATE INDEX idx_orders_tenant ON orders(tenant_id);

-- Row Level Security (optional, extra protection)

ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON orders

USING (tenant_id = current_setting('app.current_tenant')::uuid);

```

Python Implementation

```python

# tenant_context.py

from contextvars import ContextVar

from dataclasses import dataclass

from typing import Optional

@dataclass

class TenantContext:

tenant_id: str

tenant_slug: str

plan: str

features: list[str]

_tenant_context: ContextVar[Optional[TenantContext]] = ContextVar(

"tenant_context", default=None

)

def get_tenant() -> TenantContext:

tenant = _tenant_context.get()

if not tenant:

raise RuntimeError("No tenant context")

return tenant

def set_tenant(tenant: TenantContext):

return _tenant_context.set(tenant)

```

FastAPI Middleware

```python

# tenant_middleware.py

from fastapi import Request, HTTPException

from starlette.middleware.base import BaseHTTPMiddleware

class TenantMiddleware(BaseHTTPMiddleware):

async def dispatch(self, request: Request, call_next):

tenant_id = request.headers.get("x-tenant-id")

if not tenant_id:

# Try subdomain

host = request.headers.get("host", "")

subdomain = host.split(".")[0]

if subdomain not in ["www", "app", "api"]:

tenant_id = subdomain

if not tenant_id:

raise HTTPException(400, "Tenant not specified")

tenant = await db.tenants.find_unique(where={"id": tenant_id})

if not tenant:

raise HTTPException(404, "Tenant not found")

token = set_tenant(TenantContext(

tenant_id=tenant.id,

tenant_slug=tenant.slug,

plan=tenant.plan,

features=tenant.features,

))

try:

response = await call_next(request)

return response

finally:

_tenant_context.reset(token)

```

Best Practices

  1. Always filter by tenant_id - Never trust client-provided IDs alone
  2. Use middleware - Centralize tenant resolution
  3. Index tenant_id - Every tenant-scoped table needs this index
  4. Consider RLS - Extra protection layer in PostgreSQL
  5. Cache tenant config - Avoid repeated lookups

Common Mistakes

  • Forgetting tenant filter on queries (data leak!)
  • Not validating user's tenant access
  • Hardcoding tenant-specific logic
  • No index on tenant_id columns
  • Allowing cross-tenant references

More from this repository10

🎯
feature-flags🎯Skill

Enables controlled feature rollouts, A/B testing, and selective feature access through configurable flags for gradual deployment and user targeting.

🎯
design-tokens🎯Skill

Generates a comprehensive, type-safe design token system with WCAG AA color compliance and multi-framework support for consistent visual design.

🎯
file-uploads🎯Skill

Securely validates, scans, and processes file uploads with multi-stage checks, malware detection, and race condition prevention.

🎯
ai-coaching🎯Skill

Guides users through articulating creative intent by extracting structured parameters and detecting conversation readiness.

🎯
environment-config🎯Skill

Validates and centralizes environment variables with type safety, fail-fast startup checks, and multi-environment support.

🎯
community-feed🎯Skill

Generates efficient social feed with cursor pagination, trending algorithms, and engagement tracking for infinite scroll experiences.

🎯
cloud-storage🎯Skill

Enables secure, multi-tenant cloud file storage with signed URLs, direct uploads, and visibility control for user-uploaded assets.

🎯
email-service🎯Skill

Simplifies email sending, templating, and tracking with robust SMTP integration and support for multiple email providers and transactional workflows.

🎯
error-sanitization🎯Skill

Sanitizes error messages by logging full details server-side while exposing only generic, safe messages to prevent sensitive information leakage.

🎯
batch-processing🎯Skill

Optimizes database operations by collecting and batching independent records, improving throughput by 30-40% with built-in fallback processing.