ClawHub 源码分析:Convex 架构、缓存、数据流与 PostgreSQL 选型

This article is extracted from the chat log with AI. Please identify it with caution.

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. 分析快照与规模#

本文使用的源码快照如下:

项目
Repositoryopenclaw/clawhub
Commitfb0ef1d21eab78ad2b9da69f48045d7747cc73c1
Commit 时间2026-08-22
Convex1.44.0
TanStack Start / Router1.168.46 / 1.170.29
React19.2.8
Vite8.2.1
Package managerBun

静态盘点可以帮助理解项目规模,但不等同于 production deployment 的可达 API:

  • convex/schema.ts 本地定义 108 张应用表;当前 @convex-dev/authauthTables 再补入 4 张未被本地覆盖的表,因此该依赖快照的有效 Schema 共 112 张表。
  • 本地 Schema 有 515 处普通 .index() 声明,叠加当前 Auth 新增表后为 521 个普通 Index;另有 4 个 full-text searchIndex 和 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 CLInpm / 用户机器
packages/clawhub-admin/私有运维 CLIOperator 环境
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

这里有两条容易混淆的网络路径:

  1. React useQueryuseMutationuseAction 和 SSR 的 ConvexHttpClient 直接访问 VITE_CONVEX_URL,即 Convex functions endpoint。
  2. 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 管理:

  • 全局各创建一个 ConvexReactClientConvexHttpClient
  • ConvexAuthProvider 包住应用。
  • 详情、用户、权限和管理页面主要使用 reactive useQuery
  • 首页、Skills browse 和 Search 等高流量公开路径主要使用 one-shot ConvexHttpClient.query/actionuseAction
  • 筛选、tab 与 pagination 状态尽量写入 TanStack Router URL search params,短期交互状态留在 React。

这种选择不是“订阅好”或“订阅坏”,而是按数据的新鲜度语义拆分:权限或发布状态变旧可能导致错误操作,适合 reactive subscription;公开列表在一次页面停留期间允许短暂变旧,one-shot 更能控制失效放大。

3. Convex 在这个项目里究竟负责什么#

3.1 四类 Function 不是同一种 RPC#

FunctionClawHub 中的角色关键语义
Query读取详情、列表、权限上下文与内部批次Read-only、deterministic、自动 Query Cache;React client 可订阅依赖变化
Mutation发布、更新、授权、状态流转、dedupe 与批次提交对数据库读写是 atomic transaction;冲突时由 Convex 进行 OCC retry
ActionOpenAI embedding、GitHub/VirusTotal/Resend 等外部 I/O、长流程编排可以调用 Query/Mutation,但整个 Action 不是一个数据库事务,结果也不是 Query Cache
HTTP ActionCLI/API、feeds、downloads、webhooks 与 discovery接收标准 Request 并返回 Response,再通过 convex/http.ts 绑定 route

官方语义分别见 QueriesMutationsActions。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 与 ownershipuserspublisherspublisherMemberspublisherInvitesgithubOrgMembershipsapiTokensGitHub identity、个人/组织 Publisher、成员关系与 CLI token
Registry write modelskillsskillVersionspackagespackageReleases、aliases、badges、upload ticketsSkill/Plugin 的规范化来源、版本与发布状态
Search 与 public read modelskillSearchDigest、curated/topic digests、package digests、skillEmbeddingsembeddingSkillMap轻量列表、检索、排序与向量结果 hydration
Stats 与 rankingstat events、daily/hourly stats、leaderboards、global stats、Trending pools/snapshots/items把高频事件与公开读模型隔离
Moderation 与 work queuesscans、evaluations、publish attempts、reports、appeals、abuse signals、leases、dedupe rows安全审核、人工处理、重试、租约和幂等
External mirrorsGitHub 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-helpersTriggers 包装应用 Mutation。核心注册集中在 convex/functions.ts

  • skills 改变时,更新 Publisher stats,并同步或删除 Skill search digests。
  • skillVersions 只有删除状态或安全审核结果等影响公开性的字段变化时,才回到父 Skill 重建 digest。
  • packagespackageReleases 做等价处理;latest release 删除后还要重新选择 fallback。
  • userspublishers 的公开资料变化时,通过 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 失效。

