User Identity · 用户身份设计(v4,2026-10-15,Jeff Vega,卡 2610.1065)

HOOP 系统设计图纸 · 正文唯一真相是 docs/用户身份.md;改 md 再跑 ./tools/deploy_docs.sh,这页是生成的

版本 bce575154
2026-10-15 11:33

这是整个项目的用户身份设计,不是 Loop 专用。 HOOP、Loop 和以后的每个 App 都按这一份认人。 起因:Jeff 2026-10-15「Loop - User」房 07:20–10:53 的语音(第 1 节逐条列出)。10:53 定:改名为 User Identity、写详细、每个环节写清要做的任务和做在哪边、不许影响 HOOP 现在的登录、HOOP 和 Loop 的知识库放同一份。 状态:设计,还没动一行代码。 引用的现有代码按 HOOP master cd725c0a4(10-15 10:50)核过。第 14 节是房里四个 AI 的讨论分工,第 13 节是还没定的事。 旧网址 loop-identity 自动转到这里。取代 LOOP接口方案.md 第 2 节 a(Loop 自己一套账号);那份的白名单、App 不放秘密、两套各自部署回滚照旧有效。 版本:v1 09:58 连接方案 · v2 10:10 改叫法 · v3 10:40 三层 · v3.1 10:50 重复账号 · v4 10:53 改名、加任务清单、加「用户放在哪」「Loop 里看到好友」「不影响 HOOP 登录」三节。 English summary: section 15.

0. 一句话

One User · One Login · Multiple Identities · Multiple Personas(一个用户、一个登录、多个身份、每个身份多个分身)。 - 用户只有一份,放在 HOOP。邮箱、手机、Apple、Google 认的都是它,登录一次。 - 身份:个人(HOOP 用)、商业(Loop 用)、创作者。开了才有,各有一份资料。 - 分身:每个身份下面可以有几张脸(名字 + 头像),每个群自己选用哪一张。 - 每个 App 决定接受哪几种身份;封号封的是「某个身份在某个 App」。 - 有用户 ≠ 能进某个 App:要有那个 App 接受的身份,而且在那个 App 的状态是正常。

1. Jeff 10-15 定下的规则(按语音整理)

  1. Loop 注册 / 登录先到 HOOP 的登录引擎认人;HOOP 没有这个人就当场建用户(07:20、08:01、08:05)。
  2. 用户看到的是 Loop 自己的登录页,HOOP 在背后认人;「用 HOOP 一键登录」是另一件事,以后再做(08:01、08:04)。
  3. Loop 来的新人不过 HOOP 的邀请门,直接进 Loop;他在 HOOP 有用户,但 HOOP 的门没为他开就进不了 HOOP(08:53)。
  4. HOOP 新人自动进官方频道,Loop 来的人不需要(08:53)。
  5. HOOP 删号 / 封号只影响 HOOP,Loop 照常;用户留着,只是 HOOP 的状态改成不能进(08:53、08:57)。
  6. 没下载或退出 HOOP,他仍是 Loop 会员,资料还在(10:00)。
  7. 叫法:One User · One Login · Multiple Identities;身份就是资料;用户主表只记用户的东西(10:05)。
  8. 个人身份 = HOOP,商业身份 = Loop(10:07);还有创作者身份;每个身份下面多个分身,为了隐私;三层(10:27)。
  9. 每个 App 允许几种身份进来,封掉一个身份它就进不了那个 App;Loop 下面也要有分身(10:32)。
  10. 分身只是我自己的名字和头像,和公司无关;是哪家公司的成员是另一回事(10:34)。
  11. 数据合并放第二版,第一版只做账号关联;合并用验证码确认、被封的号登不进去合并不了、合并不能撤销、Sparks 合在一起(10:45、10:48)。
  12. 这份是整个项目的 User Identity,不是 Loop 的;放 HOOP 和 Loop 两边的知识库,同一份(10:53)。
  13. 不可以影响 HOOP 现在的登录;表结构要改,要很谨慎,做好再延伸到 HOOP(10:53)。
  14. Jeff 的测试:用他 HOOP 的账号(Gmail)登录 Loop,要看到他所有的朋友 —— 朋友们自己还没装 Loop,但在他的角度要看得到(10:53)。
  15. 用户一定只有一份、两边一模一样、而且最完整;放 HOOP 一边还是两边,选维护最好的(10:53)。

