You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

已配置Clerk Webhook,如何同步用户数据至Prisma数据库?

解决方案:将Clerk用户同步至Prisma+Neon数据库(Next.js14)

一、配置Prisma数据模型

首先确保你的Prisma Schema包含关联Clerk用户ID和自定义表单字段的模型:

model User {
  id            String    @id @default(cuid())
  clerkUserId   String    @unique // 关联Clerk用户ID,确保唯一性
  email         String    @unique
  name          String?   // 自定义注册表单的姓名字段
  phone         String?   // 自定义注册表单的手机号字段
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt
}

执行命令更新数据库:

npx prisma db push

二、处理Clerk Webhook(同步基础用户数据)

你已经配置了Webhook并验证交付成功,接下来在Next.js的App Router中创建Webhook端点:

创建app/api/webhooks/clerk/route.ts:

import { verifyWebhookSignature } from '@clerk/nextjs/server';
import { PrismaClient } from '@prisma/client';
import { headers } from 'next/headers';
import { NextResponse } from 'next/server';

const prisma = new PrismaClient();

export async function POST(req: Request) {
  try {
    // 获取Webhook签名和请求体
    const headerPayload = headers();
    const signature = headerPayload.get('Clerk-Signature') as string;
    const body = await req.text();

    // 验证签名(使用Clerk Dashboard中生成的Webhook Secret)
    const secret = process.env.CLERK_WEBHOOK_SECRET!;
    verifyWebhookSignature({ secret, signature, body });

    const payload = JSON.parse(body);

    // 仅处理用户创建事件
    if (payload.type === 'user.created') {
      const clerkUser = payload.data;
      // 插入或更新数据库用户(避免重复)
      await prisma.user.upsert({
        where: { clerkUserId: clerkUser.id },
        update: {
          email: clerkUser.email_addresses[0].email_address,
          name: clerkUser.first_name || clerkUser.last_name 
            ? `${clerkUser.first_name || ''} ${clerkUser.last_name || ''}`.trim() 
            : null
        },
        create: {
          clerkUserId: clerkUser.id,
          email: clerkUser.email_addresses[0].email_address,
          name: clerkUser.first_name || clerkUser.last_name 
            ? `${clerkUser.first_name || ''} ${clerkUser.last_name || ''}`.trim() 
            : null
        }
      });
    }

    return NextResponse.json({ success: true });
  } catch (error) {
    console.error('Webhook处理失败:', error);
    return NextResponse.json({ error: 'Webhook处理失败' }, { status: 400 });
  }
}

三、自定义注册表单同步额外数据

你使用useSignUp实现了自定义注册,在OTP验证完成后,调用自有API保存表单字段:

1. 自定义注册组件逻辑

'use client';

import { useSignUp } from '@clerk/nextjs';
import { useState } from 'react';

export default function SignUpForm() {
  const { signUp, isLoaded, setActive } = useSignUp();
  const [name, setName] = useState('');
  const [phone, setPhone] = useState('');
  const [email, setEmail] = useState('');
  const [otp, setOtp] = useState('');
  const [step, setStep] = useState('email');

  // 发送OTP验证码
  const handleSendOtp = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!isLoaded) return;

    try {
      await signUp.create({ emailAddress: email });
      await signUp.prepareEmailAddressVerification({ strategy: 'email_code' });
      setStep('otp');
    } catch (err) {
      console.error('发送验证码失败:', err);
    }
  };

  // 验证OTP并保存用户数据
  const handleVerifyOtp = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!isLoaded) return;

    try {
      const verificationResult = await signUp.attemptEmailAddressVerification({ code: otp });
      
      if (verificationResult.status === 'complete') {
        const clerkUserId = signUp.user.id;
        
        // 调用自有API保存表单数据
        await fetch('/api/users', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ clerkUserId, name, phone, email })
        });

        // 设置活跃会话并跳转
        await setActive({ session: verificationResult.createdSessionId });
        window.location.href = '/';
      }
    } catch (err) {
      console.error('验证验证码失败:', err);
    }
  };

  return (
    <div className="max-w-md mx-auto mt-8">
      {step === 'email' && (
        <form onSubmit={handleSendOtp} className="space-y-4">
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            placeholder="邮箱"
            required
            className="w-full p-2 border rounded"
          />
          <input
            type="text"
            value={name}
            onChange={(e) => setName(e.target.value)}
            placeholder="姓名"
            required
            className="w-full p-2 border rounded"
          />
          <input
            type="tel"
            value={phone}
            onChange={(e) => setPhone(e.target.value)}
            placeholder="手机号"
            required
            className="w-full p-2 border rounded"
          />
          <button type="submit" className="w-full p-2 bg-blue-500 text-white rounded">发送验证码</button>
        </form>
      )}

      {step === 'otp' && (
        <form onSubmit={handleVerifyOtp} className="space-y-4">
          <input
            type="text"
            value={otp}
            onChange={(e) => setOtp(e.target.value)}
            placeholder="验证码"
            required
            className="w-full p-2 border rounded"
          />
          <button type="submit" className="w-full p-2 bg-blue-500 text-white rounded">验证并注册</button>
        </form>
      )}
    </div>
  );
}

2. 创建保存用户数据的API端点

创建app/api/users/route.ts:

import { PrismaClient } from '@prisma/client';
import { NextResponse } from 'next/server';

const prisma = new PrismaClient();

export async function POST(req: Request) {
  try {
    const { clerkUserId, name, phone, email } = await req.json();

    // 插入或更新用户(和Webhook逻辑保持一致,避免冲突)
    await prisma.user.upsert({
      where: { clerkUserId },
      update: { name, phone },
      create: { clerkUserId, email, name, phone }
    });

    return NextResponse.json({ success: true });
  } catch (error) {
    console.error('保存用户数据失败:', error);
    return NextResponse.json({ error: '保存用户数据失败' }, { status: 400 });
  }
}

关键注意事项

  • 环境变量配置:确保.env文件中包含CLERK_WEBHOOK_SECRET、DATABASE_URL、NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY、CLERK_SECRET_KEY
  • 签名验证:必须验证Clerk Webhook的签名,防止恶意请求
  • Upsert逻辑:使用upsert避免重复创建用户,无论是Webhook触发还是自定义表单API触发
  • 错误处理:每个异步操作都添加try/catch便于调试排查问题

内容的提问来源于stack exchange,提问作者Omar Al Seddik

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 03:24:52