per-tenant-database について
マルチテナントアプリを構築する開発者向けのRivetKitサンプルです。テナントごとにRivet Actorを割り当て、JSON状態とリアルタイムイベントによる分離を示します。
主な機能
- 01テナントごとのActor
- 02テナント単位のデータ分離
- 03JSON状態の保存
- 04書き込み後のイベント配信
活用シーン
- 01会社ごとの従業員・プロジェクトデータの保存
- 02同一テナントのクライアントへの変更通知
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ rivet-dev/ skills --skill per-tenant-databaseインストール後、Claude Code で/per-tenant-databaseを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
BASE_SKILL.mdindex.jsonopenapi.jsonreference/actors/access-control.mdactions.mdactor-runtime-socket.mdappearance.mdauthentication.mdcommunicating-between-actors.mdconnections.mddebugging.mddesign-patterns.mddestroy.mderrors.mdevents.mdfetch-and-websocket-handler.mdhelper-types.mdhttp-api.mdinput.mdinspector-tabs.mdkeys.mdkv.mdlifecycle.mdlimits.mdmetadata.mdqueues.mdreference/actors/quickstart/backend.mdcloudflare.mdeffect.mdnext-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 クエリ、メモリ容量を超えるデータ、テナントごとのリレーショナルスキーマが必要な場合。 | SQLite | GitHub |
| SQLite + Drizzle | Actor SQLite 上で、型付きスキーマ、クエリビルダー、生成されたマイグレーションファイルを使用する場合。 | SQLite + Drizzle | GitHub |
どちらの 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 つのアクターを配置します。その会社の従業員とプロジェクトを永続状態に保持し、アクションを通じて読み取りと書き込みを処理し、接続中のクライアントに変更をブロードキャストします。
- アクション
addEmployeelistEmployeesaddProjectlistProjectsgetStats
- キュー
- なし
- イベント
employeeAddedprojectAdded
- 状態
- JSON
company_nameemployeesprojectscreated_atupdated_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 に移行してください。
リファレンスマップ
アクター
- アクセス制御
- アクション
- アクターキー
- アクターランタイムソケット
- アクターのステータス
- 認証
- Cloudflare Workers クイックスタート
- アクター間の通信
- 接続
- カスタムインスペクタータブ
- デバッグ
- 設計パターン
- アクターの破棄
- Effect.ts クイックスタート(ベータ)
- エラー
- Fetch および WebSocket ハンドラー
- ヘルパー型
- アイコンと名前
- インメモリ状態
- 入力パラメーター
- ライフサイクル
- 制限
- 低レベル HTTP リクエストハンドラー
- 低レベル KV ストレージ
- 低レベル WebSocket ハンドラー
- メタデータ
- Next.js クイックスタート
- Node.js と Bun のクイックスタート
- キューと実行ループ
- React クイックスタート
- リアルタイム
- Rust クイックスタート(ベータ)
- スケーリングと並行処理
- スケジュールと Cron
- 状態の共有と参加
- SQLite
- SQLite と Drizzle
- Supabase Functions クイックスタート
- テスト
- トラブルシューティング
- 型
- 標準 HTTP API
- バージョンとアップグレード
- ワークフロー
CLI
クライアント
クックブック
- AI エージェント
- チャットルーム
- 共同編集テキストエディター
- Cron ジョブとスケジュールタスク
- テナントごとのデータベース
- VPC またはエアギャップネットワークへの Rivet のデプロイ
- ライブカーソルとプレゼンス
- マルチプレイヤーゲーム
デプロイ
- コンテナランナー
- Amazon Web Services Lambda へのデプロイ
- AWS ECS へのデプロイ
- Cloudflare Workers へのデプロイ
- Freestyle へのデプロイ
- Google Cloud Run へのデプロイ
- Hetzner へのデプロイ
- Kubernetes へのデプロイ
- Railway へのデプロイ
- Rivet Compute へのデプロイ
- Supabase Functions へのデプロイ
- Vercel へのデプロイ
- VM およびベアメタルへのデプロイ
全般
- アクター設定
- クロスオリジンリソース共有
- LLM および AI 向けドキュメント
- エッジネットワーキング
- エンドポイント
- 環境変数
- HTTP サーバー
- ロギング
- プール設定
- 本番環境チェックリスト
- レジストリ設定
- ランタイムモード
- WASM とネイティブ SDK の比較