2. 三层:用户 → 身份 → 分身

全文只用这三个词。「资料」不是另一样东西,只是身份那一页上写的内容。

层 是什么 一个人有几个 放在哪
① 用户(User) 你是谁、怎么登录:用户 ID、验证过的邮箱和手机号、Apple / Google 绑定;关于这个人本身的事实(生日、实名认证) 一个 HOOP 现有 users 表(000001_init.up.sql:10-28),只有登录引擎能改
② 身份(Identity) 以哪种身份出现:个人 / 商业 / 创作者,每种一份资料(字段按类型不同) 每种最多一个,按需开通 新表 identities
③ 分身(Persona) 这个身份对外的一张脸:名字 + 头像;每个身份恰好一个默认分身 每个身份可以有几个 新表 personas

外加一张「身份在各 App 的使用状态」(identity_app_access)。

身份 今天在哪个 App 用 现有代码里对应什么
个人 HOOP user_profiles(000001_init.up.sql:64-84)= 默认分身;现有「分身」personas(社交 / 游戏两张脸,000031_personas.up.sql)= 其它分身
商业 Loop 全新
创作者 HOOP(Loop 以后再定) developers + 创作者资料(000131_developers.up.sql 原话「HOOP 用户的第二身份」、000175_creator_profile.up.sql)

规矩: - 事实放用户层,展示放身份层。 生日、实名只存一份;名字、头像、职业、性格每个身份各填各的,改一边不覆盖另一边。 - 分身只属于一个身份,用哪个是你的自由;每个群自己记住选的哪个。 - 隐私:别人默认看不出两个分身、两个身份是同一个人;公开资料和接口都不露这层关联。只有管理和安全审查看得到。 - 封号三级:一个分身 → 只关它;某身份在某 App → 它在这个 App 的所有分身一起关,别的 App 不跟着封;整个用户 → 全关,只用于最严重的情况。 - 分身不是公司。是哪家公司的成员是另一件事,不写进资料、不给分身任何权限;公司这一层不在这份范围。

2.1 新表(都在 HOOP 数据库)

identities —— 每人每种身份一行 - id(身份 ID)· user_id(外键 users.id;(user_id, kind) 唯一)· kind:personal / business / creator - 按类型的资料:个人(职业、性格……;已有字段仍读 user_profiles,不搬)、商业(业务介绍)、创作者(作品介绍) - created_at · deleted_at · delete_source(admin / member)—— deleted_at 只用于整个身份删掉(所有 App 都不再用);在某个 App 注销只改 identity_app_access,不碰这里(Cora R1)。

personas —— 每个身份几行 - id · identity_id · display_name · avatar_url · is_default · status(active / banned) - 默认分身只有一个读取规则:个人身份的默认分身读 user_profiles,personas 里个人身份不存 is_default 行;商业、创作者身份的默认分身读 personas(is_default,每身份唯一约束)。不会出现两个默认。群里选哪个分身,沿用 conversation_members.persona_scope(000032)的做法,改成记分身 ID。 - ⚠️ 表名和现有的 personas(000031)撞名,建表时新表另取名或把旧表并进来,H1 定。

identity_app_access —— 某身份在某 App 的状态 - 主键 (identity_id, app);app:hoop / loop · status:pending(开通中,如等 HOOP 邀请门)/ active / suspended / banned / deleted · suspended_until

每个 App 接受哪几种身份写在代码配置里(今天:HOOP = 个人、创作者;Loop = 商业),改它不用改表。 上线时给每个现有用户补「个人身份 + 个人身份在 HOOP」,状态照抄今天的 users.status;开发者再补创作者身份。

3. 用户只有一份:放在 HOOP,Loop 不存副本(回答第 1 节第 15 条)

