mcpField
P
per-tenant-database

per-tenant-database

github.com/rivet-devデータ・データベース441.1k 回インストール1 日前に更新
リポジトリ

per-tenant-database について

マルチテナントアプリを構築する開発者向けのRivetKitサンプルです。テナントごとにRivet Actorを割り当て、JSON状態とリアルタイムイベントによる分離を示します。

主な機能

  1. 01テナントごとのActor
  2. 02テナント単位のデータ分離
  3. 03JSON状態の保存
  4. 04書き込み後のイベント配信

活用シーン

  1. 01会社ごとの従業員・プロジェクトデータの保存
  2. 02同一テナントのクライアントへの変更通知

インストール

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

$npx skills add https://github.com/rivet-dev/skills --skill per-tenant-database

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

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

BASE_SKILL.md
index.json
openapi.json
reference/actors/
access-control.md
actions.md
actor-runtime-socket.md
appearance.md
authentication.md
communicating-between-actors.md
connections.md
debugging.md
design-patterns.md
destroy.md
errors.md
events.md
fetch-and-websocket-handler.md
helper-types.md
http-api.md
input.md
inspector-tabs.md
keys.md
kv.md
lifecycle.md
limits.md
metadata.md
queues.md
reference/actors/quickstart/
backend.md
cloudflare.md
effect.md
next-js.md

詳細

テナントごとのデータベース

重要:何かを始める前に、必ずこのスキルのディレクトリにある BASE_SKILL.md を読んでください。デバッグ、エラー処理、状態管理、デプロイ、プロジェクトのセットアップに関する重要なガイダンスが記載されています。これらのルールとパターンは、すべての RivetKit の作業に適用されます。以下の内容は、すでに読んで理解していることを前提としています。

動作するサンプル

参考実装が必要な場合は、次のテンプレートにある動作するサンプルコードを直接読んでください。

RivetKit でテナントごとにデータベースを分けるアーキテクチャのパターンです。すべてのテーブルに tenant_id 列を持つ共有データベースを使う代わりに、各テナントが独自の Rivet Actor を持ち、その Actor がテナントのデータセット全体を管理します。

スターターコード

GitHub にある動作するサンプルから始めて、必要に応じて調整してください。このサンプルでは、各テナントのデータセットを JSON Actor 状態に保存し、ライブイベント更新に対応した React ダッシュボードを提供します。

トピック概要
分離テナントごとに、会社名をキーとする companyDatabase Actor を 1 つ割り当てます。テナントを切り替えると、データセット全体が切り替わります。
状態employees と projects の配列、およびタイムスタンプを保持する JSON Actor 状態です。SQLite、キュー、スケジューリングは使用しません。
リアルタイム書き込みアクションごとに状態を変更し、型付きイベント(employeeAdded、projectAdded)を、そのテナントの接続中クライアントすべてにブロードキャストします。
認証ありません。サインイン画面は見た目だけのものです。本番環境向けのガイダンスはセキュリティチェックリストを参照してください。

分離モデル

Actor キーがテナント ID です。クライアントは useActor({ name: "companyDatabase", key: [companyName] }) で接続し、Actor は createState 内の c.key[0] を読み取って、そのテナントのデータセットを初期化します。これにより、次のことが実現します。

  • テナントごとに 1 つの Actor:companyDatabase[tenantId] は、特定の Actor インスタンス 1 つを指します。2 つのテナントが同じ Actor を共有することはありません。
  • テナントごとに 1 つのデータセット:すべての読み取りと書き込みは、その Actor の状態を介して行われるため、誤ったフィルタリングにつながる tenant_id 列を持つ共有テーブルはありません。テナント間のデータ漏えいは、WHERE 句の指定を忘れることではなく、誤ったキーを構築した場合にのみ発生します。
  • キーのインジェクションなし:キーは文字列に埋め込むのではなく、配列として扱われます。key: [tenantId] は、"tenant:" + tenantId の文字列連結のようにエスケープを回避されることはありません。キーを参照してください。

このサンプルのテスト(tests/per-tenant-database.test.ts)では、分離が確認できます。companyDatabase["Alpha Co"] に書き込まれたデータが companyDatabase["Beta Co"] に現れることはありません。

状態バックエンドの選択

このサンプルでは、標準の JSON Actor 状態を使用しています。キーとテナントを対応させる同じモデルは、どの Actor 状態バックエンドでも利用できます。

バックエンド使用する場面ドキュメント動作するコード
JSON Actor 状態小規模なデータセット、シンプルな読み取り、データセット全体が余裕をもってメモリに収まる場合。このサンプルで使用しています。状態GitHub
Actor SQLite(rivetkit/db)テーブル、インデックス、SQL クエリ、メモリ容量を超えるデータ、テナントごとのリレーショナルスキーマが必要な場合。SQLiteGitHub
SQLite + DrizzleActor SQLite 上で、型付きスキーマ、クエリビルダー、生成されたマイグレーションファイルを使用する場合。SQLite + DrizzleGitHub

