## Multi-tenancy là gì?
Khi xây dựng SaaS, bạn cần quyết định kiến trúc database: mỗi khách hàng một database riêng (multi-database) hay tất cả dùng chung một database nhưng có \`tenant_id\` (shared database)?
## Option 1: Shared Database + Row-level Isolation
### Schema design
\`\`\`sql
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
plan TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
email TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(tenant_id, email)
);
CREATE INDEX idx_users_tenant ON users(tenant_id);
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id),
title TEXT NOT NULL,
content TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_posts_tenant ON posts(tenant_id);
\`\`\`
### Row-level Security (RLS)
PostgreSQL RLS đảm bảo khách hàng không thể truy cập data của nhau:
\`\`\`sql
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_users ON users
USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
CREATE POLICY tenant_isolation_posts ON posts
USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
\`\`\`
### Application code (Drizzle ORM)
\`\`\`typescript
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
export async function getDbForTenant(tenantId: string) {
const client = postgres(process.env.DATABASE_URL!);
await client\`SELECT set_config('app.current_tenant_id', \${tenantId}, false)\`;
return drizzle(client);
}
// Usage trong API route
export async function GET(req: Request) {
const tenantId = req.headers.get('X-Tenant-ID');
const db = await getDbForTenant(tenantId);
const users = await db.select().from(usersTable); // Tự động filter theo tenant
return Response.json(users);
}
\`\`\`
### Ưu điểm
- **Dễ quản lý**: Chỉ một database, một connection pool
- **Cost-effective**: Phù hợp với startup nhỏ, nhiều khách hàng nhỏ
- **Backup đơn giản**: Một lần backup cho tất cả
### Hạn chế
- **Noisy neighbor**: Một tenant query nặng có thể ảnh hưởng cả hệ thống
- **Khó scale**: Không thể scale riêng cho tenant lớn
- **Compliance**: Một số khách hàng yêu cầu data isolation vật lý
## Option 2: Schema-per-Tenant
Mỗi tenant một PostgreSQL schema riêng trong cùng một database:
\`\`\`sql
-- Tạo schema cho tenant mới
CREATE SCHEMA tenant_abc123;
CREATE TABLE tenant_abc123.users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE tenant_abc123.posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES tenant_abc123.users(id),
title TEXT NOT NULL,
content TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
\`\`\`
### Application code
\`\`\`typescript
export async function getDbForTenant(tenantId: string) {
const client = postgres(process.env.DATABASE_URL!);
await client\`SET search_path TO tenant_\${tenantId}, public\`;
return drizzle(client);
}
\`\`\`
### Ưu điểm
- **Isolation tốt hơn**: Mỗi tenant có schema riêng
- **Dễ migrate**: Có thể migrate từng tenant riêng biệt
- **Performance**: Index không bị phình to bởi data của tenant khác
### Hạn chế
- **Connection pool**: Vẫn share pool, có thể gặp bottleneck
- **Migration phức tạp**: Phải chạy migration cho từng schema
## Option 3: Database-per-Tenant
Mỗi tenant một database riêng biệt:
\`\`\`typescript
const tenantDbConfigs = {
'tenant-abc': { host: 'db1.myapp.com', database: 'tenant_abc' },
'tenant-xyz': { host: 'db2.myapp.com', database: 'tenant_xyz' },
};
export function getDbForTenant(tenantId: string) {
const config = tenantDbConfigs[tenantId];
const client = postgres(config);
return drizzle(client);
}
\`\`\`
### Ưu điểm
- **Isolation tối đa**: Mỗi tenant hoàn toàn độc lập
- **Scale linh hoạt**: Tenant lớn có thể dùng RDS riêng với instance lớn hơn
- **Compliance**: Đáp ứng quy định bảo mật khắt khe
### Hạn chế
- **Chi phí cao**: Mỗi database tốn 1 RDS instance
- **Quản lý phức tạp**: Phải monitor nhiều database
- **Migration hell**: Migration 100 tenant = 100 lần chạy migration
## Khuyến nghị cho từng giai đoạn
| Giai đoạn | Số tenant | Kiến trúc |
|---|---|---|
| MVP / Startup | < 100 | Shared DB + RLS |
| Growth | 100 - 1,000 | Schema-per-tenant |
| Enterprise | > 1,000 hoặc có whale customers | Hybrid: Shared cho small, DB-per-tenant cho large |
## Kết luận
Không có giải pháp nào hoàn hảo cho mọi trường hợp. Chọn kiến trúc dựa trên:
- **Bắt đầu nhỏ**: Shared database với RLS
- **Chuẩn bị migrate**: Thiết kế code từ đầu cho phép chuyển đổi sau này
- **Monitor từ sớm**: Track query performance theo tenant để biết khi nào cần chuyển
TechCorp đã giúp 15+ khách hàng SaaS thiết kế kiến trúc multi-tenant tối ưu. Liên hệ để được tư vấn miễn phí!EngineeringCloud
Chia sẻ