Skip to content

Database Migration Guide ​

This guide demonstrates how to migrate from one database to another using AuthHero's passthrough adapter system. You'll run both databases in parallel, sync all writes to both systems, verify data integrity, and then switch over with zero downtime.

Overview ​

AuthHero's database-agnostic design allows you to:

  • Run multiple databases simultaneously during migration
  • Sync all writes to both old and new databases
  • Read from the old database while building up the new one
  • Verify data integrity before switching
  • Switch databases without downtime by changing configuration

Migration Strategy ​

The migration process follows these phases:

  1. Setup: Configure both old and new adapters
  2. Dual-Write: Write to both databases, read from old
  3. Backfill: Copy existing data to new database
  4. Verification: Compare data between databases
  5. Switchover: Make new database primary
  6. Cleanup: Remove old database once stable

Example: PostgreSQL to MySQL Migration ​

This example shows migrating from PostgreSQL (with Kysely) to MySQL (with Drizzle).

Phase 1: Setup Both Adapters ​

First, install and configure both adapters:

bash
pnpm add @authhero/kysely @authhero/drizzle

Create connections to both databases:

typescript
// src/database.ts
import { Kysely, PostgresDialect } from "kysely";
import { drizzle } from "drizzle-orm/mysql2";
import mysql from "mysql2/promise";
import { createKyselyAdapter } from "@authhero/kysely";
import { createDrizzleAdapter } from "@authhero/drizzle";

// Old database: PostgreSQL with Kysely
const postgresDb = new Kysely({
  dialect: new PostgresDialect({
    host: process.env.POSTGRES_HOST,
    database: process.env.POSTGRES_DB,
    user: process.env.POSTGRES_USER,
    password: process.env.POSTGRES_PASSWORD,
  }),
});

// New database: MySQL with Drizzle
const mysqlConnection = await mysql.createPool({
  host: process.env.MYSQL_HOST,
  database: process.env.MYSQL_DB,
  user: process.env.MYSQL_USER,
  password: process.env.MYSQL_PASSWORD,
});

const mysqlDb = drizzle(mysqlConnection);

// Create adapters
export const postgresAdapter = createKyselyAdapter(postgresDb);
export const mysqlAdapter = createDrizzleAdapter(mysqlDb);

Phase 2: Configure Dual-Write ​

Use the passthrough adapter to write to both databases:

typescript
// src/adapters.ts
import { createPassthroughAdapter } from "@authhero/adapter-interfaces";
import { postgresAdapter, mysqlAdapter } from "./database";

// Track sync errors for monitoring
const syncErrors = new Map<string, number>();

function handleSyncError(entity: string) {
  return (error: Error, method: string, args: unknown[]) => {
    const key = `${entity}.${method}`;
    syncErrors.set(key, (syncErrors.get(key) || 0) + 1);

    console.error(`MySQL sync failed for ${key}:`, {
      error: error.message,
      args: args.slice(0, 2), // Log first 2 args (usually tenantId, id)
    });

    // Send to your monitoring system
    // reportError(error, { entity, method });
  };
}

// Create dual-write adapters for each entity
export const dataAdapter = {
  // For entities being migrated, use passthrough
  users: createPassthroughAdapter({
    primary: postgresAdapter.users,
    secondaries: [
      {
        adapter: mysqlAdapter.users,
        blocking: true, // Wait to ensure MySQL write succeeds
        onError: handleSyncError("users"),
      },
    ],
  }),

  tenants: createPassthroughAdapter({
    primary: postgresAdapter.tenants,
    secondaries: [
      {
        adapter: mysqlAdapter.tenants,
        blocking: true,
        onError: handleSyncError("tenants"),
      },
    ],
  }),

  applications: createPassthroughAdapter({
    primary: postgresAdapter.applications,
    secondaries: [
      {
        adapter: mysqlAdapter.applications,
        blocking: true,
        onError: handleSyncError("applications"),
      },
    ],
  }),

  // Add all other entities...
  connections: createPassthroughAdapter({
    primary: postgresAdapter.connections,
    secondaries: [
      {
        adapter: mysqlAdapter.connections,
        blocking: true,
        onError: handleSyncError("connections"),
      },
    ],
  }),

  logs: createPassthroughAdapter({
    primary: postgresAdapter.logs,
    secondaries: [
      {
        adapter: mysqlAdapter.logs,
        blocking: false,
        onError: handleSyncError("logs"),
      },
    ], // Logs can be non-blocking
  }),

  sessions: createPassthroughAdapter({
    primary: postgresAdapter.sessions,
    secondaries: [
      {
        adapter: mysqlAdapter.sessions,
        blocking: true,
        onError: handleSyncError("sessions"),
      },
    ],
  }),

  codes: createPassthroughAdapter({
    primary: postgresAdapter.codes,
    secondaries: [
      {
        adapter: mysqlAdapter.codes,
        blocking: true,
        onError: handleSyncError("codes"),
      },
    ],
  }),

  passwords: createPassthroughAdapter({
    primary: postgresAdapter.passwords,
    secondaries: [
      {
        adapter: mysqlAdapter.passwords,
        blocking: true,
        onError: handleSyncError("passwords"),
      },
    ],
  }),

  // Include any platform-specific adapters from primary
  // These typically don't need migration
  ...postgresAdapter,
};

