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 上线方案(已落地 identity-only)加一层"客户档案"和"门店归属表",业务代码基本不动不改决策说明 why-tenant.md + 铺开进度与缺口 v1-gaps.md
Final 长期演进(以后做)把业务表里的 store_id 这种带"店"字的列改成中性的 location_id,加 Stripe / SSO / 多电话商支持4 个 repo lockstep 改 3-4 周final/migration-plan.md
通用方法论(架构师查)Pool / Silo / Hybrid / RLS / 自动化 / Stripe / 业界对照best-practices.md

V1 上线方案用人话讲

现在:
   登录 → user_id → 查询时直接 WHERE store_id IN (...)
                    没有"客户"这一层概念

V1 之后:
   登录 → user_id → 查 tenant_members 表 → 拿到 tenant_id(客户 ID)
                  → 查 tenant_store_mappings 表 → 拿到这个客户名下哪些 store
                  → 业务查询继续 WHERE store_id IN (...)
                  ↑ 业务 SQL 不变,但多了一层"客户边界"

3 件事 V1 实际做了(identity-only,完整决策说明见 why-tenant.md):

  1. 加 3 张核心表(客户档案 tenants / 用户跟客户关系 tenant_members / 客户名下有哪些店 tenant_store_mappings),放在独立的 control plane Neon project(retaintive-identity),另有 1 张过渡期兼容表 tenant_store_overrides
  2. 加 1 个解析层(callytics-common/src/control-plane/client/routing.ts)— 回答"这个 user 属于哪个客户 / 这家店属于哪个客户"
  3. 业务表加 NULLABLE tenant_id(test 已 apply,prod 待铺,见 v1-gaps.md)

V1 不做(留给 Final;其中前 3 条是最初 6 表方案的一部分,未按原样落地,旧方案已归档到 docs/archive/):

  1. DB routing(getDbForTenant / tenant_database_placements)— Silo/Pool 分库时才需要
  2. provider 归属表(tenant_provider_connections)+ onboarding job ledger + SQS webhook routing
  3. rc_stores 表改名叫 locations — 这是 4 repo lockstep,3-4 周工作量
  4. 业务表 store_id 改名 location_id — Hybrid 模式才必须(下方解释)
  5. 每个客户单独一个 Neon 数据库 — V1 全部留在当前 shared DB,叫 pool_orange_theory

业务表加列 vs 改名 — 两个独立动作

这是新人最容易搞混的地方,先讲清楚再往下读。

store_id 单字段同时背了两个职责 — "哪家店"(业务划分)+ "哪个客户"(隔离)。Final 阶段用 2 个独立 ALTER 解决,时机不同(不是 1 个 SPLIT 把单字段拆成 2 字段,是 2 件独立小事):

动作V1 做不做Final 触发
业务表加 tenant_id(NULLABLE,跟 store_id 并存)V1 就加 — 加列是 0 风险动作(不动现有 query),并且为未来 Pool/Hybrid 铺路。新写入数据 backfill 后 NOT NULL 仍是 Final 阶段做第 1 个 Hybrid / Pool 客户来时把 NULLABLE → NOT NULL,业务 query 改用 tenant_id 做隔离
store_id 改名为 location_id不做第 1 个客户要 Pool 共享 DB 时必须做(4 repo lockstep)
业务表全面用 tenant_id 替代 store_id 做隔离不做 — V1 仍用 store_id 隔离,只是字段提前加好第 1 个 Hybrid / Pool 客户来时必须做

判定标准 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 老板视角(聚合 / 单店 / 对比)

一、几个核心词,先讲人话

读后面之前,先把这几个词搞清楚。所有章节都假设你懂下面这几个词

