Convex - 全栈开发的新一代后端平台

什么是 Convex? Convex 是一款开源的 BaaS (Backend-as-a-Service) 平台，专为现代 Web 应用开发者设计。它不仅提供了一个高性能的实时数据库，还集成了文件存储、定时任务调度（Cron Jobs）等核心服务。 Convex 的生态系统非常丰富，支持通过插件扩展功能，包括： Cloudflare R2：用于大文件存储。 Rate Limiter：提供应用级的速率限制功能。 AI Agent：支持构建智能体和集成 AI 工作流。为什么选择 Convex? 端到端类型安全 (End-to-End Type Safety) 告别繁琐的原生 SQL 查询和复杂的 ORM 配置。Convex 的所有查询和变更函数均使用 TypeScript 编写，实现了从数据库 schema 到前端组件的完整类型推导，极大降低了运行时错误。默认实时 (Real-time by Default) Convex 客户端通过 WebSocket 与服务端保持连接。这意味着数据流是实时的——当数据库中的数据发生变化时，客户端会自动收到更新并重新渲染，无需手动配置轮询或订阅逻辑。强一致性 (ACID Transactions) Convex 的所有写入操作（Mutations）都是原子化的。它们在事务中执行，确保了数据的完整性和一致性，解决了分布式系统中常见的并发问题。基础用法关于 Convex 的从零开始配置和基础 CRUD 操作，请参考官方文档： Convex Overview | Convex Developer Hub 开发进阶技巧服务端组件集成 (Server Components) 在 Next.js App Router 中，如果需要实现服务端渲染 (SSR) 以优化 SEO 或首屏加载速度，可以使用 fetchQuery。注意：fetchQuery 获取的数据是静态的，不支持实时订阅。 import { fetchQuery } from "convex/nextjs"; import { api } from "@/convex/_generated/api"; export async function StaticTasks() { // 如果该 query 需要验证，则需要传入 token const tasks = await fetchQuery( api.tasks.list, { list: "default" }, { token: "your-jwt-token" }, ); return (
{tasks.map((task) => (
{task.text}
))}
); } HTTP 端点与 Webhooks Convex 支持通过 HTTP Actions 暴露标准的 API 端点，这在处理第三方 Webhooks（如 Clerk 身份验证回调）时非常有用。 import { httpRouter } from "convex/server"; import { httpAction } from "./_generated/server"; import { internal } from "./_generated/api"; import type { WebhookEvent } from "@clerk/backend"; import { Webhook } from "svix"; const http = httpRouter(); // 设置一个 API endpoint http.route({ path: "/clerk-users-webhook", method: "POST", // 注意：httpAction 中不能直接操作数据库，需调用 runQuery 或 runMutation handler: httpAction(async (ctx, request) => { const event = await validateRequest(request); if (!event) { return new Response("Error occured", { status: 400 }); } switch (event.type) { case "user.created": case "user.updated": await ctx.runMutation(internal.users.upsertFromClerk, { data: event.data, }); break; case "user.deleted": { const clerkUserId = event.data.id!; await ctx.runMutation(internal.users.deleteFromClerk, { clerkUserId }); break; } default: console.log("Ignored Clerk webhook event", event.type); } return new Response(null, { status: 200 }); }), }); async function validateRequest(req: Request): Promise { const payloadString = await req.text(); const svixHeaders = { "svix-id": req.headers.get("svix-id")!, "svix-timestamp": req.headers.get("svix-timestamp")!, "svix-signature": req.headers.get("svix-signature")!, }; const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!); try { return wh.verify(payloadString, svixHeaders) as unknown as WebhookEvent; } catch (error) { console.error("Error verifying webhook event", error); return null; } } export default http; 在 Mutation 中处理副作用 Convex 的 Mutation 必须是纯函数，不允许由外部副作用（如直接调用第三方 API）。解决方案是使用 ctx.scheduler 调度一个 Action 来处理副作用。 import { mutation } from "./_generated/server"; import { v } from "convex/values"; import { internal } from "./_generated/api"; export const mutationThatSchedulesAction = mutation({ args: { text: v.string() }, handler: async (ctx, { text }) => { // 1. 执行数据库写入 const taskId = await ctx.db.insert("tasks", { text }); // 2. 调度 Action (0 表示立即执行) await ctx.scheduler.runAfter(0, internal.myFunctions.actionThatCallsAPI, { taskId, text, }); }, }); Schema 管理与最佳实践修改表结构 Convex 不允许直接删除包含数据的字段。如果要删除字段，推荐流程如下：将字段修改为可选：v.optional(v.string())。编写脚本将该字段的历史数据更新为 undefined。确认数据清理完毕后，在 Schema 中移除该字段。索引优化索引能极大提升查询速度，但会占用存储空间。应避免创建冗余索引。 // ❌ 冗余配置：查询 team 已经可以通过 by_team_and_user 的前缀匹配完成 // const allTeamMembers = await ctx.db // .query("teamMembers") // .withIndex("by_team", (q) => q.eq("team", teamId)) // .collect(); // ✅ 推荐配置：复用复合索引 // 假设创建了索引 .index("by_team_and_user", ["team", "user"]) const allTeamMembers = await ctx.db .query("teamMembers") // 使用复合索引的第一部分进行查询 .withIndex("by_team_and_user", (q) => q.eq("team", teamId)) .collect(); const currentTeamMember = await ctx.db .query("teamMembers") .withIndex("by_team_and_user", (q) => q.eq("team", teamId).eq("user", currentUserId), ) .unique(); 利用 Convex-Helpers 简化开发 convex-helpers 是官方提供的实用工具库，能显著减少样板代码。自定义 React Hooks 封装鉴权逻辑，避免未登录时的无效请求。 import { FunctionReference } from "convex/server"; import { OptionalRestArgsOrSkip, useConvexAuth, useQueries, } from "convex/react"; import { makeUseQueryWithStatus } from "convex-helpers/react"; // 创建带状态（pending/success/error）的 hook export const useQueryWithStatus = makeUseQueryWithStatus(useQueries); /** * 自动检查鉴权状态的 Query 包装器 * 如果用户未登录，该 Query 会自动跳过 ("skip") */ export function useAuthenticatedQueryWithStatus< Query extends FunctionReference<"query">, >(query: Query, args: OptionalRestArgsOrSkip[0] | "skip") { const { isAuthenticated } = useConvexAuth(); return useQueryWithStatus(query, isAuthenticated ? args : "skip"); } 或者结合 组件保护路由，再直接使用 useQueryWithStatus。高级自定义函数 (Custom Functions) 通过扩展 ctx，可以直接在 Server Function 中获取当前用户对象，或者自动应用行级安全 (RLS) 规则。 import { customQuery, customCtx } from "convex-helpers/server/customFunctions"; import { ConvexError } from "convex/values"; const userQuery = customQuery( query, // 基础查询函数 customCtx(async (ctx) => { // 统一鉴权逻辑 const identity = await ctx.auth.getUserIdentity(); if (!identity) { throw new ConvexError("Not authenticated!"); } const user = await getUser(ctx, identity.subject); // 假设的获取用户逻辑 const db = wrapDatabaseReader({ user }, ctx.db, rules); // 应用 RLS 规则 return { user, db }; // 扩展 ctx }), ); 组件生态 (Components) 数据聚合 (Aggregate) Convex 提供了 TableAggregate 组件，用于高效处理计数、求和等聚合操作，无需遍历全表。初始化组件 const aggregate = new TableAggregate<{ Key: number; DataModel: DataModel; TableName: "mytable"; }>(components.aggregate, { sortKey: (doc) => doc._creationTime, // 允许按时间范围查询 sumValue: (doc) => doc.value, // 指定用于 .sum 计算的字段 }); 保持数据同步 需要在数据变更操作 (Mutation) 中手动触发更新： // 插入数据 const id = await ctx.db.insert("mytable", { foo, bar }); const doc = await ctx.db.get(id); await aggregate.insert(ctx, doc!); // 更新数据 const oldDoc = await ctx.db.get(id); await ctx.db.patch(id, { foo: newFoo }); const newDoc = await ctx.db.get(id); await aggregate.replace(ctx, oldDoc!, newDoc!); // 删除数据 const oldDoc = await ctx.db.get(id); await ctx.db.delete(id); await aggregate.delete(ctx, oldDoc!); 使用聚合查询 // 在 Query 或 Mutation 中直接调用 const tableCount = await aggregate.count(ctx); 自部署 Convex 支持基于 Docker 的私有化部署。以下是适配 Coolify 的 docker-compose.yml 配置示例： services: backend: image: "ghcr.io/get-convex/convex-backend:latest" volumes: - "data:/convex/data" environment: - SERVICE_FQDN_BACKEND_3210 - "INSTANCE_NAME=${INSTANCE_NAME:-self-hosted-convex}" - "INSTANCE_SECRET=${SERVICE_HEX_32_SECRET}" - "CONVEX_RELEASE_VERSION_DEV=${CONVEX_RELEASE_VERSION_DEV:-}" - "ACTIONS_USER_TIMEOUT_SECS=${ACTIONS_USER_TIMEOUT_SECS:-}" - "CONVEX_CLOUD_ORIGIN=${SERVICE_FQDN_BACKEND_3210}" - "CONVEX_SITE_ORIGIN=${SERVICE_FQDN_BACKEND_3210}/http" - "DATABASE_URL=${DATABASE_URL:-}" - "DISABLE_BEACON=${DISABLE_BEACON:-}" - "REDACT_LOGS_TO_CLIENT=${REDACT_LOGS_TO_CLIENT:-}" - "CONVEX_SELF_HOSTED_URL=${SERVICE_FQDN_CONVEX_6791}" healthcheck: test: "curl -f http://127.0.0.1:3210/version" interval: 5s start_period: 5s dashboard: image: "ghcr.io/get-convex/convex-dashboard:latest" environment: - SERVICE_FQDN_CONVEX_6791 - NEXT_PUBLIC_DEPLOYMENT_URL=$SERVICE_FQDN_BACKEND_3210 depends_on: backend: condition: service_healthy healthcheck: test: "wget -qO- http://127.0.0.1:6791/" interval: 5s start_period: 5s 参考资源 Convex 官方文档: Convex Developer Hub 自定义服务端函数: Customizing serverless functions 行级安全 (RLS): Row Level Security