per-tenant-database

작성자: rivet-dev

멀티 테넌트 데이터 격리를 위해 테넌트당 하나의 Rivet 액터를 사용합니다. 액터 키는 테넌트 ID이므로 각 테넌트는 자체 격리된 데이터셋과 마이그레이션을 갖습니다.

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

Database per Tenant

IMPORTANT: Before doing anything, you MUST read BASE_SKILL.md in this skill's directory. It contains essential guidance on debugging, error handling, state management, deployment, and project setup. Those rules and patterns apply to all RivetKit work. Everything below assumes you have already read and understood it.

Working Examples

If you need a reference implementation, read the raw working example code in these templates:

Patterns for database-per-tenant architectures with RivetKit. Instead of one shared database with a tenant_id column on every table, each tenant gets its own Rivet Actor, and that actor owns the tenant's entire dataset.

Starter Code

Start with the working example on GitHub and adapt it. The example stores each tenant's dataset in JSON actor state and serves a React dashboard with live event updates.

TopicSummary
IsolationOne companyDatabase actor per tenant, keyed by company name. Switching tenants swaps the entire dataset.
StateJSON actor state holding employees and projects arrays plus timestamps. No SQLite, no queues, no scheduling.
RealtimeEvery write action mutates state, then broadcasts a typed event (employeeAdded, projectAdded) to all connected clients of that tenant.
AuthNone. The sign-in screen is cosmetic. Production guidance is in the security checklist.

The Isolation Model

The actor key is the tenant id. The client connects with useActor({ name: "companyDatabase", key: [companyName] }) and the actor reads c.key[0] in createState to seed that tenant's dataset. This gives you:

  • One actor per tenant: companyDatabase[tenantId] addresses exactly one actor instance. Two tenants can never share an actor.
  • One dataset per tenant: All reads and writes go through that actor's state, so there is no shared table with a tenant_id column to filter incorrectly. Cross-tenant leaks require constructing the wrong key, not forgetting a WHERE clause.
  • No key injection: Keys are arrays, not interpolated strings. key: [tenantId] cannot be escaped the way "tenant:" + tenantId string concatenation can. See Keys.

The example's test (tests/per-tenant-database.test.ts) proves the isolation: data written to companyDatabase["Alpha Co"] never appears in companyDatabase["Beta Co"].

Choosing a State Backend

The example uses plain JSON actor state. The same key-equals-tenant model works with any actor state backend.

BackendUse WhenDocsWorking Code
JSON actor stateSmall datasets, simple reads, whole dataset fits comfortably in memory. What the example uses.StateGitHub
Actor SQLite (rivetkit/db)Tables, indexes, SQL queries, larger-than-memory data, per-tenant relational schema.SQLiteGitHub
SQLite + DrizzleTyped schema, query builder, and generated migration files on top of actor SQLite.SQLite + DrizzleGitHub

With either SQLite option, every tenant gets its own embedded SQLite database, since the database is scoped to the actor and the actor is scoped to the tenant.

Migrations

The per-tenant example has no migrations because JSON state has no schema. When you adopt SQLite, migrations run per tenant database:

  • Raw SQL: db({ onMigrate }) runs your migration SQL inside a SQLite savepoint before the actor serves traffic. If onMigrate throws, all migration SQL rolls back atomically and the actor does not start. See SQLite.
  • Drizzle: drizzle-kit generates migration files from your typed schema, and db({ schema, migrations }) applies them when the actor wakes. See SQLite + Drizzle.

Because each tenant has its own database, migrations roll out per actor as each tenant's actor wakes, rather than as one large migration against a shared database.

Tenant Id Must Come From Auth

The example's sign-in is cosmetic: the client picks any company string and that string becomes the actor key, so any visitor can read and write any tenant's data. Do not ship this. As a required production extension (not implemented by the example):

  • Derive the tenant id from a verified credential, such as a JWT claim, never from user input.
  • Validate the credential against c.key in onBeforeConnect (pass/fail) or createConnState (store the verified user on connection state). See Authentication and Connections.
  • Add per-action permission checks on top of connection-level auth. See Access Control.

Actors

  • Key: companyDatabase[companyName] (single-element array key; c.key[0] is the company name)
  • Responsibility: One actor per tenant. Holds that company's employees and projects in persistent state, serves reads and writes via actions, and broadcasts mutations to connected clients.
  • Actions
    • addEmployee
    • listEmployees
    • addProject
    • listProjects
    • getStats
  • Queues
    • None
  • Events
    • employeeAdded
    • projectAdded
  • State
    • JSON
    • company_name
    • employees
    • projects
    • created_at
    • updated_at