人话retaintive 例子
tenant租户。1 个付费实体 = 1 个 tenant。隔离边界:跨 tenant 数据绝对不串。Glow Beauty 公司 = 1 tenant
locationtenant 内的物理执行单位 (一家店 / 一个 call center site)。同 tenant 内 location 之间允许互看。Glow 的 SoHo / Brooklyn / Queens = 3 个 location
phone_line具体电话号码,挂在 location 下。SoHo 店的 +1-555-1234
Pool多 tenant 共享同一个数据库。成熟 Pool 通常靠 tenant_id / RLS 隔离;V1 的 Orange Theory pool 先靠 Control Plane + tenant_store_mappings + current store_id 边界。pool_orange_theory = 当前 shared Neon DB
Silo1 个 tenant 独占一个数据库 (物理隔离)。贵但合规。United Airlines 单开一个 Neon project
Hybrid同一套系统里 Pool + Silo 共存。常见组合是低价 / trial 走 Pool,高价值 / 合规客户走 Silo。Optional future;当前 Final 可以保持 Silo-first / all-Silo default
Stripe CustomerStripe 那边的付款实体。默认 1 个 tenant 对应 1 个 Stripe Customer;V1 可先 manual billing,字段预留。Glow Beauty 公司 = 1 个 Stripe Customer
CPaaS电话能力服务商。Twilio / RingCentral / Genesys 这些。我们现在用 RingCentral
BPOcall center 外包公司,自己服务 N 个 end-client。TaskUs 这种公司 (我们暂时没接)

关键认知: tenant 是设计层的概念,UI 上可以叫 "Account" / "Organization" / "Workspace" 都行。但代码 / DB / API 内部统一叫 tenant


二、为什么要改

跨店看数据已经能做,不是 UX 问题。真问题在架构里少了一层:只有"店",没有"客户"。

store_id 一个字段背了两件事 — 哪家店 + 属于谁付钱的。客户复杂起来 (3 店 1 老板 / 10 个 call center 1 公司 / 跨行业) 立刻分裂出 4 个痛点:

#痛点一句话
1换行业进不来"店"装不下 call center / 跨 vertical / 总部
2客户级东西没地方挂Stripe / SSO / tier / 品牌只能复制 N 份到每家店
3跨店要写 2 套代码单店一套,聚合一套 (现在 dashboard-summary vs dashboard-multi-store)
4边缘场景扭曲1 电话挂多店变多对多,本该是客户级归属

业界把"谁付钱的"那层叫 tenant (租户)。我们要补的就是这层

franchise_id 字段不是 legacy,是降级 — 终态当"品牌标签"用 (聚合分析 OrangeTheory 全美加盟商整体表现),不再当隔离。详见 final/migration-plan.md §六 franchise 归位。


三、现状 → 终态:一张图看完

读图前先把两个时间点分清楚:

时间点schema 重点为什么这样做
V1(已落地 identity-only)新增 Control Plane 表:tenants / tenant_members / tenant_store_mappings(DB placement / provider connections / provisioning jobs 已 DEFERRED)。Data Plane 继续用当前 rc_stores / store_id少改业务代码,先把客户边界和权限做对;DB routing / webhook routing 推迟到有真实分库需求时。
Final把业务词汇中性化:rc_stores -> locationsrc_store_phones -> phone_linesstore_members -> location_members当第 1 个 Hybrid / Pool 客户来时(2 个以上 tenant 共享同一个 DB),业务表 store_id 改名为 location_id + 新加 tenant_id 列(2 个独立 ALTER,不是 SPLIT)。为非 Orange Theory、非 RingCentral、Stripe/SSO/API/AI 等长期平台能力铺路。

下面这张表讲的是 Final 业务模型,不是 V1 上线清单。V1 只先落地其中的 Control Plane 和 routing 部分。

按层级排序 — 顶层 (客户) 在上,字段拆分在中,降级字段在底。最后一列用 Glow Beauty (3 店连锁) 套一遍,看完表就知道实际数据长啥样:

终态 (新的)定义 (人话)现状 (原来)怎么变例子 (Glow Beauty)
客户表 (tenants)谁付钱给我们 / 合同 / 总公司主体没有这张表全新增Glow Beauty 公司 = 1 行
店表 (locations)一家实体店 / 一个 call center site / 一个工厂现在叫 rc_stores改名 (现在表名绑死了 RC,换中性词)SoHo / Brooklyn / Queens = 3 行
电话表 (phone_lines)具体一根电话号码现在叫 rc_store_phones改名 + 加字段 (以后要支持 Twilio 等其他厂商)SoHo 店的 3 个号码 = 3 行
数据表里的"客户"字段 (tenant_id)每条记录属于哪个客户 — 隔离边界,跨客户绝对不串数据表里的 store_id 字段同时背了"是哪家店" + "是哪个客户" 两件事把"客户"那部分职责单独拿出来一列Glow 所有联系人这一列都填 "Glow"
数据表里的"店"字段 (location_id)每条记录属于哪家店 — 业务划分,同客户内可看store_id 的另一半职责把"店"那部分职责也拿出来,独立成一列SoHo 店的联系人这一列填 "SoHo"
客户表里的"品牌"字段 (tenants.franchise)加盟品牌名 (跨客户做整体分析用)franchise_id 散在每张数据表里,每条记录都要填把所有数据表里的"品牌"列全部删掉,只在客户表里留一个Glow Beauty = null;OrangeTheory 加盟商 = "orangeTheory"

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 §四 现状。

终态:

╔═══════════════════════════════════════════════════════════════════╗
║                       Final State (2027)                          ║
╠═══════════════════════════════════════════════════════════════════╣
║                                                                   ║
║  ┌────────────┐         ┌──────────────┐        ┌──────────────┐ ║
║  │  Stripe 侧 │   ←→    │ retaintive 侧│   ←→   │ Provider 侧  │ ║
║  ├────────────┤         ├──────────────┤        ├──────────────┤ ║
║  │ Customer   │ ──────→ │  tenants     │        │ RingCentral  │ ║
║  │ Subscription│ webhook│   (合同主体) │        │ Twilio       │ ║
║  │ Price      │         │       │      │        │ Genesys      │ ║
║  └────────────┘         │       ↓ 1:N  │        │ Five9        │ ║
║                         │  locations   │ ←───── │ Amazon Connect│ ║
║                         │   (业务划分) │  sync  └──────────────┘ ║
║                         │       │      │            ↑            ║
║                         │       ↓ 1:N  │            │            ║
║                         │  phone_lines │            │ 这些都是   ║
║                         │  (具体号码)  │ ←──────────┘ 数据源     ║
║                         │              │   每个 provider 一套    ║
║                         │ (BPO 可选)   │   ID, 我们不在顶层用    ║
║                         │ tenant_clients│   他们的词              ║
║                         └──────────────┘                          ║
║                                                                   ║
║  对外:                                                            ║
║   • Public REST API (给客户 dev 自己集成)                         ║
║   • Webhook (我们 → 客户)                                         ║
║   • AI Agent SDK / MCP server (AI 调用)                           ║
║                                                                   ║
╚═══════════════════════════════════════════════════════════════════╝

这张图里的 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_storeslocations 等)。业务表的 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 (Cognito 账号)

   ├─ tenants: 1 行
   │    name="Sarah's OTF Devon"
   │    vertical="gym"
   │    franchise="orangeTheory"     ← 品牌字段保留
   │    tier="smb"
   │    placement="pool"
   │    billing_provider_customer_id="cus_AAA"

   ├─ locations: 1 行
   │    name="Devon"

   ├─ phone_lines: 5 行
   │    (5 个 RC 号码)

   └─ Stripe: Sarah 自己刷信用卡,月付

Sarah 的查询: WHERE tenant_id = 'A' — 因为只有 1 个 location,聚合视图 = 单店视图。

客户 B:Glow Beauty Corp — 3 店连锁

Glow 总部 (Cognito 账号)

   ├─ tenants: 1 行
   │    name="Glow Beauty Corp"
   │    vertical="beauty"
   │    franchise=null                ← 独立品牌,不是加盟体系
   │    tier="pro"
   │    placement="pool"
   │    billing_provider_customer_id="cus_BBB"

   ├─ locations: 3 行
   │    SoHo / Brooklyn / Queens

   ├─ phone_lines: 9 行 (每店 3 个)

   └─ Stripe: 总部 1 张卡付全部,月付