决定(建议):用户、身份、分身、好友关系都只存在 HOOP 一边。Loop 的数据库不存任何一份用户资料,只存 Loop 自己的内容。 - Loop 的数据挂什么 ID(Gene Loop 11:03,采纳):Loop 的聊天和群用的是 hoop-core 的会话代码,里面写死用户 ID,改成身份 ID 会牵动 HOOP。所以第一版:会话、群成员照旧挂 HOOP 的用户 ID;群里用哪个分身另记一张「群 + 成员 → 分身」(每个成员在每个群各选一个;写入时校验这个分身属于该成员、它的身份获准在当前 App 使用,Cora 11:10);Loop 自己新做的数据挂身份 ID。 - 为什么不两边都存:两份就要同步,同步一定会有一刻对不上(改名、换头像、删号、封号的那几秒),还要多写一套补偿程序。一份就永远一模一样,这正是「最完整」。 - Loop 要用户资料时:Loop 后端调 HOOP 的只读接口(任务 H6),短缓存几分钟;HOOP 改了,缓存过期就是新的。 - 代价:HOOP 的账号服务挂了,Loop 新登录会失败(已登录的人凭证还能用到过期)。所以账号服务要单独看可用性。Google、Meta 那种统一账号是同样的做法、同样的代价。 - ⚠️ 代码共用 ≠ 数据共用(Cora 10:58 核过):Loop 后端今天直接引用(不是复制)hoop-core 的登录代码,但连的是它自己的数据库(库名 loop)、用的是它自己的凭证密钥 —— hoop-core 的迁移自带 users、refresh_tokens、auth_events、account_deletions、friendships、personas 等表(hoop-core/README.md 迁移 000001–000009;loop/docs/ARCHITECTURE.md 的表清单)。所以「Loop 用了 HOOP 的登录代码」不等于「Loop 和 HOOP 共用用户」。本设计的目标是 Loop 运行时接 HOOP 的中央账号和好友服务;要停用的是 Loop 库里的用户数据和那把密钥,代码还是同一份(任务 L0、L1),中央服务不可用时不能在本地另建用户。 - Loop 缓存的规矩:只缓存显示必需的最少字段(名字、头像、身份状态);HOOP 资料改了、身份被封或撤权,缓存立即作废(HOOP 推作废通知,不等几分钟过期);查不到权限时按「拒绝」处理,不按「放行」;Loop 永远不写主档。 - 共用代码怎么改:要改两边共用的能力,先在 hoop-core 改,再由 HOOP 和 Loop 各自接;不把 HOOP 主仓代码搬进 Loop(任务 C1)。Loop 那边不许有第二份 hoop-core(Jeff 11:31「不要分叉」):Loop 只按相对路径原样引用,不复制、不改、不另写同样功能;Gene Loop 拿的是只读钥匙(10-15 11:31 加在 JeffChoong99/hoop-core),推不上去。

4. 在 Loop 里看到 HOOP 的朋友(回答第 1 节第 14 条)

Jeff 的测试:用 HOOP 账号登录 Loop,要看到他所有的朋友,哪怕朋友们还没装 Loop。

做法:好友关系是用户和用户之间的,只存 HOOP 一份(好友表在 000001_init.up.sql,接口 GET /v1/friends,friend/handler.go:65)。Loop 不另建好友表,直接读这一份。 - Loop 显示好友列表时,每个朋友显示:你在 HOOP 本来就有权看到的那份名字和头像(不是统一用默认分身),以及「已在 Loop」或「还没加入 Loop」。 - 列表照 Loop 的联网规矩(铁律 25):先显示本机上一份,再向 HOOP 要新的;失败自动重试;断网给友好提示,不整页空白(Gene Veyra 11:03)。 - 还没加入的朋友:显示成灰色、可以邀请;加入以后他用自己的商业身份出现。 - 隐私:Loop 只给本来就是你好友的人看到这些,陌生人在 Loop 里搜不到你的个人身份。 - 沿用 HOOP 现有的权限(Cora 评审 10:55):拉黑、隐藏、可见范围照 HOOP 现在的规则算;HOOP 里看不到的,Loop 里也看不到。 - 不冒充、不代建:没开 Loop 的朋友只列出、标「还没加入」,不自动给他建商业身份,也不显示成已在 Loop。 - 显示哪张脸:只用 HOOP 允许展示给你的那份资料(他在你们共同场景用的分身,默认是默认分身);不能借 Loop 把他隐藏的分身之间的关联露出来。 - ⚠️ 这一条和 v3 不同:v3 写「好友各 App 管各的」,按 10:53 的测试要求改成「好友关系共用、内容(朋友圈、动态)各 App 管各的」。要 Jeff 确认(第 13 节第 1 条)。

