Skip to content

品牌线绑定模型:品牌 · realm · 用户 · 租户 · 项目 · URL 的六条关系 ​

读者:正在接入 AgentHub 的品牌产品线团队(人类工程师或 AI coding agent)——想搞清楚「品牌、身份、账户、产品」各由谁建立、记录在哪、靠什么生效。 定位:概念文档(Explanation)——心智模型与绑定关系速查,不含操作步骤;操作走 接入总纲 / 品牌自托管八步 / 模式 B 指南 / how-to。 状态基准:2026-09-15。角色权限表对齐代码事实源 backend/app/identity/service/platform_scope_catalog.py;邀请接口形状对齐 openapi.yaml。 适用范围:模式 B(浏览器 JWT 直连)+ 品牌自托管边缘(默认部署形态)。


0. 一页纸心智 ​

realm 管「哪条线」(品牌归属)· 注册管「谁」(身份)· 租户管「圈」(钱与数据的边界)· 邀请码管「进圈」(成员关系)· 项目管「账」(计费归因与会员独立)· 你的前端管「门面」(产品 URL)。

品牌域(DNS/服务器/边缘) ──► realm(身份名册) ──► 用户账号
                                                │ JIT
                                                ▼
                    邀请码 ──► 租户(运营域/钱) ──► users 行 + 钱包(租户,用户)
                                    │
                                    ▼
                              项目(归因/会员) ◄── VITE_PROJECT_ID ── 你的产品前端(品牌域路径)

