从 Supabase Auth 迁移到 Better Auth + PlanetScale PostgreSQL

本迁移指南旨在帮助你将认证从 Supabase Auth 迁移到运行在 PlanetScale PostgreSQL 上的 Better Auth。

Dagmawi Esayas·

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:

.env
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/pg

4. 生成并迁移 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 并运行。

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();

运行迁移脚本:

Terminal
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 替代方案。
  • 玩得开心!

了解更多!