所有文章
Clerk 用户生命周期事件通过已签名的 Webhook 隧道传入 Next.js App Router 端点和本地数据库。
ClerkNext.jswebhooksuser sync

在 Next.js 本地测试 Clerk Webhook

要在 Next.js 的 localhost 环境中测试 Clerk Webhook,请创建 App Router POST 路由,通过 HTTPS 隧道公开 3000 端口,并将公开 URL 添加为 Clerk Webhook 端点。在同步用户数据之前,务必使用 verifyWebhook() 验证每个请求。 请在 Clerk 中间件中保持该路由公开:签名密钥验证的是机器间请求,而不是浏览器会话。

当 Clerk webhook 是正确的同步工具时

Clerk仍然是事实的身份来源。当您的应用程序需要本地投影来进行连接、搜索、报告、授权元数据或无法按需查询 Clerk 的集成时,Webhook 非常有用。典型事件包括 user.created, user.updated, 和 user.deleted

Webhook 是异步的。用户可以在数据库投影存在之前完成注册,可以重试交付,并且更新可以同时到达。不要将投影作为注册后立即进行身份检查的唯一来源。设计读取以容忍短暂的延迟或在用户流中显式创建应用程序行并让 Webhooks 对其进行协调。

创建公共 Next.js App Router 端点

添加 app/api/webhooks/clerk/route.ts。Clerk 当前的 同步指南 用途 verifyWebhook@clerk/nextjs/webhooks。帮助器使用请求,检查标准 Webhooks 签名,并返回键入的事件数据。

// app/api/webhooks/clerk/route.ts
import { verifyWebhook } from '@clerk/nextjs/webhooks';
import { NextRequest } from 'next/server';

export const runtime = 'nodejs';

export async function POST(request: NextRequest) {
  try {
    const event = await verifyWebhook(request);

    await processOnce(event, async () => {
      switch (event.type) {
        case 'user.created':
        case 'user.updated':
          await upsertClerkUser(event.data);
          break;
        case 'user.deleted':
          if (event.data.id) await archiveClerkUser(event.data.id);
          break;
      }
    });

    return new Response('accepted', { status: 200 });
  } catch (error) {
    console.error('Clerk webhook rejected', safeError(error));
    return new Response('invalid webhook', { status: 400 });
  }
}

默认情况下,助手读取 CLERK_WEBHOOK_SIGNING_SECRET。Clerk的 验证Webhook参考 也允许显式 signingSecret 选项,但环境配置避免将秘密嵌入源中。

从会话保护中排除 Webhook 路由

Webhook 请求不携带您用户的 Clerk 会话。如果中间件调用 auth.protect() 对于每个 API 路径,Clerk 的交付都会在签名验证运行之前收到重定向、401 或 404。显式定义受保护的应用程序路由并离开 /api/webhooks/clerk 民众。

// middleware.ts for Next.js 15 and earlier
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

const isProtectedRoute = createRouteMatcher([
  '/dashboard(.*)',
  '/api/private(.*)',
]);

export default clerkMiddleware(async (auth, request) => {
  if (isProtectedRoute(request)) await auth.protect();
});

export const config = {
  matcher: [
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico)).*)',
    '/(api|trpc)(.*)',
  ],
};

Clerk的 webhook调试指南 特别指出排除 webhook 路由。在较新的 Next.js 版本中,可以使用约定 proxy.ts;遵循项目中安装的 Clerk 版本指南。公开并不意味着值得信赖: verifyWebhook() 在任何行动之前都是强制性的。

将 Clerk 连接到本地主机

  1. 跑步 npm run dev 并验证 Next.js 应用程序正在侦听端口 3000。
  2. 跑步 npx portpreview 3000
  3. 在 Clerk Dashboard 中,使用以下命令创建一个 Webhook 端点 https://your-subdomain.portpreview.dev/api/webhooks/clerk
  4. 仅选择所需的用户、会话、组织或电子邮件事件。
  5. 将端点签名密钥复制到 CLERK_WEBHOOK_SIGNING_SECRET 在您的本地环境中并重新启动 Next.js。
  6. 打开端点的测试选项卡,选择 user.created,然后选择发送示例。
  7. 确认尝试显示“成功”,您的本地路由返回 200,并且预期的数据库行更改了一次。

对于后续示例,隧道 URL 必须保持活动状态。如果发生变化,请更新端点。不要将生产端点的签名密钥重复用于本地测试;创建特定于环境的端点,以便轮换和审核历史记录保持清晰。

仔细模型用户同步

使用 Clerk 用户 ID 作为外部密钥

店铺 user_... 以独特的 clerk_user_id 柱子。更新该键,以便重试 user.created 收敛而不是失败。如果其他表已经引用了您自己的内部主键,请保留它。

慎重选择主要电子邮件