どちらの SQLite を使用する場合も、各テナントには独自の組み込み SQLite データベースが割り当てられます。データベースのスコープは Actor に限定され、Actor のスコープはテナントに限定されるためです。

マイグレーション

テナントごとの例では、JSON の状態にはスキーマがないため、マイグレーションはありません。SQLite を採用すると、マイグレーションはテナントごとのデータベースで実行されます。

  • 生の SQL:db({ onMigrate }) は、アクターがトラフィックを処理する前に、SQLite のセーブポイント内でマイグレーション SQL を実行します。onMigrate が例外をスローすると、すべてのマイグレーション SQL がアトミックにロールバックされ、アクターは起動しません。SQLite を参照してください。
  • Drizzle:drizzle-kit は、型付きスキーマからマイグレーションファイルを生成し、db({ schema, migrations }) はアクターの起動時にそれらを適用します。SQLite + Drizzle を参照してください。

各テナントは独自のデータベースを持つため、共有データベースに対する大規模なマイグレーションを一度に行うのではなく、各テナントのアクターが起動するたびに、アクターごとにマイグレーションが反映されます。

テナント ID は認証情報から取得する必要があります

この例のサインインは見せかけのものです。クライアントが任意の会社名を選び、その文字列がアクターキーになるため、訪問者は誰でも、任意のテナントのデータを読み書きできます。これは本番環境にリリースしないでください。以下は本番環境で必須の拡張事項ですが、この例には実装されていません。

  • ユーザー入力からではなく、JWT クレームなどの検証済み認証情報からテナント ID を導出します。
  • onBeforeConnect(許可/拒否)または createConnState(検証済みユーザーを接続状態に保存)で、認証情報を c.key と照合して検証します。認証 と接続を参照してください。
  • 接続レベルの認証に加えて、アクションごとの権限チェックを追加します。アクセス制御を参照してください。

アクター

  • キー:companyDatabase[companyName](単一要素の配列キー。c.key[0] は会社名です)
  • 責務:テナントごとに 1 つのアクターを配置します。その会社の従業員とプロジェクトを永続状態に保持し、アクションを通じて読み取りと書き込みを処理し、接続中のクライアントに変更をブロードキャストします。
  • アクション
    • addEmployee
    • listEmployees
    • addProject
    • listProjects
    • getStats
  • キュー
    • なし
  • イベント
    • employeeAdded
    • projectAdded
  • 状態
    • JSON
    • company_name
    • employees
    • projects
    • created_at
    • updated_at

各書き込みアクションは、同じ「変更してからブロードキャストする」形式に従います。レコードを c.state に追加し、updated_at を更新し、型付きイベントをブロードキャストして、レコードを返します。アクションとイベントを参照してください。

ライフサイクル

sequenceDiagram
	participant A as Tenant A client
	participant DA as companyDatabase A
	participant B as Tenant B client
	participant DB as companyDatabase B

	Note over A: authenticate and derive tenant id
	A->>DA: connect with key [tenantA]
	Note over DA: createState seeds company_name, employees, projects
	A->>DA: listEmployees() + listProjects() + getStats()
	A->>DA: addEmployee(name, role)
	DA-->>A: employeeAdded event
	B->>DB: connect with key [tenantB]
	Note over DB: separate actor, separate dataset
	B->>DB: listEmployees()
	DB-->>B: tenant B data only

この例では、「認証」ステップは自由入力の会社選択です。それ以外のフローは図と一致しています。初回作成時に createState がデータセットを初期化し、ダッシュボードは listEmployees、listProjects、getStats を使って読み込まれ、同じテナントに属する接続中のすべてのクライアントが employeeAdded および projectAdded イベントを受け取ります。

セキュリティチェックリスト

この例には、以下の対策は一切含まれていません。本番環境で使用する前に、すべて適用してください。

  • テナント ID:クライアントから渡された文字列ではなく、検証済みの JWT クレームからテナント ID を導出します。
  • 接続の検証:onBeforeConnect または createConnState で、認証情報のテナントクレームが c.key と一致することを検証し、一致しない場合は拒否します。
  • アクションごとの認可:接続時だけでなく、変更を行うアクション(addEmployee、addProject)の実行前に呼び出し元のロールを確認します。アクセス制御を参照してください。
  • 入力の検証:名前と役職の長さを制限し、列挙値を検証します。この例では、入力をトリミングし、フォールバックのデフォルト値に置き換えるだけです。
  • キーの構築:テナント ID は常に配列要素として渡します(key: [tenantId])。テナント ID をキー文字列に埋め込んだり、あるテナントの入力から別のテナントのアクターにアクセスするキーを構築したりしないでください。
  • 増加の制限:推奨される拡張として、employees 配列と projects 配列に上限を設けるか、ページネーションを実装します。この例では、JSON の状態で配列が無制限に増えます。データセットがメモリに収まらなくなったら、SQLite に移行してください。

リファレンスマップ

アクター
CLI
クライアント
クックブック
デプロイ
全般
セルフホスティング

コメント