diff --git a/docs/content/docs/guides/supabase-migration-guide.mdx b/docs/content/docs/guides/supabase-migration-guide.mdx
index 961633848e..09a640075a 100644
--- a/docs/content/docs/guides/supabase-migration-guide.mdx
+++ b/docs/content/docs/guides/supabase-migration-guide.mdx
@@ -9,6 +9,10 @@ In this guide, we'll walk through the steps to migrate a project from Supabase A
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.
+
+**Back up your database before running any migration scripts.** This guide modifies production data. Create a full backup of both your Supabase database and target database before proceeding.
+
+
## Before You Begin
@@ -51,17 +55,15 @@ export const auth = betterAuth({
database: new Pool({
connectionString: process.env.DATABASE_URL
}),
- emailVerification: {
- sendEmailVerification: async(user)=>{
- // send email verification email
- // implement your own logic here
- }
- },
emailAndPassword: { // [!code highlight]
enabled: true, // [!code highlight]
} // [!code highlight]
})
```
+
+
+If you want to require email verification, add the `emailVerification` config separately. See the [Email Verification docs](/docs/authentication/email-password#email-verification) for details.
+
@@ -91,9 +93,17 @@ export const auth = betterAuth({
-### Add admin, anonymous and phoneNumber plugins
+### Add plugins based on your Supabase features
-Add the [admin](/docs/plugins/admin), [anonymous](/docs/plugins/anonymous) and [phoneNumber](/docs/plugins/phone-number) plugins to your auth config. To minimize data loss from Supabase Auth during migration, these plugins must be included in the auth config.
+Add plugins that match the features you used in Supabase. Include only what you need:
+
+- **[admin](/docs/plugins/admin)**: If you have users with `is_super_admin` or `banned_until` fields
+- **[anonymous](/docs/plugins/anonymous)**: If you used anonymous authentication (`signInAnonymously`)
+- **[phoneNumber](/docs/plugins/phone-number)**: If you have users who signed up with phone numbers
+
+
+Only include plugins for features you actually used in Supabase. The migration script will automatically detect which plugins are enabled and migrate data accordingly.
+
```ts title="auth.ts"
import { betterAuth } from 'better-auth';
@@ -191,10 +201,14 @@ Now that we have the necessary tables in our database, we can run the migration
First, set up the environment variables used by the script.
```dotenv title=".env"
-FROM_DATABASE_URL= # Supabase database connection string
-TO_DATABASE_URL= # Target Postgres database connection string
+FROM_DATABASE_URL= # Supabase database connection string (reads from auth.users)
+TO_DATABASE_URL= # Target Postgres database connection string (writes to public.user)
```
+
+**Same database?** If you're migrating within the same Supabase Postgres database (from `auth` schema to `public` schema), both URLs can be the same `DATABASE_URL`. If you're migrating to a different database (e.g., PlanetScale, separate Postgres), use different URLs.
+
+
And then copy and paste the following code into the file.
```ts title="migration.ts"
@@ -446,20 +460,21 @@ const toDB = new Pool({
async function validateAuthConfig() {
const ctx = await auth.$context;
const errors: string[] = [];
+ const warnings: string[] = [];
- // Check emailAndPassword
+ // Check emailAndPassword (required)
if (!ctx.options.emailAndPassword?.enabled) {
errors.push('emailAndPassword.enabled must be true');
}
- // Check required plugins
- const requiredPlugins = ['admin', 'anonymous', 'phone-number'];
+ // Check optional plugins - warn if missing, migration will skip related data
+ const optionalPlugins = ['admin', 'anonymous', 'phone-number'];
const plugins = ctx.options.plugins || [];
const pluginIds = plugins.map((p: any) => p.id);
- for (const required of requiredPlugins) {
- if (!pluginIds.includes(required)) {
- errors.push(`Missing required plugin: ${required}`);
+ for (const plugin of optionalPlugins) {
+ if (!pluginIds.includes(plugin)) {
+ warnings.push(`Plugin '${plugin}' not found - related Supabase data will be skipped`);
}
}
@@ -495,14 +510,21 @@ async function validateAuthConfig() {
}
}
+ // Show warnings (non-blocking)
+ if (warnings.length > 0) {
+ console.warn('\n⚠️ Migration Warnings:\n');
+ warnings.forEach((warn) => console.warn(` ${warn}`));
+ console.warn('\n Add plugins for features you used in Supabase to migrate that data.\n');
+ }
+
+ // Show errors (blocking)
if (errors.length > 0) {
console.error('\n🟧 Better Auth Configuration Errors:\n');
errors.forEach((err) => console.error(` ${err}`));
console.error('\n🟧 Please update your Better Auth configuration to include:\n');
console.error(' 1. emailAndPassword: { enabled: true }');
- console.error(' 2. plugins: [admin(), anonymous(), phoneNumber()]');
console.error(
- ' 3. user.additionalFields: { userMetadata, appMetadata, invitedAt, lastSignInAt }\n',
+ ' 2. user.additionalFields: { userMetadata, appMetadata, invitedAt, lastSignInAt }\n',
);
process.exit(1);
}
@@ -681,7 +703,7 @@ async function processBatch(
`
INSERT INTO "user" (${fields.map((f) => `"${f}"`).join(', ')})
VALUES ${placeholders.join(', ')}
- ON CONFLICT (id) DO NOTHING
+ ON CONFLICT ("id") DO NOTHING
`,
values,
);
@@ -930,15 +952,17 @@ You can configure the script using `CONFIG` inside the script.
### Run the migration script
-
- **Large Datasets (100k+ users)**: Use streaming/pagination with [`pg-cursor`](https://www.npmjs.com/package/pg-cursor) or [keyset pagination](https://use-the-index-luke.com/no-offset) to avoid OOM errors. Process in batches of 1000 rows with concurrency limit of 8. Add `NODE_OPTIONS="--max-old-space-size=8192"` as temporary fix.
+
+The migration script uses **keyset pagination** (cursor-based) which efficiently handles large datasets without loading everything into memory. For very large migrations (500k+ users), you may need to increase Node's memory limit with `NODE_OPTIONS="--max-old-space-size=8192"`.
Run the migration script to migrate the users and accounts from Supabase to Better Auth.
```bash title="Terminal"
-bun migration.ts # or use node, ts-node, etc.
+npx tsx migration.ts
```
+
+You can also use other TypeScript runners like `bun migration.ts`, `ts-node migration.ts`, or compile to JS first.
### Change password hashing algorithm
@@ -947,28 +971,46 @@ bun migration.ts # or use node, ts-node, etc.
First, install bcrypt:
- ```bash
+ ```package-install
npm install bcrypt
+ ```
+
+ ```bash title="Terminal"
npm install -D @types/bcrypt
```
- Then update your auth configuration:
+ Then add the password configuration to your existing auth config:
```ts title="auth.ts"
import { betterAuth } from "better-auth";
- import bcrypt from "bcrypt";
+ import { Pool } from "pg";
+ import { admin, anonymous, phoneNumber } from 'better-auth/plugins';
+ import bcrypt from "bcrypt"; // [!code highlight]
export const auth = betterAuth({
+ database: new Pool({
+ connectionString: process.env.DATABASE_URL
+ }),
emailAndPassword: {
- password: {
- hash: async (password) => {
- return await bcrypt.hash(password, 10);
- },
- verify: async ({ hash, password }) => {
- return await bcrypt.compare(password, hash);
- }
- }
- }
+ enabled: true,
+ password: { // [!code highlight]
+ hash: async (password) => { // [!code highlight]
+ return await bcrypt.hash(password, 10); // [!code highlight]
+ }, // [!code highlight]
+ verify: async ({ hash, password }) => { // [!code highlight]
+ return await bcrypt.compare(password, hash); // [!code highlight]
+ } // [!code highlight]
+ } // [!code highlight]
+ },
+ socialProviders: {
+ // ... your social providers
+ },
+ plugins: [admin(), anonymous(), phoneNumber()],
+ user: {
+ additionalFields: {
+ // ... your additional fields
+ },
+ },
})
```
@@ -997,6 +1039,442 @@ Learn more:
+## Migrating Enterprise SSO
+
+
+**Skip this section** if you're not using Supabase's Enterprise SSO feature. This section is only for users who have SAML SSO providers configured in Supabase.
+
+
+If you're using Supabase's Enterprise SSO (SAML), follow these additional steps to migrate your SSO providers and users.
+
+
+SSO migration requires updating your Identity Provider (IdP) configuration with new callback URLs. Plan for a brief cutover window as existing SSO sessions will be invalidated.
+
+
+
+
+
+### Install the SSO plugin
+
+Install the Better Auth SSO package:
+
+```package-install
+npm install @better-auth/sso
+```
+
+Add the SSO plugin to your auth configuration:
+
+```ts title="auth.ts"
+import { betterAuth } from 'better-auth';
+import { Pool } from "pg";
+import { admin, anonymous, phoneNumber } from 'better-auth/plugins';
+import { sso } from '@better-auth/sso'; // [!code highlight]
+
+export const auth = betterAuth({
+ database: new Pool({
+ connectionString: process.env.DATABASE_URL
+ }),
+ emailAndPassword: {
+ enabled: true,
+ },
+ plugins: [
+ admin(),
+ anonymous(),
+ phoneNumber(),
+ sso(), // [!code highlight]
+ ],
+})
+```
+
+
+
+### Run the SSO database migration
+
+Run the migration to create the `ssoProvider` table:
+
+```bash title="Terminal"
+npx @better-auth/cli migrate
+```
+
+
+
+### Export your Supabase SSO providers
+
+List and export your existing SSO providers from Supabase using the CLI:
+
+```bash title="Terminal"
+# List all SSO providers
+supabase sso list --project-ref
+
+# Export each provider's details to a JSON file
+supabase sso show --project-ref -o json > sso-provider.json
+```
+
+A typical Supabase SSO provider export looks like this:
+
+```json title="sso-provider.json"
+{
+ "id": "550e8400-e29b-41d4-a716-446655440000",
+ "saml": {
+ "entity_id": "https://idp.example.com/saml",
+ "metadata_url": "https://idp.example.com/saml/metadata",
+ "attribute_mapping": {
+ "keys": {
+ "email": { "name": "mail" },
+ "first_name": { "name": "givenName" },
+ "last_name": { "name": "surname" }
+ }
+ }
+ },
+ "domains": [
+ { "id": "domain-uuid", "domain": "company.com" }
+ ]
+}
+```
+
+
+If you have multiple SSO providers, export each one separately. You'll need all of them for the migration script.
+
+
+
+
+### Add SSO types to the migration script
+
+Add these type definitions to your migration script:
+
+```ts title="migration.ts"
+// ============================================================================
+// SSO MIGRATION TYPES
+// ============================================================================
+
+type SupabaseSSOProvider = {
+ id: string;
+ saml: {
+ entity_id: string;
+ metadata_url?: string;
+ metadata_xml?: string;
+ attribute_mapping?: {
+ keys: Record;
+ };
+ name_id_format?: string;
+ };
+ domains: Array<{ id: string; domain: string }>;
+};
+
+type SSOProviderInsertData = {
+ id: string;
+ providerId: string;
+ issuer: string;
+ domain: string;
+ oidcConfig: null;
+ samlConfig: string;
+ organizationId: string | null;
+ createdAt: string;
+ updatedAt: string;
+};
+```
+
+
+
+### Add the SSO provider migration function
+
+Add this function to migrate your SSO providers:
+
+```ts title="migration.ts"
+// ============================================================================
+// SSO PROVIDER MIGRATION
+// ============================================================================
+
+/**
+ * Migrates Supabase SSO providers to Better Auth format
+ *
+ * @param providers - Array of exported Supabase SSO providers
+ * @param betterAuthUrl - Your Better Auth base URL (e.g., https://app.example.com)
+ */
+async function migrateSSOProviders(
+ providers: SupabaseSSOProvider[],
+ betterAuthUrl: string
+) {
+ console.log(`\n[SSO] Migrating ${providers.length} SSO provider(s)...\n`);
+
+ for (const provider of providers) {
+ // Create a unique provider ID that references the original Supabase ID
+ const providerId = `sso-${provider.id}`;
+
+ // Combine all domains into a comma-separated string
+ const domains = provider.domains.map(d => d.domain).join(',');
+
+ // Transform Supabase attribute mapping to Better Auth format
+ const mapping = provider.saml.attribute_mapping?.keys || {};
+
+ const samlConfig = {
+ issuer: provider.saml.entity_id,
+ // These will be auto-populated from metadata
+ entryPoint: '',
+ cert: '',
+ // Callback URL for SAML responses
+ callbackUrl: `${betterAuthUrl}/api/auth/sso/saml2/callback/${providerId}`,
+ // IdP metadata - Better Auth will fetch and parse this
+ idpMetadata: provider.saml.metadata_url
+ ? { metadataUrl: provider.saml.metadata_url }
+ : { metadata: provider.saml.metadata_xml },
+ // SP metadata configuration
+ spMetadata: {
+ entityID: `${betterAuthUrl}/api/auth/sso/saml2/sp/metadata`,
+ },
+ // Name ID format from Supabase config
+ identifierFormat: provider.saml.name_id_format ||
+ 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
+ // Map Supabase attribute names to Better Auth fields
+ mapping: {
+ id: 'nameID',
+ email: mapping.email?.name || 'email',
+ name: mapping.name?.name || 'displayName',
+ firstName: mapping.first_name?.name || 'givenName',
+ lastName: mapping.last_name?.name || 'surname',
+ },
+ };
+
+ const ssoProviderData: SSOProviderInsertData = {
+ id: generateId(),
+ providerId,
+ issuer: provider.saml.entity_id,
+ domain: domains,
+ oidcConfig: null,
+ samlConfig: JSON.stringify(samlConfig),
+ organizationId: null, // Set this if linking to a Better Auth organization
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ };
+
+ try {
+ await toDB.query(`
+ INSERT INTO "ssoProvider" (
+ "id", "providerId", "issuer", "domain",
+ "oidcConfig", "samlConfig", "organizationId",
+ "createdAt", "updatedAt"
+ )
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
+ ON CONFLICT ("providerId") DO UPDATE SET
+ "issuer" = EXCLUDED."issuer",
+ "domain" = EXCLUDED."domain",
+ "samlConfig" = EXCLUDED."samlConfig",
+ "updatedAt" = EXCLUDED."updatedAt"
+ `, [
+ ssoProviderData.id,
+ ssoProviderData.providerId,
+ ssoProviderData.issuer,
+ ssoProviderData.domain,
+ ssoProviderData.oidcConfig,
+ ssoProviderData.samlConfig,
+ ssoProviderData.organizationId,
+ ssoProviderData.createdAt,
+ ssoProviderData.updatedAt,
+ ]);
+
+ console.log(`[SSO] ✓ Migrated provider: ${providerId}`);
+ console.log(` Domains: ${domains}`);
+ console.log(` Issuer: ${provider.saml.entity_id}\n`);
+ } catch (error: any) {
+ console.error(`[SSO] ✗ Failed to migrate provider ${providerId}:`, error.message);
+ }
+ }
+
+ console.log('[SSO] Provider migration complete.\n');
+}
+```
+
+
+
+### Update user migration to link SSO accounts
+
+In the `processBatch` function, add handling for SSO identities. Find the section that processes identities and add:
+
+```ts title="migration.ts"
+// Inside the identity processing loop in processBatch()
+for (const identity of user.identities ?? []) {
+ // ... existing email and social provider handling ...
+
+ // Handle SSO identities // [!code highlight]
+ if (identity.provider.startsWith('sso:')) { // [!code highlight]
+ const supabaseProviderId = identity.provider.replace('sso:', ''); // [!code highlight]
+
+ accountsData.push({ // [!code highlight]
+ id: generateId(), // [!code highlight]
+ userId: user.id, // [!code highlight]
+ providerId: `sso-${supabaseProviderId}`, // Matches the migrated provider ID // [!code highlight]
+ accountId: identity.identity_data?.sub || identity.provider_id, // [!code highlight]
+ password: null, // [!code highlight]
+ createdAt: identity.created_at ?? user.created_at, // [!code highlight]
+ updatedAt: identity.updated_at ?? user.updated_at, // [!code highlight]
+ }); // [!code highlight]
+ } // [!code highlight]
+}
+```
+
+This ensures that users who previously signed in via SSO will have their accounts linked to the migrated SSO provider.
+
+
+
+### Run the SSO migration
+
+Create a separate script or add to your main migration to run the SSO provider migration:
+
+```ts title="run-sso-migration.ts"
+import { readFileSync } from 'fs';
+
+// Load your exported Supabase SSO providers
+const providers: SupabaseSSOProvider[] = [
+ JSON.parse(readFileSync('./sso-provider-1.json', 'utf-8')),
+ // Add more providers if you have multiple
+];
+
+// Your Better Auth application URL
+const BETTER_AUTH_URL = process.env.BETTER_AUTH_URL || 'https://app.example.com';
+
+async function main() {
+ // First, migrate users (including SSO user accounts)
+ await migrateFromSupabase();
+
+ // Then, migrate SSO providers
+ await migrateSSOProviders(providers, BETTER_AUTH_URL);
+}
+
+main();
+```
+
+Run the migration:
+
+```bash title="Terminal"
+npx tsx run-sso-migration.ts
+```
+
+
+
+### Update your Identity Provider
+
+After migrating, you need to update your IdP (Okta, Azure AD, Google Workspace, etc.) with the new Better Auth endpoints.
+
+**Update these settings in your IdP:**
+
+| Setting | Old Value (Supabase) | New Value (Better Auth) |
+|---------|----------------------|-------------------------|
+| ACS URL / Reply URL | `https://.supabase.co/auth/v1/sso/saml/acs` | `https://yourapp.com/api/auth/sso/saml2/callback/` |
+| Entity ID / Audience | `https://.supabase.co/auth/v1/sso/saml/metadata` | `https://yourapp.com/api/auth/sso/saml2/sp/metadata?providerId=` |
+
+Replace `` with your migrated provider ID (e.g., `sso-550e8400-e29b-41d4-a716-446655440000`).
+
+
+You can retrieve the SP metadata for your provider at:
+`GET https://yourapp.com/api/auth/sso/saml2/sp/metadata?providerId=`
+
+Share this URL with your IdP administrator if they need the full metadata XML.
+
+
+
+
+### Update your client code
+
+Replace Supabase SSO authentication calls with Better Auth:
+
+```ts title="Before (Supabase)"
+import { createClient } from '@supabase/supabase-js';
+
+const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
+
+// Sign in with SSO
+await supabase.auth.signInWithSSO({
+ domain: 'company.com'
+});
+```
+
+```ts title="After (Better Auth)"
+import { createAuthClient } from 'better-auth/client';
+import { ssoClient } from '@better-auth/sso/client'; // [!code highlight]
+
+const authClient = createAuthClient({
+ plugins: [ssoClient()], // [!code highlight]
+});
+
+// Sign in with SSO by domain // [!code highlight]
+await authClient.signIn.sso({ // [!code highlight]
+ domain: 'company.com', // [!code highlight]
+ callbackURL: '/dashboard', // [!code highlight]
+}); // [!code highlight]
+```
+
+You can also initiate SSO by email (the domain is extracted automatically):
+
+```ts title="auth-client.ts"
+// Sign in by email - domain is extracted from email
+await authClient.signIn.sso({
+ email: 'user@company.com',
+ callbackURL: '/dashboard',
+});
+
+// Or sign in by provider ID directly
+await authClient.signIn.sso({
+ providerId: 'sso-550e8400-e29b-41d4-a716-446655440000',
+ callbackURL: '/dashboard',
+});
+```
+
+
+
+### Test the SSO flow
+
+Before going live, test the complete SSO flow:
+
+1. **Test SP-initiated SSO**: Start from your app's login page, enter an SSO-enabled email domain
+2. **Verify user attributes**: Check that name, email, and other attributes are mapped correctly
+3. **Test existing users**: Ensure users who previously logged in via SSO can still access their accounts
+4. **Test new users**: Verify that new SSO users are created correctly
+
+```ts title="test-sso.ts"
+// Quick test - check if SSO provider is configured correctly
+const response = await fetch(
+ 'https://yourapp.com/api/auth/sso/saml2/sp/metadata?providerId=sso-your-provider-id'
+);
+const metadata = await response.text();
+console.log('SP Metadata:', metadata);
+```
+
+
+
+
+### SSO Migration Checklist
+
+Use this checklist to ensure a complete SSO migration:
+
+```
+- [ ] SSO plugin installed and configured
+- [ ] Database migration run (`ssoProvider` table created)
+- [ ] Supabase SSO providers exported
+- [ ] Providers migrated to Better Auth database
+- [ ] User accounts linked to SSO providers
+- [ ] IdP settings updated (ACS URL, Entity ID)
+- [ ] Client code updated to use Better Auth SSO
+- [ ] SP-initiated SSO flow tested
+- [ ] Existing SSO users can log in
+- [ ] New SSO users can be created
+```
+
+### Troubleshooting SSO
+
+
+**SAML Signature Errors**: If you see signature validation errors, ensure your IdP's certificate is current. Some IdPs rotate certificates periodically—check the metadata URL for the latest certificate.
+
+
+
+**Attribute Mapping Issues**: If user attributes aren't populating correctly, inspect the SAML assertion from your IdP using browser developer tools. Update the `mapping` field in your `samlConfig` to match the exact attribute names your IdP sends.
+
+
+
+**Multiple Domains**: If you have multiple domains for a single SSO provider, separate them with commas in the `domain` field: `"company.com,company.org"`.
+
+
+Learn more about SSO configuration in the [SSO Plugin Documentation](/docs/plugins/sso).
+
### Auth Protection
To protect routes with proxy(middleware), refer to the [Next.js Auth Protection Guide](/docs/integrations/next#auth-protection) or your framework's documentation.