// Export sync error stats for monitoring
export function getSyncErrorStats() {
  return Object.fromEntries(syncErrors.entries());
}

Update your application to use the new adapter:

typescript
// src/index.ts
import { init } from "@authhero/authhero";
import { dataAdapter } from "./adapters";

const app = init({
  dataAdapter,
  // ... other config
});

export default app;

Phase 3: Backfill Existing Data ​

Create a script to copy existing data from PostgreSQL to MySQL:

typescript
// scripts/backfill-data.ts
import { postgresAdapter, mysqlAdapter } from "../src/database";

async function backfillEntity<T>(
  entityName: string,
  getAll: () => Promise<T[]>,
  create: (item: T) => Promise<void>,
) {
  console.log(`Backfilling ${entityName}...`);

  const items = await getAll();
  console.log(`Found ${items.length} ${entityName} to migrate`);

  let successful = 0;
  let failed = 0;

  for (const item of items) {
    try {
      await create(item);
      successful++;

      if (successful % 100 === 0) {
        console.log(`  Migrated ${successful}/${items.length} ${entityName}`);
      }
    } catch (error) {
      failed++;
      console.error(`  Failed to migrate ${entityName}:`, error);
    }
  }

  console.log(
    `✓ Completed ${entityName}: ${successful} successful, ${failed} failed\n`,
  );
  return { successful, failed };
}

async function backfillTenants() {
  const { tenants } = await postgresAdapter.tenants.list();

  return backfillEntity(
    "tenants",
    async () => tenants,
    async (tenant) => {
      await mysqlAdapter.tenants.create(tenant);
    },
  );
}

async function backfillUsers() {
  // Get all tenants first
  const { tenants } = await postgresAdapter.tenants.list();
  let totalSuccessful = 0;
  let totalFailed = 0;

  for (const tenant of tenants) {
    console.log(`Backfilling users for tenant ${tenant.id}...`);

    const { users } = await postgresAdapter.users.list(tenant.id, {});

    const result = await backfillEntity(
      `users (${tenant.id})`,
      async () => users,
      async (user) => {
        await mysqlAdapter.users.create(tenant.id, user);
      },
    );

    totalSuccessful += result.successful;
    totalFailed += result.failed;
  }

  return { successful: totalSuccessful, failed: totalFailed };
}

async function main() {
  console.log("Starting database backfill...\n");

  // Backfill in dependency order
  await backfillTenants();
  await backfillUsers();
  // Add other entities in correct order

  console.log("Backfill complete!");
}

main().catch(console.error);

Run the backfill:

bash
tsx scripts/backfill-data.ts

Phase 4: Verification ​

Create a verification script to compare data:

typescript
// scripts/verify-migration.ts
import { postgresAdapter, mysqlAdapter } from "../src/database";

async function verifyEntity<T extends { id?: string }>(
  entityName: string,
  getPostgres: () => Promise<T[]>,
  getMySQL: () => Promise<T[]>,
) {
  console.log(`Verifying ${entityName}...`);

  const [postgresItems, mysqlItems] = await Promise.all([
    getPostgres(),
    getMySQL(),
  ]);

  console.log(`  PostgreSQL: ${postgresItems.length} records`);
  console.log(`  MySQL: ${mysqlItems.length} records`);

  if (postgresItems.length !== mysqlItems.length) {
    console.error(`  ✗ Count mismatch!\n`);
    return false;
  }

  // Check for missing records
  const postgresIds = new Set(postgresItems.map((item) => item.id));
  const mysqlIds = new Set(mysqlItems.map((item) => item.id));

  const missingInMySQL = [...postgresIds].filter((id) => !mysqlIds.has(id));
  const extraInMySQL = [...mysqlIds].filter((id) => !postgresIds.has(id));

  if (missingInMySQL.length > 0) {
    console.error(`  ✗ ${missingInMySQL.length} records missing in MySQL`);
    console.error(`  Missing IDs:`, missingInMySQL.slice(0, 5));
    return false;
  }

  if (extraInMySQL.length > 0) {
    console.warn(`  ⚠ ${extraInMySQL.length} extra records in MySQL`);
    console.warn(`  Extra IDs:`, extraInMySQL.slice(0, 5));
  }

  console.log(`  ✓ ${entityName} verified\n`);
  return true;
}

