在 Convex 聊天应用中集成 Clerk 用户认证:clerk-initial-auth 示例全解析
【免费下载链接】convex-backendThe open-source reactive database for app developers项目地址: https://gitcode.com/gh_mirrors/co/convex-backend
本篇文章基于 convex-backend 仓库中的 clerk-initial-auth 示例,完整讲解如何为一个 Convex 聊天应用接入 Clerk 身份认证:从前端登录按钮、认证 Provider 桥接,到后端函数中的身份校验与消息归属,再到切换为自己 Clerk 实例的完整配置。读完本文,你将掌握「Convex + Clerk」从零到一的最小可运行认证闭环,并能基于示例代码快速改造成自己的认证流程。
示例要解决的问题:让每条消息都有"主人"
clerk-initial-auth 是一个聚焦单一目标的最小示例:给一个基础聊天应用加上用户与认证能力。README 描述的行为闭环非常清晰:
- 应用启动后,用户首先看到的是一个"Log In"(登录)按钮;
- 用户登录成功后,其身份信息被持久化到数据库(README 描述为持久化到
users表); - 用户发送的每条消息都与发送它的用户关联;
- 用户可以通过"Log Out"(登出)按钮退出登录。
也就是说,这个示例演示的是一条完整的「认证状态驱动应用行为」链路,而不是一个孤立的登录弹窗。认证框架选型为 Clerk——一个托管式身份认证服务,负责注册、登录、会话管理等繁琐环节,而 Convex 侧则专注于身份校验与数据读写。
示例的完整文件结构如下:
npm-packages/private-demos/clerk-initial-auth/ ├── convex/ │ ├── _generated/ # Convex 自动生成的类型与 API 绑定 │ ├── auth.config.ts # 认证提供方配置(Clerk Issuer) │ ├── messages.ts # 消息相关的 query / mutation │ └── schema.ts # 数据库 schema 定义 ├── src/ │ ├── main.tsx # 应用入口:ClerkProvider + ConvexProviderWithClerk │ ├── App.tsx # 登录状态驱动的根组件 │ ├── LoginPage.tsx # 未登录页面(SignInButton) │ ├── Badge.tsx # 已登录用户信息徽章 │ └── index.css ├── index.html ├── package.json └── vite.config.mts快速运行示例
运行示例只需要一条命令(在 package.json 中定义):
npm run dev该命令实际执行的是:
convex dev --start 'vite --open'它做了两件事:启动 Convex 本地开发环境(convex dev会监听convex/目录下的函数并自动部署),同时拉起 Vite 开发服务器并自动打开浏览器(vite --open)。
示例的前端依赖集中在 package.json 中,核心包括:
convex(workspace 内部版本):Convex 客户端与类型系统;@clerk/react:Clerk 的 React 绑定,提供ClerkProvider、SignInButton、useAuth、useUser等;react/react-dom:React 18;vite+@vitejs/plugin-react:前端构建与开发服务器。
认证配置的两把"钥匙":Publishable Key 与 Issuer URL
README 的 "Using your own Clerk instance" 一节明确指出,接入自己的 Clerk 实例需要两个关键配置:
| 配置项 | 用途 | 在示例中的位置 |
|---|---|---|
| Publishable Key(可发布密钥) | Clerk 前端 SDK 初始化凭证,标识你的 Clerk 应用 | src/main.tsx |
| JWT Template Issuer URL(JWT 模板签发者地址) | Convex 后端校验 Clerk 签发的 JWT 时使用 | convex/auth.config.ts |
这两个配置的获取方式,README 指向了 Convex 官方文档中 Clerk 的 Get Started 流程(在 Clerk Dashboard 中创建应用、配置 JWT 模板,随后获得 Publishable Key 与 Issuer URL)。
前端侧:Publishable Key
在 src/main.tsx 中,Publishable Key 直接传给ClerkProvider:
import { ClerkProvider, useAuth } from "@clerk/react"; import { ConvexReactClient } from "convex/react"; import { ConvexProviderWithClerk } from "convex/react-clerk"; import { StrictMode } from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; import "./index.css"; const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL); ReactDOM.createRoot(document.getElementById("root")!).render( <StrictMode> <ClerkProvider // Replace this with your Clerk Publishable Key // or with `{import.meta.env.VITE_CLERK_PUBLISHABLE_KEY}` // and configure VITE_CLERK_PUBLISHABLE_KEY in your .env.local publishableKey="pk_test_cm9idXN0LW1hZ2dvdC0yOS5jbGVyay5hY2NvdW50cy5kZXYk" > <ConvexProviderWithClerk client={convex} useAuth={useAuth}> <App /> </ConvexProviderWithClerk> </ClerkProvider> </StrictMode>, );代码注释给出了两种取值方式:直接硬编码,或通过环境变量import.meta.env.VITE_CLERK_PUBLISHABLE_KEY(配置在.env.local中)。推荐后者,避免把密钥写进源码。
后端侧:Issuer URL
在 convex/auth.config.ts 中,Issuer URL 配置在 Convex 的AuthConfig里:
import { AuthConfig } from "convex/server"; 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: "https://robust-maggot-29.clerk.accounts.dev", applicationID: "convex", }, ], } satisfies AuthConfig;关键字段说明:
domain:Clerk 为你生成的 JWT 模板 Issuer URL,形如https://<你的实例>.clerk.accounts.dev。它同时是 OIDC 签发者地址,Convex 后端依赖它拉取 Clerk 的公钥并验证 JWT 签名。生产环境建议改用process.env.CLERK_JWT_ISSUER_DOMAIN,并在 Convex Dashboard 上配置同名环境变量,以区分开发与生产实例;applicationID:对应 Clerk JWT 模板中的 Audience(受众),示例中为"convex"。后端校验 JWT 时会检查aud声明与之一致;satisfies AuthConfig:让 TypeScript 对配置结构做类型检查。
前端认证桥接:把 Clerk 的登录态喂给 Convex
Convex 与 Clerk 的集成核心在 src/main.tsx 的 Provider 嵌套结构中,共三层:
ClerkProvider:初始化 Clerk 前端 SDK,管理登录会话;ConvexProviderWithClerk:来自convex/react-clerk的桥接 Provider,接收两个参数——client(ConvexReactClient实例,用VITE_CONVEX_URL指向 Convex 部署)和useAuth(Clerk 的useAuthhook)。它的职责是把 Clerk 维护的登录态(session token)自动转交给 Convex 客户端,使得之后所有 Convex query / mutation 请求都会携带经过 Clerk 签发的 JWT;App:业务根组件。
这种嵌套结构意味着:前端无需手动把 token 塞进每个请求——只要登录态存在,Convex 客户端就会自动附加认证信息;登出后,token 随之失效,后端调用自动回到未认证状态。
登录状态驱动的界面切换
src/App.tsx 演示了如何用 Clerk 的useAuth钩子驱动整个应用视图:
import { SignOutButton, useAuth } from "@clerk/react"; import { useMutation, useQuery } from "convex/react"; import { FormEvent, useState } from "react"; import { api } from "../convex/_generated/api"; import Badge from "./Badge"; import LoginPage from "./LoginPage"; export default function App() { const { isSignedIn, isLoaded } = useAuth(); if (!isLoaded) { return <div>Clerk is loading...</div>; } return <main>{isSignedIn ? <Content /> : <LoginPage />}</main>; }逻辑要点:
isLoaded:表示 Clerk 是否已完成会话状态初始化。在加载完成前渲染占位文案(Clerk is loading...),避免闪屏或错误分支;isSignedIn:布尔值,直接决定渲染Content(聊天界面)还是LoginPage(登录页)。
未登录:一个按钮的登录页
src/LoginPage.tsx 极简至极——核心就是一个SignInButton:
import { SignInButton } from "@clerk/react"; export default function LoginPage() { return ( <> <h1>Convex Chat</h1> <h2> <SignInButton /> </h2> </> ); }SignInButton是 Clerk 提供的现成组件,点击后会弹出 Clerk 托管的登录界面(支持邮箱、社交登录等,取决于你的 Clerk 应用配置),无需自己实现登录表单。
已登录:展示身份 + 登出 + 聊天
登录后渲染Content组件,包含三部分:
Badge(src/Badge.tsx):通过useUser()获取当前用户信息,展示 "Logged in as {fullName}":import { useUser } from "@clerk/react"; export default function Badge() { const { user } = useUser(); return ( <p className="badge"> <span>Logged in{user!.fullName ? ` as ${user!.fullName}` : ""}</span> </p> ); }SignOutButton:Clerk 现成的登出按钮,点击后清除会话,isSignedIn变为false,界面自动切回登录页;消息列表与发送表单:通过 Convex React hooks 与后端函数交互:
const messages = useQuery(api.messages.list) || []; const sendMessage = useMutation(api.messages.send); // ... await sendMessage({ body: newMessageText });渲染时每条消息显示
author、body和_creationTime(转为本地时间字符串)。
后端函数中的身份校验与数据关联
认证的另一半发生在后端。真正的身份校验逻辑在 convex/messages.ts 中,核心 API 是ctx.auth.getUserIdentity():
import { v } from "convex/values"; import { mutation, query } from "./_generated/server"; export const send = mutation({ args: { body: v.string() }, handler: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) { throw new Error("Unauthenticated call to mutation"); } await ctx.db.insert("messages", { body: args.body, author: identity.name ?? "Unknown", }); }, }); export const list = query({ args: {}, handler: async (ctx) => { return await ctx.db.query("messages").collect(); }, });这个函数值得逐行解读:
getUserIdentity():由 Convex 运行时解析请求附带的 JWT,并返回经过验证的用户身份对象(包含 Clerk 用户信息,如name、subject等)。它是 Convex 认证机制的核心入口——服务端根据auth.config.ts中配置的 Issuer 和 Audience 校验 token 的签名与声明;- 未认证拦截:
if (!identity) throw new Error("Unauthenticated call to mutation")。没有有效登录态时,mutation 直接抛错拒绝执行,防止匿名写入; - 身份落库:通过
identity.name ?? "Unknown"把用户显示名写入消息的author字段,实现"每条消息与发送者关联"; v.string()参数校验:send的body参数声明为字符串,Convex 会在执行前做运行时校验。
数据模型定义在 convex/schema.ts:
import { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values"; export default defineSchema({ messages: defineTable({ body: v.string(), author: v.string(), }), });list查询则是公开的:任何能访问应用的人都可以读取全部消息(无身份过滤),这符合聊天室场景——读公开、写受限。
关于"users 表"的说明:README 与当前实现的一处差异
README 中提到登录后用户信息 "persisted to auserstable"(持久化到users表)。但对比当前仓库中的 convex/schema.ts 可以看到,当前实现只定义了messages一张表,并没有users表。实际的数据关联方式是在每条消息上直接写入author字段(用户显示名),用户详情仍由 Clerk 侧维护。
从源码结构可以推断,这是示例为了保持最小化而采用的简化方案:真正需要完整用户档案(头像、邮箱、自定义属性等)时,可以在登录后通过 mutation 把getUserIdentity()返回的身份信息同步写入一张users表,并建立外键关联。实现时以实际 schema 为准即可。
换成你自己的 Clerk 实例:完整步骤
按 README 的指引,把示例接上自己的 Clerk 实例只需三步:
在 Clerk Dashboard 创建应用并配置 JWT 模板:按 Clerk 文档创建应用,并为其配置一个用于 Convex 的 JWT 模板,从中获得:
- Publishable Key:
pk_test_...形式的前端密钥; - JWT Template Issuer URL:形如
https://<your-instance>.clerk.accounts.dev的签发者地址。
- Publishable Key:
更新前端 src/main.tsx:把
ClerkProvider的publishableKey替换为你的 Publishable Key。更稳妥的做法是改用环境变量:<ClerkProvider publishableKey={import.meta.env.VITE_CLERK_PUBLISHABLE_KEY}>并在项目根目录的
.env.local中配置VITE_CLERK_PUBLISHABLE_KEY=...(以及已存在的VITE_CONVEX_URL)。更新后端 convex/auth.config.ts:把
providers[0].domain替换为你的 JWT 模板 Issuer URL;applicationID保持与你 JWT 模板中的 Audience 一致(示例为"convex")。同样建议用环境变量区分环境:domain: process.env.CLERK_JWT_ISSUER_DOMAIN!,并在 Convex Dashboard 的部署环境变量中配置
CLERK_JWT_ISSUER_DOMAIN,这样开发与生产实例可以各自指向不同的 Clerk 应用。
完成后重新运行npm run dev,即得到一套完整可用的「Clerk 登录 → Convex 鉴权 → 身份落库 → 登出」闭环应用。这个示例可以继续向两个方向扩展:一是新增users表持久化完整用户档案,二是为list查询增加基于身份的消息过滤——后端只要调用ctx.auth.getUserIdentity()即可拿到当前用户,扩展点清晰且成本极低。
【免费下载链接】convex-backendThe open-source reactive database for app developers项目地址: https://gitcode.com/gh_mirrors/co/convex-backend
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考