Database per Tenant
IMPORTANT: Before doing anything, you MUST read 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
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.
| Topic | Summary |
|---|
| Isolation | One actor per tenant, keyed by company name. Switching tenants swaps the entire dataset. |
| State | JSON actor state holding and arrays plus timestamps. No SQLite, no queues, no scheduling. |
| Realtime | Every write action mutates state, then broadcasts a typed event (, ) to all connected clients of that tenant. |
| Auth | None. 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
in
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 column to filter incorrectly. Cross-tenant leaks require constructing the wrong key, not forgetting a clause.
- No key injection: Keys are arrays, not interpolated strings. cannot be escaped the way 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.
| Backend | Use When | Docs | Working Code |
|---|
| JSON actor state | Small datasets, simple reads, whole dataset fits comfortably in memory. What the example uses. | State | GitHub |
| Actor SQLite () | Tables, indexes, SQL queries, larger-than-memory data, per-tenant relational schema. | SQLite | GitHub |
| SQLite + Drizzle | Typed schema, query builder, and generated migration files on top of actor SQLite. | SQLite + Drizzle | GitHub |
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: runs your migration SQL inside a SQLite savepoint before the actor serves traffic. If throws, all migration SQL rolls back atomically and the actor does not start. See SQLite.
- Drizzle: 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 in (pass/fail) or (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; 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
- Queues
- Events
- State
Every write action follows the same mutate-then-broadcast shape: push the record into
, bump
, broadcast the typed event, return the record. See
Actions and
Events.
Lifecycle
mermaid
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:
seeds the dataset on first creation, the dashboard loads with
,
, and
, and every connected client of the same tenant receives
and
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 or , verify the credential's tenant claim matches and reject mismatches.
- Per-action authorization: Check the caller's role before mutating actions (, ), 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 (). 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 and arrays. The example lets them grow unboundedly in JSON state; move to SQLite when the dataset outgrows memory.
Reference Map
Actors
- Access Control
- Actions
- Actor Keys
- Actor Scheduling
- Actor Statuses
- AI and User-Generated Rivet Actors
- Authentication
- Communicating Between Actors
- Connections
- Custom Inspector Tabs
- Debugging
- Design Patterns
- Destroying Actors
- Errors
- Fetch and WebSocket Handler
- Helper Types
- Icons & Names
- Input Parameters
- Lifecycle
- Limits
- Low-Level HTTP Request Handler
- Low-Level KV Storage
- Low-Level WebSocket Handler
- Metadata
- Next.js Quickstart
- Node.js & Bun Quickstart
- Queues & Run Loops
- React Quickstart
- Realtime
- Rust Quickstart (Preview)
- Sandbox Actor
- Scaling & Concurrency
- Sharing and Joining State
- SQLite
- SQLite + Drizzle
- State & Storage
- Testing
- Troubleshooting
- Types
- Vanilla HTTP API
- Versions & Upgrades
- Workflows
Agent Os
- Agent-to-Agent Communication
- agentOS vs Sandbox
- Authentication
- Benchmarks
- Configuration
- Core Package
- Cron Jobs
- Deployment
- Embedded LLM Gateway
- Events
- Filesystem
- Limitations
- LLM Credentials
- Multiplayer
- Networking & Previews
- Overview
- Permissions
- Persistence & Sleep
- Pi
- Processes & Shell
- Queues
- Quickstart
- Sandbox Mounting
- Security & Auth
- Security Model
- Sessions
- Software
- SQLite
- System Prompt
- Tools
- Webhooks
- Workflow Automation
Clients
- Node.js & Bun
- React
- Swift
- SwiftUI
Connect
- Deploy To Amazon Web Services Lambda
- Deploying to AWS ECS
- Deploying to Cloudflare Workers
- Deploying to Freestyle
- Deploying to Google Cloud Run
- Deploying to Hetzner
- Deploying to Kubernetes
- Deploying to Railway
- Deploying to Rivet Compute
- Deploying to Supabase Functions
- Deploying to Vercel
- Deploying to VMs & Bare Metal
Cookbook
- AI Agent
- AI Agent Workspaces
- Chat Room
- Collaborative Text Editor
- Cron Jobs and Scheduled Tasks
- Database per Tenant
- Deploying Rivet in a VPC or Air-Gapped Network
- Live Cursors and Presence
- Multiplayer Game
General
- Actor Configuration
- Architecture
- Cross-Origin Resource Sharing
- Documentation for LLMs & AI
- Edge Networking
- Endpoints
- Environment Variables
- HTTP Server
- Logging
- Pool Configuration
- Production Checklist
- Registry Configuration
- Runtime Modes
Self Hosting
- Configuration
- Docker Compose
- Docker Container
- File System
- FoundationDB (Enterprise)
- Installing Rivet Engine
- Kubernetes
- Multi-Region
- PostgreSQL
- Production Checklist
- Railway Deployment
- Render Deployment
- TLS & Certificates