AI 参与说明(Agent:
/root、/root/convex_map、/root/architecture_runtime、/root/cache_dataflow):本文由 Agent 协助完成源码盘点、数据流追踪、官方资料核验和可复现性检查。分析对象固定为 ClawHub 提交fb0ef1d,依赖版本、代码行号与架构判断均以该快照为准,资料核验于 2026-08-23。本文没有读取生产 deployment、Dashboard、日志或私有环境变量,因此生产流量、成本和部署状态不在可验证范围内。
适用范围:本文分析的是当前 ClawHub 这类“公开目录 + CLI Registry + 发布审核 + 搜索与统计”应用,不把它泛化成所有 Convex 或 PostgreSQL 项目的标准架构。历史流量优化过程与指标另见以 ClawHub 为例:如何在真实流量下优化 Convex。
先说结论#
ClawHub 的 Convex 用法,已经远远超过“用 Convex 存几张表”:它把 Convex 同时当作 transactional database、serverless function runtime、reactive data layer、HTTP API、Auth、File Storage、Scheduler/Cron、Vector Search 和应用级队列状态存储。TanStack Start/Vercel 主要负责 Web SSR、页面路由、OG 图片和一层 HTTP proxy;绝大多数业务状态机在 Convex 内完成。
当前源码最值得复用的不是某个 API,而是下面这条设计主线:
规范化写模型
→ 原子 Mutation
→ Trigger 同步轻量 digest / snapshot
→ 有界、索引化、尽量 one-shot 的公开读取
高频计数事件
→ append / dedupe
→ 分钟级聚合
→ 小时级回写主文档
→ 避免大批 reactive subscription 同时失效它有缓存,但没有一个 Redis 式的统一缓存层。缓存来自 Convex Query Cache、选择性的 reactive subscription、React 组件内 Map、HTTP Cache-Control/ETag,以及多个派生读模型。skillSearchDigest、Trending snapshot 这类数据更准确地说是 materialized projection,不能与可随时丢弃的普通缓存混为一谈。
至于 Convex 与 Drizzle + PostgreSQL:当前 ClawHub 更适合继续使用 Convex。原因不是 PostgreSQL 能力不足,而是这里比较的实际对象不是“Convex database vs PostgreSQL database”,而是:
Convex 一体化 backend
vs
Drizzle + PostgreSQL + API runtime + Auth + Object Storage
+ Realtime gateway + Queue/Worker + Cron + Rate Limiter
+ FTS/pgvector + Preview 环境 + 运维体系如果主要问题变成复杂 SQL、强 Foreign Key、跨域分析、BI、合规控制或供应商可移植性,PostgreSQL 会更有优势;但对现有项目直接迁移属于平台重写,而不是更换 ORM。较低风险的演进方式,是保留 Convex 承担 transactional/realtime 主链路,把统计事件或快照导出到 PostgreSQL/warehouse 做分析。
1. 分析快照与规模#
本文使用的源码快照如下:
| 项目 | 值 |
|---|---|
| Repository | openclaw/clawhub |
| Commit | fb0ef1d21eab78ad2b9da69f48045d7747cc73c1 |
| Commit 时间 | 2026-08-22 |
| Convex | 1.44.0 |
| TanStack Start / Router | 1.168.46 / 1.170.29 |
| React | 19.2.8 |
| Vite | 8.2.1 |
| Package manager | Bun |
静态盘点可以帮助理解项目规模,但不等同于 production deployment 的可达 API:
convex/schema.ts本地定义 108 张应用表;当前@convex-dev/auth的authTables再补入 4 张未被本地覆盖的表,因此该依赖快照的有效 Schema 共 112 张表。- 本地 Schema 有 515 处普通
.index()声明,叠加当前 Auth 新增表后为 521 个普通 Index;另有 4 个 full-textsearchIndex和 1 个 Vector Index。这反映了“用访问模式塑造 Schema”的强烈倾向。 convex/http.ts有 73 个显式http.route()注册,另有 Convex Auth routes 和通用OPTIONS处理。convex/crons.ts注册 37 个 interval jobs;Preview 或显式设置CLAWHUB_DISABLE_CRONS=1时不会注册。- 按注册函数的 TypeScript 类型统计,源码有 937 个 Query/Mutation/Action:222 个 public、715 个 internal;另有 69 个本地 HTTP Action handler。这里包含 dev seed、test fixture、migration、repair 和 backfill;
public也只表示客户端可寻址,不等于匿名开放,不能把这些数字理解成“面向用户的生产接口数量”。
后续判断优先依据具体调用链,而不是只依据这些数量。
2. Repository 与部署边界#
ClawHub 是 Bun workspace monorepo,主要边界是:
| 目录 | 职责 | 运行或发布位置 |
|---|---|---|
src/ | TanStack Start routes、React components、SSR 与浏览器逻辑 | Vercel/Nitro |
server/ | Nitro handlers、Convex proxy、OG image、少量 server-only 集成 | Vercel/Nitro |
convex/ | Schema、Functions、HTTP Actions、Auth、Cron、Storage 与业务状态机 | Convex deployment |
packages/schema/ | Web、Backend、CLI 共享的 schema 与 route constants | 编译进各消费者 |
packages/clawhub/ | 面向用户的 clawhub CLI | npm / 用户机器 |
packages/clawhub-admin/ | 私有运维 CLI | Operator 环境 |
scripts/ | Seed、部署、扫描 worker、数据集与证明脚本 | CI 或 Operator 环境 |
Repository README把 Web、Convex 和 Search 简化成三行,真实运行结构可以画成下面这样:
flowchart TD
U[Browser]
C[clawhub CLI]
V[TanStack Start on Vercel<br/>SSR · Router · Nitro]
P[Nitro Convex proxy<br/>/api and /v1/feeds]
RC[ConvexReactClient<br/>reactive functions]
HC[ConvexHttpClient<br/>one-shot functions]
HS[Convex HTTP Actions<br/>*.convex.site]
CF[Convex Queries · Mutations · Actions]
DB[Convex Database]
FS[Convex File Storage]
SC[Scheduler · 37 Cron jobs]
E[GitHub · OpenAI · VirusTotal<br/>Resend · skills.sh · workers]
U --> V
V --> HC
U --> RC
U --> P
C --> P
P --> HS
RC --> CF
HC --> CF
HS --> CF
CF --> DB
CF --> FS
SC --> CF
CF --> E这里有两条容易混淆的网络路径:
- React
useQuery、useMutation、useAction和 SSR 的ConvexHttpClient直接访问VITE_CONVEX_URL,即 Convex functions endpoint。 - CLI 与
/api/v1/*请求经 Nitro proxy 转发到VITE_CONVEX_SITE_URL,即 Convex HTTP Actions endpoint。/v1/feeds/*也会映射到对应/api/v1/feeds/*。
所以 Vercel 不是业务数据库前的统一 API gateway。浏览器中的 direct Convex calls 可以绕过 Nitro;与 HTTP route 绑定的应用级限流,也不会自动包住这些 direct calls。
2.1 Web 状态如何分层#
项目没有引入 TanStack Query、Redux 或 Zustand。TanStack 在这里主要负责 Start、Router、SSR 和 URL search params;远程状态由 Convex React 管理:
- 全局各创建一个
ConvexReactClient与ConvexHttpClient。 ConvexAuthProvider包住应用。- 详情、用户、权限和管理页面主要使用 reactive
useQuery。 - 首页、Skills browse 和 Search 等高流量公开路径主要使用 one-shot
ConvexHttpClient.query/action或useAction。 - 筛选、tab 与 pagination 状态尽量写入 TanStack Router URL search params,短期交互状态留在 React。
这种选择不是“订阅好”或“订阅坏”,而是按数据的新鲜度语义拆分:权限或发布状态变旧可能导致错误操作,适合 reactive subscription;公开列表在一次页面停留期间允许短暂变旧,one-shot 更能控制失效放大。
3. Convex 在这个项目里究竟负责什么#
3.1 四类 Function 不是同一种 RPC#
| Function | ClawHub 中的角色 | 关键语义 |
|---|---|---|
| Query | 读取详情、列表、权限上下文与内部批次 | Read-only、deterministic、自动 Query Cache;React client 可订阅依赖变化 |
| Mutation | 发布、更新、授权、状态流转、dedupe 与批次提交 | 对数据库读写是 atomic transaction;冲突时由 Convex 进行 OCC retry |
| Action | OpenAI embedding、GitHub/VirusTotal/Resend 等外部 I/O、长流程编排 | 可以调用 Query/Mutation,但整个 Action 不是一个数据库事务,结果也不是 Query Cache |
| HTTP Action | CLI/API、feeds、downloads、webhooks 与 discovery | 接收标准 Request 并返回 Response,再通过 convex/http.ts 绑定 route |
官方语义分别见 Queries、Mutations 与 Actions。ClawHub 的重要实践是:外部 I/O 放在 Action,真正建立不变量的状态提交放回小型 Mutation;这样不会把“调用 OpenAI 成功”和“数据库提交成功”误当成同一个原子操作。
此外,项目只安装两个 Convex Components:@convex-dev/migrations 与 @convex-dev/rate-limiter。其它队列、lease、snapshot 和 worker 状态主要由应用自己的表与 Functions 实现,并不是隐藏在某个大型 Component 中。
3.2 Schema 是按领域拆表、按读取路径加索引#
108 张应用表大致可分为六组:
| 领域 | 代表表 | 目的 |
|---|---|---|
| Identity 与 ownership | users、publishers、publisherMembers、publisherInvites、githubOrgMemberships、apiTokens | GitHub identity、个人/组织 Publisher、成员关系与 CLI token |
| Registry write model | skills、skillVersions、packages、packageReleases、aliases、badges、upload tickets | Skill/Plugin 的规范化来源、版本与发布状态 |
| Search 与 public read model | skillSearchDigest、curated/topic digests、package digests、skillEmbeddings、embeddingSkillMap | 轻量列表、检索、排序与向量结果 hydration |
| Stats 与 ranking | stat events、daily/hourly stats、leaderboards、global stats、Trending pools/snapshots/items | 把高频事件与公开读模型隔离 |
| Moderation 与 work queues | scans、evaluations、publish attempts、reports、appeals、abuse signals、leases、dedupe rows | 安全审核、人工处理、重试、租约和幂等 |
| External mirrors | GitHub sources/content/candidates、skills.sh catalog/mirror runs/pages/digests/details/facets | 外部 Registry 同步、证明与灰度发布 |
Convex 的 v.id("table") 给 TypeScript 和运行时 validator 提供 typed ID,但它不是 PostgreSQL 的 Foreign Key constraint:不会自动提供级联删除、可延迟约束或任意关系上的数据库级唯一性。ClawHub 主要用 Mutation helper、索引查询和状态机维护引用完整性。对 transactional path 这可以工作,但也解释了为什么它有大量 repair/backfill/internal functions;如果团队把“数据库必须拒绝任何孤儿关系”视为硬性要求,PostgreSQL 更自然。
3.3 Trigger 把写模型投影为读模型#
项目没有在每个 Mutation 中手写同一套 digest 同步,而是用 convex-helpers 的 Triggers 包装应用 Mutation。核心注册集中在 convex/functions.ts:
skills改变时,更新 Publisher stats,并同步或删除 Skill search digests。skillVersions只有删除状态或安全审核结果等影响公开性的字段变化时,才回到父 Skill 重建 digest。packages与packageReleases做等价处理;latest release 删除后还要重新选择 fallback。users或publishers的公开资料变化时,通过 Scheduler 按 100 条一页刷新其所有相关 digest。
skillSearchDigest 不是简单复制一份 skills。它把卡片需要的 owner、latest version、moderation、stats、topics 与 normalized tokens 放在一条较小的 document 中。Schema 注释给出了设计目标:
embeddingSkillMap约 100 bytes,用于把 vector result ID 映射回 Skill。skillSearchDigest约 800 bytes,用于替代搜索 hydration 时读取约 3–5 KB 的完整 Skill。- 完整
skillEmbeddings因包含 vector,约 12 KB;正常 hydration 不应先读它。
同步函数还会先比较字段,只有实际变化才 patch,避免 no-op write。这不仅省一次写入,也减少与这些 document 有依赖关系的 Query 失效。
需要注意三个边界:
- digest 是一致性责任,不是可以任意删除的缓存;删除后公开列表和搜索会缺数据,必须 backfill 或回源。
- 它显著减少 join,但没有完全消灭 join。某些公开 hydration 仍会查询 official Publisher,缺少或不一致的版本摘要也会回读 version/skill。
- Trigger 只覆盖使用自定义 Mutation wrapper 的写入。Migration、fixture 或特殊维护代码有意使用 raw server functions 时,必须明确承担同步或修复责任。
4. 五条代表性数据流#
4.1 公开 Browse:one-shot + digest + deterministic cursor#
公开目录是最能体现 Convex 优化思路的路径:
TanStack route loader / React interaction
→ ConvexHttpClient.query(listPublicPageV4)
→ indexed skillSearchDigest read
→ deterministic cursor page
→ public shape hydration
→ component-local MaplistPublicPageV4明确使用 convex-helpers 的 getPage(),目的是让同一参数对应稳定 cursor,使不同客户端更容易复用 Convex Query Cache。无过滤路径读取所需数量;有过滤路径也设置了最多 500 行、12 个 transport pages 等边界,而不是无限扫描。
浏览器端同样有限制:连续空 transport page 只跳过有限次数,防止筛选条件很稀疏时无界追页。首页则用一个以 kind/tab/category/fetchLimit 组合为 key 的组件级 Map 保存已取列表。
这条路径有两个细节:
- one-shot 只是“不建立长期 subscription”,并不意味着服务端 Query Cache 失效;Query 仍可按相同函数与参数复用缓存。
newtab 把“当前时间减 14 天”作为createdAfter参数,不同请求时刻会形成不同 args,因此它天然比固定参数更难跨用户共享 Query Cache。
4.2 详情页:SSR snapshot + reactive handoff#
详情页需要及时反映 latest version、删除、审核与 owner 变化,所以采用另一种策略:
- TanStack Start loader 用
ConvexHttpClient读取 metadata/README,给 SSR 首屏。 - 浏览器 hydration 后使用
useQuery订阅最新公开或 staff shape。 - reactive result 到达后替换 SSR initial value。
这比“全站都 one-shot”更合理:首屏不等待 WebSocket 才有内容,同时不会让已经打开的管理或详情页面长期展示过期状态。
4.3 Search:Action + OpenAI + Vector/lexical hybrid#
Search 必须访问 OpenAI,因此入口是 Action,而不是 Query:
flowchart TD
Q[Search text]
A[searchSkills Action]
O[OpenAI text-embedding-3-small]
V[Convex Vector Search<br/>1536 dimensions · visibility filter]
L[Exact · prefix · full-text<br/>lexical candidates]
X[skills.sh mirror candidates]
M[embeddingSkillMap]
D[skillSearchDigest]
R[Usage · trust · popularity ranking]
F[Final results]
Q --> A
A --> O
O --> V
A --> L
A --> X
V --> M
M --> D
L --> D
X --> R
D --> R
R --> FskillEmbeddings.by_embedding 是 1536 维 Vector Index,并以 visibility 为 filter field。Search 同时组合 exact slug、prefix、full-text、vector 与 skills.sh mirror candidates,再按 usage、trust 和 popularity 统一排序。OpenAI 失败时会退化为 lexical search,而不是让整个搜索不可用。
性能边界也很明确:不同召回路径用 take() 或显式 candidate limit,usage read 分小批执行。源码特别说明,一个 Convex Query 只能有一个 paginated read,因此多路召回不是把多个 .paginate() 硬塞进同一个 Query,而是改为有界读取和 Action 编排。
缓存方面要保守表述:
searchSkillsAction 的最终结果不属于 Query Cache。- 源码没有搜索结果缓存或 embedding memoization;正常搜索会尝试调用 OpenAI。
- Action 内部
ctx.runQuery的子读取仍遵循 Query 语义,但不能据此说“整个搜索已缓存”。 - CLI
/api/v1/search受 HTTP read quota 包裹;网页 directuseAction不经过那一层 HTTP route wrapper。
4.4 Publish:Upload ticket + File Storage + 原子状态提交#
Skill 是 SKILL.md 与支持文件的集合,不能只把正文塞进数据库。发布流程因此分成字节上传与 metadata 提交:
CLI 本地扫描、规范化和限制检查
→ 请求短期 upload ticket / upload URL
→ 文件直接写入 Convex File Storage
→ 服务端复核 size、SHA-256、MIME 与 Storage metadata
→ Publish Mutation 原子消费 ticket,创建 Skill Version
→ Trigger 更新 public digests
→ Scheduler/worker 启动静态扫描、VirusTotal 与评估流程当前限制为单文件 10 MiB、总计 50 MiB。skillVersions.files[] 保存 path、size、storageId、SHA-256 和 MIME,真正的 bytes 位于 Convex File Storage。Ticket 有过期和清理逻辑,Publish Mutation 再消费它,避免客户端仅凭一个任意 storageId 拼装版本。
外部安全检查不是数据库事务的一部分。正确性来自状态机、幂等键、lease、retry/watchdog 和最终 Mutation,而不是假设 GitHub、OpenAI、VirusTotal 与 Convex 可以进入同一个 distributed transaction。
4.5 Download、Stats 与 Trending:把热写入移出主文档#
下载路径把授权、存储与统计拆得很细:
flowchart TD
D[CLI / Browser download]
A[Convex authorization]
M[Short-lived signed manifest]
Z[Nitro validates JWS<br/>streams deterministic ZIP]
J[Best-effort metric<br/>0–60s jitter]
U[Per identity · target · day dedupe]
E[Append skillStatEvent]
S[Every 15 min<br/>daily/hourly aggregates]
P[Every 6 h<br/>leased skill document sync]
G[Trigger updates digest]
T[Every 1 h<br/>canonical Trending snapshot]
D --> A
A --> M
M --> Z
Z --> J
J --> U
U --> E
E --> S
S --> P
P --> G
S --> THosted artifact 的特殊路径是:Convex 完成可见性与权限判断,返回短期签名 manifest;Nitro 验证 Vercel OIDC/JWS 后,从 Storage 拉取各文件并流式生成 deterministic ZIP。这样 Vercel 不需要持有一份长期下载授权,同时 Convex Action 也不必独自承担所有 ZIP proxy 工作。
ZIP 成功路径会 best-effort 调度 metric,并加入 0–60 秒随机延迟分散写峰值。同一 identity、target、day 只记录一次,所以“downloads”是去重后的产品指标,不是原始 HTTP 请求数。匿名请求若没有可信 forwarded IP,也可能没有可用 metric identity。
统计写入没有直接竞争同一个 skills document:
- 每 15 分钟先把 events 聚合进 daily/hourly stats,不修改 Skill 主文档。
- 每 6 小时再用 committed lease、有限 batch 和 continuation 把累计变化写回 Skill。
convex/crons.ts直接写明,低频主文档同步是为了避免 thundering-herd reactive query invalidation。- 主文档实际变化后,Trigger 再更新 digest;字段没变则 digest guard 跳过
patch。 - canonical Trending 每小时从 rolling stats 与 digest 构建候选池和 snapshot,但对外 Trending HTTP response 仍是
no-store,读取时也会验证当前公开资格。
这条链路的本质是:用 event log、dedupe、projection 与 lease 把高频、弱一致计数从强一致且被广泛订阅的 document 中隔离出来。
5. 到底有没有缓存#
答案是“有多层缓存,没有统一 Redis”。依赖与源码中没有发现 Redis、Upstash、Memcached 或通用分布式 LRU。
| 层 | 位置 | 生命周期与失效 | 适合什么 |
|---|---|---|---|
| Convex Query Cache | Convex platform | 相同 function + args 在匹配的 auth context 下复用;依据 read set 在写入时失效 | deterministic Query |
| Reactive subscription | useQuery / usePaginatedQuery | WebSocket subscription 存活期间,依赖变化后重跑并推送 | 详情、账号、权限、管理状态 |
| One-shot Query | ConvexHttpClient.query | 只取一次,不保留 subscription;仍可能复用服务端 Query Cache | 高流量公开 browse |
Component Map | 首页、文件预览、diff UI | 无 TTL、无持久化、通常无容量上限;组件卸载或刷新即消失 | 同一次页面交互避免重复请求 |
| HTTP/CDN cache | Convex HTTP Actions、Nitro | 由 Cache-Control、ETag、Last-Modified 和 URL content hash 控制 | Feed、immutable assets、OG image |
| Process-local cache | Nitro/Action process | 只在单实例进程内复用,例如 Remote JWK Set、font buffer、单次 Search owner promise | 小型只读资源或单请求去重 |
| Materialized projection | digest、leaderboard、feed publication、global stats、Trending snapshot | 由 Trigger/Cron/Mutation 维护;删除后影响业务读取 | 热路径小 document、预排序结果 |
| CLI discovery config | 用户机器 | 把 Registry endpoint 写入 CLI 配置 | endpoint discovery,不是内容或 ZIP cache |
HTTP 策略不是“一律 CDN 缓存”:
- Catalog feed 使用 ETag、Last-Modified,并设置
public, max-age=60, s-maxage=300, stale-while-revalidate=86400。 - 以 SHA-256 定址的 presentation asset 使用一年
immutable。 - 带精确 version 的 OG image 一年
immutable;“latest” 类 URL 只缓存一小时。 - ZIP 通常是
private, max-age=60或no-store,Trending JSON 也是no-store。 - 其它动态 JSON 多数默认
no-store,不会因为前面有 Vercel 就自然获得安全的 public cache。
因此,问“ClawHub 有没有缓存”时,不能只搜 cache 变量;需要分别回答 Query、subscription、browser memory、HTTP/CDN 与 materialized projection 五个层面。
6. Auth、Authorization 与限流边界#
Web 登录使用 Convex Auth + GitHub OAuth。账号关联只接受 immutable GitHub numeric provider ID,不按 email 自动合并;这是防止 GitHub email 变化或撞名造成账户串联的重要边界。OAuth scope 包含 read:user、user:email 与 read:org,后者服务于 GitHub Organization Publisher membership。
CLI/API 使用另一条认证面:
- API token 在表中保存 hash,可撤销,并对
lastUsedAt做降频 touch。 - GitHub Actions publish token 是独立的短期能力凭证。
- Device authorization flow 用
cliDeviceCodes完成远程/无头登录。 - Public Query 返回经过裁剪的 public shape;Mutation/Action 再通过
requireUser、role、owner/publisher membership helper 进行授权。
项目安装了 Convex rate limiter Component,但 HTTP quota 是通过 route registration wrapper 安装的 fixed-window/sharded limiter。它覆盖 CLI/API HTTP surface,不自动覆盖浏览器 direct Query/Mutation/Action。这个差异不等于 direct calls “完全没有任何限制”——Convex platform 仍有系统限制——但做 abuse model 时不能把两者视为同一层防护。
本文是架构分析,不是完整 security audit;没有对 production secrets、token rotation、部署 ACL 或真实请求头信任配置作结论。
7. 本地运行、测试和部署#
7.1 最小本地流程#
Repository 给出的基础流程是:
git clone https://github.com/openclaw/clawhub.git
cd clawhub
git checkout fb0ef1d21eab78ad2b9da69f48045d7747cc73c1
bun install --frozen-lockfile
cp .env.local.example .env.local
# Terminal A:local Convex backend
bunx convex dev
# Terminal B:TanStack Start,默认 http://localhost:3000
bun run dev
# 可选:写入本地 QA fixtures 与 public corpus
bun run seed:dev本地通常需要:
VITE_CONVEX_URL=http://127.0.0.1:3210
VITE_CONVEX_SITE_URL=http://127.0.0.1:3211
CONVEX_SITE_URL=http://127.0.0.1:3211
SITE_URL=http://localhost:3000
CONVEX_DEPLOYMENT=anonymous:anonymous-clawhubGitHub OAuth client 和 Convex Auth JWT/JWKS 应通过 bunx convex env set ... 与 bunx @convex-dev/auth 配到 backend environment,不应把真实 secret 提交进 .env.local。Callback 指向 local Convex site 的 /api/auth/callback/github。
源码快照声明 local Convex backend 支持 Node 18/20/22/24,不支持 Node 25+;实际启动前应先核对 node --version。Worktree 快速路径还依赖 Worktrunk。默认本地环境不会自动运行全部外部 security/card workers,所以相关 job 保持 pending 不一定是应用故障。Local development 说明
7.2 两个主部署单元#
生产发布不是一个 bundle:
- Convex backend 独立 deploy,负责 Schema、Indexes、Functions、HTTP routes 与 Cron。
- TanStack Start/Nitro 由 Vercel Git deployment 发布。
生产 workflow 先暂停必要的 external-skill rollout,设置 build SHA/deploy time,执行 Convex deploy 与 contract verification,再等待同一 Git SHA 的 Vercel deployment 并做 smoke test。Workflow 中的顺序
Vercel Preview 则创建配套 Convex preview deployment、绑定对应 URL、seed preview data,并禁用 Cron;Preview proxy 只允许 read-only GET/HEAD。这避免 Preview Web 错连 production backend,也避免每个 PR 都启动一套后台定时任务。
7.3 本文做过的可复现检查#
在固定提交上执行:
bun install --frozen-lockfile
bun run ci:types-buildci:types-build 会依次检查根 TypeScript、共享 Schema、公开 CLI、Admin CLI,再以占位 VITE_CONVEX_URL 构建 Web。本文没有启动 local Convex 或跑 production E2E,因为审计环境没有 OAuth/Convex backend secrets,且 Node 版本不在仓库支持范围;这不影响静态数据流判断,但不应把本文称作 production runtime verification。
上述 install、TypeScript checks 与 Web production build 均通过;构建仅出现 Vite 对未来 configLoader: native 的 import-extension compatibility warning,固定快照没有产生 tracked diff。
8. Convex 与 Drizzle + PostgreSQL,哪个更适合#
先纠正比较单位:Drizzle 是 TypeScript ORM/query toolkit,不提供完整 backend runtime。公平比较必须把 PostgreSQL 周围的服务一起算进去。
| ClawHub 需求 | Convex 当前实现 | Drizzle + PostgreSQL 实现 |
|---|---|---|
| Transactional write | Mutation 是 atomic transaction,OCC 冲突自动 retry | PostgreSQL transaction、isolation、row/advisory locks 更可控;Drizzle 提供 typed API 与 SQL escape hatch |
| 关系完整性 | typed ID + 应用 Mutation/helper 维护 | Foreign Key、Unique/Check constraint、cascade/deferrable constraint 更强 |
| Public hot reads | 自动 Query Cache + digest + index + bounded reads | 普通/partial/covering index、join、materialized view;缓存需另配 |
| Realtime UI | useQuery 原生 subscription、依赖跟踪和一致 snapshot | Drizzle 不提供;需 WebSocket/SSE + invalidation/CDC/polling。LISTEN/NOTIFY 只是事件通道,不等于依赖跟踪 Query subscription |
| Search | Convex full-text/Vector Index + Action ranking | PostgreSQL FTS、pg_trgm、pgvector 与 SQL ranking 更灵活,需自行部署和调优 extension |
| Artifact | Convex File Storage、upload URL 与 Storage metadata | 应使用 R2/S3 等 Object Storage,再处理 signed URL、生命周期与 DB/Storage 一致性 |
| Jobs | Scheduler、Cron、Functions,业务表实现 lease/watchdog | 需 queue/worker、scheduler、lease/retry、dead-letter 与 observability |
| Auth 与 API | Convex Auth、direct functions、HTTP Actions | 需 Auth/session、TanStack/Nitro API layer 与 authorization helpers |
| Preview | Convex preview deployment 与 Vercel Preview 配套 | 需创建 Preview DB/schema、seed、Storage namespace、worker 与 secret isolation |
| SQL/analytics | 依赖 projection 与批量 functions,ad-hoc join 不方便 | Join、CTE、window function、materialized view、BI/ETL 生态明显更强 |
| 运维与可移植性 | 平台集成度高,团队少维护基础设施,但绑定 Convex runtime | PostgreSQL 供应商选择、自托管、备份复制和迁移工具更丰富,团队也要承担更多运维 |
Drizzle 的 PostgreSQL setup、Transactions、Indexes & Constraints 与 custom SQL migrations 足以覆盖 typed SQL 层;PostgreSQL 官方文档则提供 Foreign Key constraints、Full Text Search、LISTEN 与 Materialized Views。这些是 PostgreSQL 的真实优势,但它们不会自动补出 ClawHub 当前依赖的整套应用 runtime。
8.1 为什么当前项目应继续用 Convex#
ClawHub 的核心负载恰好同时需要:
- Web 详情/权限状态的 realtime subscription;
- 公开 browse 的 one-shot + shared Query Cache;
- Publish Mutation 与 File Storage ticket;
- Search Action 的外部 embedding 与 Vector Search;
- 37 个 Cron jobs、Scheduler continuation、lease/watchdog;
- 直接面向 CLI 的 HTTP Actions;
- Vercel Preview 对应的 isolated backend;
- 大量已实现的 Convex tests、migration、repair 与 operational scripts。
这些耦合使迁移成本远高于 108 张表的 DDL 重写。即使把 documents 导入 PostgreSQL,应用仍没有 realtime invalidation、artifact storage、worker runtime、HTTP/Auth 和 Preview pipeline。对现有系统而言,迁移收益必须大到足以支付一次完整平台替换,而目前静态源码没有提供这种证据。
8.2 什么条件下 PostgreSQL 反而更合适#
出现下面任一“硬约束”时,应该重新评估:
- 核心需求转为复杂跨域 joins、运营 SQL、BI 或近实时 analytics,维护大量 digest 比 SQL 查询更昂贵。
- Foreign Key、deferred constraint、数据库级唯一性和复杂 transaction isolation 是合规或财务正确性的前提。
- 必须自托管、使用既有 PostgreSQL fleet,或要在多个云/供应商之间可迁移。
- 团队已经拥有成熟的 R2/S3、Queue/Worker、Auth、Realtime gateway、scheduler、backup 与 observability 平台,额外基础设施并不是新成本。
- 真实 Insights、账单与压测证明 Convex 的边际成本持续高于平台集成收益;不能只凭“PostgreSQL 通常更便宜”作判断。
对于同功能的 greenfield Registry,如果团队已在 Cloudflare + PostgreSQL 体系内,也可以采用:TanStack Start + Drizzle/PostgreSQL/pgvector + R2 + Queues/Workflows + 独立 realtime channel。它会换来 SQL 和基础设施控制权,同时也把一致性、重试、订阅失效和多环境部署重新交给团队。
8.3 更实际的中间路线#
如果当前痛点只是报表与分析,不建议先迁主库:
Convex transactional/realtime source of truth
→ 增量导出 stat events / audit events / snapshots
→ PostgreSQL 或 analytics warehouse
→ SQL、BI、长期留存与离线模型这会引入 eventual consistency、幂等导出和重放设计,但风险仍显著低于同时替换 Web data layer、Storage、Auth、API 与 worker runtime。
9. 源码中值得继续关注的边界#
这不是 bug 清单,而是后续演进时最值得观测的地方:
- 单文件复杂度很高。
convex/skills.ts与convex/packages.ts都超过一万行,领域边界主要靠 helper 与命名维持;新增状态机前应优先拆清 public/internal surface。 - 派生表一致性成本真实存在。 digest、topic/curated digest、stats、feed 与 Trending 各有同步规则、backfill 和 fallback;它们优化读放大,也增加写链路和 rollout 风险。
- 公开 browse 仍有少量回源。 official Publisher 与旧数据 fallback 说明 digest 是渐进优化,不应假设每行永远只读一条 document。
- 时间参数会影响 shared cache。
Date.now() - 14 days作为 Query arg 会制造不同 cache key;若 Insights 证明它仍昂贵,可把边界量化到固定时间 bucket,但必须接受更粗的新鲜度。 - Search Action 需要独立保护。 没有 embedding memoization,且 Web direct Action 不经过 HTTP quota wrapper;应该结合实际调用量、OpenAI cost 与 Convex metrics 决定是否增加应用级配额或短期结果缓存。
- 组件内
Map很轻,也很有限。 没有 TTL、上限或跨路由持久化;它适合去掉一次会话内重复请求,不应被当作离线或全局 cache。 - Admin CLI 存在源码级耦合。 它直接把公共 CLI source 纳入 TypeScript 编译范围,而不是只依赖稳定 package surface;公共 CLI 重构时要连带验证。
10. 建议的源码阅读顺序#
如果要继续深入,不建议从 4,479 行 Schema 或 14,000 行 skills.ts 顺序读起。下面的路线更容易建立闭环:
README.md与package.json:确认产品、packages 与 scripts。vite.config.ts、src/convex/client.ts与server/convexProxy.ts:区分 direct functions 与 HTTP proxy。convex/schema.ts的 Skills、versions、embeddings、digests,再看convex/functions.ts的 Trigger。src/routes/skills/-useSkillsBrowseModel.ts→convex/skills.ts:listPublicPageV4:追公开列表。src/routes/skills/index.tsx→convex/search.ts:searchSkills:追 Search Action。packages/clawhub/src/cli/commands/publish.ts→ upload HTTP handler → Publish Mutation:追发布。convex/downloads.ts→convex/downloadMetrics.ts→convex/skillStatEvents.ts→convex/canonicalTrending.ts:追统计与 Trending。convex/crons.ts、deploy workflow 与相关 tests:最后再看后台运行和 rollout safety。
一手资料#
- ClawHub 固定源码快照
- ClawHub README
- Convex Queries:Caching、Reactivity 与 Consistency
- Convex Mutations
- Convex Actions
- Convex File Storage
- Convex Scheduled Functions 与 Cron Jobs
- Convex Vector Search
- Drizzle ORM:PostgreSQL
- Drizzle ORM:Transactions
- PostgreSQL:Constraints
- PostgreSQL:Full Text Search
- PostgreSQL:Materialized Views