multi-tenant-architecture について
Cloudflare または Vercel 上のマルチテナントプラットフォームを設計する開発スキルです。テナント識別、ルーティング、コンピュートとデータの分離、ドメイン、プラットフォーム制限の判断を支援し、固有の参考資料と検証手順を示します。
主な機能
- 01Cloudflare または Vercel の構成選択
- 02テナントのドメインと識別方法の計画
- 03コンピュートとデータの分離設計
- 04ルーティングとテナントコンテキスト伝播の指定
- 05カスタムドメインとプラットフォーム制限への対応
- 06検証コマンドによる境界確認
活用シーン
- 01マルチテナント SaaS プラットフォームの計画
- 02テナントのルーティングと分離の設計
- 03カスタムドメイン導入手順の準備
- 04テナント境界の実装確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ mblode/ agent-skills --skill multi-tenant-architectureインストール後、Claude Code で/multi-tenant-architectureを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
evals/evals.jsonreferences/cloudflare-platform.mddata-isolation.mdlimits-and-quotas.mdpsl.mdvercel-domains.mdvercel-platform.md詳細
マルチテナントプラットフォームアーキテクチャ(Cloudflare または Vercel)
- 対象: プラットフォームの選択、ドメイン戦略と PSL、テナントの識別、コンピューティングとデータの分離、ホスト名によるルーティング、テナントコンテキストの伝播、カスタムドメインと SSL、テナントごとの静的ファイル、プラットフォームの制限をプランに反映する方法。
- 対象外: 一般的なフォルダー構成やモジュールの契約(
codebase-architectureを使用)、新しいリポジトリのスキャフォールディング(scaffold-nextjsを使用)、ルーティングで動的に配信するテナントごとの SEO ファイルの内容(サイトマップのエントリー、canonical URL、構造化データ、インデックス登録ポリシー。seoを使用)。
目次
- プラットフォームの振り分け(最初に決定)
- 参照ファイル
- ワークフロー(順序が重要)
- 注意点
- 出力スキーマ
- エビデンスコマンド
- 関連スキル
プラットフォームの振り分け(最初に決定)
| シグナル | プラットフォーム | モデル |
|---|---|---|
| テナントが独自のコードをアップロードまたは生成する。コードレベルで分離する。KV、D1、Durable Objects、R2 上でエッジコンピューティングを行う。 | Cloudflare | テナントごとの Worker を配置したディスパッチ名前空間の前段にディスパッチ Worker を置く。カスタムホスト名には Cloudflare for SaaS を使用する。 |
| すべてのテナントが同じ Next.js コードベースを実行し、コンテンツ、ブランディング、プランのみが異なる。ISR、Server Components、Vercel デプロイを使用する。 | Vercel | 1 つのデプロイを使用する。proxy.ts でホスト名からテナントを解決する。プロジェクトにワイルドカードドメインとカスタムドメインを設定する。 |
- 製品ごとにプラットフォームを 1 つ選択してください。Vercel アプリの前段に Cloudflare プロキシを置くと TLS とリダイレクトのレイヤーが二重になり、通常、リダイレクトループや証明書発行の失敗の原因になります。
- テナントが Vercel 上で独自のコードをデプロイする方式は、マルチプロジェクトモデル(SDK で作成する、テナントごとに 1 つの Vercel プロジェクト)です。分離に関する考え方は Cloudflare の項目に準じます。このスキルで扱う Vercel の参照資料は、単一デプロイモデルのみを対象としています。
参照ファイル
| ファイル | 参照するタイミング |
|---|---|
| cloudflare-platform.md | Cloudflare を選択した場合:ディスパッチ名前空間、ルーティング、Cloudflare for SaaS のカスタムホスト名、分離モード、KV と D1(ステップ 3~7) |
| vercel-platform.md | Vercel を選択した場合:proxy.ts による解決、App Router のレイアウト、Global Config のルックアップ、テナントごとの静的ファイル、カスタムサブパス、ローカル開発(ステップ 4~7) |
| vercel-domains.md | Vercel を選択した場合:SDK によるドメインのライフサイクル、DNS ターゲット、検証、ワイルドカードネームサーバー、SSL、トラブルシューティング(ステップ 7) |
| data-isolation.md | いずれのプラットフォームでもステップ 3:RLS を使った共有スキーマ、テナントごとのスキーマ、テナントごとのデータベース、Postgres/Supabase/Drizzle のポリシーパターン |
| psl.md | テナントがコンテンツを公開するか、兄弟サブドメイン上でコードを実行する場合のステップ 1:適格性、申請、暫定的な Cookie 制御 |
| limits-and-quotas.md | ステップ 8:Cloudflare、Vercel、Neon の制限名と、それぞれの公式情報源。プランへの反映に使用 |
ワークフロー(順序が重要)
このチェックリストをコピーして進捗を管理してください。
Multi-tenant progress:
- [ ] Step 1: Domain strategy and PSL decision
- [ ] Step 2: Tenant identification strategy
- [ ] Step 3: Isolation model (compute and data)
- [ ] Step 4: Deterministic routing
- [ ] Step 5: Tenant context propagation
- [ ] Step 6: Tenant config and least-privilege bindings
- [ ] Step 7: Custom domains and per-tenant static files
- [ ] Step 8: Limits mapped to plans, evidence captured
- ドメイン戦略を選択する
- テナントのワークロードには、専用の登録可能ドメインを使用してください(テナント用に
acme.app、ブランド用にacme.com)。x.acme.com上のフィッシングを行うテナントが 1 つでもあれば、ドメイン全体がブロックリストに登録される可能性があります。また、Domain=acme.comを設定したテナントの Cookie はダッシュボードにも送信されます。 - ダッシュボードと認証には、テナントのサブドメインとは異なる apex ドメインを使用してください(コンソールには
app.acme.com、テナントには*.acme.app)。 - テナントがコンテンツを公開するか、兄弟サブドメイン上でコードを実行する場合は、テナント名の直上にあるラベル(
acme.app、または<tenant>.sites.acme.appの場合はsites.acme.app)を PSL に申請し、今すぐ着手してください。SLA はありません。テナント所有のカスタムドメインには PSL のエントリーは必要ありません。それ以外の場合は、理由を添えてNo PSLと記録してください。
- テナントの識別方法を選択します(主要な方法を1つ選び、アップグレード時にカスタムドメインを利用します)。
- サブドメイン
tenant.acme.app:ワイルドカード DNS とワイルドカード証明書を使用します。デフォルトです。 - カスタムドメイン
tenant.com:テナントが CNAME を設定して、ドメインをお客様に向けます。料金を支払うテナント向けです。レピュテーションはテナント側に移り、ステップ 7 のオンボーディングライフサイクルが必要です。 - パス
acme.app/tenant:テナントごとの DNS や証明書は不要ですが、Cookie の分離もブランディングもできません。テナントにホスト名を割り当てることが決してない場合にのみ選択してください。
- 分離モデルを定義します。
- コンピューティング、Cloudflare: 信頼できないモードでディスパッチ名前空間を1つ使用します。プランごとに、呼び出し単位の
cpuMsとsubRequestsの制限を設定します。テナントのコードがインターネットに接続できる場合は、アウトバウンド Worker を使用します。 - コンピューティング、Vercel: デプロイメントは1つで、テナントのコードは実行されません。テナントがコードをデプロイする必要がある場合、アプリ内でサンドボックス化するのではなく、Vercel のマルチプロジェクトまたは Cloudflare に移行してください。
- データ: 共有スキーマのテナント対応テーブルすべてに
tenant_idを設け、RLS を適用する方法がデフォルトです。規制対象のテナントやリソース消費の激しいテナントには、プランごとにデータベースを分ける方法を選択できます。data-isolation.md を参照してください。
- テナントがルーティングに影響を及ぼしたり、互いの情報を見たりできないよう、決定的にルーティングします。
- Cloudflare: SaaS ゾーン上の単一の
*/*ルートをディスパッチ Worker に向けます。ホスト名からテナントレコードを検索し(KV、見つからない場合は D1)、次にenv.DISPATCHER.get(script)を実行します。Worker not foundの場合は 404 を返します。 - Vercel:
proxy.ts(Next.js 16。15 ではruntime: 'nodejs'を指定したmiddleware.ts)でhostを読み取り、Global Config またはデータベースからテナントを検索して、テナントセグメントに書き換えます。不明なホスト名には 404 を返し、ブランドサイトには決してルーティングしません。 - テナントへの書き換えより先に
/.well-knownを通します。robots.txt、sitemap.xml、llms.txtはテナントセグメントにルーティングし、テナントごとに内容が変わるようにします。
- テナントコンテキストの唯一の信頼できる情報源を定め、そこから伝播させます。
- 受信した
x-tenant-*ヘッダーをすべて削除し、解決済みのテナントからx-tenant-id、x-tenant-slug、x-tenant-planを設定して、リクエストに転送します(NextResponse.next({ request: { headers } }))。Server Components ではawait headers()を、ルートハンドラーではrequest.headersを読み取ります。Cloudflare では、ディスパッチ Worker がヘッダーを設定するか、fetchの前にパラメーターを渡します。 - プロキシの役割はルーティングであり、認可ではありません。Server Functions、ルートハンドラー、ジョブではセッションからテナントを再特定し、データレイヤーでそれを強制します(RLS または
tenant_idの述語を使用)。
- テナントに必要なものだけをバインドします。
- Cloudflare: 各ユーザー Worker に独自のバインディング(KV 名前空間、D1 データベース、R2 プレフィックス)を割り当てます。バインディングの追加には明示的な再デプロイが必要です。共有グローバルは使用しません。
- Vercel: Global Config に保持するのは
hostname -> { id, slug, plan }のみです。データベースを信頼できる情報源とし、ドメインの検証時に書き込みを反映します。機能フラグとブランディングは、テナント ID をキーとしてデータベースから取得します。
- カスタムドメインとテナントごとの静的ファイルに対応します。
- 設計して記録するライフサイクル:ドメインを追加 -> DNS の向き先を表示 -> 所有権を検証 -> 証明書を発行 -> マッピングを有効化 -> 削除または失敗時の対応。
- Cloudflare: SaaS ゾーン上で Cloudflare for SaaS のカスタムホスト名を使用し、プロキシされたフォールバックオリジン、
customers.<you>.comの CNAME ターゲット、httpまたはtxtによる検証を設定します。DNS の切り替え前に事前検証します。cloudflare-platform.md を参照してください。 - Vercel:
projectsAddProjectDomain-> プロジェクトのドメインカードに表示される DNS 値 -> ドメインがすでに Vercel 上にある場合のみ_vercelTXT ->projectsVerifyProjectDomain-> Let's Encrypt の HTTP-01。詳しくは vercel-domains.md を参照してください。 robots.txt、sitemap.xml、llms.txtは、明示的なContent-Typeを指定するテナントセグメント内のルートハンドラーとして実装します。テナント固有のものを/publicに置いてはいけません。その内容はseoの領域です。
- 制限を計画として明示し、証拠を取得する
- limits-and-quotas.md を参照して制限と計画の表を埋め、各現在値を参照元の URL から取得して日付を記載します。ルーティング層で適用してください(Cloudflare の
limits、Vercel のプランヘッダーとサーバー側のチェック)。 - リクエスト処理中に時間のかかる処理を実行しないでください。Cloudflare Queues または Workflows、Vercel のバックグラウンド関数または cron を使います。
- すべてのテナント操作(テナントの作成、ドメインの追加、検証、削除)は、UI と同等の権限で HTTP 経由で実行できるようにします。ダッシュボードでしか実行できない場合、プラットフォームの詳細が UI に漏れています。
- 証拠コマンドを実行し、結果を出力に貼り付けます。
注意点
プラットフォームとデータに関する注意点は、その手順のリファレンスに記載されています。これらはリファレンスが読み込まれる前に発生します。
- インバウンドのテナントヘッダーを転送すると、
curl -H "x-tenant-id: <other>"によって別のテナントのデータが配信されます。/.well-knownやブランドサイトなど、解決処理をスキップする経路も含め、プロキシまたはディスパッチ Worker を通るすべての経路でx-tenant-*を削除または上書きしてください。 - 制限された文字セット(Global Config では
^[\w-]+$のみ許可)をキーにしたホスト名マップには、衝突のないエンコーディングまたはハッシュが必要です。ピリオドをアンダースコアに置き換えると、a-b.example.comとa.b-example.comが同じキーにマップされます。レコードには元のホスト名を保持し、読み取り時に照合してください。 - カスタムドメインをロードマップに含めたままパスベースで開始すると、後で URL の書き換え、Cookie の変更、DNS の移行が必要になります。
出力スキーマ
長さは決定事項に応じて調整します。該当しないセクションは埋めるのではなく削除してください。
# Multi-tenant architecture
## Platform decision
- Platform: Cloudflare | Vercel
- Why this platform:
- Rejected platform and reason:
## Domain map
- Brand domain:
- Tenant domain:
- Tenant subdomains:
- Custom domains:
- PSL decision: Submit (suffix, owner, PR link, _psl TXT date) | No PSL (reason)
## Routing matrix
| Host pattern | Resolver | Destination | Unknown tenant behavior |
|---|---|---|---|
## Tenant context flow
- Authority: proxy.ts | dispatch Worker
- Headers set and stripped:
- Server read path:
- Data-layer enforcement:
## Isolation model
- Compute isolation:
- Data isolation (and per-plan variant):
- Config/binding isolation:
## Custom-domain lifecycle
1. DNS target:
2. Ownership verification:
3. Certificate provisioning:
4. Routing activation:
5. Removal/failure path:
## Limits-to-plan table
| Limit | Source URL / access date | Free | Pro | Enterprise | Enforcement point |
|---|---|---:|---:|---:|---|
## Validation evidence
| Check | Command | Expected | Result |
|---|---|---|---|
証拠コマンド
設計を完了とする前に、ローカル環境またはプレビュー環境で実行してください。該当しない場合は理由を添えて N/A と記載します。
| チェック項目 | コマンド | 期待される結果 |
|---|---|---|
| テナント境界がコードに存在する | rg -n "x-tenant-id|CREATE POLICY|FORCE ROW LEVEL SECURITY|DISPATCHER.get" . | プロキシまたはディスパッチ Worker とスキーマ内に該当箇所がある |
| 不明なホストが 404 になる | curl -sI -H "Host: nope.acme.app" <url> | 404 |
| 偽造ヘッダーが無視される | curl -s -H "Host: a.acme.app" -H "x-tenant-id: tenant-b" <url>/api/whoami | テナント A |
| 静的ファイルの内容が異なる | curl -s -H "Host: a.acme.app" <url>/robots.txt と -H "Host: b.acme.app" | 本文が異なり、Content-Type: text/plain |
| アプリのロールで RLS が機能する | psql -c "BEGIN; SET LOCAL ROLE app_user; SELECT set_config('app.tenant_id','<t1>',true); SELECT count(*) FROM posts; ROLLBACK;" | テナント t1 の行のみ |
| ACME パスに到達できる | curl -sI -H "Host: tenant.com" <url>/.well-known/acme-challenge/test | テナントのパスセグメントへのリダイレクトではない |
| 制限が最新である | 制限の表で各 URL の横にアクセス日を記載する | 計画期間内の日付が記載されている |
関連スキル
codebase-architecture:アプリケーション自体のフォルダー構成、モジュールの契約、リクエストコンテキストのパイプライン。scaffold-nextjs:これらのマルチテナントパターンを適用する前に、Next.js の Turborepo をブートストラップします。seo:ルーティングで配信するようになった後の、テナントごとのrobots.txt、sitemap.xml、llms.txtの内容、canonical URL、構造化データ。
メンテナンス専用:evals/evals.json には、このスキルの変更に対する回帰シナリオが含まれています。ユーザーのタスク中には読み込まれません。
コメント
Markdown 対応