关于 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,以公司名称为键。切换租户时会切换整个数据集。 |
| 状态 | JSON Actor 状态中包含 employees 和 projects 数组及时间戳。不使用 SQLite、队列或调度。 |
| 实时更新 | 每个写入操作都会修改状态,然后向该租户的所有已连接客户端广播类型化事件(employeeAdded、projectAdded)。 |
| 身份验证 | 无。登录界面仅用于展示。生产环境的指导见安全检查清单。 |
隔离模型
Actor 键就是租户 ID。客户端通过 useActor({ name: "companyDatabase", key: [companyName] }) 连接,Actor 在 createState 中读取 c.key[0],以初始化该租户的数据集。这带来以下优势:
- 每个租户一个 Actor:
companyDatabase[tenantId]精确对应一个 Actor 实例。两个租户绝不会共用同一个 Actor。 - 每个租户一个数据集:所有读写都通过该 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 })会在 Actor 开始处理流量前,在 SQLite 保存点内运行你的迁移 SQL。如果onMigrate抛出错误,所有迁移 SQL 都会原子性回滚,Actor 也不会启动。请参阅 SQLite。 - Drizzle:
drizzle-kit根据你的类型化架构生成迁移文件,db({ schema, migrations })会在 Actor 唤醒时应用这些迁移。请参阅 SQLite + Drizzle。
由于每个租户都有自己的数据库,迁移会随着各租户的 Actor 唤醒而逐个推出,而不是针对共享数据库一次性执行大型迁移。
租户 ID 必须来自身份验证
示例中的登录只是表面流程:客户端可以选择任意公司字符串,该字符串会成为 Actor 键,因此任何访客都可以读取和写入任何租户的数据。请勿将其用于正式环境。以下是正式环境必须实现的扩展(示例中未实现):
- 从已验证的凭据(例如 JWT 声明)中获取租户 ID,绝不能使用用户输入。
- 在
onBeforeConnect(判断通过或失败)或createConnState(将已验证的用户存入连接状态)中,依据c.key验证凭据。请参阅身份验证和连接。 - 在连接级身份验证之外,为每个操作添加权限检查。请参阅访问控制。
Actors
- 键:
companyDatabase[companyName](单元素数组键;c.key[0]是公司名称) - 职责:每个租户一个 Actor。将该公司的员工和项目保存在持久化状态中,通过操作提供读写服务,并向已连接的客户端广播变更。
- 操作
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 事件。
安全检查清单
示例中没有实现以下任何措施。正式部署前请全部落实。
- 租户身份:从已验证的 JWT 声明中获取租户 ID,绝不能使用客户端提供的字符串。
- 连接验证:在
onBeforeConnect或createConnState中,验证凭据中的租户声明是否与c.key匹配,并拒绝不匹配的连接。 - 逐操作授权:在执行变更操作(
addEmployee、addProject)前检查调用者的角色,而不只是在连接时检查。请参阅访问控制。 - 输入验证:限制名称和角色的长度,并验证枚举值。示例只会清理输入两端的空白,并使用备用默认值。
- 键构造:始终将租户 ID 作为数组元素传入(
key: [tenantId])。绝不要将租户 ID 插入键字符串,也不要使用一个租户的输入构造键来访问另一个租户的 Actor。 - 增长限制:建议扩展此示例,对
employees和projects数组设置上限或分页。示例中的 JSON 状态允许数组无限增长;当数据集超出内存容量时,请迁移到 SQLite。
参考索引
Actors
- 访问控制
- 操作
- Actor 密钥
- Actor 运行时套接字
- Actor 状态
- 身份验证
- Cloudflare Workers 快速入门
- Actor 之间的通信
- 连接
- 自定义检查器选项卡
- 调试
- 设计模式
- 销毁 Actor
- Effect.ts 快速入门(Beta)
- 错误
- Fetch 和 WebSocket 处理器
- 辅助类型
- 图标与名称
- 内存中状态
- 输入参数
- 生命周期
- 限制
- 底层 HTTP 请求处理器
- 底层 KV 存储
- 底层 WebSocket 处理器
- 元数据
- Next.js 快速入门
- Node.js 与 Bun 快速入门
- 队列与运行循环
- React 快速入门
- 实时功能
- Rust 快速入门(Beta)
- 扩展与并发
- 计划任务与 Cron
- 共享与加入状态
- SQLite
- SQLite + Drizzle
- Supabase Functions 快速入门
- 测试
- 故障排除
- 类型
- 原生 HTTP API
- 版本与升级
- 工作流
命令行工具
客户端
教程集
部署
- 容器运行器
- 部署到 Amazon Web Services Lambda
- 部署到 AWS ECS
- 部署到 Cloudflare Workers
- 部署到 Freestyle
- 部署到 Google Cloud Run
- 部署到 Hetzner
- 部署到 Kubernetes
- 部署到 Railway
- 部署到 Rivet Compute
- 部署到 Supabase Functions
- 部署到 Vercel
- 部署到虚拟机和裸金属服务器
通用
- Actor 配置
- 跨源资源共享
- 面向大模型和 AI 的文档
- 边缘网络
- 端点
- 环境变量
- HTTP 服务端
- 日志记录
- 池配置
- 生产环境检查清单
- Registry 配置
- 运行时模式
- WASM 与原生 SDK