Convex 数据库是一个开源的响应式后端即服务(Backend-as-a-Service)平台,专为现代全栈应用开发设计。它集成了数据库、服务器函数、文件存储、实时同步、身份验证等功能,开发者只需编写 TypeScript 代码即可构建高性能、实时更新的应用。
集成convex
安装依赖
指南:docs.convex.dev/quickstart/...
pnpm install convex
运行convex
pnpm dlx convex dev
结束后项目根目录下会生成配置文件:.env.local ,并且看板会生成新的convex项目:

添加测试数据
这是一个测试
sampleData.jsonl
typescript
{"text": "Buy groceries", "isCompleted": true}
{"text": "Go for a swim", "isCompleted": true}
{"text": "Integrate Convex", "isCompleted": false}
将数据导入数据库:pnpm dlx convex import --table tasks sampleData.jsonl

创建查询请求:convex/tasks.ts
typescript
import { query } from "./_generated/server";
export const get = query({
args: {},
handler: async (ctx) => {
return await ctx.db.query("tasks").collect();
},
});
创建**ConvexClientProvider**
创建 app/ConvexClientProvider.tsx
typescript
"use client";
import { ConvexProvider, ConvexReactClient } from "convex/react";
import { ReactNode } from "react";
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
export function ConvexClientProvider({ children }: { children: ReactNode }) {
return <ConvexProvider client={convex}>{children}</ConvexProvider>;
}
在 app/layout.tsx 中使用:
tsx
<ConvexClientProvider>
{children}
</ConvexClientProvider>
展示数据
app/page.tsx
tsx
"use client";
import Image from "next/image";
import { useQuery } from "convex/react";
import { api } from "../convex/_generated/api";
export default function Home() {
const tasks = useQuery(api.tasks.get);
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
{tasks?.map(({ _id, text }) => <div key={_id}>{text}</div>)}
</main>
);
}
启动服务,发现数据显示在页面上,到此convex集成完毕,可以把测试数据删掉。

集成clerk进行认证
官方指南:docs.convex.dev/auth/clerk#...
clerk创建应用
clerk创建新应用,并选择认证方式:

Clerk创建 JWT 模板
选择convex然后保存

配置
配置1:
复制上面的 Issuer url 添加到配置中:.env.local
NEXT_PUBLIC_CLERK_FRONTEND_API_URL=
配置2:
创建 convex/auth.config.ts
typescript
export default {
providers: [
{
// Replace with your own Clerk Issuer URL from your "convex" JWT template
// or with `process.env.CLERK_JWT_ISSUER_DOMAIN`
// and configure CLERK_JWT_ISSUER_DOMAIN on the Convex Dashboard
// See https://docs.convex.dev/auth/clerk#configuring-dev-and-prod-instances
domain: process.env.CLERK_JWT_ISSUER_DOMAIN,
applicationID: "convex",
},
]
};
并在 convex 中配置 CLERK_JWT_ISSUER_DOMAIN,值还是之前复制的 issuer url:

然后尝试重新启动convex:pnpm dlx convex dev,没有异常就可以。
安装clerk依赖
pnpm install @clerk/nextjs
配置 clerk keys
复制 clerk keys 到 .env.local文件中
plain
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=xxx
CLERK_SECRET_KEY=xxx
添加 Clerk middleware
创建 src/middleware.ts
typescript
import { clerkMiddleware } from '@clerk/nextjs/server'
export default clerkMiddleware()
export const config = {
matcher: [
// Skip Next.js internals and all static files, unless found in search params
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
// Always run for API routes
'/(api|trpc)(.*)',
],
}
配置 ConvexProviderWithClerk
修改文件 app/ConvexClientProvider.tsx
tsx
"use client";
import { ConvexReactClient } from "convex/react";
import { ConvexProviderWithClerk } from "convex/react-clerk";
import { ReactNode } from "react";
import { useAuth } from '@clerk/nextjs'
if (!process.env.NEXT_PUBLIC_CONVEX_URL) { // 新增
throw new Error('Missing NEXT_PUBLIC_CONVEX_URL in your .env file')
}
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
export function ConvexClientProvider({ children }: { children: ReactNode }) {
// ConvexProvider 替换成 ConvexProviderWithClerk,并添加 ClerkProvider 包裹
return (
<ClerkProvider>
<ConvexProviderWithClerk client={convex} useAuth={useAuth}>
{children}
</ConvexProviderWithClerk>
</ClerkProvider>
)
}
认证
ConvexClientProvider.tsx,区分认证和未认证:
tsx
export function ConvexClientProvider({ children }: { children: ReactNode }) {
return (
<ClerkProvider>
<ConvexProviderWithClerk client={convex} useAuth={useAuth}>
{/* 认证通过 */}
<Authenticated>{children}</Authenticated>
{/* 认证中 */}
<AuthLoading> 认证中... </AuthLoading>
{/* 未认证 */}
<Unauthenticated>
未认证
<SignInButton />
</Unauthenticated>
</ConvexProviderWithClerk>
</ClerkProvider>
)
}
app/page.tsx
tsx
export default function Home() {
return (
<>
认证通过
<UserButton />
</>
);
}
未认证的状态:

当点击 Sign in 按钮,跳转到登录表单:

登录中:

登录后:

到此就完成了登录认证能力。
用户数据同步到convex
当前用户信息只会体现在clerk中,可以在clerk创建webhook,同步数据到convex中。
创建webhook
先在clerk中创建webhook,endpoint url 前部分填写 convex http actions url(convex看板 -> settings -> URL & Deploy Key -> Http Actions URL),后部分自定义 clerk-users-webhook

点击创建,复制 Signing Secret,配置到 .env.local 文件中(convex 看板 变量中也同步添加):
CLERK_WEBHOOK_SECRET=xxx
创建用户schema
项目中声明用户schema:
typescript
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
users: defineTable({
clerkId: v.string(), // Clerk 的 user.id
email: v.string(),
username: v.string(),
imageUrl: v.string(),
})
.index("by_clerkId", ["clerkId"])
.index("by_email", ["email"]),
});
保存之后,convex 看板会自动创建 users 这个表格。
创建用户 CRUD
typescript
// convex/users.ts
import { v } from 'convex/values';
import { internalMutation, internalQuery } from './_generated/server';
export const create = internalMutation({
args: {
username: v.string(),
imageUrl: v.string(),
clerkId: v.string(),
email: v.string()
},
handler: async (ctx, args) => {
await ctx.db.insert("users", args);
}
})
export const get = internalQuery({
args: {
clerkId: v.string(),
},
handler: async (ctx, args) => {
await ctx.db.query("users")
.withIndex("by_clerkId", q => q.eq("clerkId", args.clerkId))
.unique();
}
})
webhook接收器
typescript
// convex/http.ts
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
import { internal } from "./_generated/api";
import { WebhookEvent } from "@clerk/nextjs/server";
import { Webhook } from "svix";
async function validatePayload(req: Request): Promise<WebhookEvent | undefined> {
const payload = await req.text();
// 1. 安全校验
const svixHeaders = {
"svix-id": req.headers.get("svix-id")!,
"svix-signature": req.headers.get("svix-signature")!,
"svix-timestamp": req.headers.get("svix-timestamp")!
}
const webhook = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);
try {
return webhook.verify(payload, svixHeaders) as WebhookEvent;
} catch (err) {
console.error("Webhook signature verification failed", err);
return ;
}
}
const handleClerkWebhook = httpAction(async (ctx, req) => {
const event = await validatePayload(req);
if (!event) {
return new Response("Could not validate Clerk payload", {
status: 400
});
}
switch (event.type) {
case "user.created":
const user = await ctx.runQuery(internal.users.get, {clerkId: event.data.id});
if (user) {
console.log(`Updating user ${event.data.id} with: ${event.data}`);
}
case "user.updated":
console.log("Creating/Updating User:", event.data.id);
await ctx.runMutation(internal.users.create, {
username: `${event.data.first_name} ${event.data.last_name}`,
imageUrl: event.data.image_url,
clerkId: event.data.id,
email: event.data.email_addresses[0].email_address
})
break;
default:
console.log("Clerk webhook event not supported:", event.type);
}
return new Response("OK", { status: 200 });
})
const http = httpRouter();
http.route({
path: "/clerk-users-webhook",
method: "POST",
handler: handleClerkWebhook
})
export default http;
然后先清空clerk所有的用户,重新登录,观察clerk数据是否同步到convex中:

说明数据正常同步。