Files
Genarrative/docs/technical/【后台管理】多账号与Tab访问权限方案-2026-07-14.md
kdletters b72e2163d7
Project CI / Frontend tests (push) Successful in 29s
Project CI / Repository checks (push) Successful in 39s
Project CI / Native shell tests (push) Successful in 2m10s
Project CI / Backend tests (push) Failing after 2m50s
恢复后台灰度发布控制面
恢复灰度发布导航、权限、前端契约与页面编译链
解除灰度页面对旧创作入口配置的依赖
补充路由、API、页面回归测试及当前架构文档
2026-07-23 14:23:18 +08:00

26 KiB
Raw Permalink Blame History

后台管理多账号与 Tab 访问权限方案

更新时间:2026-07-23

1. 文档定位

本文定义陶泥儿后台从单一环境变量管理员扩展为“1 个 owner 引导账号 + 多个 member 持久账号”的编码契约,并为每个一级 Tab 建立前后端一致的访问权限。

本次只增加后台管理员账号与整页访问权限,不引入页面内按钮级、字段级或只读权限。正式实现必须同时完成前端导航过滤和后端 API 鉴权;前端过滤只改善体验,不能作为安全边界。

2. 当前基线与目标

当前后台由 GENARRATIVE_ADMIN_USERNAMEGENARRATIVE_ADMIN_PASSWORD 提供唯一管理员账号,apps/admin-web/src/app/adminRoutes.ts 定义 15 个可分配业务 Tab,并另有 owner-only 的“账号管理”Tabserver-rs/crates/api-server/src/modules/admin.rs 中的后台路由只校验统一的管理员 JWT。

改造后的目标如下:

  1. 现有环境变量账号升级为 owner,仍由部署环境提供,不迁移、不复制到 SpacetimeDB。
  2. owner 始终拥有全部 15 个业务 Tab 权限,并独占“账号管理”Tab 和账号管理 API。
  3. owner 可以创建、修改、启停 membermember 保存在 SpacetimeDB 私有表 admin_account
  4. member 按一级 Tab 分配权限;获得一个 Tab 权限即获得该页面内全部读写能力,页面内部二级 Tab、弹窗和操作区继承一级权限。
  5. member JWT 每次请求都重新读取当前账号并校验 enabledtoken_version 和实时权限,权限、密码或启停变更应立即让旧 JWT 失效。

3. 角色与不可变规则

3.1 owner

  • owner 用户名和密码继续读取 GENARRATIVE_ADMIN_USERNAMEGENARRATIVE_ADMIN_PASSWORD
  • owner 是环境变量构造的虚拟账号,不写入 admin_account,不允许通过后台改名、改密、禁用或删除。
  • owner 始终拥有本文列出的全部 15 个可分配权限,不能在前端取消,也不从数据库加载权限。
  • “账号管理”是 owner-only 能力。它可以作为新增一级路由 accounts / #accounts 展示,但 accounts 不进入 ADMIN_TAB_PERMISSIONS,不能写入 member 的 permissions_json
  • owner 会话返回 accountRole = "owner"roles = ["admin", "owner"];账号管理权限必须根据服务端确认的 accountRole 判断,不能只相信前端角色字符串。
  • owner 配置缺失时,后台整体保持未启用状态;不能依赖数据库中的 member 绕过 owner 引导配置启动后台。

3.2 member

  • member 只来自 admin_account,不新增第二套环境变量账号。
  • member 会话返回 accountRole = "member"roles = ["admin", "member"] 和当前实时 tabPermissions
  • member 永远不能访问账号管理页面或账号管理 API,也不能给自己或他人分配 accounts
  • member 的一个一级 Tab 权限覆盖该页面的查询、创建、修改、启停、退款等全部现有操作,不拆成 read / write
  • 页面内二级 Tab、筛选视图、抽屉、弹窗和共享详情弹窗继承触发它的一级 Tab 权限,不另设 permission id。

4. 权限标识

ADMIN_TAB_PERMISSIONS 必须是 shared-contracts 与 admin-web 共用的闭合集合,值与现有 AdminRouteId 一致。15 个可分配权限如下,顺序同时作为前端寻找“第一可访问项”的稳定顺序:

permission id 一级 Tab hash
dashboard Dashboard #dashboard
overview 服务总览 #overview
tables 表查询 #tables
debug API 调试 #debug
tracking 埋点数据 #tracking
gray-release 灰度发布 #gray-release
redeem 兑换码 #redeem
invite 邀请码 #invite
profile-wallet 账号配置 #profile-wallet
tasks 任务配置 #tasks
recharge-products 充值商品 #recharge-products
recharge-orders 充值管理 #recharge-orders
editor-generation-pricing 模型定价 #editor-generation-pricing
editor-showcase 精选审核 #editor-showcase
editor-assets 素材查询 #editor-assets

权限数组必须去重并按上表顺序规范化后保存。保存时拒绝未知值和 accounts;读取旧数据时遇到未知值应忽略并记录告警,绝不能将未知值解释为全权限。空数组合法,表示 member 可以登录但没有业务页面权限。

后续新增一级 Tab 时,必须在同一次改动中更新:

  • shared-contracts 的 ADMIN_TAB_PERMISSIONS
  • admin-web 的路由定义、权限标签和第一可访问项顺序。
  • 本文 API-to-Tab 权限矩阵。
  • 后端路由权限测试;没有明确权限映射的新 /admin/api/* 路由必须默认拒绝 member,而不是默认放行。

5. 数据模型

新增 SpacetimeDB 私有表 admin_account。表不能标记 public,浏览器不能订阅或直查;所有读写都由 api-server -> spacetime-client facade -> 受限 procedure 完成。

字段 Rust / SpacetimeDB 类型 约束与语义
account_id String 主键;服务端生成不可变 opaque id,建议 admin-account-<uuid>,请求体不得指定
username String unique;登录名,创建后不可修改;按 trim + ASCII lowercase 规范化
display_name String 展示名,去除首尾空白后 1 至 64 字符
password_hash String Argon2id PHC 字符串;只在内部登录查询中返回给 api-server,永不进入 HTTP DTO、日志或前端状态
permissions_json String 规范化后的 Tab permission JSON;只允许第 4 节 15 个值,空数组为 []
enabled bool 是否允许登录和继续使用现有 JWT
token_version u64 初始为 1;权限、密码或启停状态发生有效变化时加 1
created_by String 创建者后台 subject;当前只能是 owner subject
updated_by String 最近更新者后台 subject;当前只能是 owner subject
created_at Timestamp 创建时间,使用 ctx.timestamp
updated_at Timestamp 最近更新时间,使用 ctx.timestamp

账号规则:

  • username 建议限制为 3 至 64 个字符,只允许 ASCII 字母、数字、._-;规范化后做唯一性校验。
  • owner 用户名属于保留名称。创建 member 时必须同时与当前规范化后的 owner 用户名比较并拒绝冲突,不能只依赖 admin_account.username 唯一索引。
  • 密码明文只存在于登录、创建和改密请求生命周期内;限制为 6 至 128 个字符,并复用 platform-auth 的 Argon2id 哈希与校验能力。Argon2id 必须在 blocking 任务中执行,api-server 通过有界信号量限制同时 hash / verify 数量,不得占用 Tokio worker 或无界堆积高成本任务。
  • 不提供物理删除 API。离职或停用通过 enabled = false 完成,以保留 created_byupdated_by 和账号标识。
  • display_name 单独变化只更新 updated_byupdated_at,不要求递增 token_version;权限、密码、enabled 任一有效变化必须在同一事务中递增版本。
  • u64 版本到达上限时更新失败关闭,不能回绕。

6. SpacetimeDB 与 facade 边界

建议新增 server-rs/crates/spacetime-module/src/admin_account.rs,并在 spacetime-client 增加对应 admin facade。至少提供以下 typed procedures

procedure 用途 是否可返回 password_hash
get_admin_account_by_username_and_return member 登录查询 是,仅返回给 api-server 内部认证路径
get_admin_account_by_id_and_return member JWT 逐请求校验
list_admin_accounts_and_return owner 账号列表
create_admin_account_and_return owner 创建 member
update_admin_account_and_return owner 更新展示名、密码 hash、权限、启停

所有 admin_account procedures 都必须在事务入口调用现有 require_editor_generation_runtime_service_identity(...) 等价的统一 runtime service identity 守卫,只允许 api-server 当前 runtime service identity 调用。不能因为它们位于后台命名空间就接受任意 SpacetimeDB client identity,也不能新增 public table/view 暴露账号或 hash。

写 procedure 接收由 api-server 从已认证 owner 会话生成的 actor_subject,校验非空后写入 created_by / updated_by。SpacetimeDB 仍以 ctx.sender() 校验调用方是 runtime service identity;不能把请求体中的 actor 当成调用身份。

procedure result 使用 typed snapshot,不使用不透明 row_json。账号列表 snapshot 明确排除 password_hash;登录专用 snapshot 与普通账号 DTO 分离,避免序列化时误回传 hash。

7. 登录与 JWT 契约

7.1 登录优先级

POST /admin/api/login 按下列固定顺序处理:

  1. 规范化提交的用户名。
  2. 若用户名等于 owner 用户名,直接校验 GENARRATIVE_ADMIN_PASSWORD
  3. owner 密码不匹配时返回统一的“管理员用户名或密码错误”,不得继续查询同名 member。
  4. 用户名不等于 owner 时,按规范化用户名查询 admin_account
  5. member 不存在、enabled = false 或 Argon2id 校验失败时返回同一登录错误,不泄露账号是否存在或被停用。
  6. member 登录成功后,用当前 account_idtoken_version 签发后台 JWT。

owner 优先既保持原账号行为,也防止数据库同名记录遮蔽或降级 owner。密码比较不得写日志;member 必须复用 Argon2id 校验,不能存明文或可逆密文。未知 member、已停用 member 和 owner 错误密码路径仍执行同成本 dummy Argon2id 校验,避免从响应耗时枚举启用账号。

7.2 JWT claims 与逐请求校验

后台 JWT 继续使用后台独立 TTL 与签名配置。claims 至少能区分:

  • account_type: owner | member
  • owner 的稳定 subject,或 member 的 account_id subject。
  • token_versionmember 使用表中当前值;owner 使用固定虚拟值,不能由数据库覆盖。
  • roles:保留 admin,并追加 ownermember

权限不作为 JWT 中的授权真相。即使为调试在 claims 中携带 permissions,后端也必须忽略它并读取实时账号。

require_admin_auth 每次请求都要构造“当前账号”:

  1. 验签并校验后台 issuer、过期时间和 admin role。
  2. owner JWT:与当前环境变量构造的 owner subject 匹配,得到始终启用、全权限的虚拟当前账号;owner 不查 admin_account
  3. member JWT:按 claim 中 account_id 调用 get_admin_account_by_id_and_return,账号不存在或 enabled = false 时拒绝。只有明确的账号不存在才视为凭据失效;SpacetimeDB 超时、断连或 procedure 故障必须失败关闭但保留 502/503 依赖错误语义,不能伪装成 401 导致前端清除 token。
  4. member claim 的 token_version 必须与表中当前值完全一致,否则返回 401 Unauthorized 并要求重新登录。
  5. 将服务端实时构造的 AuthenticatedAdmin 放入 request extensions,后续权限 middleware 只读取该对象,不再相信原始 claims。

权限、密码、启停更新递增 token_version 后,目标 member 的所有旧 JWT 从下一次请求开始失效。owner 修改自己的环境变量密码仍通过部署配置和服务重启完成;owner 不入表,因此不使用 member 的 token_version 机制。

7.3 会话响应

AdminSessionPayload 在现有字段基础上增加:

accountRole: "owner" | "member"
tabPermissions: string[]

owner 返回全部 15 个 permission idmember 返回数据库中的实时规范化数组。GET /admin/api/me 同样执行逐请求校验并返回实时权限,供刷新页面后恢复导航。

后台所有面向运营展示的管理员身份统一使用 displayName。审计表继续保存稳定 subject,例如 owner subject 或 admin-account-<uuid>api-server 在返回兑换码、邀请码等操作记录时,按 owner 运行态和 admin_account 批量解析显示名称,同时兼容历史用户名记录。已无法解析的历史主体统一展示“已停用管理员”,前端不得直接渲染 operatorUserId、账号 ID 或登录用户名代替显示名称。对写接口,显示名目录必须在主事务前加载,或在主事务成功后降级为占位文案;不得因二次读取失败把已提交写入伪装成失败。

8. 权限中间件与错误语义

在统一 require_admin_auth 之后增加可复用的权限守卫,支持:

  • require_admin_permission(permission)owner 自动通过;member 必须包含该 permission。
  • require_any_admin_permission([permission...])owner 自动通过;member 至少包含一个,用于共享 API。
  • require_admin_owner:只接受服务端确认的 owner。

返回语义统一如下:

  • 401 Unauthorized:token 缺失、无效、过期,member 不存在、被停用或 token_version 过期。
  • 403 Forbidden:会话有效但缺少目标 Tab 权限,或 member 请求 owner-only API。
  • 前端收到 401 清除本地 token 并回到登录页;收到 403 不应伪装成掉线,应刷新 /me 权限并跳转到第一可访问项或零权限空态。

9. API-to-Tab 权限矩阵

下表覆盖 server-rs/crates/api-server/src/modules/admin.rs 当前全部路由,并追加账号管理 API。A OR B 表示 member 拥有任一权限即可;owner 对全部行自动通过。

Method 路径 权限
POST /admin/api/login 公开登录入口,不要求 JWT
GET /admin/api/me 任意有效后台会话
GET /admin/api/overview overview
GET /admin/api/dashboard dashboard
POST /admin/api/debug/http debug
GET /admin/api/tracking/events tracking
GET /admin/api/tracking/event-keys tracking OR tasks
GET /admin/api/database/tables tables
GET /admin/api/database/tables/{table_name}/rows tables
GET /admin/api/feature-gates gray-release
PUT /admin/api/feature-gates gray-release
GET /admin/api/editor-generation-pricing editor-generation-pricing
POST /admin/api/editor-generation-pricing editor-generation-pricing
GET /admin/api/editor-assets editor-assets
GET /admin/api/assets/read-url editor-assets OR editor-showcase
GET /admin/api/editor-showcase/assets editor-showcase
POST /admin/api/editor-showcase/assets/review editor-showcase
POST /admin/api/editor-showcase/assets/display editor-showcase
GET /admin/api/editor-showcase/campaign editor-showcase
POST /admin/api/editor-showcase/campaign editor-showcase
POST /admin/api/editor-showcase/campaign/image-upload-ticket editor-showcase
GET /admin/api/profile/redeem-codes redeem
POST /admin/api/profile/redeem-codes redeem
POST /admin/api/profile/redeem-codes/disable redeem
GET /admin/api/profile/invite-codes invite
POST /admin/api/profile/invite-codes invite
GET /admin/api/profile/tasks tasks
POST /admin/api/profile/tasks tasks
POST /admin/api/profile/tasks/disable tasks
GET /admin/api/profile/wallet-config profile-wallet
POST /admin/api/profile/wallet-config profile-wallet
GET /admin/api/profile/recharge-products recharge-products
POST /admin/api/profile/recharge-products recharge-products
GET /admin/api/profile/recharge-orders recharge-orders
POST /admin/api/profile/recharge-refunds/preview recharge-orders
POST /admin/api/profile/recharge-refunds/execute recharge-orders
POST /admin/api/profile/recharge-refunds/register recharge-orders
POST /admin/api/profile/recharge-refunds/manual-review/resolve recharge-orders
GET /admin/api/profile/users/detail tables OR tracking OR recharge-orders OR editor-showcase OR editor-assets
POST /admin/api/profile/wallet-restriction recharge-orders
GET /admin/api/accounts owner-only
POST /admin/api/accounts owner-only
PUT /admin/api/accounts/{account_id} owner-only

两个共享读取接口必须按 OR 规则实现,不能为了复用简单中间件扩大成“任意 member 可访问”:

  • /admin/api/assets/read-url 只服务素材查询和精选审核。
  • /admin/api/profile/users/detail 只服务当前实际包含用户详情入口的表查询、埋点数据、充值管理、精选审核和素材查询页面。

gray-release 页面只调用 GET/PUT /admin/api/feature-gates;旧 /admin/api/creation-entry/config* 已退役,不能再作为灰度页面初始化依赖。页面固定目标只登记现役功能,其他通用 gate 仍可通过 Gate Key 直接管理。

10. 账号管理 HTTP 契约

账号管理 API 放在现有 /admin/api 命名空间,统一使用现有 success/error envelope。

10.1 GET /admin/api/accounts

返回:

accounts: Array<{
  accountId,
  username,
  displayName,
  tabPermissions,
  enabled,
  tokenVersion,
  createdBy,
  updatedBy,
  createdAt,
  updatedAt
}>

首项由 api-server 根据环境变量合成只读 owner 记录,后续 member 按用户名和账号 ID 稳定排序。不得返回 passwordpasswordHashowner 记录必须带 accountRole = "owner" 且前后端都拒绝编辑。

10.2 POST /admin/api/accounts

请求:

{
  username: string,
  displayName: string,
  password: string,
  tabPermissions: string[],
  enabled?: boolean
}

password 创建时必填;enabled 默认 true。api-server 完成用户名、密码和权限校验,使用 Argon2id 生成 hash 后调用创建 procedure。用户名冲突返回 409 Conflict;参数非法返回 400 Bad Request。响应为不含任何密码字段的 account

10.3 PUT /admin/api/accounts/{account_id}

请求:

{
  displayName: string,
  password?: string,
  tabPermissions: string[],
  enabled: boolean
}

usernameaccount_id 不可修改。更新请求完整提交显示名称、Tab 权限和启停状态;密码省略表示不修改,空字符串密码作为非法参数拒绝。api-server 只在提供新密码时生成新 hash。procedure 比较有效变化,在权限、密码或启停任一变化时只递增一次 token_version,并在同一事务写入账号字段、updated_byupdated_at。响应仍不返回密码或 hash。

11. admin-web 行为

11.1 路由与导航

  • adminRoutes 增加权限元数据;15 个业务路由使用同名 permission id。
  • accounts 路由只在 admin.accountRole === "owner" 时加入侧栏和移动底栏,不属于 member 可分配列表。
  • member 导航只渲染 admin.tabPermissions 包含的业务路由。页面组件也必须只在当前路由已授权时挂载,避免隐藏导航后仍发起无权限 API。
  • owner 渲染全部业务路由和账号管理路由。

11.2 hash 回落

登录成功、GET /me 恢复会话、权限刷新和 hashchange 时都执行同一解析:

  1. 当前 hash 对应可访问路由时保持不变。
  2. hash 未知、属于无权限业务 Tab,或 member 访问 #accounts 时,使用 replaceState 回落到按第 4 节顺序找到的第一可访问业务 Tab。
  3. member 权限为空时,不回落 Dashboard;渲染独立的零权限空态,只保留账号信息和退出登录,不挂载任何业务页,也不发起业务 API。
  4. owner 的默认项仍可保持 Dashboard;账号管理不改变 15 个业务路由的排序。

后端返回 403 时,前端重新请求 /me 获取实时权限并执行上述回落。即使前端状态陈旧或被篡改,后端权限 middleware 仍必须拒绝越权请求。

11.3 账号管理页

  • 权限编辑器展示 15 个明确的 checkbox,每项使用现有 Tab 中文名称;不能展示或提交 accounts
  • 创建和编辑使用独立弹窗或抽屉,不在列表下方追加表单。
  • 编辑时密码字段默认空,空表示请求中省略 password;页面永不展示现有密码或 hash。
  • 停用使用开关并二次确认。保存成功后以 API 返回 account snapshot 更新列表。
  • owner 以只读项显示在账号列表首位,页面顶部同时标明当前 owner;owner 行不可进入编辑表单,后端也拒绝以 owner subject 调用更新接口。

12. 实现落点

建议按以下边界落地,避免在前端或 api-server 重新发明持久化规则:

  • shared-contractsADMIN_TAB_PERMISSIONS、扩展后的 AdminSessionPayload、账号管理 request/response DTO。
  • spacetime-module:私有表、输入类型、typed procedures、唯一性与版本递增事务。
  • spacetime-client:生成绑定、row mapper、登录查询与账号管理 facade。
  • api-serverowner/member 登录编排、Argon2id、逐请求账号解析、权限 middleware、账号管理 handlers。
  • apps/admin-web:权限感知路由、hash 回落、零权限空态、owner-only 账号管理页。

不能将 permissions_json 的解析与授权只放在前端;不能让 admin-web 直连 SpacetimeDB;不能用进程内 member 列表替代 admin_account

13. 迁移、绑定与发布顺序

admin_account 是新增私有表,没有旧数据回填。原环境变量 owner 不入表,因此迁移不创建 owner 行。

实现 schema 后必须:

  1. admin_account 加入 server-rs/crates/spacetime-module/src/migration.rs 的导入导出/迁移表目录,保证备份迁移保留 member。
  2. 将表和 procedures 加入 docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md 的机器可读表目录与后台契约。
  3. 生成并提交 spacetime-client bindings,不手改生成文件。
  4. 先发布 SpacetimeDB module,再发布依赖新 procedures 的 api-server,最后发布 admin-web。

本地生成与门禁命令:

npm run spacetime:generate
npm run check:admin-account-procedures
npm run check:spacetime-runtime-access
npm run check:spacetime-schema
npm run check:server-rs-ddd

本地迁移联调优先使用项目脚本:

npm run dev:spacetime
npm run dev:api-server

需要人工发布到指定 SpacetimeDB 时必须显式目标,不使用 spacetime --root-dir

spacetime publish <database> \
  --server <server-url> \
  --module-path server-rs/crates/spacetime-module \
  --yes=migrate

回滚旧 api-server 时保留 admin_account 表和数据;旧版本只认识 owner,不会读取 member。不得为了回滚删除表或清空 member 数据。

14. 测试与验收

14.1 后端与数据

  • owner 使用原环境变量账号密码登录成功,且不生成 admin_account 行。
  • owner 用户名匹配但密码错误时,不回退到 member 查询。
  • member 密码使用 Argon2id 校验;账号不存在、密码错误、停用账号返回相同登录错误。
  • member 私表不能被普通 SpacetimeDB identity 查询或调用 procedure;只有 runtime service identity 可读写。
  • 创建重复规范化用户名、owner 保留用户名、未知权限或 accounts 权限均失败。
  • GET/POST/PUT 账号 API 任何响应和日志都不包含明文密码或 password_hash
  • 权限、密码、启停更新各自会递增 token_version;同一次请求修改多项只递增一次;仅改展示名不递增。
  • member 被停用、改密或改权限后,旧 JWT 下一次请求返回 401;重新登录后获得实时权限。
  • API-to-Tab 矩阵逐路由覆盖 modules/admin.rs,每条路由至少测试 owner 成功、具备权限的 member 成功、缺权限 member 返回 403。
  • 两个共享读取接口分别覆盖每个允许 permission 的成功用例,以及无关 permission 的 403 用例。
  • owner-only 账号 API 对任意 member 都返回 403,即使其 permissions_json 被污染为包含 accounts

14.2 前端

  • owner 看到 15 个业务 Tab 和账号管理;member 只看到被分配的业务 Tab。
  • 每个一级 Tab 内的二级 Tab、弹窗和写操作继承一级权限并正常使用,不出现“页面可见但内部 API 403”的错误映射。
  • 直接输入无权限 hash 自动替换为第一可访问项,不短暂挂载无权限页面。
  • 当前 Tab 权限被 owner 收回后,下一请求触发重新登录;新会话恢复后落到第一可访问项。
  • 零权限 member 登录后显示空态,不回落 Dashboard、不发送 Dashboard 或其它业务请求,并可正常退出。
  • 桌面侧栏和移动底栏应用同一过滤结果;账号创建/编辑弹层在移动端和桌面端都可操作。
  • 编辑账号时页面不读取、不显示、不回填密码;不改密码时 PUT 请求不包含 password

14.3 建议验证命令

cargo test -p spacetime-module --manifest-path server-rs/Cargo.toml admin_account
cargo test -p spacetime-client --manifest-path server-rs/Cargo.toml admin_account
cargo test -p api-server --manifest-path server-rs/Cargo.toml admin
npm run check:admin-account-procedures
npx vitest run apps/admin-web/src/app/adminRoutes.test.ts \
  apps/admin-web/src/app/AdminApp.test.tsx \
  apps/admin-web/src/pages/AdminAccountManagementPage.test.tsx
npm run admin-web:typecheck
npm run check:encoding
git diff --check

API smoke 使用 npm run dev:api-server 启动后端,先检查 /healthz,再用 owner 和至少两个不同权限集合的 member 验证登录、/admin/api/me、共享 OR 路由、越权 403 和 token_version 即时失效。

15. 非目标

  • 不提供 member 自助改密、忘记密码、MFA、SSO 或外部身份源。
  • 不提供按钮级、字段级、只读/读写分离权限。
  • 不提供 owner 数据库化、多个 owner 或 member 删除。
  • 不改变普通用户认证、普通用户 token_version 或主站权限体系。
  • 不把后台账号暴露为公开 SpacetimeDB 表、浏览器 subscription 或普通用户账号。