5. 登录流程

现有登录入口(backend/internal/domain/auth/handler.go:26-31)不换路径,请求里多一个字段 product(不带 = hoop,旧版 HOOP App 完全不受影响)。

a) 在 Loop 用邮箱或手机验证码登录 1. Loop App 请求验证码,带 product: loop → 发 Loop 品牌的邮件 / 短信(auth.Config.Brand 已能按产品给文案)。 2. 用户填验证码 → 登录引擎核对 → 用邮箱 / 手机号找用户(FindUserByContact,service.go:427)。 3. 没有 → 建用户(CreateUser,service.go:465),跳过邀请门、不跑 HOOP 的建号钩子(官方频道)。 4. 查他的商业身份和「商业身份在 Loop」:没有就建(商业身份 + 默认分身 + Loop 使用状态在一个事务里一起建;重试按用户 + 种类幂等,不会建出两个);封禁 / 已删除就拒绝。 5. 发一张只给 Loop 用的登录凭证(第 7 节)。

b) 在 Loop 用 Apple / Google:先按 Apple / Google 编号找用户;找不到再按邮箱找,只认提供方声明「已验证」的邮箱(Google email_verified;Apple 的真实邮箱),而且 HOOP 那边这个邮箱也必须是已验证的;对上了还要让用户用验证码确认已有账号,才把新登录方式绑上去。Apple 的坑见第 8 节第 1 条。

c) 只有商业身份的人打开 HOOP:认出用户,发现没有个人身份 → 这时才走 HOOP 邀请门;过了就建个人身份、跑官方频道钩子。⚠️ 今天「老用户登录不查邀请门」(service.go:427-432),这一步必须和 Loop 登录同时上,否则 Loop 成了绕过邀请门的路。

d) 已有 HOOP 的人第一次进 Loop(Jeff 的测试就是这条):找到用户 → 建商业身份和它的默认分身 → 进 Loop。默认分身要不要先填上 HOOP 的名字和头像,见第 13 节第 2 条。

6. 每种情况怎么算

表里「个人身份」= 个人身份在 HOOP 的状态,「商业身份」= 商业身份在 Loop 的状态。

情况 用户 个人身份 商业身份
HOOP 邀请门 不管 建个人身份时才查 不查
建号后进官方频道 — 建个人身份时跑 不跑
HOOP 管理员删号 留着 deleted 不动
HOOP 管理员封号 / 暂停 留着 banned / suspended 不动
Loop 管理员删号 / 封号 留着 不动 deleted / banned
被 HOOP 管理员删掉的邮箱 / 手机号 30 天内再注册 — 挡住 不挡
用户在某个 App 里注销 看他选 选了才删 选了才删
所有身份都删了 进现有 30 天保留期,到期匿名化(purge.go) — —
退出登录 / 所有设备退出 — 只退 HOOP 只退 Loop

注销页让用户选「只注销这个 App」或「全部一起注销」。

7. 登录凭证

今天 HOOP 的凭证是 HS256 的 JWT,没有「给哪个 App」,只验签法和签发者(token.go:24-67)。 - HOOP 的凭证格式一字不改:现有未过期、未撤销、且身份状态正常的 HOOP 凭证继续有效,不因这次升级强制重新登录。旧的续期记录 product 回填为 hoop;旧客户端不传 product 默认 hoop,已有的续期链不受影响。 - Loop 的凭证另签:Ed25519,写 aud: loop。HOOP 中间件只认 HS256 + 签发者 hoop,Loop 的凭证到 HOOP 自然被拒。 - Loop 后端只拿公钥(/.well-known/jwks.json),本地验,拿不到任何能签凭证的秘密。Loop 后端只接受下面每一项都通过的凭证(只查 aud 不够):签名有效;签法固定 EdDSA;签发者是可信的 HOOP;aud: loop;exp / nbf 时间有效;kid 只用来在事先配置的可信公钥列表里选钥匙,不接受凭证里指定的任何远程密钥地址;最后查身份在 Loop 的状态。公钥可轮换(新旧两把同时发布一段时间)。 - 续期凭证(refresh_tokens,000001_init.up.sql:133-145)加 product 列:Loop 的续期只能换 Loop 的凭证。 - HOOP 凭证不变,不等于 HOOP 接口行为完全不变:续期、退出、所有设备退出、身份状态都按产品隔离 —— Loop 退出不影响 HOOP,反过来也一样。 - 每次请求还要查状态:HOOP 查「用户没删且个人身份在 HOOP 正常」(今天查用户状态,blocked.go:163-167);Loop 查「用户没删且商业身份在 Loop 正常」。