Glow 老板的 3 种 dashboard 视图都能轻松实现:

聚合视图     WHERE tenant_id = 'B'                          → 75 条 (3 店合计)
单店视图     WHERE tenant_id = 'B' AND location_id = 'soho' → 32 条
对比视图     WHERE tenant_id = 'B' GROUP BY location_id     → SoHo 32, BK 18, Queens 25

痛点解决了: 老板 1 次登录,聚合 / 切店 / 对比都通。

客户 C:United Airlines — 10 个 call center

United 公司合同

   ├─ tenants: 1 行
   │    name="United Airlines"
   │    vertical="airline"
   │    franchise=null
   │    tier="platinum"
   │    placement="silo"          ← 关键: 走独立 DB
   │    region="us-east-1+eu-west-1"
   │    billing_provider_customer_id="cus_CCC"

   ├─ locations: 10 行
   │    Tampa CC / SLC CC / CVG / DFW / ATL / SEA / 4 个 UK site

   ├─ phone_lines: ~5000 行
   │    每 site ~500 DID + tollfree

   └─ Stripe: 公司合同,invoice / annual contract

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 成本开始吃掉毛利时才考虑。

                   ┌──── Pool 区 (共享 Neon DB) ────┐
                   │                                  │
                   │  contacts 表                     │
                   │  ┌─────────────────────────────┐│
                   │  │ tenant_id │ phone │ name ...││
                   │  ├───────────┼───────┼─────────┤│
                   │  │ Sarah-A   │ +555  │ Anna    ││
                   │  │ Sarah-A   │ +666  │ Bob     ││
                   │  │ Glow-B    │ +777  │ Carol   ││  ← 都在一张表
                   │  │ Glow-B    │ +888  │ Dave    ││  ← 靠 tenant_id 隔离
                   │  │ ...       │ ...   │ ...     ││
                   │  └─────────────────────────────┘│
                   │  WHERE tenant_id = 'A' 只看 Sarah│
                   │  WHERE tenant_id = 'B' 只看 Glow │
                   │                                  │
                   │  成本: 共享 compute,小客户摊薄低  │
                   │  适合: 大量低价 / trial tenants   │
                   └──────────────────────────────────┘

                   ┌──── Silo 区 (独立 Neon project) ────┐
                   │                                       │
                   │  proj-united (United 专用)            │
                   │  ┌──────────────────────────────────┐│
                   │  │ contacts 表 (只有 United 数据)    ││
                   │  │ phone │ name │ ...                ││
                   │  │ +999  │ Eve  │ ...                ││  ← 物理隔离
                   │  └──────────────────────────────────┘│
                   │                                       │
                   │  成本: 独立 endpoint / secret / compute│
                   │        / migration / monitoring        │
                   │  适合: 高价值 / 合规 / enterprise      │
                   │  好处: 物理隔离 / 性能 / blast radius  │
                   └───────────────────────────────────────┘

                   Hybrid = 上面两个区同时存在
                           tenants.placement 字段决定走哪个

怎么选:

  • 当前 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。

                    Sarah (SMB)    Glow (Pro)     United (Platinum)
                  ──────────────  ──────────────  ──────────────────
数据面            共享 Neon DB    共享 Neon DB    ★ 独立 Neon project
  (DB / 存储)     共享 S3 前缀    共享 S3 前缀    ★ 独立 S3 bucket

接入面            共享 Cognito    共享 Cognito    ☆ 独立 SAML SSO
  (登录 / API)    共享 API host   共享 API host   ☆ 独立 endpoint

运维面            共享监控        共享监控        ★ 独立 CloudWatch
  (监控 / 审计)   共享 audit      共享 audit      ★ 独立 audit 流

  ★ = 必须独立 (合规硬要求)
  ☆ = 按需独立 (客户选,默认共享)

