multi-tenant-saas-architecture について
Multi-Tenant SaaS Architecture は、SaaS のテナント境界、権限、パネル、セッション、監査記録を設計・点検するエージェントスキルです。テナント分離と明示的な認可を重視し、設計案や根拠に基づく指針を作成します。
主な機能
- 01テナント分離とデータ範囲の整理
- 02管理者、テナント、エンドユーザーパネルの定義
- 03権限の優先順位と拒否ルールの設計
- 04セッション、JWT、テナント ID の計画
- 05監査イベントと移行計画の策定
活用シーン
- 01新規 SaaS のマルチテナント設計
- 02シングルテナントアプリの移行
- 03テナント間アクセスリスクの監査
- 04ロール上書きと特権操作監査の計画
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ peterbamuhigire/ chwezi-dev-engine --skill multi-tenant-saas-architectureインストール後、Claude Code で/multi-tenant-saas-architectureを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
documentation/migration.mdreferences/database-schema.mdpermission-model.mdsaas-deployment-models-decision-tree.mdtenant-context-propagation.md詳細
マルチテナント SaaS アーキテクチャ
謝辞: Peter Bamuhigire、techguypeter.com、+256 784 464178 氏より共有。
必要な入力
| 入力 | 必須 | 用途 |
|---|---|---|
| テナント、製品、ライフサイクルのスコープ | はい | SaaS の意思決定を範囲内に収める |
| 現在のアーキテクチャ、計画、ポリシー、制約 | はい | 強制可能な動作を保持する |
| 本番データまたは検証済みの証跡 | 条件付き | しきい値と移行を検証する |
ケイパビリティと権限の契約
デフォルトでは読み取り専用の分析とします。構成、課金、アイデンティティ、テナントデータ、インフラストラクチャ、顧客コミュニケーションを変更するのは、明示的な権限、最小権限の資格情報、テナントスコープ、ロールバック、監査可能な承認がある場合に限ります。シークレットやテナント境界を越えて公開することは決してありません。
縮退モード
本番アクセス、ポリシー、テレメトリ、または権威ある記録が利用できない場合は、ラベル付きの設計またはドライランプランを作成します。デプロイ、照合、削除、配信、または測定された結果を主張してはいけません。不足している証跡と検証を列挙します。
決定ルール
| 条件 | アクション | 停止条件 |
|---|---|---|
| テナント分離、金銭、アイデンティティ、または削除が影響を受ける | 承認とロールバックの証跡を要求する | スコープまたは権限が曖昧である |
| 証跡が可逆的な変更を支持している | ステージング、テスト、および記録を行う | 受け入れチェックが失敗する |
| 部分的なコンテキストのみが利用可能である | 前提条件と検証を返す | 本番の主張を検証できない |
ドメインのアンチパターン
- あるテナントのポリシーやデータを別のテナントに適用する。修正: すべての境界でテナントスコープを強制する。
- 助言的なリクエストから本番を変更する。修正: 権限が明示されるまで読み取り専用を維持する。
- 制限、価格、メトリクス、またはコンプライアンスの主張を捏造する。修正: 権威ある記録を使用するか、未解決としてマークする。
- ロールバックと監査の証跡なしに出荷する。修正: ステージングし、変更前後の証拠を保持する。
- 欠落している依存関係を成功として扱う。修正: ブロックされた検証を明示する。
使用する場合
- 新しい SaaS のテナント設計、または既存の SaaS をクロステナント漏洩に対して堅牢化する場合。
- SaaS を 3 つのパネル (スーパー管理者、テナント/フランチャイズ管理者、エンドユーザー) に分割し、それぞれ異なるスコープを持たせる場合。
- テナントごとのロールオーバーライド、権限の優先順位付け、および特権アクセスの監査証跡を設計する場合。
- 下流のデータベース、API、セキュリティ、およびデリバリのスキルが依存するセッション、JWT、および
tenant_idの配線を計画する場合。
使用しない場合
- タスクがテナント化の計画がないシングルテナントアプリである場合 — このスキルの制約は利益なしにオーバーヘッドを追加します。
- 作業がテナント境界ではなくビジネスモジュールの構成に関するものである場合 —
modular-saas-architectureを使用してください。 - 作業がすでに定義されたテナントモデル内の純粋なスキーマ整形である場合 —
database-design-engineeringとmysql-best-practicesを使用してください。
必要な入力
system-architecture-designからのコンテキストマップとクリティカルフローテーブル。database-design-engineeringからのアクセスパターンリスト (分離モデルが実際のクエリと一致するように)。vibe-security-skillからの脅威モデル / 不正利用ケース。- 製品要件からのパネルとユーザータイプのリスト (スーパー管理者、テナント管理者、エンドユーザー、およびそれらのサブタイプ)。
ワークフロー
- 出力で指定された成果物として、テナント分離マップ、パネルレイアウト、権限モデル、および監査計画を作成します。
出力
- テナント分離マップ、パネル定義、権限優先順位モデル、および監査計画 (以下の出力テーブルを参照)。
生成される証跡
| カテゴリ | 成果物 | 形式 | 例 |
|---|---|---|---|
| データ安全性 | テナント分離ノート | 行レベル vs スキーマレベルの分離、RLS ポリシー、およびテナントスコープのインデックスを扱う Markdown ドキュメント | docs/saas/tenant-isolation-note.md |
| セキュリティ | テナント認可監査 | スーパー管理者 vs テナント管理者 vs ユーザー特権境界および認可テストを扱う Markdown ドキュメント | docs/saas/tenant-authz-audit.md |
リファレンス
- 以下のコアワークフローを読んだ後、詳細が必要な場合は
references/ディレクトリを使用してください。 - 実装の補足詳細や移行に関するメモについては、
documentation/ディレクトリを使用してください。
3 パネル分離、ゼロトラストの徹底、厳格なテナント分離、および包括的な監査証跡を備えた本番グレードのマルチテナント SaaS アーキテクチャです。このスキルは、下流のあらゆるモジュール、API、データクエリが継承するテナント境界と認証境界を定義します。
前提条件
まず、以下の順番で読み込んでください。
world-class-engineering— リポジトリ全体の品質基準。system-architecture-design— このスキルが利用するコンテキストマップとクリティカルフローを生成します。database-design-engineering— 分離モデルを形作るアクセスパターン一覧を生成します。vibe-security-skill— このスキルが組み込む脅威モデルと認証ルールを生成します。
このスキルが適用される場面
- 新しい SaaS のテナント設計をゼロから行う場合。
- シングルテナントアプリをマルチテナントに変換する場合(
documentation/migration.mdを参照)。 - 既存の SaaS について、テナント間のリーク、
tenant_idフィルターの欠落、監査されていない管理者操作を監査する場合。 - 3 パネルのファイルおよびルートレイアウト(
/、/adminpanel/、/memberpanel/)を計画する場合。 - 権限の優先順位ラダー(ユーザー拒否 > ユーザー許可 > テナント上書き > ロール > デフォルト拒否)を設計する場合。
- どの操作を監査対象とすべきか、および監査レコードをどのくらい保持するかを定義する場合。
入力
| 成果物 | 生成元 | 必須? | 理由 |
|---|---|---|---|
| コンテキストマップ | system-architecture-design | 必須 | サービス、パネル、所有権を定義します |
| クリティカルフロー表 | system-architecture-design | 必須 | 認証とセッションの境界を形作ります |
| アクセスパターン一覧 | database-design-engineering | 必須 | tenant_id を用いた共有 DB かテナントごとの DB かを選択します |
| 脅威モデル | vibe-security-skill | 必須 | 監査ルールと不正利用ケースに反映されます |
| パネルおよびユーザータイプ一覧 | 製品要件 | 必須 | パネルごとのロールとスコープを決定します |
出力
| 成果物 | 利用先 | テンプレート |
|---|---|---|
テナント分離マップ(分離モデル + テーブルごとの tenant_id ルール) | database-design-engineering、api-design-first | インライン(このスキル) |
| パネル定義(スーパー管理者、テナント管理者、エンドユーザー) | api-design-first、kubernetes-saas-delivery | インライン |
| 権限優先順位モデル(ユーザー / テナント上書き / ロール / デフォルト拒否) | vibe-security-skill、api-design-first | インライン + references/permission-model.md |
| セッションおよび JWT 計画(プレフィックス、有効期間、ローテーション) | api-design-first、vibe-security-skill | インライン |
| 監査イベントセットと保持ポリシー | observability-monitoring、reliability-engineering | インライン |
| 移行計画(シングルテナントからマルチテナントへ) | database-design-engineering、deployment-release-engineering | documentation/migration.md |
譲れない原則
- フランチャイズ / テナントスコープのすべてのテーブルは、
tenant_id(またはfranchise_id)をNOT NULLとし、tenantsへの外部キーを持ちます。 - テナントスコープのテーブルに対するすべてのクエリは、
WHERE句にtenant_idを含めます。 tenant_idは常にセッションまたは JWT から取得します — クライアント入力(ボディ、クエリ、ヘッダー)から取得してはいけません。- デフォルトの認可判断は拒否です。すべての操作には明示的な権限チェックが必要です。
- テナントデータに触れるすべてのスーパー管理者操作は、実行者、対象テナント、正当性の理由とともに監査されます。
- 存在すべきでないクロステナントアクセスは
404 Not Foundを返し、403 Forbiddenは決して返しません(403 はリソースが存在することを確認してしまうため)。
決定ルール
分離モデル
Tenants share data schema AND tenants < ~10k AND regulated data not tenant-partitioned
-> Shared DB, row-level tenant_id (default choice for this skill)
Strong regulatory boundary (HIPAA/PCI) per tenant
-> Schema-per-tenant OR DB-per-tenant; app injects connection by tenant
Single very large tenant needs isolation from the rest
-> Hybrid: shared DB for small tenants, dedicated DB for large tenant
Wrong choice failure modes:
- Shared DB without tenant_id discipline -> data leakage, impossible to audit
- DB-per-tenant at small scale -> migration and operational cost explodes
- Schema-per-tenant without tooling -> migrations drift across schemas
新機能のパネル配置
Feature controls platform config, billing, tenant lifecycle, or impersonation
-> /adminpanel/ (super_admin only, audited)
Feature is tenant-admin workspace (manage own tenant, staff, catalogue)
-> /public/ root (tenant owners/staff, scoped by tenant_id)
Feature is self-service for end user (customer/member/student/patient)
-> /memberpanel/ (own records only, scoped by tenant_id AND user_id)
Wrong choice failure modes:
- Admin feature in /public/ -> tenant users see platform levers
- Tenant feature in /memberpanel/ -> end users escalate to admin-only data
- End-user feature in /public/ -> collides with tenant admin routes
権限解決の優先順位
1. user_permissions.denied match -> DENY for tenant-scoped actions
2. actor is super_admin -> ALLOW only for an explicitly scoped administrative permission (always audit)
3. user_permissions.granted match -> ALLOW
4. tenant_role_overrides match -> ALLOW/DENY per is_enabled
5. role -> permission via template -> ALLOW
6. no match -> DENY
Wrong ordering failure mode: if role is checked before user deny, an
explicit revocation never takes effect. Always deny-before-grant at each tier.
セッションプレフィックスの有無
One SaaS codebase hosts multiple apps on the same origin -> prefix required
Only one app on origin AND no shared subdomain sessions -> prefix optional
SSO / impersonation across apps planned -> prefix required
Without a prefix, two SaaS apps on the same host collide in $_SESSION and the
bug only appears in production when both apps are logged in.
3 パネルアーキテクチャ
+-------------------------------------------------------------+
| Shared Infrastructure Layer |
| Data (tenant isolated) | Business Logic | Session system |
+-------------------------------------------------------------+
| | |
+--------v--------+ +---------v------+ +---------v-------+
| /public/ | | /adminpanel/ | | /memberpanel/ |
| (ROOT) | | | | |
| Tenant Admin | | Super Admin | | End User |
| Workspace | | System | | Portal |
| owner, staff | | super_admin | | member/student |
+-----------------+ +----------------+ +-----------------+
重要: /public/ ルートはテナント管理者のワークスペースであり、エンドユーザーパネルではありません。この 2 つを混同することが最もよくある間違いです。
ファイルレイアウト:
public/
index.php # Landing
sign-in.php # Login
dashboard.php # Tenant admin dashboard
adminpanel/ # Super admin panel
includes/
memberpanel/ # End user portal
includes/
includes/ # Shared includes
assets/
パネル定義
| パネル | パス | ユーザー | スコープ | 備考 |
|---|---|---|---|---|
| テナント管理者 | /public/ | owner、staff | 単一テナント | すべてのクエリに tenant_id = ? が含まれます。プラットフォーム設定には触れられません |
| スーパー管理者 | /public/adminpanel/ | super_admin | クロステナント | tenant_id は NULL の場合があります。すべての操作が監査されます |
| エンドユーザー | /public/memberpanel/ | member、student、customer、patient | テナント内の自身のレコード | クエリは tenant_id AND user_id でスコープされます |
テナント分離モデル
ユーザータイプごとの tenant_id ルール
| ユーザータイプ | tenant_id カラム |
|---|---|
| super_admin | nullable |
| テナント owner | 必須、NOT NULL |
| テナント staff | 必須、NOT NULL |
| member / student / customer / patient | 必須、NOT NULL |
テーブルパターン
CREATE TABLE students (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
email VARCHAR(100),
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
INDEX idx_tenant (tenant_id),
INDEX idx_tenant_email (tenant_id, email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
複合インデックスは tenant_id を先頭にします。すべての箇所で utf8mb4_unicode_ci を使用します。ファイルの大文字小文字を正確に一致させてください(Linux のファイルシステムは大文字小文字を区別します)。
クエリの強制
// Extract tenant from session/JWT. Never from client input.
$tenantId = getSession('tenant_id');
// Regular users: filter by tenant_id
$stmt = $db->prepare('SELECT * FROM students WHERE tenant_id = ? AND id = ?');
$stmt->execute([$tenantId, $studentId]);
// Super admin: cross-tenant access is allowed but always audited
if (getSession('user_type') === 'super_admin') {
auditLog('CROSS_TENANT_ACCESS', [
'admin_user_id' => getSession('user_id'),
'target_tenant_id' => $requestedTenantId,
'action' => 'VIEW_STUDENTS',
'justification' => $request['justification'] ?? null,
]);
}
セッションと JWT の計画
セッションプレフィックスシステム
define('SESSION_PREFIX', 'saas_app_'); // e.g. 'school_', 'restaurant_'
setSession('user_id', $userId); // writes $_SESSION['saas_app_user_id']
$userId = getSession('user_id'); // reads $_SESSION['saas_app_user_id']
セッション Cookie 設定
| 設定 | 値 |
|---|---|
| HttpOnly | true |
| Secure | HTTPS を自動検出(ポート 443 または $_SERVER['HTTPS']) |
| SameSite | Strict |
| 有効期間 | アイドル 30 分 |
| 再生成 | ログイン時および権限変更時 |
$isHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| (int) $_SERVER['SERVER_PORT'] === 443;
ini_set('session.cookie_secure', $isHttps ? '1' : '0');
モバイルと API 用の JWT
| 設定 | 値 |
|---|---|
| アクセストークンの有効期間 | 15 分 |
| リフレッシュトークンの有効期間 | 30 日 |
| ローテーション | リフレッシュのたびに実施 |
| 失効 | jti をキーとする永続化テーブル |
権限モデル
優先順位(高い順):ユーザー拒否 > ユーザー許可 > テナント上書き > ロール権限 > デフォルト拒否。完全なスキーマ、解決アルゴリズム、キャッシュ戦略、シードデータについては references/permission-model.md を参照してください。
function hasPermission(userId, tenantId, permission) {
if (userPermissions.denied(userId, tenantId, permission)) return false;
if (user.type === 'super_admin' && isScopedAdministrativePermission(permission)) {
auditLog('PERMISSION_BYPASS', { userId, permission });
return true;
}
if (userPermissions.granted(userId, tenantId, permission)) return true;
for (const role of getUserRoles(userId, tenantId)) {
const override = tenantRoleOverride(tenantId, role.id, permission);
if (override !== null) return override.isEnabled;
if (roleHasPermission(role, permission)) return true;
}
return false; // default deny
}
ゼロトラストチェックリスト
- テナントスコープのテーブルに対するすべてのクエリに
tenant_idを含める(監査付きの super_admin を除く)。 - プリペアドステートメントのみを使用する — SQL への文字列連結は行わない。
- すべての変更操作の前に権限チェックを行う。
- スーパー管理者アクセスには MFA を必須とする。
- パスワードはユーザーごとのソルト(32 文字)とサーバー全体のペッパー(64 文字以上)を用いた Argon2ID として保存する。
- 5 回の失敗試行後にアカウントをロックアウトし、指数バックオフを適用する。
- レート制限:テナント 1000 リクエスト/分、ユーザー 100 リクエスト/分。
- すべてのパネルで HTTPS + HSTS を適用する。
- スーパー管理者の操作は正当性の理由とともに監査する。
- クロステナントアクセスは 403 ではなく 404 を返す。
API 設計
# Tenant-scoped (default)
GET /api/v1/orders # list within tenant
POST /api/v1/orders # create within tenant
GET /api/v1/orders/{id} # show; 404 if wrong tenant
DELETE /api/v1/orders/{id} # delete; 404 if wrong tenant
# Super admin (cross-tenant, audited)
GET /api/v1/admin/tenants
POST /api/v1/admin/impersonate
レスポンスエンベロープ:
{ "success": true, "data": {}, "meta": { "page": 1, "total": 100 } }
エラーエンベロープ:
{ "success": false, "error": { "code": "PERMISSION_DENIED", "message": "..." } }
監査とコンプライアンス
常に監査する対象:すべてのスーパー管理者操作、なりすまし、権限変更、テナントの作成/停止、データエクスポート、認証失敗、クロステナントアクセスの試行。
{
"id": "uuid",
"timestamp": "2026-04-07T10:30:00Z",
"actor_user_id": 123,
"actor_type": "super_admin",
"action": "IMPERSONATE_USER",
"target_tenant_id": 456,
"justification": "Support request #12345",
"ip_address": "203.0.113.1",
"changes": { "before": {}, "after": {} }
}
保持期間:セキュリティログ 1 年、監査証跡 7 年、運用ログ 90 日。
監視アラート
- クロステナントアクセスの試行(目標値 = ゼロ)。
- 新しい IP からのスーパー管理者ログイン。
- 認証失敗の急増 > 100/分。
- テナントスコープのテーブルに対する
tenant_idを含まないデータベースクエリ。 - API エラー率 > 5%。
テナントのライフサイクル
PENDING -> ACTIVE -> SUSPENDED -> ARCHIVED
アーカイブは、設定された保持期限まで可逆です。その後、アーカイブされたテナントはカスケードによって完全に削除されます。
アンチパターン
- クライアントが指定するテナント識別子。
$franchiseId = $_POST['franchise_id'];— どのユーザーでもこれを別のテナントに設定できてしまいます。修正: 常にgetSession('tenant_id')を使うか、検証済みの JWT クレームから読み取ります。 - クエリにテナントスコープがない。
SELECT * FROM students WHERE id = ?— どのテナントの生徒でも返してしまいます。修正:SELECT * FROM students WHERE tenant_id = ? AND id = ?とし、行が見つからない場合は 404 として扱います。 - 監査なしのスーパー管理者による変更。
deleteStudent($studentId);がスーパー管理者パネルからログなしで実行されます。修正: すべてのスーパー管理者による変更をauditLog('ADMIN_DELETE_STUDENT', [...])でラップし、実行者、対象テナント、正当性を記録します。 - クロステナントアクセスに対して 403 を返す。 リソースが存在することを確認できてしまい、テナント ID の列挙を可能にします。修正: クロステナントのミスは 404 にマッピングし、リソースの存在を隠します。
- ビジネステーブルの
tenant_idが NULL 許容。tenant_id BIGINT NULL— 所有者のいない行を許容し、すべてのテナントクエリがそれを黙って除外してしまいます。修正: 外部キー付きのtenant_id BIGINT UNSIGNED NOT NULLとし、NOT NULL に切り替える前にバックフィルします。 - マルチアプリホストでプレフィックスのないセッションキー。 同一オリジン上の 2 つのアプリが互いの
$_SESSION['user_id']を上書きしてしまいます。修正: アプリごとにSESSION_PREFIXを設定し、常にsetSession()/getSession()を経由します。 - インデックス順序が
(tenant_id, status)ではなく(status, tenant_id)。 クエリプランナーが複合インデックスを使って 1 テナントの範囲を切り出せません。修正: すべての複合インデックスの先頭をtenant_idにします。 - 全テーブルを一度にビッグバンマイグレーション。 ロールバックのリスクを高め、数時間のダウンタイムを生じます。修正:
documentation/migration.mdにある 6 ステップのパターンで 1 ドメインずつマイグレーションし、カラムを NOT NULL に切り替える前に NULL のtenant_idがないことを検証します。
次に読む
database-design-engineering— 分離モデルを選んだ後のスキーマ、インデックス、マイグレーションの規律。api-design-first— このスキルのパネルが依存する REST 規約、認証モデル、エラーエンベロープ。vibe-security-skill— このスキルに供給される脅威モデリング、濫用ケース、認証/認可マトリクス。kubernetes-saas-delivery— このモデルに基づくテナントごとのデプロイ、名前空間分離、段階的ロールアウト。modular-saas-architecture— ここで定義されたテナント内で、プラットフォームがプラグイン可能なビジネスモジュールも組み合わせる場合。mysql-best-practices— ここにあるスキーマパターンの MySQL エンジン固有の実行方法。
参考資料
references/database-schema.md— テナント、ユーザー、監査、テナントスコープのテーブルスキーマと、インデックスおよびパーティショニング。references/permission-model.md— RBAC スキーマ、権限解決アルゴリズム、キャッシュ、ミドルウェア、階層的および条件付き権限。documentation/migration.md—tenant_idを安全に追加する方法、ゼロダウンタイムマイグレーション、シングルテナントからマルチテナントへのフェーズ、ロールバック。
AI サービス分離の補足
AI が関与する場合、テナント分離パターンはさらに 1 段階深く適用されます。このスキルのトランザクションパターン(RLS / テナントごとのスキーマ / テナントごとの DB)は、AI の資産クラス — ベクトルストア、プロンプト、ファインチューニング、評価データセット、会話ログ、検索キャッシュ、監査ログのペイロード — に拡張されます。専用のスキルは ai-tenant-isolation-patterns です。
クロスリファレンス:
ai-on-saas-architecture— AI+SaaS アーキテクチャの統合。ai-tenant-isolation-patterns— ベクトルストアのパーティショニング、多層防御、BYOK、データブリードテストスイート。ai-rag-multi-tenant— RAG 固有の分離。ai-model-gateway— リクエスト境界でテナントスコープを強制します。ai-prompt-injection-and-tenant-safety— プロンプトレイヤーの敵対的要素を補完します。