组织、团队与租户隔离
决定客户如何协作,并调整默认的组织角色、邀请、席位上限与租户边界。
已根据 starter 提交
2a1a04a验证。
先回答产品问题,再回答代码问题:你的 SaaS 是卖给个人、共享工作区,还是需要一人管理多个工作区?Sushi SaaS 默认采用“可成长的单人团队”模型。每位客户从个人组织开始,所有可计费资源都属于该组织。
默认协作模型
| 行为 | 默认值 | 以后可选的扩展 |
|---|---|---|
| 资源所有权 | 组织内共享 | 通过 can() 增加创建者限制或逐资源共享 |
| 角色 | owner、admin、member | 增加产品特有角色或 Better Auth 动态权限 |
| 计费 | 每个组织固定价格 | 增加席位数量与经过测试的 prorate 策略 |
| 导航 | 使用 ?org=<slug> 的工作区切换器 | 如果 URL 需要更强层级,再增加带组织的路径段 |
| 团队 | 组织成员与邀请 | 只有产品确实需要子组时才启用嵌套团队 |
每个账号都有组织
注册会创建个人组织及 owner 成员关系。系统没有另一套“用户拥有资源”的路径:文件、任务、订单、订阅、积分和预约都使用 org_uuid。
Better Auth 提供成员表与邀请机制;Sushi 补充租户范围、应用权限、席位限制、共享余额和并发保护。
角色与能力
角色层级为 owner ⊃ admin ⊃ member。
| 操作 | Member | Admin | Owner |
|---|---|---|---|
| 读取组织数据、创建/删除文件、消费积分 | ✓ | ✓ | ✓ |
| 邀请、移除成员、修改角色 | ✓ | ✓ | |
| 更新组织 | ✓ | ✓ | |
| 管理计费或删除组织 | ✓ |
使用 getOrgContext(request, optionalSlug) 获取用户、组织 UUID 和角色,再用 can(ctx, action, resource) 进行授权。绝不从请求 body 接受 org_uuid。
租户隔离
只有 model 查询数据库。所有租户表查询都使用 scopedToOrg(column, ctx.orgUuid),插入必须写入组织 UUID。架构测试会扫描这些规则。
应用使用 organizations.uuid;Better Auth 的 organizations.id 仅供其内部关系使用。带品牌的 OrgUuid 类型防止误传用户 UUID。
邀请与席位上限
邀请 72 小时后过期,只能由被邀请邮箱接受;接受与拒绝使用独立端点。重新邀请会替换旧的待处理邀请。
| 方案 | 总席位 |
|---|---|
| Free | 1 |
| Plus | 5 |
| Max | 20 |
发送和接受时都会检查“已加入成员 + 有效待处理邀请”。两个检查共享组织级 PostgreSQL advisory lock,避免两个并发请求同时占用最后一个席位。管理员后台可设置带审计记录的临时或永久例外。
安全不变量
- 组织不能失去最后一个 owner。
- 用户不能离开自己的唯一组织。
- 成员不能授予高于自己的角色。
- 降级不会移除现有成员,只会阻止新的邀请与接受。
- 积分和 Stripe Customer 属于组织,而不是点击结账的成员。
用户界面位于 /{locale}/account/team,API 位于 /api/account/team/*。组织支持操作位于独立管理应用中。
starter 已提供基于查询参数的工作区切换器,但暂未提供组织 slug 路由、自定义角色、嵌套团队或 Stripe 按席位数量计费;这些是需要产品策略的扩展。
修改该模型前,请写清楚客户数据归谁所有、谁能邀请或移除成员、普通成员能否删除共享内容以及由谁付款。当两个组织无法读取彼此的数据、最后一个 owner 无法被移除、并发邀请无法超过最后席位,且降级会保留现有数据和成员时,协作模型才算验证完成。
相关内容: 用套餐与权益设置席位上限,并通过 Sushi SaaS 的代码结构判断组织优先的数据归属是否适合你的产品。