Multi-Tenant 架构总览(从这里开始)
这份是 multi-tenant 这套文档的入口。读完能回答 3 件事:为什么要补 tenant 这一层 / 现在打算怎么做 / 以后会演进成什么样。
一句话讲清楚问题
retaintive 现在的数据库里只有"店"这一层(store_id),没有"客户"这一层。
健身房单店没事。客户复杂起来立刻分裂:
Glow Beauty 公司开了 3 家店(SoHo / Brooklyn / Queens)。总部老板的 Stripe 信用卡 / SSO 配置 / 合同 tier 应该挂在哪? 现在只能复制 3 份到每家店,改 1 个字段要同步 3 次,还可能漏。
业界把"谁付钱的"那层叫 tenant(租户)。Slack 叫 workspace,Linear 叫 workspace,Salesforce 叫 Account — 名字不一样,概念是同一个。我们要补的就是这层。
这套设计分两步走,不是一次性大重构
V1 上线方案用人话讲
3 件事 V1 实际做了(identity-only,完整决策说明见 why-tenant.md):
- 加 3 张核心表(客户档案
tenants/ 用户跟客户关系tenant_members/ 客户名下有哪些店tenant_store_mappings),放在独立的 control plane Neon project(retaintive-identity),另有 1 张过渡期兼容表tenant_store_overrides - 加 1 个解析层(
callytics-common/src/control-plane/client/routing.ts)— 回答"这个 user 属于哪个客户 / 这家店属于哪个客户" - 业务表加 NULLABLE
tenant_id列(test 已 apply,prod 待铺,见v1-gaps.md)
V1 不做(留给 Final;其中前 3 条是最初 6 表方案的一部分,未按原样落地,旧方案已归档到 docs/archive/):
DB routing(getDbForTenant/tenant_database_placements)— Silo/Pool 分库时才需要provider 归属表(tenant_provider_connections)+onboarding job ledger+SQS webhook routing把— 这是 4 repo lockstep,3-4 周工作量rc_stores表改名叫locations业务表— Hybrid 模式才必须(下方解释)store_id改名location_id每个客户单独一个 Neon 数据库— V1 全部留在当前 shared DB,叫pool_orange_theory
业务表加列 vs 改名 — 两个独立动作
这是新人最容易搞混的地方,先讲清楚再往下读。
store_id 单字段同时背了两个职责 — "哪家店"(业务划分)+ "哪个客户"(隔离)。Final 阶段用 2 个独立 ALTER 解决,时机不同(不是 1 个 SPLIT 把单字段拆成 2 字段,是 2 件独立小事):
判定标准 1 句话:只要我们还是"每个客户一个独立数据库"(Silo),业务表的 store_id 单字段够用。一旦有 2 个以上客户共享同一个 Neon 数据库,必须有 tenant_id 列才能做 RLS 隔离。
→ 详细工程拆分见 final/migration-plan.md 阶段 1-4。
读完这份文档能回答的 8 个问题(分 3 类):
- 生命周期(客户自己变化):扩张 1→N、缩小 N→1、搬家 Pool↔Silo、tier 升降、改付款人、改 vertical、M&A 合并
- 平台接入(外部对接):3 方 CPaaS、BPO 转售
- 2026 潮流(新增能力):对外 API、AI 介入、Hybrid 部署、Dashboard 老板视角(聚合 / 单店 / 对比)
一、几个核心词,先讲人话
读后面之前,先把这几个词搞清楚。所有章节都假设你懂下面这几个词。
关键认知: tenant 是设计层的概念,UI 上可以叫 "Account" / "Organization" / "Workspace" 都行。但代码 / DB / API 内部统一叫 tenant。
二、为什么要改
跨店看数据已经能做,不是 UX 问题。真问题在架构里少了一层:只有"店",没有"客户"。
store_id 一个字段背了两件事 — 哪家店 + 属于谁付钱的。客户复杂起来 (3 店 1 老板 / 10 个 call center 1 公司 / 跨行业) 立刻分裂出 4 个痛点:
业界把"谁付钱的"那层叫 tenant (租户)。我们要补的就是这层。
franchise_id 字段不是 legacy,是降级 — 终态当"品牌标签"用 (聚合分析 OrangeTheory 全美加盟商整体表现),不再当隔离。详见 final/migration-plan.md §六 franchise 归位。
三、现状 → 终态:一张图看完
读图前先把两个时间点分清楚:
下面这张表讲的是 Final 业务模型,不是 V1 上线清单。V1 只先落地其中的 Control Plane 和 routing 部分。
按层级排序 — 顶层 (客户) 在上,字段拆分在中,降级字段在底。最后一列用 Glow Beauty (3 店连锁) 套一遍,看完表就知道实际数据长啥样:
1 个客户 → N 家店是弹性的,不动 schema:
- Sarah 单店 → 1 客户,1 店 (Devon)
- Glow Beauty → 1 客户,3 店 (SoHo / Brooklyn / Queens)
- United Airlines → 1 客户,10 店 (10 个 call center site)
关键:Glow Beauty 公司本身(客户)和 SoHo 这家店(店)以前混在 store_id 一个字段里,现在分开。谁付钱 / SSO / 合同等级 挂在客户层,电话 / 员工 / 营业时间 挂在店层。
现状一句话:单层 store_id 隔离 + 只接 RingCentral + 没接 Stripe + 没有客户层概念。详细 schema 数字 (22 张表分类 / 67 文件) → 见 final/migration-plan.md §四 现状。
终态:
这张图里的 tenants 不只是某个业务 DB 里的普通表。终态会有一个中心的 Control Plane 管这些平台级数据:tenant 是谁、谁能访问、Stripe / SSO / custom domain 绑定到谁、每个 tenant 的数据在 Pool 还是 Silo。业务数据本身放在 Data Plane,也就是 Pool DB 或 tenant 专属 Silo DB / Neon branch。
V1 实际先上线的是这套终态里的身份层(identity-only):Control Plane 建了 3 张表(tenants / tenant_members / tenant_store_mappings),DB placement / provider connections / provisioning jobs 推迟;Orange Theory 现有数据先保留在 pool_orange_theory,不急着全量 rename 业务表(rc_stores → locations 等)。业务表的 NULLABLE tenant_id 列已在 schema 加好(prod migration 待铺,见 v1-gaps.md)。
下面所有章节都在解释这张图的细节。
四、3 个真实客户怎么映射
举例用 3 个虚构客户贯穿全文。下面是 Final 模型的概念例子;V1 里的 Orange Theory tenants 可以先映射到 pool_orange_theory,并通过 tenant_store_mappings 约束能访问哪些 current rc_stores。
客户 A:Sarah — 单店健身房加盟商
Sarah 的查询: WHERE tenant_id = 'A' — 因为只有 1 个 location,聚合视图 = 单店视图。
客户 B:Glow Beauty Corp — 3 店连锁
Glow 老板的 3 种 dashboard 视图都能轻松实现:
痛点解决了: 老板 1 次登录,聚合 / 切店 / 对比都通。
客户 C:United Airlines — 10 个 call center
United 的数据物理上不在主 Neon DB,在专属 Neon project proj-united。代码读 tenants.placement 字段路由过去。
五、Pool / Silo / Hybrid 是啥 (初学者第一图)
新人最容易困惑的地方。用最简单的图建立直觉。
先记住当前决策:这节是概念解释,不是 V1 默认方案。V1 是 pool_orange_theory + Control Plane routing;Final default 可以继续 Silo-first / all-Silo。Pool/Hybrid 只有当低价客户很多、free trial 很多、或 branch-per-tenant 的 compute / migration / secret / monitoring 成本开始吃掉毛利时才考虑。
怎么选:
- 当前 Orange Theory:保留在
pool_orange_theory,但通过 Control Plane 明确 tenant / store / placement 边界。 - 高价值客户或合规客户:可以 promote 到 Silo branch/project。
- 大量低价 self-serve / free trial:以后才考虑成熟 Pool / Hybrid,用
tenant_id/ RLS 做共享 DB 隔离。
Silo-first 的硬前提:必须把 onboarding / migration / monitoring 自动化。手工管 5 个客户的 Neon project 还行,10 个就 human error 频出。详细需要自动化的 7 件事(Stripe / Neon provision / secret / migration fan-out / smoke test / provider rebind / churn delete)见
best-practices.md§Silo 自动化必备的 7 件事。第 6 个 Silo 客户来之前 4 件必须做完,第 10 个之前 7 件全做完。
六、Hybrid 的真实形态:不是 DB 二选一,是 3 面分别决定
上面那张图是直觉。但真实的 Hybrid 不是"DB 二选一" — 它是 3 个面 (数据 / 接入 / 运维) 各自决定 Pool 或 Silo。
3 个关键认知:
- Hybrid 不是开关,是每个面逐项决定。
- Platinum tier ≠ 全部独立。AWS Account 通常仍共享 (太贵),Neon / 监控才必须独立。
tenants.placement单字段不够 — 真实落地是placement_profile(预定义组合)。
→ 完整 8 维度细节 (Cognito / SAML / DynamoDB / S3 prefix / Rate limit / sub-account / namespace / audit 流) 和 placement_profile 字段设计 → 见 best-practices.md。
七、9 个常见生命周期变化
每个客户的真实变化我们怎么处理。80% 是改 1 个字段,只有 1 个真贵。
第 7 项 promote-to-silo 是最贵的:开独立 Neon branch/project / 历史数据回填 / 切读 / 切写 / 校验。早期可以用 maintenance window;要做到无停机时才需要双写期和 reconciliation。
→ 完整工程细节 + 函数实现 + GDPR delete → 见 final/migration-plan.md 的 long-term migration sections。
八、Dashboard:老板想"聚合看 / 分开看 / 对比看"
Glow Beauty 老板 (3 店) 想看 3 种视图:今天 3 店一共多少电话 / SoHo 店本周怎样 / 3 店对比谁好。
终态:1 次登录,顶部切换器选视角,后端自动按 tenant_id 或 location_id 过滤。
3 种视图怎么查:
→ 底层数据带"客户 + 店"两层维度,前端切换器决定怎么过滤。1 套数据服务 3 种视角(对比 二、为什么要改 痛点 3 的"写 2 套代码"问题)。
九、Provider 怎么对接 — 我们用统一词,他们各家不同
我们要从 RingCentral / Twilio / Genesys / Five9 / Amazon Connect 等服务商拿电话数据。他们各家命名都不一样。
核心做法:我们不跟任何一家对齐。内部统一用 tenant / location / phone_line,中间加一层 adapter 翻译到各家。
怎么实现:加 2 张新表 —
- 凭证表:存 OAuth / API key,记哪个 tenant 接了哪家 provider
- 映射表:把我们的 location 映射到 provider 那边的 site / subaccount / queue
→ 同一个 location 可以同时挂多个 provider (例:United Tampa 用 Genesys 当主路由,Amazon Connect 当溢出备份)。
→ 完整表结构 DDL + adapter 接口设计 + 多 provider 数据合并策略 → 见 best-practices.md §六。
3 个客户怎么映射:
- Sarah 用 RingCentral → tenant 下 1 个 location (Devon),location 挂到 RC 的某个 site,拉 5 个电话
- Glow Beauty 用 RingCentral → tenant 下 3 个 location,各自挂到 RC 的 3 个 site,共拉 9 个电话
- United 用 Genesys + Amazon Connect 混搭 → tenant 下 10 个 location,每个 location 同时挂 2 个 provider (主备切换),拉 ~5000 个电话
5 大主流 CPaaS 命名速查
核心:所有家顶层用的都是中性词 (Account / Organization / Domain / Instance)。没有任何一家用业务词 (store / merchant)。我们用 tenant 跟业界对齐。
前提:别把 provider 的词漏到主表。现在 franchise_id 也是历史包袱要清 (见 §二)。
十、BPO 是啥?未来场景
BPO = Business Process Outsourcing,call center 外包公司。我们暂时不做,但设计上要预留。
所以 tenant_clients 是: BPO 客户自己内部的细分,我们给他们一个业务字段支持 (contacts.client_id),不是隔离边界。
如果不做 BPO 这门生意: tenant_clients 表永远是空,Sarah / Glow / United 完全不用,留 NULL。
十一、2026 潮流 — API + AI 能撑住吗?
未来想做:
- 公开 API:让客户自己开发集成 (类似 Stripe 给开发者用的 API)
- AI 介入:客户的 AI Agent (Claude / GPT 等) 通过 MCP 直接调用我们的能力
这套设计专门为 API + AI 准备 (见 §三 final state 图右下角的"对外"层)。
API platform 怎么挂
每个客户拿到 1 个 API key,所有 endpoint 自动 scope 到该 tenant (拿不到别人的数据)。
→ 核心:API 看到的 tenant 就是数据库里的 tenant,不需要重新设计。Stripe / Twilio / Slack 都是这套。
AI / MCP 怎么挂
客户的 AI Agent 通过 MCP 协议连过来,调用我们提供的 tool (比如 "搜联系人 / 看通话纪要 / 创建任务")。
→ 核心:AI 想越界访问别人 tenant 也做不到 — 因为 tenant 参数它根本碰不到,是 server 强制塞进去的。
为什么 tenant 命名对 AI 友好:AI 训练数据里 Stripe / Slack / Salesforce 都用 tenant 概念,AI 一看就懂。如果叫 store_id,AI 还得猜"是零售 store 还是 SaaS 用语"。
→ 2026 潮流:中性词 = AI 友好,业务词 = AI 困惑。
→ 完整 MCP 工具列表 + tenant 注入实现细节 → 见 best-practices.md §七。
十二、5 条硬规则 + 8 治理项 (速查)
5 条硬规则 (不可破)
- 隔离: 跨 tenant 数据绝对不串。同 tenant 内跨 location 允许互看。
- 计费: 默认 1 tenant = 1 Stripe Customer;V1 可 manual billing,但 Stripe 字段要预留。
- BPO: BPO 公司是 tenant,end-clients 是
tenant_clients(业务字段,不是隔离层)。 - 扩张: tenant ↔ location 是 1:N,N 改字段不动 schema。
- Placement ↔ 部署: 部署位置由 Control Plane 的 placement 决定。V1 默认
pool_orange_theory;Final default 可以 Silo-first;Pool/Hybrid 只是 future option。