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);
}
}
```