multitenancy

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Multi-Tenant Architecture

多租户架构

Isolation Strategies

隔离策略

StrategyIsolationComplexityCost
Database per tenantHighestHighHigh
Schema per tenantHighMediumMedium
Shared schema (tenant_id column)MediumLowLow
Row-level security (RLS)Medium-HighMediumLow
策略隔离级别复杂度成本
单租户单数据库最高
单租户单Schema
共享Schema(含tenant_id列)
行级安全(RLS)中高

Shared Schema with Tenant ID (most common)

带租户ID的共享Schema(最常用)

typescript
// Middleware: resolve tenant from subdomain or header
function tenantMiddleware(req: Request, res: Response, next: NextFunction) {
  const host = req.hostname; // acme.myapp.com
  const subdomain = host.split('.')[0];
  const tenant = await tenantRepo.findBySubdomain(subdomain);
  if (!tenant) return res.status(404).json({ error: 'Tenant not found' });
  req.tenantId = tenant.id;
  next();
}

// Always filter by tenant
app.get('/api/products', async (req, res) => {
  const products = await db.product.findMany({
    where: { tenantId: req.tenantId },
  });
  res.json(products);
});
typescript
// Middleware: resolve tenant from subdomain or header
function tenantMiddleware(req: Request, res: Response, next: NextFunction) {
  const host = req.hostname; // acme.myapp.com
  const subdomain = host.split('.')[0];
  const tenant = await tenantRepo.findBySubdomain(subdomain);
  if (!tenant) return res.status(404).json({ error: 'Tenant not found' });
  req.tenantId = tenant.id;
  next();
}

// Always filter by tenant
app.get('/api/products', async (req, res) => {
  const products = await db.product.findMany({
    where: { tenantId: req.tenantId },
  });
  res.json(products);
});

Prisma with Tenant Scoping

结合Prisma实现租户范围限定

typescript
// Extension to auto-apply tenant filter
const prisma = new PrismaClient().$extends({
  query: {
    $allOperations({ args, query, operation }) {
      if (['findMany', 'findFirst', 'count', 'updateMany', 'deleteMany'].includes(operation)) {
        args.where = { ...args.where, tenantId: getCurrentTenantId() };
      }
      if (['create', 'createMany'].includes(operation)) {
        args.data = { ...args.data, tenantId: getCurrentTenantId() };
      }
      return query(args);
    },
  },
});
typescript
// Extension to auto-apply tenant filter
const prisma = new PrismaClient().$extends({
  query: {
    $allOperations({ args, query, operation }) {
      if (['findMany', 'findFirst', 'count', 'updateMany', 'deleteMany'].includes(operation)) {
        args.where = { ...args.where, tenantId: getCurrentTenantId() };
      }
      if (['create', 'createMany'].includes(operation)) {
        args.data = { ...args.data, tenantId: getCurrentTenantId() };
      }
      return query(args);
    },
  },
});

PostgreSQL Row-Level Security

PostgreSQL行级安全(RLS)

sql
-- Enable RLS
ALTER TABLE products ENABLE ROW LEVEL SECURITY;

-- Policy: users see only their tenant's data
CREATE POLICY tenant_isolation ON products
  USING (tenant_id = current_setting('app.tenant_id')::uuid);

-- Set tenant context per request
SET app.tenant_id = 'tenant-uuid-here';
SELECT * FROM products; -- auto-filtered
typescript
// Set tenant context on each request
pool.on('connect', async (client) => {
  // Set after getting connection from pool
});

async function withTenant<T>(tenantId: string, fn: () => Promise<T>): Promise<T> {
  const client = await pool.connect();
  try {
    await client.query(`SET app.tenant_id = $1`, [tenantId]);
    return await fn();
  } finally {
    await client.query('RESET app.tenant_id');
    client.release();
  }
}
sql
-- Enable RLS
ALTER TABLE products ENABLE ROW LEVEL SECURITY;

-- Policy: users see only their tenant's data
CREATE POLICY tenant_isolation ON products
  USING (tenant_id = current_setting('app.tenant_id')::uuid);

-- Set tenant context per request
SET app.tenant_id = 'tenant-uuid-here';
SELECT * FROM products; -- auto-filtered
typescript
// Set tenant context on each request
pool.on('connect', async (client) => {
  // Set after getting connection from pool
});

async function withTenant<T>(tenantId: string, fn: () => Promise<T>): Promise<T> {
  const client = await pool.connect();
  try {
    await client.query(`SET app.tenant_id = $1`, [tenantId]);
    return await fn();
  } finally {
    await client.query('RESET app.tenant_id');
    client.release();
  }
}

Schema-Per-Tenant

单租户单Schema模式

typescript
// Dynamic schema selection
function getTenantSchema(tenantId: string): string {
  return `tenant_${tenantId.replace(/-/g, '_')}`;
}

async function createTenantSchema(tenantId: string) {
  const schema = getTenantSchema(tenantId);
  await db.query(`CREATE SCHEMA IF NOT EXISTS ${schema}`);
  await db.query(`SET search_path TO ${schema}`);
  await runMigrations(); // Apply schema migrations
}
typescript
// Dynamic schema selection
function getTenantSchema(tenantId: string): string {
  return `tenant_${tenantId.replace(/-/g, '_')}`;
}

async function createTenantSchema(tenantId: string) {
  const schema = getTenantSchema(tenantId);
  await db.query(`CREATE SCHEMA IF NOT EXISTS ${schema}`);
  await db.query(`SET search_path TO ${schema}`);
  await runMigrations(); // Apply schema migrations
}

Tenant Resolution Strategies

租户解析策略

StrategyExampleBest For
Subdomain
acme.myapp.com
B2B SaaS
Path prefix
myapp.com/acme/...
Simpler setup
Custom header
X-Tenant-ID: acme
API-first
JWT claim
{ tenantId: "acme" }
Authenticated APIs
策略示例最佳适用场景
子域名
acme.myapp.com
B2B SaaS
路径前缀
myapp.com/acme/...
简单部署场景
自定义请求头
X-Tenant-ID: acme
API优先场景
JWT声明
{ tenantId: "acme" }
已认证API场景

Anti-Patterns

反模式

Anti-PatternFix
No tenant filter on queriesUse middleware or ORM extension to auto-apply
Tenant ID from client without validationDerive from auth token or subdomain
No tenant data isolation testingWrite tests that verify cross-tenant isolation
Shared cache without tenant prefixPrefix all cache keys with tenant ID
No tenant-aware rate limitingRate limit per tenant, not globally
反模式修复方案
查询未添加租户过滤使用中间件或ORM扩展自动应用租户过滤
直接使用客户端传入的未验证租户ID从认证Token或子域名中获取租户信息
未进行租户数据隔离测试编写自动化测试验证跨租户数据隔离
未带租户前缀的共享缓存所有缓存键添加租户ID前缀
未按租户进行限流按租户维度限流,而非全局限流

Production Checklist

生产环境检查清单

  • Tenant resolution middleware on all routes
  • Data isolation verified with automated tests
  • Cache keys prefixed with tenant ID
  • Rate limiting per tenant
  • Tenant-scoped background jobs
  • Tenant provisioning and deprovisioning flow
  • Cross-tenant query prevention (RLS or ORM enforcement)
  • 所有路由均配置租户解析中间件
  • 通过自动化测试验证数据隔离
  • 缓存键添加租户ID前缀
  • 按租户维度进行限流
  • 租户范围的后台任务
  • 租户创建与销毁流程
  • 防止跨租户查询(通过RLS或ORM强制实现)