8. 要提前知道的坑

  1. Apple 登录两个 App 对不上号。Apple 编号按开发者团队分:HOOP CL4ZMA59T6,Loop YZD2N9XXN4(LOOP记忆.md §7);隐藏邮箱的转发地址也不同。对不上的人,登录后给「关联已有的 HOOP 账号」入口(验证码确认)。Google 的编号全局一样,但 Jeff 用 Gmail 测试前仍要核两件事:HOOP 里他的 Gmail 是已验证的;Loop 的 Google 登录凭证 aud 是 Loop 的 App 编号、HOOP 按产品认它(H4)。
  2. 今天的删号、封号改的是用户本身。管理员删号走 auth.SoftDeleteTx(repository.go:611-679),封号走 risk.go:211-221,都改 users.status。不改的话 HOOP 一删号 Loop 就登不上(任务 H3)。
  3. 两家公司共用用户资料要写进条款。Loop 的 Apple 账号是 ONECARE WELLNESS SDN. BHD.;两边隐私条款写明「用 HOOP 账号登录」,注册页也写一句。
  4. 重复账号(Jeff 10:45 / 10:48 定):
  5. 第一版不做数据合并。一个用户可以绑好几种登录方式;HOOP 已有登录后绑 Apple / Google(/v1/auth/apple/bind、/v1/auth/google/bind,handler.go:39-40)。注册时邮箱和已有用户的已验证邮箱一样,就提示「你已经有账号了」。
  6. 关联必须发生在建号之前(Cora 11:04):Apple 隐藏邮箱对不上时,Loop 在建新用户之前先问「你已经有 HOOP 账号吗?」,用验证码证明后把 Apple 绑到那个用户上,不建新号。一旦已经变成两个独立用户,绑定解决不了,那就是第二版的合并。
  7. 第二版合并:另一个号收一次验证码确认是本人;被封的号登不进去,本来就合并不了;合并不能撤销;Sparks 合在一起;旧用户 ID 留「旧 → 新」对照。Loop 新做的数据挂在身份上,合并时只要把身份换个用户挂。

9. 不影响 HOOP 现在的登录:守则(回答第 1 节第 13 条)

  1. 只加不改:新表、新列、新接口;不删、不改名任何现有列和接口。迁移号从 backend/migrations/LEDGER.md 取。
  2. 默认等于今天:登录请求不带 product 就是 HOOP、走今天的每一步;HOOP 凭证、签发者、密钥都不动,没有人会被踢下线。
  3. 先补数据、再换读法:先上表并回填(H1),核对「个人身份在 HOOP 的状态」和 users.status 一行不差,才让中间件改读新表(H2)。
  4. 开关和回滚分两种情况(Cora R1):还没有任何 Loop 用户时,可以整套回到旧版;一旦有了 Loop 用户,回滚只关 Loop 入口,身份隔离(H2、H3)不回退 —— 回到按 users.status 判断,会让「只有 Loop 身份的人绕过邀请门」「注销串 App」重新出现。Loop 相关的路(product: loop、Loop 凭证、只读接口)全挂在一个服务器开关后面,关掉就回到今天。
  5. 会红的回归测试,每一步上线前跑:HOOP 验证码 / Apple / Google 登录、续期、退出、所有设备退出、邀请门、官方频道、管理员删号 / 恢复 / 封号、用户注销,结果和今天一样。
  6. 上线顺序:只走 tools/deploy_backend.sh;先测试环境、再生产;每步可单独回滚。