Clerk用户数据包含电子邮件地址记录和主电子邮件地址 ID。通过 ID 解析主记录,而不是采用第一个数组元素。电子邮件可以更改;不要将其用作不可变的外键。

确定删除的含义

一个 user.deleted 事件包含的数据可能少于创建或更新的数据。根据您的策略,使用 ID 进行匿名化、软删除或启动保留工作流程。盲目级联删除可能会破坏法规要求您保留的计费或审计记录。

不要镜像一切

仅保留您的应用程序需要的字段。每个复制的配置文件字段都会产生隐私、保留和过时义务。按需获取很少使用的 Clerk 数据,而不是复制整个有效负载。

使重试和排序无害

Clerk 记录非 2xx 响应会导致事件重试。在应用效果之前,将签名的 Webhook 元数据或标头中的 Webhook 消息标识符记录为唯一收据。如果您的 SDK 在标头中公开标准 Webhooks ID,请将它们与事件类型和时间戳一起保留。对于完整的副本,返回 200。

对于用户更新,比较事件时间戳或使用上次写入规则来防止较旧的事件覆盖较新的配置文件数据。对于高值状态,验证后从 Clerk 检索当前用户,并将 webhook 视为协调信号。将收据插入、用户更新和发件箱作业保留在一个数据库事务中。

await db.transaction(async (tx) => {
  const inserted = await tx.webhookReceipt.insertIfAbsent(messageId);
  if (!inserted) return;

  await tx.user.upsert({
    clerkUserId: clerk.id,
    primaryEmail: findPrimaryEmail(clerk),
    sourceUpdatedAt: eventTimestamp,
  });
  await tx.outbox.enqueue('profile-synced', { clerkUserId: clerk.id });
});

此模式可防止重复的欢迎电子邮件和部分写入。看 Webhook 重试和幂等性 对于架构选项。

解决当地Clerk送货问题

404、重定向或 HTML 而不是您的处理程序

验证文件路径、POST 导出和完整隧道 URL。检查中间件和区域设置重写。 Webhook 不应通过登录重定向或 CSRF 表单检查。使用基本 POST 测试公共 URL;预计您的路线会拒绝签名,而不是框架 404。

verifyWebhook() 总是抛出

设置签名密钥后重新启动 Next.js。确认密钥属于此端点和环境。在将请求主体传递给帮助器之前,请勿错误地解析、克隆或使用请求主体。确保隧道保留标准 Webhooks 签名标头。

仪表板显示重试

查看确切的尝试响应。仅在持久接受后返回 2xx,但在提供者超时以下继续处理。数据库迁移、唯一约束错误以及同步调用不可用的服务是常见的 500 个原因。

行重复或过时

为 Clerk ID 和 webhook 消息 ID 添加唯一约束。制作 user.createduser.updated 两个安全的更新插入,然后防止较旧的事件时间戳。每次修复后重播该示例以证明重复处理。

安全检查表

  • 在记录有效负载字段或写入数据之前,请与 Clerk 的助手验证每个请求。
  • 保持路由未经用户会话中间件验证,但受 Webhook 签名保护。
  • 对本地、预览、暂存和生产环境使用单独的端点机密。
  • 消除重复的签名消息 ID 并唯一地限制用户 ID。
  • 从正常日志中编辑电子邮件、电话、令牌、机密和完整有效负载。
  • 可以选择添加 Clerk/Svix 记录的 IP 控制作为深度防御,但切勿用 IP 过滤代替签名验证。
  • 对无效流量进行速率限制、限制正文大小,并轮换出现在日志或源代码管理中的机密。

Clerk的 Webhook 概述 解释签名验证和可选的 Svix IP 限制。对于 Next.js 原始主体基础知识和路由行为,请继续 Next.js 本地主机 webhook 指南

常见问题

如何在 Next.js 本地测试 Clerk Webhook?
创建 App Router POST 路由,通过 HTTPS 隧道公开 3000 端口,在 Clerk Dashboard 中添加完整的公开路由,设置 CLERK_WEBHOOK_SIGNING_SECRET,然后从端点的测试选项卡发送示例。
Clerk Webhook 路由应该由 clerkMiddleware 保护吗?
该路由必须在没有用户会话时也能访问,因此不要对它运行 auth.protect()。应使用 verifyWebhook() 和端点签名密钥验证机器请求。
在 Clerk verifyWebhook() 之前需要解析原始请求体吗?
不需要。请将原始 Request 直接传给 verifyWebhook()。不要先调用 request.json() 或 request.text(),因为 helper 需要已签名的请求体和请求头。
如何处理 Clerk user.created 的重试?
按唯一的 Clerk 用户 ID 执行 upsert,使用已签名的 Webhook 消息 ID 去重,并对已处理的事件返回 200。将非必要的副作用放入队列,避免重试时重复发送邮件。