async function main() {
  console.log("Starting migration verification...\n");

  const results = await Promise.all([
    verifyEntity(
      "tenants",
      async () => (await postgresAdapter.tenants.list()).tenants,
      async () => (await mysqlAdapter.tenants.list()).tenants,
    ),

    verifyEntity(
      "users",
      async () => {
        const { tenants } = await postgresAdapter.tenants.list();
        const allUsers = [];
        for (const tenant of tenants) {
          const { users } = await postgresAdapter.users.list(tenant.id, {});
          allUsers.push(...users);
        }
        return allUsers;
      },
      async () => {
        const { tenants } = await mysqlAdapter.tenants.list();
        const allUsers = [];
        for (const tenant of tenants) {
          const { users } = await mysqlAdapter.users.list(tenant.id, {});
          allUsers.push(...users);
        }
        return allUsers;
      },
    ),

    // Add other entities...
  ]);

  const allPassed = results.every((result) => result);

  if (allPassed) {
    console.log("✓ All verifications passed! Ready to switch over.");
  } else {
    console.error("✗ Verification failed. Do not switch over yet.");
    process.exit(1);
  }
}

main().catch(console.error);

Run verification regularly:

bash
tsx scripts/verify-migration.ts

Phase 5: Monitor Sync Health ​

Add a monitoring endpoint to track sync errors:

typescript
// src/routes/admin.ts
import { Hono } from "hono";
import { getSyncErrorStats } from "./adapters";

const admin = new Hono();

admin.get("/migration-status", (c) => {
  const errors = getSyncErrorStats();
  const totalErrors = Object.values(errors).reduce(
    (sum, count) => sum + count,
    0,
  );

  return c.json({
    status: totalErrors === 0 ? "healthy" : "errors",
    totalErrors,
    errorsByEntity: errors,
  });
});

export default admin;

Phase 6: Switchover ​

Once verification passes and sync errors are minimal, switch to MySQL as primary:

typescript
// src/adapters.ts
export const dataAdapter = {
  users: createPassthroughAdapter({
    primary: mysqlAdapter.users, // ← Changed to MySQL
    secondaries: [
      {
        adapter: postgresAdapter.users, // ← Keep PostgreSQL as backup
        blocking: false, // Non-blocking now
        onError: handleSyncError("users (postgres-backup)"),
      },
    ],
  }),

  // Update all other entities similarly...
};

Deploy this change and monitor for issues. All reads now come from MySQL, while writes still go to both databases.

Phase 7: Cleanup ​

After running successfully for a reasonable period (e.g., 1 week):

  1. Remove PostgreSQL secondaries:
typescript
// src/adapters.ts
export const dataAdapter = mysqlAdapter; // Use MySQL directly
  1. Decommission PostgreSQL database (after taking a final backup)

  2. Clean up old code and dependencies:

bash
pnpm remove @authhero/kysely kysely pg

Migration Checklist ​

Use this checklist to track your migration progress:

  • [ ] Install and configure new adapter
  • [ ] Set up passthrough adapters for all entities
  • [ ] Deploy dual-write configuration
  • [ ] Monitor sync errors (should be zero)
  • [ ] Run backfill script
  • [ ] Verify backfill completed successfully
  • [ ] Run verification script regularly
  • [ ] Monitor application performance with dual-write
  • [ ] Plan switchover window
  • [ ] Switch primary to new database
  • [ ] Monitor new database performance
  • [ ] Keep old database as backup for 1-2 weeks
  • [ ] Final verification before cleanup
  • [ ] Remove old database
  • [ ] Clean up old adapter dependencies

Best Practices ​

Use Blocking for Critical Data ​

typescript
// Critical data: use blocking to ensure consistency
users: createPassthroughAdapter({
  primary: oldAdapter.users,
  secondaries: [{ adapter: newAdapter.users, blocking: true }],
}),

