从 Clerk 迁移到 Better Auth
在本指南中,我们将逐步介绍如何将项目从 Clerk 迁移到 Better Auth——包括带适当哈希的电子邮件/密码、社会/外部账户、电话号码、二因素数据等。
此迁移将使所有活动会话失效。此指南目前未展示如何迁移 Organization,但通过额外步骤和 Organization 插件应该可以实现。
在开始之前
在开始迁移过程之前,在您的项目中设置 Better Auth。请遵循 安装指南 来入门。然后前往
连接到您的数据库
您需要连接到您的数据库来迁移用户和账户。您可以使用任何您想要的数据库,但在这个示例中,我们将使用 PostgreSQL。
npm install pg然后您可以使用以下代码连接到您的数据库。
import { Pool } from "pg";
export const auth = betterAuth({
database: new Pool({
connectionString: process.env.DATABASE_URL
}),
})启用电子邮件和密码(可选)
在您的认证配置中启用电子邮件和密码,并实现您自己的逻辑来发送验证电子邮件、重置密码电子邮件等。
import { betterAuth } from "better-auth";
export const auth = betterAuth({
database: new Pool({
connectionString: process.env.DATABASE_URL
}),
emailAndPassword: {
enabled: true,
},
emailVerification: {
sendVerificationEmail: async({ user, url })=>{
// implement your logic here to send email verification
}
},
})请参阅 电子邮件和密码 以获取更多配置选项。
设置社会提供商(可选)
在您的认证配置中添加您在 Clerk 项目中启用的社会提供商。
import { betterAuth } from "better-auth";
export const auth = betterAuth({
database: new Pool({
connectionString: process.env.DATABASE_URL
}),
emailAndPassword: {
enabled: true,
},
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
}
}
})添加插件(可选)
您可以根据需要将以下插件添加到您的认证配置中。
Admin 插件将允许您管理用户、用户模拟和应用级角色与权限。
Two Factor 插件将允许您为应用程序添加二因素认证。
Phone Number 插件将允许您为应用程序添加电话号码认证。
Username 插件将允许您为应用程序添加用户名认证。
import { Pool } from "pg";
import { betterAuth } from "better-auth";
import { admin, twoFactor, phoneNumber, username } from "better-auth/plugins";
export const auth = betterAuth({
database: new Pool({
connectionString: process.env.DATABASE_URL
}),
emailAndPassword: {
enabled: true,
},
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}
},
plugins: [admin(), twoFactor(), phoneNumber(), username()],
})生成 Schema
如果您使用自定义数据库适配器,请生成 schema:
npx @better-auth/cli generate或者如果您使用默认适配器,您可以使用以下命令:
npx @better-auth/cli migrate导出 Clerk 用户
前往 Clerk 仪表板并导出用户。请查看如何操作 这里。它将下载一个包含用户数据的 CSV 文件。您需要将其保存为 exported_users.csv 并放置在项目根目录中。
创建迁移脚本
在 scripts 文件夹中创建一个名为 migrate-clerk.ts 的新文件,并添加以下代码:
import { generateRandomString, symmetricEncrypt } from "better-auth/crypto";
import { auth } from "@/lib/auth"; // import your auth instance
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(); // this is the file you downloaded from 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
}
// Helper function to safely convert timestamp to Date
function safeDateConversion(timestamp?: number): Date {
if (!timestamp) return new Date();
// Convert seconds to milliseconds
const date = new Date(timestamp * 1000);
// Check if the date is valid
if (isNaN(date.getTime())) {
console.warn(`Invalid timestamp: ${timestamp}, falling back to current date`);
return new Date();
}
// Check for unreasonable dates (before 2000 or after 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);
// create user
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),
// # Two Factor (if you enabled two factor plugin)
...(isTwoFactorEnabled ? {
twoFactorEnabled: clerkUser?.two_factor_enabled
} : {}),
// # Admin (if you enabled admin plugin)
...(isAdminEnabled ? {
banned: clerkUser?.banned,
banExpiresAt: clerkUser?.lockout_expires_in_seconds,
role: "user"
} : {}),
// # Username (if you enabled username plugin)
...(isUsernameEnabled ? {
username: username,
} : {}),
// # Phone Number (if you enabled phone number plugin)
...(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
}]
})
})
// 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: externalAccount.provider_user_id,
scope: approved_scopes,
userId: createdUser?.id,
createdAt: safeDateConversion(created_at),
updatedAt: safeDateConversion(updated_at),
password: password_digest,
}
})
} else {
await ctx.adapter.create({
model: "account",
data: {
id,
providerId: provider.replace("oauth_", ""),
accountId: externalAccount.provider_user_id,
scope: approved_scopes,
userId: createdUser?.id,
createdAt: safeDateConversion(created_at),
updatedAt: safeDateConversion(updated_at),
},
forceAllowId: true
})
}
}
}
//two factor
if (isTwoFactorEnabled) {
await ctx.adapter.create({
model: "twoFactor",
data: {
userId: createdUser?.id,
secret: totp_secret,
backupCodes: await generateBackupCodes(totp_secret)
}
})
}
}
}
migrateFromClerk()
.then(() => {
console.log('Migration completed');
process.exit(0);
})
.catch((error) => {
console.error('Migration failed:', error);
process.exit(1);
});请确保用您自己的 Clerk 密钥替换 process.env.CLERK_SECRET_KEY。您可以根据需要自定义脚本。
运行迁移
运行迁移:
bun run script/migrate-clerk.ts # you can use any thing you like to run the script请确保:
- 先在开发环境中测试迁移
- 监控迁移过程以发现任何错误
- 在继续之前验证 Better Auth 中的迁移数据
- 在迁移完成之前保持 Clerk 已安装和配置
验证迁移
运行迁移后,通过检查数据库验证所有用户是否已正确迁移。
更新您的组件
现在数据已迁移,您可以开始更新组件以使用 Better Auth。以下是登录组件的示例:
import { authClient } from "better-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;
}
// Handle successful sign in
};
return (
<form onSubmit={handleSignIn}>
<button type="submit">Sign in</button>
</form>
);
};更新中间件
用 Better Auth 的中间件替换您的 Clerk 中间件:
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"],
};移除 Clerk 依赖项
一旦您验证 Better Auth 一切正常工作,您就可以移除 Clerk:
pnpm remove @clerk/nextjs @clerk/themes @clerk/types附加资源
Goodbye Clerk, Hello Better Auth – Full Migration Guide!
总结
恭喜!您已成功从 Clerk 迁移到 Better Auth。
Better Auth 提供更大的灵活性和更多功能——请务必探索 文档 以解锁其全部潜力。