10. 任务清单

「测试必需」= Jeff 第 1 节第 14 条那个测试之前必须做完。认领人是建议,第 14 节讨论定。

# 做在哪 做什么 依赖 测试必需 建议认领
H0 全部 盘点现状:HOOP / hoop-core / Loop 三边的用户、好友、身份相关表和接口,旧凭证契约;写进本页附录 — ✅ Vega + Gene Loop
H1 HOOP 后端 建 identities / personas / identity_app_access,回填现有用户(个人身份 + HOOP 状态;开发者补创作者身份);回填可重复跑;核对一行不差;不替没用 Loop 的人开商业身份 H0 ✅ Vega
H2 HOOP 后端 HOOP 中间件改读「个人身份在 HOOP」;第 9 节回归测试全绿 H1 ✅ Vega
H3 HOOP 后端 + HOOP App 管理员删号 / 封号、用户注销、30 天匿名化改成按身份;注销页加「只注销这个 App / 全部」;必须在 Loop 接上共用用户之前完成,回滚也要保留这层隔离(Cora 11:04) H2 ✅ Vega(App 部分等 NOVA 出包)
H4 HOOP 后端 登录入口加 product(任意 product 值都不能绕过邀请门,未知值拒绝);验 Apple / Google 时按产品认对应的 App 编号(HOOP 和 Loop 各一套,Gene Veyra 11:03);Loop 品牌验证码;Loop 跳过邀请门和官方频道;5c「只有商业身份的人开 HOOP 要过邀请门」 H2 H3 ✅ Vega
H5 HOOP 后端 Loop 凭证(Ed25519 + aud: loop)、公钥地址、续期按产品分 H4 ✅ Vega
H6 HOOP 后端 给 Loop 的只读接口:我的用户 + 身份 + 分身;我的好友(带对方默认名字头像、是否已在 Loop);身份状态 H1 ✅ Vega
H7 HOOP 后端 分身接口(建 / 改 / 删分身、群里选分身);现有 personas 和 user_profiles 接进去 H1 否 Vega
H8 HOOP 后端 绑定登录方式支持 product: loop;同邮箱提示 H4 否 Vega
L0 Loop 后端 盘点 Loop 现有用户和内容;按已验证邮箱 / 手机号对上 HOOP 用户,对不上的在 HOOP 建;建「Loop 旧 ID → HOOP 用户 ID」对照并换掉旧记录;旧用户表改只读、不再登录,不删(删之前问 Jeff 和 vzhai) H1 H3 ✅ Gene Loop
L1 Loop 后端 停用 Loop 自己的注册和密钥;接 hoop-core 里的「公钥验证 + 检查给哪个产品」(和 H5、C1 一起做);「群 + 成员 → 分身」表 H5 C1 L0 ✅ Gene Loop
L2 Loop App 登录页走 HOOP 登录引擎(product: loop),Apple / Google 用 Loop 的 App 编号 H4 H5 ✅ Gene Loop / Gene Veyra
L3 Loop App 好友列表显示 HOOP 好友(已在 Loop / 还没加入;沿用 HOOP 拉黑和可见规则;不代建身份;先显示本机缓存、自动重试、友好离线) H6 L1 ✅ Gene Loop / Gene Veyra
L4 Loop App 商业身份的分身:建、换头像、每个群选分身 H7 否 Gene Loop / Gene Veyra
L5 Loop App Apple 隐藏邮箱:建号之前问「已有 HOOP 账号吗」,验证码确认后绑定,不建新号 H8 否 Gene Loop / Gene Veyra
C1 hoop-core 共用能力(登录按产品、凭证目标校验、好友只读)先在 core 改,HOOP 和 Loop 再接 H4 H5 H6 ✅ Vega
G1 上线 测试环境 → 生产灰度(开关只对测试账号开)→ 全开;每步的回滚办法写好 以上 ✅ ✅ Vega + Gene Loop
D1 两边知识库 HOOP:本页 + docs/00-目录.md;Loop:loop 仓库 docs/00-目录.md 加一行指向本页(同一份,不复制) — — Vega(HOOP)/ Gene Loop(Loop)
R1 审查 每个 H 任务上线前审一轮,重点第 9 节守则 — ✅ Cora
T1 测试 第 11 节,Jeff 亲测 以上 ✅ — Jeff