1. 品牌 ↔ realm:一条命令,一次成型 ​

  • 谁做:基座 owner / 运维,在基座节点执行 scripts/new-realm.sh <realm> <品牌域名> --client-id <realm>-web。
  • 一条命令全部带出:Keycloak realm(强制 frontendUrl=https://<品牌域>/auth,签发者从此钉死在品牌域)+ 一个 PKCE 公共 SPA client + 5 个 protocol mapper(aud / realm-roles / tenant_id / user_id / permissions)+ 基座 keycloak_realms 信任注册表登记 + 注册开关(邮箱即用户名、强制邮箱验证、Google IdP 占位、独立 SMTP)。
  • 时序:域名 / DNS / 边缘服务器不存在之前 realm 建不了;建成后 api 约 300 秒自动生效(栈启动时注册表为空的首个 realm 除外,需重启 api)。
  • 一次性:此后这条品牌线的所有身份设施不再需要任何 Keycloak 管理操作。
  • 品牌资料载体(可选增强,与上面的一条命令无关):品牌名 / slogan / 简介 / 明暗 logo / 明暗条幅不在 Keycloak 里,存基座 brand_profiles 表——每 realm 一行(ADR-0067)。首行由基座 ops 建运营域租户时勾选「设为品牌运营租户」造册(或运维 seed),此后运营租户管理员在品牌域 builder「品牌设置」页或经 PATCH /api/v1/brand/profile 维护;渲染接线(博客品牌呈现 BA2 / KC 登录主题)完成前,录入内容不出现在任何对外页面(「可录不可见」)。

2. 用户 ↔ realm:入口即归属,无选择动作 ​

  • 谁做:终端用户自己——打开品牌域网站(如 https://<品牌域>),点注册 / 登录。
  • 机制:品牌前端构建时烙死 VITE_KEYCLOAK_REALM / VITE_KEYCLOAK_CLIENT_ID;登录跳转 URL 的路径段(/auth/realms/<realm>/...)就是「选 realm」这个动作的全部——经品牌边缘 /auth/ 反代到基座 Keycloak,注册页由该 realm 渲染,账号物理创建在该 realm 的名册里(容器关系,非事后绑定)。
  • 推论:注册页上没有 realm 选择器;同一邮箱在两条品牌线 = 两个互不相认的独立账号(三 realm 三本名册,平台明确接受);不存在「把用户迁移到另一个 realm」的操作。

3. 用户 → 租户管理员:JIT 开户 + 升格 ​

  • JIT(自动,首登):登录回调后前端调一次 GET /api/v1/me,基座自动三件事——建平台侧 users 行(keycloak_user_id 引用 Keycloak 用户 UUID)+ 建个人租户(自动写 keycloak_realm_id=<realm>)+ 把 tenant_id / user_id / permissions 回写进 Keycloak 用户属性(经 mapper 进入后续每个 token)。R1 不变式(token realm ≠ 租户 realm → 403)两头对齐。
  • 升格(人工一次):用户在 Builder 租户引导页调 POST /api/v1/tenants,把个人租户原地升级为正式运营租户,本人角色变为 tenant_admin(本租户管理员)。升格后需重新登录一次刷新 token,新权限才生效。
  • 边界:tenant_admin 只是本租户内的管理员——碰不了其他租户的数据(RLS 硬隔离),也碰不了平台运维面。

4. 用户 → 加入指定租户:邀请码兑换(当前唯一自助通道) ​

  • 管理员侧(需 tenant:user_manage;Builder「设置 → 租户控制台」或直接调 API):

    bash
    POST /api/v1/tenants/current/invites
    { "role_code": "saas_user",      # 入域角色,默认普通成员,可给 builder
      "max_uses": 100,               # 一码最多 100 人用(1~100,默认 1)
      "expires_in_hours": 168 }      # 有效期(1~720 小时,默认 72)

    码经任何渠道发给申请者;C 端批量拉新用大 max_uses 的共享码。

  • 申请者侧:POST /api/v1/tenants/invites/redeem(带码)→ membership 建立,角色按码内 role_code → POST /api/v1/me/active-tenant 切换活跃租户(响应恒 token_refresh_required: true,前端强制刷新 token)→ 此后请求以该租户身份发出,域内钱包 (该租户, 用户) 开户。一个用户可同时是多个租户的成员(多归属),同一时刻只有一个活跃租户。

  • 注意:新用户在受邀前只属于自己的个人租户,看不到运营租户的 Agent 目录(RLS);入域 ≠ 有积分——能开聊还需运营租户管理员(持 billing:points:tenant_grant,经 builder 控制台的「发放积分」入口)给用户本人发过积分(真收款开闸前的入账方式)。

5. 管理员 / builder → 项目:品牌域下的 Builder 创建,两种角色都能建 ​

  • 入口:品牌域下的 Builder 控制台(https://<品牌域>/builder/——按品牌参数重新构建的同一份 SPA,与门户共用线级 client、同源免二次登录)。不是 基座域的 /builder/:那是平台 realm 的门,品牌账号进不去(例外:S0 演练期在平台 realm 走七步流程,用平台 realm 账号)。
  • 权限:POST /api/v1/builder/projects 要求 scope builder:project:write,持有者为 tenant_admin 和 builder 两个角色:
角色来源建项目/Agent/模型/KB租户管理·邀请·发积分·M2M·内容专栏会话消费
tenant_admin租户升格时的创始人✅✅✅
builder邀请码 role_code=builder✅❌✅
saas_user邀请码默认角色❌❌✅
  • 产出:项目创建成功返回 project_id(UUID)——仅此而已,平台不生成任何 URL。一个租户可建多个项目(= 多产品):同租户内用户与充值共享、会员与报表按项目独立。

6. 消费者 → 使用产品:URL 是你自己造的,nginx 与项目 id 无关 ​

  • 产品 URL 的三个组成部分全部出自品牌方:域名(品牌域)+ 路径(派生前端时的 VITE_BASE_PATH,如 /english/)+ 内容(从 web-saas-template 派生、构建、部署在自己服务器上的前端)。
  • nginx 路由的职责边界:只做「路径 → 本机前端服务」的转发(location /english/),不知道也不需要知道 project_id。
  • project_id 的真实位置:构建期烙进前端包的常量(VITE_PROJECT_ID);前端调 API 时携带,平台按它做计费归因。用户浏览器地址栏里永远不会出现它。
  • 请求全链路:用户开 https://<品牌域>/english/ → 边缘直出你的前端 → 前端经 /api/(边缘反代到基座)调 GET /app/agents 拉该租户已发布 Agent → 开会话(SSE 流式)→ 每轮按单价扣 (租户, 用户) 钱包、流水记到项目。
  • 消费前置:打开 URL 只算进了门;真正能聊需要 ①是该运营租户成员(§4)②钱包有积分,否则 402 引导充值。

7. 六条串起来的最小操作剧本 ​

基座运维跑一次 new-realm.sh(§1)→ 你在品牌域注册(§2)→ 升格运营租户成为 tenant_admin(§3)→ 发邀请码拉进用户与 builder(§4)→ 在品牌域 Builder 建项目拿 project_id、创建并发布 Agent(§5)→ 派生部署前端、把 project_id 焊进构建(§6)→ 发积分、开聊。全链 worked example 见 first-saas 教程。

8. 常见误区速查 ​

误区事实
注册时让用户「选择」realm / 品牌没有这个步骤——访问哪个品牌域,就自动在哪个 realm 注册(§2)
品牌运营者登录基座域的 /builder 管项目进不去——那是平台 realm 的门;品牌线用品牌域下的 Builder(§5)
平台会生成/展示项目的访问 URL平台只发 project_id;产品 URL = 你自己部署的前端地址(§6)
nginx 按项目 id 路由nginx 只按路径转发到前端服务;project_id 是前端构建期常量,不参与路由(§6)
邀请入域后用户就能消费还需给用户本人发积分;402 = 引导充值(§4/§6)
同一邮箱可跨品牌线通用不支持——三 realm 三本名册;同线多产品才共享账号(§2/§5)
把用户迁到另一个 realm无此操作;换线 = 在新 realm 重新注册(§2)