mcpField
M
multitenancy

multitenancy

github.com/claude-dev-suite開発4696 回インストール2 日前に更新
リポジトリ

multitenancy について

マルチテナントアプリケーションの設計を扱うスキルです。テナント分離方式やリクエスト単位の識別に加え、実装パターン、アンチパターン、本番向けチェックリストを紹介します。

主な機能

  1. 01データベース・スキーマ・行単位の分離を比較
  2. 02共有スキーマでのテナントスコープ例
  3. 03PostgreSQLの行レベルセキュリティを解説
  4. 04テナント識別方法を列挙
  5. 05本番向け分離チェックリスト

活用シーン

  1. 01SaaSの分離方式を選定
  2. 02Prismaクエリをテナント単位に限定
  3. 03PostgreSQLの行レベルセキュリティを適用
  4. 04本番前にテナント分離を確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill multitenancy

インストール後、Claude Code で/multitenancyを入力して読み込まれたか確認します。

詳細

マルチテナントアーキテクチャ

分離戦略

戦略分離レベル複雑さコスト
テナントごとにデータベースを用意最高高高
テナントごとにスキーマを用意高中中
共有スキーマ(tenant_id 列)中低低
行レベルセキュリティ(RLS)中〜高中低

テナント ID を使用した共有スキーマ(最も一般的)

// 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
// 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 の行レベルセキュリティ

-- 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
// 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();
  }
}

テナントごとのスキーマ

// 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
}

テナント解決戦略

戦略例最適な用途
サブドメインacme.myapp.comB2B SaaS
パスプレフィックスmyapp.com/acme/...よりシンプルなセットアップ
カスタムヘッダーX-Tenant-ID: acmeAPI ファースト
JWT クレーム{ tenantId: "acme" }認証済み API

アンチパターン

アンチパターン対策
クエリにテナントフィルターがないミドルウェアまたは ORM 拡張を使って自動的に適用する
クライアントから渡されたテナント ID を検証しない認証トークンまたはサブドメインから導出する
テナント間のデータ分離をテストしていないテナント間の分離を検証するテストを作成する
テナントプレフィックスのない共有キャッシュすべてのキャッシュキーにテナント ID をプレフィックスとして付ける
テナントを考慮したレート制限がないグローバルではなく、テナントごとにレート制限する

本番環境チェックリスト

  • すべてのルートにテナント解決ミドルウェアを設定する
  • 自動テストでデータ分離を検証する
  • キャッシュキーにテナント ID をプレフィックスとして付ける
  • テナントごとにレート制限する
  • テナントスコープのバックグラウンドジョブを設定する
  • テナントのプロビジョニングおよびデプロビジョニングのフローを用意する
  • テナント間クエリを防止する(RLS または ORM による適用)

コメント