需要注意三个边界:

  1. digest 是一致性责任,不是可以任意删除的缓存;删除后公开列表和搜索会缺数据,必须 backfill 或回源。
  2. 它显著减少 join,但没有完全消灭 join。某些公开 hydration 仍会查询 official Publisher,缺少或不一致的版本摘要也会回读 version/skill。
  3. 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 Map

listPublicPageV4明确使用 convex-helpersgetPage(),目的是让同一参数对应稳定 cursor,使不同客户端更容易复用 Convex Query Cache。无过滤路径读取所需数量;有过滤路径也设置了最多 500 行、12 个 transport pages 等边界,而不是无限扫描。

浏览器端同样有限制:连续空 transport page 只跳过有限次数,防止筛选条件很稀疏时无界追页。首页则用一个以 kind/tab/category/fetchLimit 组合为 key 的组件级 Map 保存已取列表。

这条路径有两个细节:

  • one-shot 只是“不建立长期 subscription”,并不意味着服务端 Query Cache 失效;Query 仍可按相同函数与参数复用缓存。
  • new tab 把“当前时间减 14 天”作为 createdAfter 参数,不同请求时刻会形成不同 args,因此它天然比固定参数更难跨用户共享 Query Cache。

4.2 详情页:SSR snapshot + reactive handoff#

详情页需要及时反映 latest version、删除、审核与 owner 变化,所以采用另一种策略:

  1. TanStack Start loader 用 ConvexHttpClient 读取 metadata/README,给 SSR 首屏。
  2. 浏览器 hydration 后使用 useQuery 订阅最新公开或 staff shape。
  3. 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 --> F

skillEmbeddings.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 编排。

缓存方面要保守表述:

  • searchSkills Action 的最终结果不属于 Query Cache。
  • 源码没有搜索结果缓存或 embedding memoization;正常搜索会尝试调用 OpenAI。
  • Action 内部 ctx.runQuery 的子读取仍遵循 Query 语义,但不能据此说“整个搜索已缓存”。
  • CLI /api/v1/search 受 HTTP read quota 包裹;网页 direct useAction 不经过那一层 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 --> T

Hosted 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 CacheConvex platform相同 function + args 在匹配的 auth context 下复用;依据 read set 在写入时失效deterministic Query
Reactive subscriptionuseQuery / usePaginatedQueryWebSocket subscription 存活期间,依赖变化后重跑并推送详情、账号、权限、管理状态
One-shot QueryConvexHttpClient.query只取一次,不保留 subscription;仍可能复用服务端 Query Cache高流量公开 browse
Component Map首页、文件预览、diff UI无 TTL、无持久化、通常无容量上限;组件卸载或刷新即消失同一次页面交互避免重复请求
HTTP/CDN cacheConvex HTTP Actions、NitroCache-Control、ETag、Last-Modified 和 URL content hash 控制Feed、immutable assets、OG image
Process-local cacheNitro/Action process只在单实例进程内复用,例如 Remote JWK Set、font buffer、单次 Search owner promise小型只读资源或单请求去重
Materialized projectiondigest、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=60no-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:useruser:emailread: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-clawhub

GitHub 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:

  1. Convex backend 独立 deploy,负责 Schema、Indexes、Functions、HTTP routes 与 Cron。
  2. 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-build