// Logs: can be non-blocking
logs: createPassthroughAdapter({
  primary: oldAdapter.logs,
  secondaries: [{ adapter: newAdapter.logs, blocking: false }],
}),

Monitor Everything ​

typescript
const metrics = {
  syncErrors: new Map(),
  syncLatency: new Map(),
};

function handleSyncError(entity: string) {
  return (error: Error, method: string, args: unknown[]) => {
    // Increment error counter
    const key = `${entity}.${method}`;
    metrics.syncErrors.set(key, (metrics.syncErrors.get(key) || 0) + 1);

    // Alert if error rate is high
    if (metrics.syncErrors.get(key)! > 10) {
      sendAlert(`High sync error rate for ${key}`);
    }
  };
}

Gradual Migration ​

Don't migrate everything at once. Start with less critical entities:

  1. Week 1: Logs and sessions (can tolerate issues)
  2. Week 2: Applications and connections
  3. Week 3: Users and tenants (most critical)
  4. Week 4: Switchover

Have a Rollback Plan ​

Keep the rollback path simple - just change the configuration back:

typescript
// Rollback: swap primary back to old database
export const dataAdapter = {
  users: createPassthroughAdapter({
    primary: oldAdapter.users, // ← Change back
    secondaries: [{ adapter: newAdapter.users, blocking: false }],
  }),
};

Troubleshooting ​

High Sync Latency ​

If writes are taking too long:

typescript
// Use non-blocking for less critical data
secondaries: [
  {
    adapter: newAdapter.users,
    blocking: false, // Don't wait for sync
  },
];

Sync Failures ​

If you see sync errors:

  1. Check the onError logs for details
  2. Verify database connectivity
  3. Check for schema mismatches
  4. Ensure data types are compatible
  5. Verify foreign key constraints exist

Data Mismatches ​

If verification fails:

  1. Check the verification script output
  2. Run backfill again for affected entities
  3. Investigate specific mismatched records
  4. Check for ongoing sync errors

Advanced: Cross-Region Migration ​

For cross-region migrations with different ORMs:

typescript
// Region 1: PostgreSQL + Kysely
const region1Adapter = createKyselyAdapter(region1Db);

// Region 2: MySQL + Drizzle
const region2Adapter = createDrizzleAdapter(region2Db);

// Write to both regions
const dataAdapter = {
  users: createPassthroughAdapter({
    primary: region1Adapter.users,
    secondaries: [
      {
        adapter: region2Adapter.users,
        blocking: false, // Non-blocking for cross-region
        onError: (error) => {
          // Queue for retry
          retryQueue.add({ entity: "users", error });
        },
      },
    ],
  }),
};

Special Case: Migrating from External Auth Providers ​

The same passthrough adapter pattern can be used to migrate from external authentication services like Auth0, Okta, AWS Cognito, or other identity providers to AuthHero. This approach uses an auth-provider adapter that proxies to the external service.

Auth-Provider Adapter Concept ​

An auth-provider adapter implements the AuthHero adapter interfaces but proxies calls to an external authentication service's API:

typescript
// Conceptual example - NOT provided by AuthHero
import { createPassthroughAdapter } from "@authhero/adapter-interfaces";

// Custom adapter that proxies to external auth service
const externalAuthAdapter = createExternalAuthAdapter({
  domain: "your-tenant.auth-provider.com",
  managementToken: process.env.AUTH_PROVIDER_TOKEN,
  clientId: process.env.AUTH_PROVIDER_CLIENT_ID,
  clientSecret: process.env.AUTH_PROVIDER_CLIENT_SECRET,
});

// AuthHero database adapter
const authHeroAdapter = createKyselyAdapter(db);

// Phase 1: External auth primary, AuthHero secondary (dual-write)
const dataAdapter = {
  users: createPassthroughAdapter({
    primary: externalAuthAdapter.users, // Read from external service
    secondaries: [
      {
        adapter: authHeroAdapter.users, // Write to AuthHero
        blocking: true,
      },
    ],
  }),

  sessions: createPassthroughAdapter({
    primary: externalAuthAdapter.sessions,
    secondaries: [
      {
        adapter: authHeroAdapter.sessions,
        blocking: false,
      },
    ],
  }),

  // ... other entities
};

What Needs to be Implemented ​

To create an auth-provider adapter, you need to implement these key components:

1. User Management Adapter ​

