# 从 Clerk 迁移到 Better Auth (/docs/guides/clerk-migration-guide)

从 Clerk 迁移到 Better Auth 的分步指南。



在本指南中，我们将演示如何将项目从 Clerk 迁移到 Better Auth——包括带有正确哈希处理的邮箱/密码登录、社交及外部账户、手机号、两步验证数据等等。

<Callout type="warn">
  此迁移将使所有活动会话失效。此指南目前未展示如何迁移组织（Organization），但可以通过额外步骤和 [组织](/docs/plugins/organization) 插件实现。
</Callout>

## 开始之前 [#开始之前]

在开始迁移过程之前，请在您的项目中设置 Better Auth。按照 [安装指南](/docs/installation) 进行操作。然后前往

<Steps>
  <Step>
    ### 连接到您的数据库 [#连接到您的数据库]

    您需要连接到数据库以迁移用户和账户。可以使用任意数据库，但本示例使用 PostgreSQL。

    <CodeBlockTabs defaultValue="npm" groupId="persist-install">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="npm">
          npm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="pnpm">
          pnpm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="yarn">
          yarn
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="bun">
          bun
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="npm">
        ```bash
        npm install pg
        ```
      </CodeBlockTab>

      <CodeBlockTab value="pnpm">
        ```bash
        pnpm add pg
        ```
      </CodeBlockTab>

      <CodeBlockTab value="yarn">
        ```bash
        yarn add pg
        ```
      </CodeBlockTab>

      <CodeBlockTab value="bun">
        ```bash
        bun add pg
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    然后可以使用以下代码连接到数据库：

    ```ts title="auth.ts"
    import { Pool } from "pg";

    export const auth = betterAuth({
        database: new Pool({ 
            connectionString: process.env.DATABASE_URL 
        }),
    })
    ```
  </Step>

  <Step>
    ### 启用邮箱/密码（可选） [#启用邮箱密码可选]

    在您的认证配置中启用邮箱和密码，并自行实现发送验证邮件、重置密码邮件等逻辑。

    <Callout type="info">
      **对 Clerk 迁移很重要**：Clerk 使用 bcrypt 对密码进行哈希，而 Better Auth 默认使用 `scrypt`。为确保迁移后的用户可以使用其现有密码登录，您需要配置 Better Auth 使用 bcrypt 进行密码验证。
    </Callout>

    首先安装 bcrypt：

    <CodeBlockTabs defaultValue="npm" groupId="persist-install">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="npm">
          npm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="pnpm">
          pnpm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="yarn">
          yarn
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="bun">
          bun
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="npm">
        ```bash
        npm install pnpm add bcrypt
        pnpm add -D @types/bcrypt
        ```
      </CodeBlockTab>

      <CodeBlockTab value="pnpm">
        ```bash
        pnpm add pnpm add bcrypt
        pnpm add -D @types/bcrypt
        # couldn't auto-convert command
        ```
      </CodeBlockTab>

      <CodeBlockTab value="yarn">
        ```bash
        yarn add pnpm add bcrypt
        pnpm add -D @types/bcrypt
        # couldn't auto-convert command
        ```
      </CodeBlockTab>

      <CodeBlockTab value="bun">
        ```bash
        bun add pnpm add bcrypt
        pnpm add -D @types/bcrypt
        # couldn't auto-convert command
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    然后配置 Better Auth 使用 bcrypt：

    ```ts title="auth.ts"
    import { betterAuth } from "better-auth";
    import bcrypt from "bcrypt";

    export const auth = betterAuth({
        emailAndPassword: { 
            enabled: true,
            password: { // [!code highlight]
                hash: async (password) => { // [!code highlight]
                    return await bcrypt.hash(password, 10); // [!code highlight]
                }, // [!code highlight]
                verify: async ({ hash, password }) => { // [!code highlight]
                    return await bcrypt.compare(password, hash); // [!code highlight]
                }, // [!code highlight]
            }, // [!code highlight]
        }, // [!code highlight]
    })
    ```

    有关更多配置选项，请参见 [邮箱/密码](/docs/authentication/email-password)。
  </Step>

  <Step>
    ### 设置社交登录提供程序（可选） [#设置社交登录提供程序可选]

    在您的认证配置中添加在 Clerk 项目中启用的社交登录提供程序。

    ```ts title="auth.ts"
    import { betterAuth } from "better-auth";

    export const auth = betterAuth({
        database: new Pool({ 
            connectionString: process.env.DATABASE_URL 
        }),
        emailAndPassword: { 
            enabled: true,
        },
        socialProviders: { // [!code highlight]
            github: { // [!code highlight]
                clientId: process.env.GITHUB_CLIENT_ID, // [!code highlight]
                clientSecret: process.env.GITHUB_CLIENT_SECRET, // [!code highlight]
            } // [!code highlight]
        } // [!code highlight]
    })
    ```
  </Step>

  <Step>
    ### 添加插件（可选） [#添加插件可选]

    您可以根据需要为以下认证配置添加插件。

    [管理员](/docs/plugins/admin) 插件可让您管理用户、用户模拟以及应用级别的角色和权限。

    [两步验证](/docs/plugins/2fa) 插件可为您的应用添加两步验证功能。

    [手机号](/docs/plugins/phone-number) 插件可为您的应用添加手机号认证功能。

    [用户名](/docs/plugins/username) 插件可为您的应用添加用户名认证功能。

    ```ts title="auth.ts"
    import { Pool } from "pg";
    import { betterAuth } from "better-auth";
    import { admin, twoFactor, phoneNumber, username } from "better-auth/plugins";
    import bcrypt from "bcrypt";

    export const auth = betterAuth({
        database: new Pool({ 
            connectionString: process.env.DATABASE_URL 
        }),
        emailAndPassword: { 
            enabled: true,
            password: {
                hash: async (password) => {
                    return await bcrypt.hash(password, 10);
                },
                verify: async ({ hash, password }) => {
                    return await bcrypt.compare(password, hash);
                },
            },
        },
        socialProviders: {
            github: {
                clientId: process.env.GITHUB_CLIENT_ID!,
                clientSecret: process.env.GITHUB_CLIENT_SECRET!,
            }
        },
        plugins: [admin(), twoFactor(), phoneNumber(), username()], // [!code highlight]
    })
    ```
  </Step>

  <Step>
    ### 生成数据库架构 [#生成数据库架构]

    如果您使用的是自定义数据库适配器，请生成架构：

    ```sh
    npx auth generate
    ```

    或者，如果您使用的是默认适配器，可以使用以下命令：

    ```sh
    npx auth migrate
    ```
  </Step>

  <Step>
    ### 导出 Clerk 用户 [#导出-clerk-用户]

    前往 Clerk 控制台并 [导出用户](https://clerk.com/docs/deployments/exporting-users#export-your-users-data-from-the-clerk-dashboard)。这将下载一个包含用户数据的 CSV 文件。您需要将其保存为 `exported_users.csv` 并放到项目根目录中。
  </Step>

  <Step>
    ### 创建迁移脚本 [#创建迁移脚本]

    在 `scripts` 文件夹中创建一个名为 `migrate-clerk.ts` 的新文件，并添加以下代码：

    ```ts title="scripts/migrate-clerk.ts"
    import { generateRandomString, symmetricEncrypt } from "better-auth/crypto";

    import { auth } from "@/lib/auth"; // 导入您的认证实例

    function getCSVData(csv: string) {
      const lines = csv.split('\n').filter(line => line.trim());
      const headers = lines[0]?.split(',').map(header => header.trim()) || [];
      const jsonData = lines.slice(1).map(line => {
          const values = line.split(',').map(value => value.trim());
          return headers.reduce((obj, header, index) => {
              obj[header] = values[index] || '';
              return obj;
          }, {} as Record<string, string>);
      });

      return jsonData as Array<{
          id: string;
          first_name: string;
          last_name: string;
          username: string;
          primary_email_address: string;
          primary_phone_number: string;
          verified_email_addresses: string;
          unverified_email_addresses: string;
          verified_phone_numbers: string;
          unverified_phone_numbers: string;
          totp_secret: string;
          password_digest: string;
          password_hasher: string;
      }>;
    }

    const exportedUserCSV = await Bun.file("exported_users.csv").text(); // 这是您从 Clerk 下载的文件

    async function getClerkUsers(totalUsers: number) {
      const clerkUsers: {
          id: string;
          first_name: string;
          last_name: string;
          username: string;
          image_url: string;
          password_enabled: boolean;
          two_factor_enabled: boolean;
          totp_enabled: boolean;
          backup_code_enabled: boolean;
          banned: boolean;
          locked: boolean;
          lockout_expires_in_seconds: number;
          created_at: number;
          updated_at: number;
          external_accounts: {
              id: string;
              provider: string;
              identification_id: string;
              provider_user_id: string;
              approved_scopes: string;
              email_address: string;
              first_name: string;
              last_name: string;
              image_url: string;
              created_at: number;
              updated_at: number;
          }[]
      }[] = [];
      for (let i = 0; i < totalUsers; i += 500) {
          const response = await fetch(`https://api.clerk.com/v1/users?offset=${i}&limit=${500}`, {
              headers: {
                  'Authorization': `Bearer ${process.env.CLERK_SECRET_KEY}`
              }
          });
          if (!response.ok) {
              throw new Error(`Failed to fetch users: ${response.statusText}`);
          }
          const clerkUsersData = await response.json();
          // biome-ignore lint/suspicious/noExplicitAny: <explanation>
          clerkUsers.push(...clerkUsersData as any);
      }
      return clerkUsers;
    }


    export async function generateBackupCodes(
      secret: string,
    ) {
      const key = secret;
      const backupCodes = Array.from({ length: 10 })
          .fill(null)
          .map(() => generateRandomString(10, "a-z", "0-9", "A-Z"))
          .map((code) => `${code.slice(0, 5)}-${code.slice(5)}`);
      const encCodes = await symmetricEncrypt({
          data: JSON.stringify(backupCodes),
          key: key,
      });
      return encCodes
    }

    // 辅助函数：将时间戳安全转换为 Date
    function safeDateConversion(timestamp?: number): Date {
      if (!timestamp) return new Date();

      const date = new Date(timestamp);

      // 检查日期是否有效
      if (isNaN(date.getTime())) {
          console.warn(`Invalid timestamp: ${timestamp}, falling back to current date`);
          return new Date();
      }

      // 检查不合理的日期（早于 2000 年或晚于 2100 年）
      const year = date.getFullYear();
      if (year < 2000 || year > 2100) {
          console.warn(`Suspicious date year: ${year}, falling back to current date`);
          return new Date();
      }

      return date;
    }

    async function migrateFromClerk() {
      const jsonData = getCSVData(exportedUserCSV);
      const clerkUsers = await getClerkUsers(jsonData.length);
      const ctx = await auth.$context
      const isAdminEnabled = ctx.options?.plugins?.find(plugin => plugin.id === "admin");
      const isTwoFactorEnabled = ctx.options?.plugins?.find(plugin => plugin.id === "two-factor");
      const isUsernameEnabled = ctx.options?.plugins?.find(plugin => plugin.id === "username");
      const isPhoneNumberEnabled = ctx.options?.plugins?.find(plugin => plugin.id === "phone-number");
      for (const user of jsonData) {
          const { id, first_name, last_name, username, primary_email_address, primary_phone_number, verified_email_addresses, unverified_email_addresses, verified_phone_numbers, unverified_phone_numbers, totp_secret, password_digest, password_hasher } = user;
          const clerkUser = clerkUsers.find(clerkUser => clerkUser?.id === id);

          // 创建用户
          const createdUser = await ctx.adapter.create<{
              id: string;
          }>({
              model: "user",
              data: {
                  id,
                  email: primary_email_address,
                  emailVerified: verified_email_addresses.length > 0,
                  name: `${first_name} ${last_name}`,
                  image: clerkUser?.image_url,
                  createdAt: safeDateConversion(clerkUser?.created_at),
                  updatedAt: safeDateConversion(clerkUser?.updated_at),
                  // # 二步验证（如果已启用二步验证插件）
                  ...(isTwoFactorEnabled ? {
                      twoFactorEnabled: clerkUser?.two_factor_enabled
                  } : {}),
                  // # 管理员（如果已启用管理员插件）
                  ...(isAdminEnabled ? {
                      banned: clerkUser?.banned,
                      banExpires: clerkUser?.lockout_expires_in_seconds
                         ? new Date(Date.now() + clerkUser.lockout_expires_in_seconds * 1000)
                         : undefined,
                      role: "user"
                  } : {}),
                  // # 用户名（如果已启用用户名插件）
                  ...(isUsernameEnabled ? {
                      username: username,
                  } : {}),
                  // # 手机号（如果已启用了手机号插件）  
                  ...(isPhoneNumberEnabled ? {
                      phoneNumber: primary_phone_number,
                      phoneNumberVerified: verified_phone_numbers.length > 0,
                  } : {}),
              },
              forceAllowId: true
          }).catch(async e => {
              return await ctx.adapter.findOne<{
                  id: string;
              }>({
                  model: "user",
                  where: [{
                      field: "id",
                      value: id
                  }]
              })
          })
          if (!createdUser) {
              throw new Error(`Unable to create or find user ${id}`);
          }
          // create external account
          const externalAccounts = clerkUser?.external_accounts;
          if (externalAccounts) {
              for (const externalAccount of externalAccounts) {
                  const { id, provider, identification_id, provider_user_id, approved_scopes, email_address, first_name, last_name, image_url, created_at, updated_at } = externalAccount;
                  if (externalAccount.provider === "credential") {
                      await ctx.adapter.create({
                          model: "account",
                          data: {
                              id,
                              providerId: provider,
                              accountId: createdUser.id,
                              scope: approved_scopes,
                              userId: createdUser.id,
                              createdAt: safeDateConversion(created_at),
                              updatedAt: safeDateConversion(updated_at),
                          }
                      })
                  } else {
                      const providerId = provider.replace("oauth_", "");
                      await ctx.adapter.create({
                          model: "account",
                          data: {
                              id,
                              providerId,
                              accountId: externalAccount.provider_user_id,
                              scope: approved_scopes,
                              userId: createdUser.id,
                              createdAt: safeDateConversion(created_at),
                              updatedAt: safeDateConversion(updated_at),
                          },
                          forceAllowId: true
                      })
                  }
              }
          }

          // 两步验证
          if (isTwoFactorEnabled) {
              await ctx.adapter.create({
                  model: "twoFactor",
                  data: {
                      userId: createdUser.id,
                      secret: totp_secret,
                      backupCodes: await generateBackupCodes(totp_secret)
                  }
              })
          }
      }
    }

    migrateFromClerk()
      .then(() => {
          console.log('迁移完成');
          process.exit(0);
      })
      .catch((error) => {
          console.error('迁移失败：', error);
          process.exit(1);
      });
    ```

    请务必将 `process.env.CLERK_SECRET_KEY` 替换为您自己的 Clerk 密钥。您可以根据需要自定义此脚本。
  </Step>

  <Step>
    ### 运行迁移 [#运行迁移]

    运行迁移命令：

    ```sh
    bun run script/migrate-clerk.ts # 您也可以使用任意方式运行该脚本
    ```

    <Callout type="warning">
      请确保：

      1. 首先在开发环境中测试迁移
      2. 监控迁移过程中的任何错误
      3. 在继续操作之前验证 Better Auth 中的迁移数据
      4. 在迁移完成前保持 Clerk 已安装并配置
    </Callout>
  </Step>

  <Step>
    ### 验证迁移 [#验证迁移]

    迁移完成后，请检查数据库以验证所有用户是否已正确迁移。
  </Step>

  <Step>
    ### 更新您的组件 [#更新您的组件]

    现在数据已迁移，您可以开始更新组件以使用 Better Auth。以下是一个登录组件的示例：

    ```tsx title="components/auth/sign-in.tsx"
    import { authClient } from "@/lib/auth-client";

    export const SignIn = () => {
      const handleSignIn = async () => {
        const { data, error } = await authClient.signIn.email({
          email: "user@example.com",
          password: "password",
        });
        
        if (error) {
          console.error(error);
          return;
        }
        // 处理成功登录
      };

      return (
        <form onSubmit={handleSignIn}>
          <button type="submit">Sign in</button>
        </form>
      );
    };
    ```
  </Step>

  <Step>
    ### 更新中间件 [#更新中间件]

    将您的 Clerk 中间件替换为 Better Auth 的中间件：

    ```ts title="middleware.ts"

    import { NextRequest, NextResponse } from "next/server";
    import { getSessionCookie } from "better-auth/cookies";
    export async function middleware(request: NextRequest) {
      const sessionCookie = getSessionCookie(request);
      const { pathname } = request.nextUrl;
      if (sessionCookie && ["/login", "/signup"].includes(pathname)) {
        return NextResponse.redirect(new URL("/dashboard", request.url));
      }
      if (!sessionCookie && pathname.startsWith("/dashboard")) {
        return NextResponse.redirect(new URL("/login", request.url));
      }
      return NextResponse.next();
    }

    export const config = {
      matcher: ["/dashboard", "/login", "/signup"],
    };
    ```
  </Step>

  <Step>
    ### 移除 Clerk 依赖 [#移除-clerk-依赖]

    确认一切在 Better Auth 中正常工作后，您可以移除 Clerk 依赖：

    ```bash title="移除 Clerk"
    pnpm remove @clerk/nextjs @clerk/themes @clerk/types
    ```
  </Step>
</Steps>

## 额外资源 [#额外资源]

[告别 Clerk，迎接 Better Auth – 全面迁移指南！](https://www.youtube.com/watch?v=Za_QihbDSuk)

## 总结 [#总结]

恭喜！您已成功从 Clerk 迁移到 Better Auth。

Better Auth 提供了更大的灵活性和更多功能——务必浏览[文档](/docs)以充分发挥其潜力。