ci: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 writeMutation 是 atomic transaction,OCC 冲突自动 retryPostgreSQL 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 UIuseQuery 原生 subscription、依赖跟踪和一致 snapshotDrizzle 不提供;需 WebSocket/SSE + invalidation/CDC/polling。LISTEN/NOTIFY 只是事件通道,不等于依赖跟踪 Query subscription
SearchConvex full-text/Vector Index + Action rankingPostgreSQL FTS、pg_trgm、pgvector 与 SQL ranking 更灵活,需自行部署和调优 extension
ArtifactConvex File Storage、upload URL 与 Storage metadata应使用 R2/S3 等 Object Storage,再处理 signed URL、生命周期与 DB/Storage 一致性
JobsScheduler、Cron、Functions,业务表实现 lease/watchdog需 queue/worker、scheduler、lease/retry、dead-letter 与 observability
Auth 与 APIConvex Auth、direct functions、HTTP Actions需 Auth/session、TanStack/Nitro API layer 与 authorization helpers
PreviewConvex 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 runtimePostgreSQL 供应商选择、自托管、备份复制和迁移工具更丰富,团队也要承担更多运维

Drizzle 的 PostgreSQL setupTransactionsIndexes & Constraintscustom SQL migrations 足以覆盖 typed SQL 层;PostgreSQL 官方文档则提供 Foreign Key constraintsFull Text SearchLISTENMaterialized 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 清单,而是后续演进时最值得观测的地方:

  1. 单文件复杂度很高。 convex/skills.tsconvex/packages.ts 都超过一万行,领域边界主要靠 helper 与命名维持;新增状态机前应优先拆清 public/internal surface。
  2. 派生表一致性成本真实存在。 digest、topic/curated digest、stats、feed 与 Trending 各有同步规则、backfill 和 fallback;它们优化读放大,也增加写链路和 rollout 风险。
  3. 公开 browse 仍有少量回源。 official Publisher 与旧数据 fallback 说明 digest 是渐进优化,不应假设每行永远只读一条 document。
  4. 时间参数会影响 shared cache。 Date.now() - 14 days 作为 Query arg 会制造不同 cache key;若 Insights 证明它仍昂贵,可把边界量化到固定时间 bucket,但必须接受更粗的新鲜度。
  5. Search Action 需要独立保护。 没有 embedding memoization,且 Web direct Action 不经过 HTTP quota wrapper;应该结合实际调用量、OpenAI cost 与 Convex metrics 决定是否增加应用级配额或短期结果缓存。
  6. 组件内 Map 很轻,也很有限。 没有 TTL、上限或跨路由持久化;它适合去掉一次会话内重复请求,不应被当作离线或全局 cache。
  7. Admin CLI 存在源码级耦合。 它直接把公共 CLI source 纳入 TypeScript 编译范围,而不是只依赖稳定 package surface;公共 CLI 重构时要连带验证。

10. 建议的源码阅读顺序#

如果要继续深入,不建议从 4,479 行 Schema 或 14,000 行 skills.ts 顺序读起。下面的路线更容易建立闭环:

  1. README.mdpackage.json:确认产品、packages 与 scripts。
  2. vite.config.tssrc/convex/client.tsserver/convexProxy.ts:区分 direct functions 与 HTTP proxy。
  3. convex/schema.ts 的 Skills、versions、embeddings、digests,再看 convex/functions.ts 的 Trigger。
  4. src/routes/skills/-useSkillsBrowseModel.tsconvex/skills.ts:listPublicPageV4:追公开列表。
  5. src/routes/skills/index.tsxconvex/search.ts:searchSkills:追 Search Action。
  6. packages/clawhub/src/cli/commands/publish.ts → upload HTTP handler → Publish Mutation:追发布。
  7. convex/downloads.tsconvex/downloadMetrics.tsconvex/skillStatEvents.tsconvex/canonicalTrending.ts:追统计与 Trending。
  8. convex/crons.ts、deploy workflow 与相关 tests:最后再看后台运行和 rollout safety。

一手资料#

本文共 11458 字,创建于 Aug 23, 2026

相关标签: Convex, Database, TypeScript, TanStack, React, 源码分析, ByAI