3 个关键认知:

  1. Hybrid 不是开关,是每个面逐项决定
  2. Platinum tier ≠ 全部独立。AWS Account 通常仍共享 (太贵),Neon / 监控才必须独立。
  3. 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 个真贵。

#客户的变化我们做什么改动级别
1Sarah 新开 1 店加 1 行 location
2Sarah 关 1 店标记关闭(数据保留)
3Sarah 改 vertical (健身房加美容)加 1 行 location,类型标 "beauty"
4Sarah 改付款人 (转给老婆)改 1 个字段(Stripe Customer ID)
5Sarah 拆品牌账单新建 tenant,把对应店转过去⭐⭐
6升 tier smb→proStripe 那边改套餐,我们 webhook 自动同步
7单个 tenant promote 到 Silo把该 tenant 的 store-scoped 数据搬到独立 DB,先可 maintenance window,以后再做无停机⭐⭐⭐⭐
8Sarah 取消订阅 (churn)软删,30/60/90 天保留期,期满删干净
9M&A 合并 (2 tenant 合 1)把店全转到留下的那个 tenant⭐⭐⭐

第 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_idlocation_id 过滤。

              老板 (User) 登录

                    ├─→ 拥有 tenant=tnt_002 (Glow Beauty Corp)
                    │     │
                    │     └─→ 3 个 location: SoHo, Brooklyn, Queens

                    └─→ Dashboard 顶部有个切换器:

               ┌────────────────────────────────┐
               │ [Glow Beauty Corp ▼]           │  ← tenant 级
               │   • All Locations (聚合)       │  ← 默认: 跨所有 location
               │   • SoHo                       │  ← 单 location 视角
               │   • Brooklyn                   │
               │   • Queens                     │
               │   • Compare: SoHo vs BK        │  ← 对比视角
               └────────────────────────────────┘

3 种视图怎么查:

视图过滤条件
聚合 (默认)tenant_id 拿 Glow 的所有数据
单店tenant_id + location_id 拿 SoHo 的
对比tenant_id 拿全部,然后按 location_id 分组

→ 底层数据带"客户 + 店"两层维度,前端切换器决定怎么过滤。1 套数据服务 3 种视角(对比 二、为什么要改 痛点 3 的"写 2 套代码"问题)。


九、Provider 怎么对接 — 我们用统一词,他们各家不同

我们要从 RingCentral / Twilio / Genesys / Five9 / Amazon Connect 等服务商拿电话数据。他们各家命名都不一样。

核心做法:我们不跟任何一家对齐。内部统一用 tenant / location / phone_line,中间加一层 adapter 翻译到各家。

        我们的内部 model (统一)             Provider 各家的 model (各自)
        ─────────────────────             ──────────────────────────

        tenants
          │                                 RingCentral:
          │                                   Account → Site → Extension

        locations  ←──── adapter ─────┬──  Twilio:
          │              (映射表)        │   Account → Subaccount → Phone
          │                              │
          ↓                              ├──  Genesys:
        phone_lines ←──── adapter ──────┤   Org → Site → Queue

                                        ├──  Five9:
                                        │   Domain → Campaign → Skill

                                        └──  Amazon Connect:
                                            Instance → Routing Profile → Queue

怎么实现:加 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 命名速查

平台顶层 (相当于我们 tenant)子层 (相当于我们 location)
RingCentralAccountSite → Extension → Phone
TwilioAccountSubaccount → Phone
Genesys CloudOrganizationSite → Queue → Agent
Five9DomainCampaign → Skill → Agent
Amazon ConnectInstanceRouting Profile → Queue → User

核心:所有家顶层用的都是中性词 (Account / Organization / Domain / Instance)。没有任何一家用业务词 (store / merchant)。我们用 tenant 跟业界对齐。

前提:别把 provider 的词漏到主表。现在 franchise_id 也是历史包袱要清 (见 §二)。


十、BPO 是啥?未来场景