typescript
export function createExternalUsersAdapter(config: ExternalAuthConfig) {
  return {
    async get(tenantId: string, userId: string) {
      // Call external auth provider's API
      const response = await fetch(
        `https://${config.domain}/api/users/${userId}`,
        {
          headers: { Authorization: `Bearer ${config.managementToken}` },
        },
      );

      // Transform external format to AuthHero format
      const externalUser = await response.json();
      return transformToAuthHeroUser(externalUser);
    },

    async list(tenantId: string, params) {
      // Query users from external service
      // May need to filter by tenant using metadata or tags
      const response = await fetch(
        `https://${config.domain}/api/users?tenant=${tenantId}`,
        {
          headers: { Authorization: `Bearer ${config.managementToken}` },
        },
      );

      const externalUsers = await response.json();
      return {
        users: externalUsers.map(transformToAuthHeroUser),
      };
    },

    // Write operations during migration
    async create(tenantId: string, user) {
      // Option 1: Proxy to external service
      // Option 2: Throw error (read-only during migration)
      throw new Error("User creation not supported during migration");
    },

    async update(tenantId: string, userId: string, updates) {
      // Proxy updates to external service
      const response = await fetch(
        `https://${config.domain}/api/users/${userId}`,
        {
          method: "PATCH",
          headers: {
            Authorization: `Bearer ${config.managementToken}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify(transformToExternalFormat(updates)),
        },
      );

      return transformToAuthHeroUser(await response.json());
    },
  };
}

2. Session/Token Management ​

typescript
export function createExternalSessionsAdapter(config: ExternalAuthConfig) {
  return {
    async get(tenantId: string, sessionId: string) {
      // External auth services typically don't expose sessions
      // Store mapping of AuthHero session ID -> external tokens

      const mapping = await getSessionMapping(sessionId);
      if (!mapping) return null;

      // Validate external token is still valid
      const isValid = await validateExternalToken(
        mapping.external_access_token,
      );

      if (!isValid) {
        // Try to refresh using external refresh token
        const refreshed = await refreshExternalToken(
          mapping.external_refresh_token,
        );

        if (refreshed) {
          await updateSessionMapping(sessionId, refreshed);
          return transformToAuthHeroSession(refreshed);
        }

        return null;
      }

      return transformToAuthHeroSession(mapping);
    },

    async create(tenantId: string, session) {
      // When proxying authentication:
      // 1. External service handles actual auth
      // 2. Receive external tokens
      // 3. Store mapping for future use

      const sessionId = generateSessionId();

      await storeSessionMapping(sessionId, {
        tenant_id: tenantId,
        user_id: session.user_id,
        external_access_token: session.external_access_token,
        external_refresh_token: session.external_refresh_token,
        external_id_token: session.external_id_token,
        expires_at: session.expires_at,
      });

      return {
        id: sessionId,
        tenant_id: tenantId,
        user_id: session.user_id,
        created_at: new Date().toISOString(),
        expires_at: session.expires_at,
      };
    },
  };
}

3. Authentication Flow Proxying ​

typescript
// In your AuthHero initialization
export function createAuthFlowProxy(config: ExternalAuthConfig) {
  return {
    // Proxy OAuth authorization to external service
    async handleAuthorize(params) {
      // Redirect to external auth service
      return {
        redirect: `https://${config.domain}/authorize?${new URLSearchParams({
          client_id: config.clientId,
          redirect_uri: params.redirect_uri,
          response_type: params.response_type,
          scope: params.scope,
          state: params.state,
        })}`,
      };
    },

    // Proxy token exchange to external service
    async handleTokenExchange(params) {
      const response = await fetch(`https://${config.domain}/oauth/token`, {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: new URLSearchParams({
          grant_type: params.grant_type,
          code: params.code,
          client_id: config.clientId,
          client_secret: config.clientSecret,
          redirect_uri: params.redirect_uri,
        }),
      });

      const tokens = await response.json();

      // Store in AuthHero session format
      await authHeroAdapter.sessions.create(tenantId, {
        user_id: tokens.sub,
        external_access_token: tokens.access_token,
        external_refresh_token: tokens.refresh_token,
        expires_at: new Date(
          Date.now() + tokens.expires_in * 1000,
        ).toISOString(),
      });

      return tokens;
    },
  };
}

Migration Process ​

The migration follows the same phases as database migration:

Phase 1: Dual-Write Mode ​

