news 2026/9/23 21:38:16

在 Convex 聊天应用中集成 Clerk 用户认证:clerk-initial-auth 示例全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
在 Convex 聊天应用中集成 Clerk 用户认证:clerk-initial-auth 示例全解析

在 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 描述的行为闭环非常清晰:

  1. 应用启动后,用户首先看到的是一个"Log In"(登录)按钮;
  2. 用户登录成功后,其身份信息被持久化到数据库(README 描述为持久化到users表);
  3. 用户发送的每条消息都与发送它的用户关联;
  4. 用户可以通过"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 绑定,提供ClerkProviderSignInButtonuseAuthuseUser等;
  • 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 嵌套结构中,共三层:

  1. ClerkProvider:初始化 Clerk 前端 SDK,管理登录会话;
  2. ConvexProviderWithClerk:来自convex/react-clerk的桥接 Provider,接收两个参数——clientConvexReactClient实例,用VITE_CONVEX_URL指向 Convex 部署)和useAuth(Clerk 的useAuthhook)。它的职责是把 Clerk 维护的登录态(session token)自动转交给 Convex 客户端,使得之后所有 Convex query / mutation 请求都会携带经过 Clerk 签发的 JWT;
  3. 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组件,包含三部分:

  1. 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> ); }
  2. SignOutButton:Clerk 现成的登出按钮,点击后清除会话,isSignedIn变为false,界面自动切回登录页;

  3. 消息列表与发送表单:通过 Convex React hooks 与后端函数交互:

    const messages = useQuery(api.messages.list) || []; const sendMessage = useMutation(api.messages.send); // ... await sendMessage({ body: newMessageText });

    渲染时每条消息显示authorbody_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 用户信息,如namesubject等)。它是 Convex 认证机制的核心入口——服务端根据auth.config.ts中配置的 Issuer 和 Audience 校验 token 的签名与声明;
  • 未认证拦截if (!identity) throw new Error("Unauthenticated call to mutation")。没有有效登录态时,mutation 直接抛错拒绝执行,防止匿名写入;
  • 身份落库:通过identity.name ?? "Unknown"把用户显示名写入消息的author字段,实现"每条消息与发送者关联";
  • v.string()参数校验sendbody参数声明为字符串,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 实例只需三步:

  1. 在 Clerk Dashboard 创建应用并配置 JWT 模板:按 Clerk 文档创建应用,并为其配置一个用于 Convex 的 JWT 模板,从中获得:

    • Publishable Keypk_test_...形式的前端密钥;
    • JWT Template Issuer URL:形如https://<your-instance>.clerk.accounts.dev的签发者地址。
  2. 更新前端 src/main.tsx:把ClerkProviderpublishableKey替换为你的 Publishable Key。更稳妥的做法是改用环境变量:

    <ClerkProvider publishableKey={import.meta.env.VITE_CLERK_PUBLISHABLE_KEY}>

    并在项目根目录的.env.local中配置VITE_CLERK_PUBLISHABLE_KEY=...(以及已存在的VITE_CONVEX_URL)。

  3. 更新后端 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 21:37:02

OpenClaw:跨越AI从聊天到执行的能力鸿沟

1. 从聊天到干活的AI进化论去年我在调试一个智能客服系统时&#xff0c;发现个有趣现象&#xff1a;当用户问"帮我查订单"时&#xff0c;AI能完美回答查询步骤&#xff0c;但当用户直接说"订单号XXXX&#xff0c;查物流"时&#xff0c;系统就卡壳了。这让我…

作者头像 李华
网站建设 2026/9/23 21:32:02

技术博文标题设计规范与输入完整性要求

我无法基于“2021-10-30”这一纯日期型标题生成符合要求的高质量博文。原因如下&#xff1a;该标题不具备可拆解的项目属性&#xff1a;无技术载体&#xff08;如软件、硬件、协议、工具&#xff09;、无明确动作&#xff08;如“搭建”“修复”“迁移”“优化”&#xff09;、…

作者头像 李华
网站建设 2026/9/23 21:31:14

主域控与辅助域控搭建及FSMO角色迁移全流程指南

简介&#xff1a;面向Windows Server 2003环境下需要搭建主/辅助域控并完成域控制器迁移的系统管理员与运维学习者&#xff0c;这份资料将搭建与迁移全过程整理成可直接跟做的操作笔记。内容先从主域控安装向导开始&#xff0c;涵盖DNS全名与NETBIOS名设置、目录还原密码等关键…

作者头像 李华
网站建设 2026/9/23 21:30:59

VMware精简置备虚拟磁盘越删越大?空间回收与VMDK瘦身实战

简介&#xff1a;一份面向VMware虚拟化运维与存储管理人员的实用技术文档&#xff0c;围绕精简置备&#xff08;Thin&#xff09;磁盘在vmfs5文件系统下无法自动回收空间的问题展开&#xff0c;系统梳理了两种成熟的回收方案。该文档为可编辑的docx格式&#xff0c;共1个文件&a…

作者头像 李华
网站建设 2026/9/23 21:29:13

论文写作流程怎么安排?一份从开题到提交的指南

论文写作流程怎么安排&#xff1f;一份从开题到提交的指南 工具不是越多越好&#xff0c;关键是放在正确环节。每位学弟学妹在撰写论文时&#xff0c;都会经历从选题、资料收集、写作到最终提交的各个阶段。在这些环节中&#xff0c;合理利用工具和方法&#xff0c;可以大大提…

作者头像 李华