BPO = Business Process Outsourcing,call center 外包公司。我们暂时不做,但设计上要预留。

                  ┌─────────────────────────────────────────────┐
                  │  retaintive (我们)                          │
                  └─────────────────────────────────────────────┘

                          │ 谁付 retaintive 的钱? = tenant

        ┌─────────────────┼──────────────────┐
        │                 │                  │
   Sarah 健身房      United Airlines     TaskUs (BPO 外包)
   tenant=Sarah    tenant=United        tenant=TaskUs

                                            │ TaskUs 自己干嘛?
                                            │ 给别人做 call center

                                ┌───────────┼───────────┐
                                │           │           │
                              Airbnb     Stripe      Doordash
                            (TaskUs 服务的) (TaskUs 服务的)

                          这 3 个是 TaskUs 的 "client"
                          但他们不付钱给 retaintive
                          → 不是 tenant
                          → 但 TaskUs 想在 dashboard 里
                             按 client 分别看报表
                          → 这就是 tenant_clients

所以 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 (拿不到别人的数据)。

公开 REST API (api.retaintive.ai)

  ├─→ 客户带 API key 请求
  │       ↓
  │   服务端从 key 解出 tenant,再路由到该 tenant 的数据库(future 的 getDbForTenant,尚未落地)
  │   Pool 模式才需要业务表 WHERE tenant_id / RLS

  ├─→ 提供联系人 / 通话 / 任务 / 消息等 endpoint

  ├─→ 每个 tenant 独立的 rate limit (按 tier 不同)

  └─→ Webhook 反向推数据给客户 (新联系人 / 通话分析完成 / 新任务)

核心:API 看到的 tenant 就是数据库里的 tenant,不需要重新设计。Stripe / Twilio / Slack 都是这套。

AI / MCP 怎么挂

客户的 AI Agent 通过 MCP 协议连过来,调用我们提供的 tool (比如 "搜联系人 / 看通话纪要 / 创建任务")。

客户的 AI Agent (Claude / GPT / Custom)

  │ 通过 MCP server 连接 (带 API key)

retaintive MCP server

  │ Tool 名字 (自然语言):
  │   - search_contacts(query)
  │   - get_call_summary(call_id)
  │   - create_task(...)

  ▼ 每个 tool 自动注入 tenant 边界 (来自 API key)
   AI 看不到 tenant 参数, server 强制注入

核心: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 条硬规则 (不可破)

  1. 隔离: 跨 tenant 数据绝对不串。同 tenant 内跨 location 允许互看。
  2. 计费: 默认 1 tenant = 1 Stripe Customer;V1 可 manual billing,但 Stripe 字段要预留。
  3. BPO: BPO 公司是 tenant,end-clients 是 tenant_clients (业务字段,不是隔离层)。
  4. 扩张: tenant ↔ location 是 1:N,N 改字段不动 schema。
  5. Placement ↔ 部署: 部署位置由 Control Plane 的 placement 决定。V1 默认 pool_orange_theory;Final default 可以 Silo-first;Pool/Hybrid 只是 future option。

8 治理项 (要承认存在,不一定立刻做)

#啥意思现在有吗
1Lifecycle 状态机trial → active → suspended → churned → deleted
2GDPR delete一键导出 + cascade delete 整个 tenant
3Audit log per tenant谁看了谁的数据,航空业必须有
4Admin impersonate我们员工怎么 debug 客户问题
5Per-tenant feature flag不同 tier 开不同功能⚠️ 半
6Region failoverPlatinum 客户的 DR
7Migration toolpromote-to-silo / churn 删除 / future Pool↔Silo
8Provider unbind客户换 CPaaS 不掉数据

十三、想深入看哪份?

想看什么去哪
业界怎么想 multi-tenant,通用方法论,完整 8 维度 Hybrid,DDL 表结构,MCP 实现细节best-practices.md
Final / long-term refactor 怎么从 V1 演进到中性 schema、Silo-first、future Pool/Hybridfinal/migration-plan.md
当前 store_id 隔离设计 (legacy, 已 ship)store-level-isolation.md