11. Jeff 的测试怎么走

  1. 装上 Jeff Sora 出的、含 L2 和 L3 的那一版 Loop 测试包(版本号到时写在这里)→ 选 Google → 用他 HOOP 的 Gmail 登录。
  2. 不需要邀请码,直接进 Loop;后台看到:同一个用户 ID,多了商业身份和「商业身份在 Loop:正常」。
  3. 好友列表:HOOP 里的每个朋友都在,显示 HOOP 的名字和头像;没装 Loop 的标「还没加入」。
  4. 打开 HOOP:一切照旧,登录没变,没被踢下线,资料没变。
  5. 用另一个只在 Loop 注册的测试号打开 HOOP:被邀请门挡住(证明没有绕过)。 另外几条必须核到的(Cora 10:58):
  6. 你用 Gmail 登录 Loop 拿到的是原来那个用户 ID,没有新建。
  7. Loop 好友列表和 HOOP 里你看得到的好友逐人对照、总数一致,包括没用 Loop 的人 —— 不能只看第一页。
  8. 断网、重试、同时点两次,都不会多出第二个用户。
  9. HOOP 封了某个身份,Loop 不受影响;反过来也一样。
  10. 「有这个好友」不会被显示成「他已经开了 Loop」。
  11. 在 Loop 登录、续期、退出、注销,和在 HOOP 退出,互不影响。
  12. 登录请求乱填 product 也绕不过邀请门。
  13. L0 迁移前后,HOOP 的老登录方式(验证码 / Apple / Google / 续期)回归全绿。
  14. 把 HOOP 账号服务关掉时,Loop 登录失败并给友好提示,不在本地另建用户。 做完这些,第 10 节标 ✅ 的任务才算交付。

12. 多个身份,这样设计好不好

好,比只加一个「能不能进」的开关更好:Meta 的账户中心就是一个账号、Facebook 和 Instagram 各有主页和封禁状态;HOOP 自己已经有「开发者 = 第二身份」和「分身」;Jeff 定的规则不用写特例就成立。

13. 还没定的

  1. ~~好友共用~~ —— 已定:Jeff 10:53 的测试要求就是看得到没用 Loop 的 HOOP 好友;好友关系共用,朋友圈和动态各 App 管各的。
  2. 已有 HOOP 的人第一次进 Loop,商业身份的默认分身要不要先填上 HOOP 的名字和头像(他可以再改)?
  3. ~~注销默认~~ —— 已定:默认只注销当前这个 App(Jeff 08:57「删了 HOOP,Loop 还是要保留」)。
  4. Loop 的后端放哪台机器(LOOP接口方案.md §9 第 1 条)。
  5. Jeff 的测试要看到真实朋友,就要读生产的用户和好友数据(只读)。不必等全部任务都上生产才开始验:可以先用受控的现有服务读取做验收;凡是必须上生产的新改动,逐项验过开关和回滚再发(Cora R1)。
  6. 朋友圈 / 动态要不要跨 App 同步(第 4 节只定了好友关系共用)。

14. 讨论分工(Jeff 10:53「每一个细节你们各自讨论然后定下来」)

谁 负责讨论定什么
Jeff Vega 本文档;HOOP 侧 H1–H8;第 3、7、9 节
Gene Loop Loop 后端 L1;Loop 知识库 D1;Loop 现有用户表怎么停用
Gene Veyra Loop App L2–L5;第 4 节好友在 Loop 里怎么显示
Jeff Cora 审查 R1;第 6 节每种情况、第 8 节坑有没有漏

每条讨论定了,就改本页对应那一节和第 10 节那一行;不另开卡,都记在卡 2610.1065。

15. English summary

This is the project-wide User Identity design, not a Loop-only document. HOOP, Loop and every future app identify people this way.

Model. One User · One Login · Multiple Identities · Multiple Personas. The User exists once, in HOOP: email, phone, Apple and Google all lead to it. Identities are personal (used in HOOP), business (used in Loop) and creator; each has its own profile fields. Personas are faces (name + avatar) under an identity, and each group remembers which persona you use. Each app decides which identity kinds it accepts, and a ban targets "this identity in this app". New HOOP tables: identities, personas, identity_app_access. Existing user_profiles, personas and developers map in.