Every write action follows the same mutate-then-broadcast shape: push the record into c.state, bump updated_at, broadcast the typed event, return the record. See Actions and Events.

Lifecycle

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

In the example, the "authenticate" step is a free-text company picker. The rest of the flow matches the diagram: createState seeds the dataset on first creation, the dashboard loads with listEmployees, listProjects, and getStats, and every connected client of the same tenant receives employeeAdded and projectAdded events.

Security Checklist

The example ships with none of these. Apply all of them before production.

  • Tenant identity: Derive the tenant id from a verified JWT claim, never from a client-supplied string.
  • Connection validation: In onBeforeConnect or createConnState, verify the credential's tenant claim matches c.key and reject mismatches.
  • Per-action authorization: Check the caller's role before mutating actions (addEmployee, addProject), not just at connect time. See Access Control.
  • Input validation: Clamp name and role lengths and validate enums. The example only trims input and substitutes fallback defaults.
  • Key construction: Always pass the tenant id as an array element (key: [tenantId]). Never interpolate tenant ids into key strings, and never build keys from one tenant's input to address another tenant's actor.
  • Growth limits: As a recommended extension, cap or paginate the employees and projects arrays. The example lets them grow unboundedly in JSON state; move to SQLite when the dataset outgrows memory.

Reference Map

Actors

Cli

Clients

Cookbook

Deploy

General

Self Hosting

rivet-dev의 다른 스킬

ai-agent
rivet-dev
지속적 메모리를 갖춘 AI 에이전트 백엔드 구축: 대화당 하나의 Rivet Actor, 대기열 메시지 처리, 실시간 이벤트로 스트리밍되는 LLM 응답.
official
ai-agent-workspace
rivet-dev
모든 AI 에이전트에게 자신만의 컴퓨터를 부여하세요: 경량 인프로세스 상에서 파일 시스템, 프로세스, 셸, 네트워킹 및 에이전트 세션을 갖춘 영구 작업 공간입니다…
official
chat-room
rivet-dev
Rivet Actors로 실시간 채팅방 백엔드 구축: 방마다 하나의 액터, SQLite 기반 메시지 기록, 모든 연결된 클라이언트에 WebSocket 브로드캐스트.
official
collaborative-text-editor
rivet-dev
Yjs CRDT와 Rivet Actors를 사용하여 협업 텍스트 편집기 백엔드를 구축합니다: 문서별 액터가 동기화 및 인식 업데이트를 중계하고 스냅샷을 유지합니다.
official
cron-jobs
rivet-dev
Rivet Actors를 사용한 내구성 있는 크론 작업: schedule.after 및 schedule.at 타이머는 재시작과 충돌에도 유지되며, 반복 작업 재설정 및 멱등성 핸들러를 지원합니다.
official
live-cursors
rivet-dev
Rivet Actors를 사용한 라이브 커서 및 멀티플레이어 프레즌스: 연결별 커서 상태, 이벤트 또는 원시 WebSocket을 통한 실시간 업데이트, 스로틀링.
official
rivetkit-client-javascript
rivet-dev
JavaScript 클라이언트로, 무상태 또는 상태 저장 연결을 통해 Rivet Actors에 연결합니다. 브라우저, Node.js 및 Bun 환경을 지원하며, 환경 변수 또는 명시적 구성을 통한 자동 엔드포인트 감지 기능을 제공합니다. 독립적인 요청을 위한 무상태 액션 호출과 실시간 이벤트 구독이 가능한 상태 저장 연결의 두 가지 상호작용 모드를 제공합니다. onRequest 또는 onWebSocket 핸들러를 구현하는 액터를 위한 저수준 HTTP 및 WebSocket 액세스를 포함하며, 복합 배열 기반...
official
rivetkit-client-react
rivet-dev
React 클라이언트로, 훅과 실시간 상태 관리를 통해 Rivet Actors에 연결합니다. createRivetKit()으로 타입이 지정된 훅을 생성하고, useActor()로 키와 선택적 매개변수를 사용하여 액터 인스턴스에 연결합니다. useEvent()로 액터 이벤트를 구독하고, connStatus와 error 상태를 통해 연결 수명 주기를 모니터링합니다. createClient()를 사용하여 상태 비저장 단일 호출, 액터 검색 메서드(get, getOrCreate, create, getForId), 그리고 저수준 HTTP/WebSocket 액세스를 수행합니다. 복합 배열 키를 지원합니다...
official