从 Supabase Auth 迁移到 Better Auth

在本文中,我们将介绍从 Supabase Auth 迁移到 Better Auth 的步骤。

这次迁移将使所有活跃会话失效。虽然本指南目前不包括迁移双因素认证(2FA)或行级安全(RLS)配置,但通过额外的步骤,这两者都应该是可能的。

开始之前

在开始迁移过程之前,请在你的项目中设置 Better Auth。按照安装指南开始。

连接到你的数据库

你需要连接到数据库以迁移用户和账户。从你的 Supabase 项目复制 DATABASE_URL 并使用它连接到你的数据库。对于这个示例,我们需要安装 pg 来连接数据库。

npm install pg

然后你可以使用以下代码连接到你的数据库。

auth.ts
import { Pool } from "pg";
 
export const auth = betterAuth({
    database: new Pool({ 
        connectionString: process.env.DATABASE_URL 
    }),
})

启用电子邮件和密码(可选)

在你的认证配置中启用电子邮件和密码。

auth.ts
import { admin, anonymous } from "better-auth/plugins";
 
export const auth = betterAuth({
    database: new Pool({ 
        connectionString: process.env.DATABASE_URL 
    }),
    emailAndPassword: { 
        enabled: true, 
    } 
})

设置社交提供商(可选)

在你的认证配置中添加你在 Supabase 项目中启用的社交提供商。

auth.ts
import { admin, anonymous } 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, 
        } 
    } 
})

添加管理员和匿名插件(可选)

在你的认证配置中添加 adminanonymous 插件。

auth.ts
import { admin, anonymous } 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(), anonymous()], 
})

运行迁移

运行迁移以在你的数据库中创建必要的表。

Terminal
npx @better-auth/cli migrate

这将在你的数据库中创建以下表:

这些表将在 public 模式中创建。

复制迁移脚本

现在我们在数据库中有了必要的表,我们可以运行迁移脚本将用户和账户从 Supabase 迁移到 Better Auth。

首先在你的项目中创建一个 .ts 文件。

Terminal
touch migration.ts

然后将以下代码复制并粘贴到该文件中。

migration.ts
import { Pool } from "pg";
import { auth } from "./auth";
import { User as SupabaseUser } from "@supabase/supabase-js";
 
type User = SupabaseUser & {
	is_super_admin: boolean;
	raw_user_meta_data: {
		avatar_url: string;
	};
	encrypted_password: string;
	email_confirmed_at: string;
	created_at: string;
	updated_at: string;
	is_anonymous: boolean;
	identities: {
		provider: string;
		identity_data: {
			sub: string;
			email: string;
		};
		created_at: string;
		updated_at: string;
	};
};
 
const migrateFromSupabase = async () => {
	const ctx = await auth.$context;
	const db = ctx.options.database as Pool;
	const users = await db
		.query(`
			SELECT 
				u.*,
				COALESCE(
					json_agg(
						i.* ORDER BY i.id
					) FILTER (WHERE i.id IS NOT NULL),
					'[]'::json
				) as identities
			FROM auth.users u
			LEFT JOIN auth.identities i ON u.id = i.user_id
			GROUP BY u.id
		`)
		.then((res) => res.rows as User[]);
	for (const user of users) {
		if (!user.email) {
			continue;
		}
		await ctx.adapter
			.create({
				model: "user",
				data: {
					id: user.id,
					email: user.email,
					name: user.email,
					role: user.is_super_admin ? "admin" : user.role,
					emailVerified: !!user.email_confirmed_at,
					image: user.raw_user_meta_data.avatar_url,
					createdAt: new Date(user.created_at),
					updatedAt: new Date(user.updated_at),
					isAnonymous: user.is_anonymous,
				},
			})
			.catch(() => {});
		for (const identity of user.identities) {
			const existingAccounts = await ctx.internalAdapter.findAccounts(user.id);
 
			if (identity.provider === "email") {
				const hasCredential = existingAccounts.find(
					(account) => account.providerId === "credential",
				);
				if (!hasCredential) {
					await ctx.adapter
						.create({
							model: "account",
							data: {
								userId: user.id,
								providerId: "credential",
								accountId: user.id,
								password: user.encrypted_password,
								createdAt: new Date(user.created_at),
								updatedAt: new Date(user.updated_at),
							},
						})
						.catch(() => {});
				}
			}
			const supportedProviders = Object.keys(ctx.options.socialProviders || {})
			if (supportedProviders.includes(identity.provider)) {
				const hasAccount = existingAccounts.find(
					(account) => account.providerId === identity.provider,
				);
				if (!hasAccount) {
					await ctx.adapter.create({
						model: "account",
						data: {
							userId: user.id,
							providerId: identity.provider,
							accountId: identity.identity_data?.sub,
							createdAt: new Date(identity.created_at ?? user.created_at),
							updatedAt: new Date(identity.updated_at ?? user.updated_at),
						},
					});
				}
			}
		}
	}
};
migrateFromSupabase();

自定义迁移脚本(可选)

  • name:迁移脚本将使用用户的电子邮件作为名称。如果你在数据库中有用户显示名称,你可能想要自定义它。
  • socialProviderList:迁移脚本将使用你在认证配置中启用的社交提供商。如果你有额外的社交提供商尚未在认证配置中启用,你可能想要自定义它。
  • role:如果你不使用 admin 插件,请移除 role
  • isAnonymous:如果你不使用 anonymous 插件,请移除 isAnonymous
  • 更新引用 users 表的其他表以使用 id 字段。

运行迁移脚本

运行迁移脚本将用户和账户从 Supabase 迁移到 Better Auth。

Terminal
bun migration.ts # 或使用 node, ts-node 等

更新你的代码

将你的代码库从 Supabase 认证调用更新为 Better Auth API。

以下是 Supabase 认证 API 调用及其 Better Auth 对应项的列表。

  • supabase.auth.signUp -> authClient.signUp.email
  • supabase.auth.signInWithPassword -> authClient.signIn.email
  • supabase.auth.signInWithOAuth -> authClient.signIn.social
  • supabase.auth.signInAnonymously -> authClient.signIn.anonymous
  • supabase.auth.signOut -> authClient.signOut
  • supabase.auth.getSession -> authClient.getSession - 你也可以使用 authClient.useSession 获取响应式状态

了解更多:

  • 基本用法:了解如何使用认证客户端进行注册、登录和登出。
  • 电子邮件和密码:了解如何为你的项目添加电子邮件和密码认证。
  • 匿名:了解如何为你的项目添加匿名认证。
  • 管理员:了解如何为你的项目添加管理员认证。
  • 电子邮件一次性密码:了解如何为你的项目添加电子邮件一次性密码认证。
  • 钩子:了解如何使用钩子监听事件。
  • Next.js:了解如何在 Next.js 项目中使用认证客户端。

中间件

要使用中间件保护路由,请参阅 Next.js 中间件指南 或你的框架文档。

总结

恭喜!你已成功从 Supabase Auth 迁移到 Better Auth。

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

On this page