typescript
// External auth is primary, AuthHero is secondary
const dataAdapter = {
  users: createPassthroughAdapter({
    primary: externalAuthAdapter.users,
    secondaries: [{ adapter: authHeroAdapter.users, blocking: true }],
  }),
};

All reads from external service, all writes go to both.

Phase 2: Backfill Historical Data ​

typescript
// scripts/backfill-from-external-auth.ts
async function backfillUsers() {
  const { users } = await externalAuthAdapter.users.list("tenant-id", {});

  for (const user of users) {
    await authHeroAdapter.users.create("tenant-id", user);
  }
}

// Run backfill
await backfillUsers();

Phase 3: Session Continuity Strategy ​

Unlike database migration, session continuity is possible through token proxying:

typescript
// During migration, sessions from external auth continue to work
// AuthHero proxies token validation/refresh to external service
// New authentications create both external and AuthHero sessions

// Progressive migration: new users -> AuthHero, existing -> external
async function routeAuthentication(userId: string) {
  const user = await authHeroAdapter.users.get("tenant-id", userId);

  if (user && user.migrated_to_authhero) {
    // Use AuthHero authentication
    return authHeroAuth(user);
  }

  // Proxy to external auth
  return externalAuth(userId);
}

Phase 4: Verification ​

typescript
// Verify users match between systems
const externalUsers = await externalAuthAdapter.users.list("tenant-id", {});
const authHeroUsers = await authHeroAdapter.users.list("tenant-id", {});

console.log(`External: ${externalUsers.users.length}`);
console.log(`AuthHero: ${authHeroUsers.users.length}`);

// Check for discrepancies
const missing = externalUsers.users.filter(
  (eu) => !authHeroUsers.users.find((au) => au.user_id === eu.user_id),
);
console.log(`Missing in AuthHero: ${missing.length}`);

Phase 5: Switchover ​

typescript
// Switch to AuthHero as primary
const dataAdapter = {
  users: createPassthroughAdapter({
    primary: authHeroAdapter.users, // Now primary
    secondaries: [
      {
        adapter: externalAuthAdapter.users, // Keep as backup
        blocking: false,
      },
    ],
  }),
};

Phase 6: Remove External Adapter ​

typescript
// Once confident, use AuthHero directly
const dataAdapter = authHeroAdapter;

Key Challenges ​

  1. Session Format Differences: External auth tokens must be mapped to AuthHero sessions
  2. Tenant Mapping: External services may not have native multi-tenancy support
  3. Feature Parity: Not all features may map 1:1 (custom database scripts, rules, etc.)
  4. Rate Limits: External API rate limits may slow down backfill
  5. Token Expiry: Must handle token refresh during migration period

Benefits of This Approach ​

  • Zero downtime: External auth keeps working during migration
  • Session continuity: Existing sessions remain valid through proxying
  • Gradual rollout: Migrate users progressively
  • Reversible: Can switch back to external auth if issues arise
  • Familiar pattern: Same as database migration with passthrough adapters

Example: Auth0-to-AuthHero Migration ​

typescript
// Custom Auth0 adapter implementation
const auth0Adapter = createAuth0Adapter({
  domain: "your-tenant.auth0.com",
  managementToken: process.env.AUTH0_MGMT_TOKEN,
  clientId: process.env.AUTH0_CLIENT_ID,
  clientSecret: process.env.AUTH0_CLIENT_SECRET,
});

const authHeroAdapter = createKyselyAdapter(db);

// Week 1-2: Dual-write, Auth0 primary
const phase1Adapter = {
  users: createPassthroughAdapter({
    primary: auth0Adapter.users,
    secondaries: [{ adapter: authHeroAdapter.users, blocking: true }],
  }),
};

// Week 3: Backfill and verify
await backfillFromAuth0();
await verifyDataSync();

// Week 4: Switch to AuthHero primary, Auth0 backup
const phase2Adapter = {
  users: createPassthroughAdapter({
    primary: authHeroAdapter.users,
    secondaries: [{ adapter: auth0Adapter.users, blocking: false }],
  }),
};

// Week 5+: Remove Auth0 once stable
const finalAdapter = authHeroAdapter;

WARNING

Note: AuthHero does not currently provide adapters for external authentication services (Auth0, Okta, AWS Cognito, etc.). These would need to be implemented as custom adapters following the patterns described above.

If you're migrating from an external auth provider and need assistance designing an adapter, please open a discussion on GitHub.

Dual-licensed: AGPL-3.0-only or commercial license.