Migrating from Supabase Auth to Better Auth

In this guide, we'll walk through the steps to migrate a project from Supabase Auth to Better Auth.

This migration will invalidate all active sessions. While this guide doesn't currently cover migrating two-factor (2FA) or Row Level Security (RLS) configurations, both should be possible with additional steps.

Before You Begin

Before starting the migration process, set up Better Auth in your project. Follow the installation guide to get started.

Connect to your database

You'll need to connect to your database to migrate the users and accounts. Copy your DATABASE_URL from your Supabase project and use it to connect to your database. And for this example, we'll need to install pg to connect to the database.

npm install pg

And then you can use the following code to connect to your database.

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

Enable Email and Password (Optional)

Enable the email and password in your auth config.

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

Setup Social Providers (Optional)

Add social providers you have enabled in your Supabase project in your auth config.

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, 
        } 
    } 
})

Add admin and anonymous plugins (Optional)

Add the admin and anonymous plugins to your auth config.

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()], 
})

Run the migration

Run the migration to create the necessary tables in your database.

Terminal
npx @better-auth/cli migrate

This will create the following tables in your database:

This tables will be created on the public schema.

Copy the migration script

Now that we have the necessary tables in our database, we can run the migration script to migrate the users and accounts from supabase to better auth.

Start by creating a .ts file in your project.

Terminal
touch migration.ts

And then copy and paste the following code into the file.

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

Customize the migration script (Optional)

  • name: the migration script will use the user's email as the name. You might want to customize it if you have the user display name in your database.
  • socialProviderList: the migration script will use the social providers you have enabled in your auth config. You might want to customize it if you have additional social providers that you haven't enabled in your auth config.
  • role: remove role if you're not using the admin plugin
  • isAnonymous: remove isAnonymous if you're not using the anonymous plugin.
  • update other tables that reference the users table to use the id field.

Run the migration script

Run the migration script to migrate the users and accounts from supabase to better auth.

Terminal
bun migration.ts # or use node, ts-node, etc.

Update your code

Update your codebase from supabase auth calls to better auth api.

Here's a list of the supabase auth api calls and their better auth counterparts.

  • 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 - you can also use authClient.useSession for reactive state

Learn more:

  • Basic Usage: Learn how to use the auth client to sign up, sign in, and sign out.
  • Email and Password: Learn how to add email and password authentication to your project.
  • Anonymous: Learn how to add anonymous authentication to your project.
  • Admin: Learn how to add admin authentication to your project.
  • Email OTP: Learn how to add email OTP authentication to your project.
  • Hooks: Learn how to use the hooks to listen for events.
  • Next.js: Learn how to use the auth client in a Next.js project.

Middleware

To protect routes with middleware, refer to the Next.js middleware guide or your framework's documentation.

Wrapping Up

Congratulations! You've successfully migrated from Supabase Auth to Better Auth.

Better Auth offers greater flexibility and more features—be sure to explore the documentation to unlock its full potential.