为什么 retaintive 需要 Tenant:从 user-store 到 customer boundary 的决策说明
当前 rc_stores.user_id + store_members 是一个好的 store access model,它回答"某个 user 能不能访问某家 store"。但它不是 customer ownership model,回答不了"这些 stores 共同属于哪个客户/公司/付费主体"。当系统开始支持一个客户多家店、多个用户、一个账单、多 RingCentral accounts、套餐等级、quota、owner transfer 和跨店 dashboard 时,tenant 就是分水岭。
V1 是 IDENTITY ONLY,已落地的是 control plane(独立 Neon project retaintive-identity)的三张表:tenants / tenant_members / tenant_store_mappings(外加过渡期兼容表 tenant_store_overrides)。provider connections、DB placement、provisioning job ledger 全部 DEFERRED。
旧文档描述的"6 张表 + getDbForTenant + SQS webhook"方案没有按原样落地,已归档。schema 细节以 callytics-common/src/control-plane/schema/tenants.ts 为准,本文不复制 DDL。
当前铺开进度和已知缺口见 v1-gaps.md — 目前还没正式上 prod(pre-launch 阶段),control plane 只对 test 数据面生效;设计承诺和机制现状之间的差距(fallback 退役、对账、status 语义等)都在那份文档里,读本文时请配对读。
一、这次讨论最终在回答什么
行业共识:B2B SaaS 的标准分层
有 tenant 这一层不是 retaintive 自己发明的复杂化,而是 B2B SaaS 的主流建模方式。成熟系统通常把 User(登录身份)、Tenant/Organization(客户主体)、Membership(某个 user 在某个客户里的角色)、Resource/Location(客户拥有的业务资源)分开。这样才能把 billing、quota、SSO、team role、audit 和数据隔离放在稳定的客户边界上。
外部产品命名不同,但模式相似:Auth0 叫 Organizations,WorkOS 叫 Organizations,AWS SaaS Lens 讨论 tenant isolation,Stripe 的 Customer 是付款侧客户对象。我们的 tenant 是产品/数据模型里的客户主体,不等于 RingCentral account,也不等于 Stripe Customer,但会和它们建立映射。
参考官方资料:AWS SaaS Lens: Tenant isolation、Auth0 Organizations、WorkOS Organizations、Stripe Customer object。
二、当前模型:user-store access model
当前 schema 的强项是:从登录 user 出发,算出这个 user 能访问哪些 stores。它不是差的设计,只是抽象层级停在 store access。
三、真正的分水岭
系统里是否存在一个业务对象,它不是某个 user,也不是某家 store,但它拥有多家 stores、多个 users、一个账单、一个套餐、一组 quota 和一套客户级配置。如果存在,这个对象就是 tenant。
四、没有 tenant 时,复杂度会散落到 user/store/provider
这个模型的关键问题不是"查不出数据",而是公司主体没有一行稳定记录。payer、tier、quota、provider ownership、owner transfer、company audit 都只能靠 user/store/provider 的组合推断。
五、目标模型:tenant 是客户主体,store 是业务地点
各层类型的用法 — 每一层只回答一个问题:
六、Billing、Plan Tier 和 Quota 为什么必须按 tenant 算
七、Provider/RingCentral 关系:不要画成两个 source of truth
正确语义:RingCentral 是 provider topology 和 provisioning input;tenant_store_mappings 是我们内部的 customer ownership/index。它们不是两套并列真相。
八、权限模型:老板看全部,manager 看部分
Owner/admin 的默认边界是 tenant 下所有 stores;viewer/manager 类用户再用 store_members 缩小到指定 stores。未来如果需要"区域"这个业务对象,再加 store_groups 或 capability,不要把区域经理升成 tenant owner。
注意两个容易混的命名:tenant 层的 viewer(客户层角色,默认不带任何业务数据可见范围)和 store 层的 VIEWER(store_members 里对某家店的只读授权)是两个不同的东西,详见 v1-gaps.md 的命名对照。
九、如何从当前状态 establish tenant
各 step 的实际铺开进度(哪些做完了、哪些卡在半路)见 v1-gaps.md。
十、最终给团队的一句话
不要把 user 当公司。user_id 是登录身份,会离职、换邮箱、被禁用、成为多个角色;store_id 是业务地点,会新增、关闭、重组;provider_account_id 是外部连接,会重连、换供应商。tenant_id 是稳定客户主体,承载 billing、quota、tier、SSO、cross-store dashboard、audit 和 future isolation;provider ownership 可以在 future 通过显式 mapping 接进来。
十一、未来演进:只加,不重做
按本文设计演进,control plane 基本只加不重做;data plane 不需要重新设计,但 Final 阶段有一次"改名 + 收紧约束"的工程。分三层:
第一层:纯加(deferred 的 3 张表 + 客户级配置)
原 6 表方案里没落地的 3 张表,终态仍然需要,各有明确触发条件——触发前不建(空转纯开销),触发后加表挂在 tenant_id 上即可,不动现有表:
加回来时不要照抄归档 DDL(
docs/archive/multi-tenant-v1-plan/rollout.md只作起点参考),按当时现实重推。
第二层:要改,但是改名/收紧,不是重新设计(即 final/migration-plan.md 的 Final 阶段)
第三层:唯一一个设计时标注的 revisit 点
tenant_members 上"一个 user 只属一个 active tenant"的 partial unique index。将来出现 agency/BPO 代管多客户时放开——tenants.ts 注释原话:"THIS index is the one decision to revisit"。也只是删一个索引,不是重做表。
这个"只加不重做"的保证来自三个设计选择:① 归属放 tenant_store_mappings 独立行而不是塞列进 rc_stores(改归属模型 = 改 mapping,业务数据不动);② tenant_id 列提前加、启用推迟,两个动作解耦;③ 改名和拆职责是两件独立小事,不是一次 SPLIT。
"只加不重做"成立的前提是 v1-gaps.md 缺口 2-4 的机制先补上。fallback 不退役、mapping 不对账,rc_stores.user_id 的旧语义就一直赖着,Final 阶段就要同时处理"改名 + 退役 + 补账"三件事,工程量不再是改名那么简单。
Source of Truth
callytics-common/src/control-plane/schema/tenants.ts:live schema 唯一来源。V1 是 IDENTITY ONLY:tenants、tenant_members、tenant_store_mappings 已落地;provider connections / DB placement / provisioning 仍是 DEFERRED。callytics-common/src/control-plane/client/routing.ts:store→tenant / user→tenant 的解析逻辑(mapping 优先,过渡期保留 derived fallback)。callytics-common/src/db/schema/rc-stores.ts:当前 store identity、owner user、provider account、phone mapping。callytics-common/src/db/schema/user-stores.ts:当前store_members是 store-level shared access,不存 OWNER。callytics-common/src/db/schema/user-connections.ts:当前 user 到 RingCentral OAuth account 的连接。overview.md:multi-tenant 文档集入口 — tenant 是付费实体,location/store 是 tenant 内业务单位。v1-gaps.md:V1 已知缺口与补齐路线(生产实测证据)。