One copy (Jeff: "the user must be one, identical, most complete"). User, identities, personas and friendships live only in HOOP. Loop's database stores no user copy, only Loop content keyed by identity ID. It reads users via HOOP read-only APIs with short caching. Loop's current own user table must be retired (L1).

Seeing HOOP friends in Loop (Jeff's test). Friendship is user-to-user and stored once in HOOP. Loop shows your HOOP friends with their HOOP default name and avatar, marked "on Loop" or "not on Loop yet". This changes v3's "friends per app"; Jeff to confirm.

Login. The existing endpoints gain product (missing = HOOP, unchanged). Loop login finds or creates the User, skips HOOP's invite gate and official channel, and creates the business identity. A Loop-only user opening HOOP must pass the invite gate. Loop tokens are Ed25519 with aud: loop. The Loop backend accepts a token only if every check passes: signature, alg fixed to EdDSA, trusted issuer, aud, exp/nbf, kid resolved against a pre-configured trusted key set (never a key URL from the token), and app status. HOOP token format is untouched: unexpired, unrevoked tokens with an active identity stay valid, old refresh rows backfill product = hoop, and clients that omit product keep their refresh chains.

Do not break HOOP login. Changes are additive only. Requests without product behave as today. Backfill first and verify row-for-row before switching reads. Loop paths sit behind a server switch. Regression tests run before every step, and deployment goes through tools/deploy_backend.sh, test before production.

Code sharing is not data sharing. Loop runs hoop-core code against its own database (its own users, friendships, personas tables). The goal is runtime use of HOOP's central account and friends services; Loop's local tables are retired and Loop never creates a local user if HOOP is down. Loop caches only minimal display fields, invalidates on change or revocation, and fails closed. Shared capabilities change in hoop-core first (C1).

Tasks. H0 inventory; HOOP: H1 tables and backfill, H2 middleware read, H3 per-identity delete/ban, H4 product login, H5 Loop tokens, H6 read-only APIs, H7 persona APIs, H8 login-method binding. Loop: L1 retire own users, L2 login page, L3 friends list, L4 personas, L5 Apple linking. Also C1 hoop-core, G1 staged rollout with rollback, D1 knowledge bases, R1 review, T1 Jeff's test (same user ID, full friend list matched one by one including non-Loop friends, no duplicate users on retry, bans don't cross apps). Needed for the test: H0, H1, H2, H3 (isolation must land before Loop joins), H4, H5, H6, C1, L0 (inventory and map existing Loop users, keep the old table read-only), L1, L2, L3, G1. First version: hoop-core conversations stay on HOOP user IDs, with a separate (group, member) → persona table, validated against the member and the app; new Loop data hangs off identity IDs. Apple hidden-email linking must happen before a new user is created; once two users exist, that is a v2 merge.

Review fixes (Cora R1). identity_app_access gains pending. Per-app deletion only changes app access; identities.deleted_at means the whole identity is gone. There is one rule for reading default personas: personal → user_profiles, others → personas.is_default (unique). Creating the business identity, its default persona and its app access is one transaction, idempotent on retry. Email matching trusts only provider-verified emails, requires HOOP's copy to be verified, and needs a code confirmation before binding. Loop token checks cover alg, issuer, aud and kid, with key rotation; refresh, logout and status are isolated per product; cache revocation is pushed immediately. Rollback before any Loop user exists can revert fully; after that, only the Loop entry closes and identity isolation stays. H4 depends on H2 and H3. More acceptance checks: per-product logout independence, an arbitrary product cannot pass the gate, regression before and after L0, and no local user when HOOP is down.

Traps. Apple IDs differ per developer team (Gmail testing is unaffected). Today's delete and ban change the User itself. Both privacy policies must say "sign in with your HOOP account". Duplicate accounts: v1 binds several login methods; merging is v2.

Open. Shared friendships? Pre-fill the Loop default persona from HOOP? Default deletion scope? Loop server location? The test needs H1–H6 on HOOP production (additive, behind a switch).