Supabase Auth 到 Better Auth + PlanetScale PostgreSQL 迁移指南
最近,PlanetScale 宣布支持 PostgreSQL。这对开发者来说是令人振奋的消息,也是数据库行业的重要进步。
我们注意到一些用户正在从 Supabase 迁移到 PlanetScale PostgreSQL,但由于他们也依赖 Supabase Auth,这带来了一些挑战。本文将帮助你将认证从 Supabase Auth 迁移到运行在 PlanetScale PostgreSQL 上的 Better Auth。
1. 设置 PlanetScale 数据库
打开 PlanetScale 控制台
创建一个新数据库
获取你的连接字符串(PostgreSQL URI)
postgresql://<用户名>:<密码>@<主机>/postgres?sslmode=verify-full将数据库 URL 保存在你的 .env 文件,稍后用于 Better Auth:
DATABASE_URL =
postgresql://<用户名>:<密码>@<主机>/postgres?sslmode=verify-full这将作为我们认证配置中的 database 字段内容
2. 安装 Better Auth
安装 Better Auth
npm install better-auth按照并完成以下基础设置 文档
请确保按照文档设置所有必需的环境变量。
3. 安装 PostgreSQL 客户端
安装 pg 包及其类型:
npm install pg
npm install --save-dev @types/pg4. 生成并迁移 Better Auth 模式
运行此 CLI 命令生成设置 Better Auth 所需的全部模式:
npx auth generate 接着运行此命令将生成的模式应用到你的 PlanetScale 数据库:
npx auth migrate 现在你应该已经在 PlanetScale 中拥有所需的认证表。
5. 快速检查
你的认证配置应类似如下:
import { Pool } from "pg";
import { betterAuth } from "better-auth";
export const auth = betterAuth({
baseURL: "http://localhost:3000",
database: new Pool({
connectionString: process.env.DATABASE_URL,
}),
emailAndPassword: {
enabled: true,
},
});import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: "http://localhost:3000",
});
export const { signIn, signUp, useSession } = createAuthClient();6. 有趣的部分
现在到了有趣的环节。你已完成全部设置,现在只需查找代码中使用 Supabase Auth 客户端的地方,替换为 Better Auth 客户端即可。我们这里看几个示例。
// Supabase Auth
await supabase.auth.signUp({
email,
password,
});
// Better Auth
await authClient.signUp.email({
email,
password,
name: "John",
});// Supabase
await supabase.auth.signInWithPassword({
email,
password,
});
// Better Auth
await authClient.signIn.email({
email,
password,
});// Supabase
const { data, error } = await supabase.auth.getClaims();
// Better Auth
const { data, error } = await authClient.useSession();7. 从 Supabase Auth 迁移用户
此迁移会使所有活跃会话失效。虽然当前指南未涵盖两因素认证(2FA)或行级安全(RLS)配置的迁移,但这些都应可通过额外步骤实现。
更多详细指南请参考我们提供的迁移指南。
基本上,你可以将以下代码复制到 migration.ts 并运行。
import { Pool } from "pg";
import { auth } from "./lib/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: { providerId: string }) =>
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: { providerId: string }) =>
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();运行迁移脚本:
bun migration.ts # 或使用 node, ts-node 等工具运行8. 迁移其余数据
如果你在 Supabase 中还有其他与用户相关的数据,可以使用Supabase 到 PlanetScale 的迁移工具。
9. 清理代码库中所有 Supabase Auth 相关代码
你现在已完全掌控认证,建议开始移除所有与 Supabase Auth 相关的代码。
10. 完成!🎉
你已成功将认证从 Supabase Auth 迁移到了 PlanetScale 上的 Better Auth。
小贴士
- 上线前请仔细检查生产环境中的所有环境变量是否正确定义。
- 测试所有认证流程(注册、登录、重置密码、会话刷新)确保正常。
- 这只是基础操作,如果你在很多地方集成了 Supabase Auth 的功能,需在这里找到合适的 Better Auth 替代方案。
- 玩得开心!