外观
VMax 服务端逻辑数据模型
1. 设计原则与状态
- 来源 MUST:PostgreSQL 是权威存储;服务端只保存路由、公开材料、密文信封和必要运维状态。
- 来源 MUST:私钥、消息正文、会话密钥、消息预览、主密码、假密码和备份密码不得入库。
- 来源 MUST:队列按
recipient_device_id隔离配额;ACK 与删除状态事务化;写接口幂等结果保留 24 小时。 - 已选架构:Redis 承载后台任务队列,但每个需要可靠执行的业务意图先写入 PostgreSQL outbox;Redis 记录是可重建投影,不属于权威业务数据。
- 范围边界:S3 兼容对象模型属于 P1 附件能力;MVP 不创建上传会话、不接受图片/视频/文件对象。
- 建议方案:使用 UUID/ULID 类公开标识、数据库内部 surrogate key 与行级约束;实际标识编码待 ADR-003/ADR-004 决策。
- 已验证事实:当前没有迁移文件或数据库实现,以下为逻辑模型,不代表已部署 schema。

图中实体关系是审阅摘要,字段、约束和待决物理结构仍以下文为准。查看图示说明。
2. 核心实体
2.1 instances
| 字段 | 语义 | 约束 |
|---|---|---|
instance_id | 实例公开稳定标识 | 唯一;格式待总控决策 |
signing_public_key | 实例长期签名公钥 | 只存公钥;版本化 |
policy_version | 注册、配额、保留策略版本 | 单调递增 |
created_at | 创建时间 | 数据库时间 |
实例签名私钥必须由独立 secret 管理,不得作为普通表字段、镜像层或数据库备份内容保存。
2.2 identities 与 devices
| 表 | 关键字段 | 约束与隐私 |
|---|---|---|
identities | identity_id, public_address, identity_public_bundle, status | 不保存姓名、邮箱、手机号、私钥;地址格式待 ADR-003 |
devices | device_id, identity_id, device_public_key, status, revoked_at | MVP 每身份一个活动设备;模型允许未来扩展但不启用多设备能力 |
prekeys | device_id, key_id, public_key, state, reserved_at, consumed_at | 只存公钥;一次性消费必须原子化;编码待 ADR-004 |
预密钥状态建议为 available -> reserved -> consumed,并为超时 reservation 提供回收或明确消耗策略。最终协议语义属于密码协议线路,待总控决策。
2.3 auth_challenges 与 token 状态
auth_challenges 至少保存 challenge 的哈希、设备、实例/域上下文、用途、过期时间和消费时间。challenge 默认 60 秒且只能成功消费一次。长期 bearer token 不允许;刷新模型与撤销粒度待认证设计冻结。
2.4 envelopes
建议字段:
text
envelopes
envelope_pk database internal key
message_id sender generated public id
recipient_device_id routing target
protocol_version opaque envelope version gate
envelope_bytes bytea
envelope_size validated byte count
padding_class bounded enum or null
accepted_at database receive time
expires_at min(instance policy, sender requested TTL)
acked_at nullable
cleanup_after nullable关键约束:
- 唯一键至少覆盖接收设备和
message_id,避免不同设备或未来多设备语义被提前错误绑定。 envelope_size由服务端实际字节数计算,不信任客户端声明。expires_at > accepted_at,且不超过实例策略上限;默认 7 天,可配置范围 1–30 天,发送端可请求更短 TTL。- ACK 不立即依赖物理删除成功;事务先记录
acked_at/cleanup_after,清理任务最长 1 小时内删除。 - 服务端不得解析
envelope_bytes内的正文或生成预览;只验证外层版本、长度和路由字段。
2.5 idempotency_records
text
idempotency_records
actor_scope_hash
operation
idempotency_key_hash
request_fingerprint
response_status
response_body_safe
resource_reference
created_at
expires_at- 唯一键为
(actor_scope_hash, operation, idempotency_key_hash)。 - 相同键与相同请求指纹返回首次结果;相同键配不同指纹返回稳定冲突错误。
response_body_safe只能保存可安全重放的最小响应,不复制密文或令牌。- 记录保留至少 24 小时;过期后的行为必须在接口契约中声明,不能默认为永久去重。
2.6 message_acks 与游标
MVP 可将 delivered ACK 状态放在 envelopes;若需要事件审计或批量性能,可拆成 message_acks。无论物理形态如何,重复 ACK 必须成功且不得延长数据保留。
拉取游标应是服务端签名或不可篡改的 opaque token,编码稳定排序键,例如 (accepted_at, envelope_pk)。客户端不得依赖或构造数据库主键。
2.7 push_routes 与 outbox_events
| 表 | 保存内容 | 禁止内容 |
|---|---|---|
push_routes | 随机 wake_route_id、设备关联、模式、Gateway 引用、状态 | 消息正文、联系人名、会话 ID |
outbox_events | event_id、聚合类型/引用、任务类型、版本、状态、尝试次数、下次重试时间、创建时间 | 消息正文、私钥、完整密文副本、发送方可读身份 |
官方 Gateway 是否直接保存 APNs token、以及 token 的加密/分离存储方式,属于 ADR-006 的待总控决策。
outbox_events 建议状态为 pending -> published -> completed,另有 retry_wait/dead_letter。业务事务只创建 pending;dispatcher 发布带相同 event_id 的 Redis job;Worker handler 以 event_id + task_type + task_version 幂等执行。Redis job payload 只带定位所需的内部引用和版本,不复制消息密文、token 或用户可读字段。
2.8 object_uploads 与 encrypted_objects(P1)
只有启用附件范围时才启用对象存储相关迁移和端点:
object_uploads:短期上传会话、所有者设备、预期密文字节数、对象 key、过期时间和状态。encrypted_objects:对象引用、密文字节数、客户端提供的密文哈希、TTL、完成/删除状态。
不得保存文件名明文、MIME 推断结果、缩略图明文或文件密钥。对象 key 使用服务端随机值,不嵌入地址、设备或原文件名。S3 生命周期与数据库清理通过 outbox 对账,避免孤儿对象;MVP schema 可以保留独立的未来迁移文件,但默认部署不得执行或启用附件表。
2.9 audit_events
只保存稳定事件类型、请求 ID、短期 actor 哈希、结果、配置版本和时间。不得保存密文全值、完整地址/IP 长期历史、token、APNs token、备份路径或任何口令材料。审计事件不是聊天历史。
3. 索引与并发
- 拉取:
(recipient_device_id, accepted_at, envelope_pk)的活动信封索引。 - 过期清理:按
expires_at的部分索引;分批FOR UPDATE SKIP LOCKED处理。 - ACK 清理:按
cleanup_after的部分索引。 - outbox:按
(state, next_attempt_at, event_id)建立领取索引;dispatcher 使用短事务与FOR UPDATE SKIP LOCKED,不得在持锁期间调用 Redis。 - 配额:每设备待收数量与字节必须在同一事务内检查和更新,避免并发超配。实现可采用配额计数行锁或可验证的聚合查询。
- 一次性预密钥:通过条件更新/行锁确保只有一个请求取得可消费材料。
4. 生命周期与删除语义
| 数据 | 默认生命周期 | 删除语义 |
|---|---|---|
| 未送达密文信封 | 7 天,策略可设 1–30 天 | 到期后后台清理;发送端可请求更短 TTL |
| 已 ACK 信封 | 立即进入清理,最长 1 小时 | 重复 ACK 仍成功 |
| 一次性预密钥 | 成功消费后立即失效 | 协议状态要求待总控决策 |
| challenge | 60 秒或成功使用 | 单次消费 |
| 幂等记录 | 24 小时 | 仅保留安全响应摘要 |
| 限流状态 | 不超过 24 小时 | 不得用于长期画像 |
| 脱敏服务日志 | 默认 7 天 | 由运维策略自动清理 |
| Redis 任务 | 短期直到完成/超时 | 可由 PostgreSQL outbox 重建,不作为业务保留副本 |
| S3 密文对象 | P1,跟随附件 TTL/引用 | MVP 不启用;具体保留待附件范围批准 |
逻辑删除、备份副本和底层介质可能延迟物理清除。对外只能承诺定义的逻辑删除与保留策略,不得宣称可验证物理擦除。
5. 数据库迁移约束
- 每次迁移具有唯一版本、前置版本和数据兼容说明;CI 在真实 PostgreSQL 上验证升级和回退/安全失败。
- 在线变更采用 expand-contract:先新增可空字段/新表,再双读或回填,最后在旧版本退场后收紧约束。
- 不重解释既有
protocol_version、状态枚举或游标含义;需要改变语义时创建新字段或新 API 主版本。 - 大表回填限速、可恢复,并监控锁等待、复制延迟和磁盘余量。
- 备份恢复后必须验证迁移版本、信封数量/字节、过期队列和幂等约束。
- outbox 与 Redis job schema 分别版本化;滚动升级期间新 Worker 必须能安全识别旧任务,未知任务版本进入隔离而不是丢弃。
- P1 对象表和 S3 lifecycle 作为独立能力迁移,不能随 MVP 核心 schema 默认启用。
6. 待总控决策
- 身份与设备标识格式:UUID、ULID 或定长随机 bytes。建议以不可枚举随机 bytes 为模型、展示编码跟随 ADR-003;验证碰撞、排序泄漏和跨语言解析。
- 协议对象编码:数据库只保留版本与 opaque bytes;canonical encoding 跟随 ADR-004;验证跨语言固定向量与非规范输入拒绝。
- 备份格式:数据库备份与客户端
.vmaxbak是不同边界;客户端格式跟随 ADR-007;验证不可互相冒充或误导恢复能力。 - 配额具体值:需要压测与成本模型后冻结。建议至少支持每设备信封数、总字节、单信封字节三维限制;验证并发超限不会影响其他设备。
- Redis 队列库与 job envelope:选择前审查许可证、维护状态、确认语义与失败恢复;job envelope 不复用 E2EE canonical encoding。
- 附件对象元数据与保留:在 P1 独立立项后决定;不得因已选 S3 架构把图片、视频或文件并入 MVP。