Data Migration
Stytch to MojoAuth Migration

This guide walks you through migrating from Stytch to MojoAuth's identity CIAM platform. You'll learn about different migration approaches—bulk, JIT, and hybrid—along with key security considerations, API compatibility mapping, and strategies to minimize risk while ensuring seamless passwordless authentication continuity.

  • Zero-downtime migration with preserved passwordless experience
  • Complete API compatibility and feature parity analysis
  • Cost-optimized migration with transparent pricing structure
  • Enhanced passwordless capabilities with advanced customization options

Organizations are migrating from Stytch to MojoAuth to gain better pricing predictability, enhanced customization options, advanced enterprise features, and superior developer experience with comprehensive documentation and support.


Migration Overview

Organizations are migrating from Stytch to MojoAuth for several compelling reasons:

  • Pricing Transparency: Stytch's complex pricing tiers vs. MojoAuth's simple, predictable pricing
  • Enterprise Features: Advanced enterprise capabilities available across all MojoAuth plans
  • Customization Flexibility: Greater UI/UX customization and white-labeling options
  • Global Deployment: Multi-region deployment options and data residency controls
  • Developer Experience: More comprehensive SDKs, documentation, and integration examples
  • Support Quality: Dedicated enterprise support with faster response times

Why MojoAuth

MojoAuth offers an enterprise-grade passwordless identity platform that addresses key limitations organizations face with Stytch:

  • Enhanced Passwordless Options: Advanced biometrics, passkeys, WebAuthn, and custom authentication methods
  • Predictable Pricing: Transparent subscription-based pricing without hidden costs or usage surprises
  • Superior Customization: Complete white-labeling, custom domains, and branded authentication experiences
  • Enterprise-Grade Security: Advanced threat detection, compliance frameworks, and audit capabilities
  • Global Infrastructure: Multi-cloud deployment with regional data residency options
  • Comprehensive APIs: Feature-rich APIs with extensive customization and integration capabilities

Stytch vs MojoAuth Feature Comparison

Feature CategoryStytchMojoAuth
Authentication MethodsEmail OTP, SMS OTP, Magic Links, OAuthEmail OTP, SMS OTP, Magic Links, Passkeys, WebAuthn, Biometrics
Pricing ModelUsage-based with tier restrictionsPredictable subscription with all features included
CustomizationLimited UI customizationComplete white-labeling and custom branding
Enterprise FeaturesAvailable in higher tiersIncluded across all plans
Multi-tenancyBasic tenant separationAdvanced multi-brand/tenant management
ComplianceSOC 2, basic complianceSOC 2, HIPAA, GDPR, industry-specific compliance
Global DeploymentUS-centric with limited regionsMulti-region with data residency controls
Developer ToolsGood SDKs and documentationComprehensive SDKs, extensive docs, code examples

Choosing the Right Migration Strategy

Select the optimal migration strategy based on your organization's characteristics:

Organizational CharacteristicsRecommended StrategyBusiness Benefits
Active Passwordless Users (>80%)Just-In-Time (JIT)- Seamless passwordless continuity
- No user re-enrollment
- Progressive feature adoption
Mixed Authentication MethodsHybrid Migration- Prioritized passwordless user migration
- Gradual traditional auth transition
- Risk-distributed approach
Enterprise B2B FocusBulk with Validation- Complete tenant data governance
- Comprehensive audit trails
- Enterprise security compliance
High-Volume Consumer AppsPhased JIT Migration- Load distribution
- Performance optimization
- User experience continuity
Regulatory Compliance RequirementsBulk with Compliance Validation- Complete data lineage
- Regulatory documentation
- Audit-ready migration process

Migration Strategy Details

Bulk Migration

  • Transfers all users simultaneously using Stytch's export APIs and MojoAuth's import capabilities
  • Optimal for well-defined user bases requiring complete data governance
  • Requires secure API-based data extraction with comprehensive user profile mapping
  • Risk profile: Medium (concentrated execution with comprehensive validation)
  • Timeline: 2-3 weeks for preparation + 1-2 days execution

Just-In-Time (JIT) Migration

  • Migrates users during their next authentication attempt
  • Perfect for active passwordless users prioritizing zero-downtime experience
  • Leverages webhook integration to trigger MojoAuth account creation
  • Risk profile: Low (gradual migration with natural load distribution)
  • Timeline: 3-4 weeks setup + ongoing migration over 2-4 months

Hybrid Migration (Recommended)

  • Strategic combination of Bulk + JIT approaches
  • Bulk migration for enterprise accounts, JIT for consumer users
  • Risk profile: Low (diversified approach with multiple validation points)
  • Timeline: 4-5 weeks setup + 2-3 months complete migration

Pre-Migration Assessment

Stytch Environment Analysis

Conduct a comprehensive analysis of your current Stytch implementation:

# Install Stytch CLI and Node.js SDK for analysis npm install stytch   # Analyze your Stytch project configuration curl -X GET "https://api.stytch.com/v1/projects" \  -H "Authorization: Bearer $STYTCH_SECRET_KEY"   # Get user statistics and authentication methods curl -X GET "https://api.stytch.com/v1/users" \  -H "Authorization: Bearer $STYTCH_SECRET_KEY" \  -G -d "limit=100"

Data Inventory Assessment

// Stytch environment assessment script const stytch = require('stytch');   class StytchAssessment {  constructor() {  this.client = new stytch.Client({  project_id: process.env.STYTCH_PROJECT_ID,  secret: process.env.STYTCH_SECRET_KEY,  env: stytch.envs.live // or stytch.envs.test  });  }    async assessEnvironment() {  const assessment = {  project_info: await this.getProjectInfo(),  user_analytics: await this.analyzeUsers(),  auth_methods: await this.analyzeAuthMethods(),  oauth_providers: await this.getOAuthProviders(),  webhooks: await this.getWebhookConfig(),  custom_claims: await this.getCustomClaims()  };    console.log('Stytch Environment Assessment:', assessment);  return assessment;  }    async analyzeUsers() {  let allUsers = [];  let cursor = null;    do {  const response = await this.client.users.search({  limit: 200,  cursor: cursor  });    allUsers = allUsers.concat(response.users);  cursor = response.has_more ? response.next_cursor : null;  } while (cursor);    return {  total_users: allUsers.length,  auth_method_breakdown: this.analyzeUserAuthMethods(allUsers),  email_verified: allUsers.filter(u => u.emails.some(e => e.verified)).length,  phone_verified: allUsers.filter(u => u.phone_numbers.some(p => p.verified)).length,  oauth_users: allUsers.filter(u => u.oauth_registrations.length > 0).length,  creation_timeline: this.analyzeCreationTimeline(allUsers)  };  }    analyzeUserAuthMethods(users) {  const methods = {  email_otp: 0,  sms_otp: 0,  magic_links: 0,  oauth: 0,  webauthn: 0,  totp: 0  };    users.forEach(user => {  if (user.emails.length > 0) methods.email_otp++;  if (user.phone_numbers.length > 0) methods.sms_otp++;  if (user.oauth_registrations.length > 0) methods.oauth++;  if (user.webauthn_registrations.length > 0) methods.webauthn++;  if (user.totps.length > 0) methods.totp++;  });    return methods;  }    async getOAuthProviders() {  try {  const response = await this.client.oauth.providers.list();  return response.oauth_providers.map(provider => ({  type: provider.type,  client_id: provider.client_id,  scopes: provider.scopes  }));  } catch (error) {  console.error('Error fetching OAuth providers:', error);  return [];  }  } }   // Run assessment const assessment = new StytchAssessment(); assessment.assessEnvironment()  .then(results => {  require('fs').writeFileSync(  'stytch_assessment.json',  JSON.stringify(results, null, 2)  );  })  .catch(console.error);

Migration Readiness Checklist

  • User Data Analysis: Total users, authentication methods, activity levels
  • Integration Points: Applications using Stytch authentication
  • OAuth Providers: Social login configurations and credentials
  • Webhooks: Event handling and integration dependencies
  • Custom Attributes: User metadata and custom claim mappings
  • Compliance Requirements: Data residency, regulatory considerations
  • Performance Baselines: Current authentication volumes and latency
  • Team Readiness: Development team training and preparation

Bulk Migration Process

Phase 1: Data Export from Stytch

User Data Export Script

// Comprehensive Stytch user export const stytch = require('stytch'); const fs = require('fs');   class StytchExporter {  constructor() {  this.client = new stytch.Client({  project_id: process.env.STYTCH_PROJECT_ID,  secret: process.env.STYTCH_SECRET_KEY,  env: stytch.envs.live  });  }    async exportAllUsers() {  console.log('Starting Stytch user export...');    let allUsers = [];  let cursor = null;  let batchCount = 0;    do {  batchCount++;  console.log(`Processing batch ${batchCount}...`);    const response = await this.client.users.search({  limit: 200,  cursor: cursor  });    // Process each user to get complete profile  for (const user of response.users) {  const completeUser = await this.getCompleteUserProfile(user.user_id);  allUsers.push(completeUser);    // Add small delay to respect rate limits  await this.sleep(50);  }    cursor = response.has_more ? response.next_cursor : null;    } while (cursor);    console.log(`Export complete: ${allUsers.length} users exported`);    // Save raw export  fs.writeFileSync(  'stytch_users_raw_export.json',  JSON.stringify(allUsers, null, 2)  );    return allUsers;  }    async getCompleteUserProfile(userId) {  try {  const response = await this.client.users.get({ user_id: userId });  const user = response.user;    return {  // Core identity  user_id: user.user_id,  name: user.name,  emails: user.emails,  phone_numbers: user.phone_numbers,    // Authentication methods  oauth_registrations: user.oauth_registrations,  webauthn_registrations: user.webauthn_registrations,  totps: user.totps,  biometric_registrations: user.biometric_registrations,    // Metadata  attributes: user.attributes || {},  created_at: user.created_at,  status: user.status,    // Custom fields  trusted_metadata: user.trusted_metadata || {},  untrusted_metadata: user.untrusted_metadata || {}  };  } catch (error) {  console.error(`Failed to get user ${userId}:`, error);  return null;  }  }    sleep(ms) {  return new Promise(resolve => setTimeout(resolve, ms));  } }   // Execute export const exporter = new StytchExporter(); exporter.exportAllUsers().catch(console.error);

Phase 2: Data Transformation

Transform Stytch user data to MojoAuth schema:

// Data transformation from Stytch to MojoAuth format const fs = require('fs'); const crypto = require('crypto');   class StytchToMojoAuthTransformer {  constructor() {  this.providerMapping = {  'google': 'google',  'microsoft': 'microsoft',   'facebook': 'facebook',  'apple': 'apple',  'github': 'github',  'gitlab': 'gitlab',  'linkedin': 'linkedin',  'slack': 'slack',  'discord': 'discord'  };  }    transformUsers(stytchUsers) {  console.log(`Transforming ${stytchUsers.length} users...`);    const transformedUsers = stytchUsers  .filter(user => user !== null) // Remove failed exports  .map(user => this.transformSingleUser(user))  .filter(user => user !== null); // Remove transformation failures    console.log(`Successfully transformed ${transformedUsers.length} users`);    return transformedUsers;  }    transformSingleUser(stytchUser) {  try {  // Extract primary email  const primaryEmail = this.getPrimaryEmail(stytchUser.emails);  if (!primaryEmail) {  console.warn(`User ${stytchUser.user_id} has no valid email, skipping`);  return null;  }    // Base MojoAuth user object  const mojoAuthUser = {  // Core identity  external_id: stytchUser.user_id,  email: primaryEmail.email,  email_verified: primaryEmail.verified,    // Name handling  given_name: this.extractGivenName(stytchUser.name),  family_name: this.extractFamilyName(stytchUser.name),  name: stytchUser.name?.name || primaryEmail.email,    // Phone number  phone_number: this.getPrimaryPhoneNumber(stytchUser.phone_numbers),  phone_verified: this.isPhoneVerified(stytchUser.phone_numbers),    // Timestamps  created_at: stytchUser.created_at,  updated_at: new Date().toISOString(),    // Social connections  identities: this.transformOAuthConnections(stytchUser.oauth_registrations),    // Authentication factors  authenticators: this.transformAuthenticators(stytchUser),    // Metadata  app_metadata: {  migration_source: 'stytch',  migration_date: new Date().toISOString(),  stytch_user_id: stytchUser.user_id,  original_status: stytchUser.status,    // Preserve Stytch trusted metadata  ...stytchUser.trusted_metadata  },    user_metadata: {  // Preserve Stytch untrusted metadata  ...stytchUser.untrusted_metadata,    // Add Stytch-specific attributes  ...stytchUser.attributes  }  };    return mojoAuthUser;    } catch (error) {  console.error(`Failed to transform user ${stytchUser.user_id}:`, error);  return null;  }  }    getPrimaryEmail(emails) {  if (!emails || emails.length === 0) return null;    // Prefer verified emails  const verifiedEmail = emails.find(e => e.verified);  return verifiedEmail || emails[0];  }    extractGivenName(nameObj) {  if (!nameObj) return null;    if (nameObj.first_name) return nameObj.first_name;  if (nameObj.name) {  const parts = nameObj.name.split(' ');  return parts[0];  }    return null;  }    extractFamilyName(nameObj) {  if (!nameObj) return null;    if (nameObj.last_name) return nameObj.last_name;  if (nameObj.name) {  const parts = nameObj.name.split(' ');  return parts.length > 1 ? parts.slice(1).join(' ') : null;  }    return null;  }    getPrimaryPhoneNumber(phoneNumbers) {  if (!phoneNumbers || phoneNumbers.length === 0) return null;    const verifiedPhone = phoneNumbers.find(p => p.verified);  const primaryPhone = verifiedPhone || phoneNumbers[0];    return primaryPhone ? primaryPhone.phone_number : null;  }    isPhoneVerified(phoneNumbers) {  return phoneNumbers && phoneNumbers.some(p => p.verified);  }    transformOAuthConnections(oauthRegistrations) {  if (!oauthRegistrations || oauthRegistrations.length === 0) {  return [];  }    return oauthRegistrations.map(oauth => ({  provider: this.providerMapping[oauth.provider_type] || oauth.provider_type,  user_id: oauth.provider_subject,  email: oauth.profile_picture_url, // Preserve profile data  connected_at: oauth.created_at || new Date().toISOString(),    // Preserve additional OAuth data  raw_profile: {  locale: oauth.locale,  profile_picture_url: oauth.profile_picture_url  }  }));  }    transformAuthenticators(stytchUser) {  const authenticators = [];    // WebAuthn/FIDO2 authenticators  if (stytchUser.webauthn_registrations) {  stytchUser.webauthn_registrations.forEach(webauthn => {  authenticators.push({  type: 'webauthn',  id: webauthn.webauthn_registration_id,  name: webauthn.name || 'WebAuthn Device',  created_at: webauthn.created_at,  verified: webauthn.verified,    // Preserve WebAuthn metadata  metadata: {  authenticator_type: webauthn.authenticator_type,  backup_eligible: webauthn.backup_eligible,  backup_state: webauthn.backup_state  }  });  });  }    // TOTP authenticators   if (stytchUser.totps) {  stytchUser.totps.forEach(totp => {  authenticators.push({  type: 'totp',  id: totp.totp_id,  name: 'TOTP Authenticator',  created_at: totp.created_at,  verified: totp.verified  });  });  }    // Biometric registrations  if (stytchUser.biometric_registrations) {  stytchUser.biometric_registrations.forEach(biometric => {  authenticators.push({  type: 'biometric',  id: biometric.biometric_registration_id,  name: 'Biometric Authentication',  created_at: biometric.created_at,  verified: biometric.verified  });  });  }    return authenticators;  } }   // Execute transformation function runTransformation() {  const stytchUsers = JSON.parse(  fs.readFileSync('stytch_users_raw_export.json', 'utf8')  );    const transformer = new StytchToMojoAuthTransformer();  const mojoAuthUsers = transformer.transformUsers(stytchUsers);    // Save transformed data  fs.writeFileSync(  'mojoauth_import_ready.json',  JSON.stringify(mojoAuthUsers, null, 2)  );    console.log(`Transformation complete: ${mojoAuthUsers.length} users ready for import`);    // Generate transformation report  const report = {  input_users: stytchUsers.length,  output_users: mojoAuthUsers.length,  transformation_rate: (mojoAuthUsers.length / stytchUsers.length) * 100,  timestamp: new Date().toISOString()  };    fs.writeFileSync(  'transformation_report.json',  JSON.stringify(report, null, 2)  ); }   runTransformation();

Phase 3: Data Validation

Validate transformed data before import:

// Comprehensive validation for MojoAuth import class MojoAuthDataValidator {  constructor() {  this.validationRules = {  email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,  phone: /^\+[1-9]\d{1,14}$/,  required_fields: ['email', 'external_id']  };  }    validateUsers(users) {  const results = {  total_users: users.length,  valid_users: 0,  invalid_users: 0,  errors: [],  warnings: [],  statistics: {  email_verified: 0,  phone_verified: 0,  social_connections: 0,  authenticators: 0,  duplicate_emails: []  }  };    const emailTracker = new Map();    users.forEach((user, index) => {  const userErrors = this.validateSingleUser(user, index);    if (userErrors.length === 0) {  results.valid_users++;    // Update statistics  if (user.email_verified) results.statistics.email_verified++;  if (user.phone_verified) results.statistics.phone_verified++;  if (user.identities?.length > 0) results.statistics.social_connections++;  if (user.authenticators?.length > 0) results.statistics.authenticators++;    } else {  results.invalid_users++;  results.errors.push(...userErrors);  }    // Track email duplicates  if (user.email) {  if (emailTracker.has(user.email)) {  results.statistics.duplicate_emails.push({  email: user.email,  indices: [emailTracker.get(user.email), index]  });  } else {  emailTracker.set(user.email, index);  }  }  });    // Generate validation report  results.success_rate = (results.valid_users / results.total_users) * 100;    console.log('Validation Results:', results);    return results;  }    validateSingleUser(user, index) {  const errors = [];    // Required field validation  this.validationRules.required_fields.forEach(field => {  if (!user[field]) {  errors.push({  user_index: index,  field: field,  type: 'required_field_missing',  message: `Required field '${field}' is missing`  });  }  });    // Email validation  if (user.email && !this.validationRules.email.test(user.email)) {  errors.push({  user_index: index,  field: 'email',  type: 'invalid_format',  message: 'Invalid email format',  value: user.email  });  }    // Phone validation  if (user.phone_number && !this.validationRules.phone.test(user.phone_number)) {  errors.push({  user_index: index,  field: 'phone_number',  type: 'invalid_format',  message: 'Invalid phone number format (should be E.164)',  value: user.phone_number  });  }    // Social identity validation  if (user.identities) {  user.identities.forEach((identity, idIndex) => {  if (!identity.provider || !identity.user_id) {  errors.push({  user_index: index,  field: `identities[${idIndex}]`,  type: 'incomplete_social_identity',  message: 'Social identity missing provider or user_id'  });  }  });  }    // Authenticator validation  if (user.authenticators) {  user.authenticators.forEach((auth, authIndex) => {  if (!auth.type || !auth.id) {  errors.push({  user_index: index,  field: `authenticators[${authIndex}]`,  type: 'incomplete_authenticator',  message: 'Authenticator missing type or id'  });  }  });  }    return errors;  } }   // Run validation function validateImportData() {  const users = JSON.parse(  fs.readFileSync('mojoauth_import_ready.json', 'utf8')  );    const validator = new MojoAuthDataValidator();  const validationResults = validator.validateUsers(users);    // Save validation report  fs.writeFileSync(  'validation_report.json',  JSON.stringify(validationResults, null, 2)  );    // Filter out invalid users for import  const validUsers = users.filter((user, index) => {  return !validationResults.errors.some(error => error.user_index === index);  });    fs.writeFileSync(  'mojoauth_import_validated.json',  JSON.stringify(validUsers, null, 2)  );    console.log(`Validation complete: ${validUsers.length} valid users ready for import`);    return validationResults; }   validateImportData();

Phase 4: Bulk Import to MojoAuth

// MojoAuth bulk import with advanced error handling const axios = require('axios'); const fs = require('fs');   class MojoAuthBulkImporter {  constructor(apiKey, baseUrl = 'https://api.mojoauth.com') {  this.apiKey = apiKey;  this.baseUrl = baseUrl;  this.batchSize = 500; // Optimal batch size for Stytch migrations  this.retryAttempts = 3;  this.retryDelay = 1000; // 1 second initial delay  }    async importUsers(users) {  console.log(`Starting import of ${users.length} users from Stytch...`);    const batches = this.createBatches(users, this.batchSize);  const results = [];    for (let i = 0; i < batches.length; i++) {  const batch = batches[i];  console.log(`Processing batch ${i + 1}/${batches.length} (${batch.length} users)`);    try {  const batchResult = await this.importBatchWithRetry(batch, i + 1);  results.push(batchResult);    // Progress reporting  const completed = results.reduce((sum, r) => sum + (r.imported_count || 0), 0);  console.log(`Progress: ${completed}/${users.length} users imported`);    // Rate limiting delay  await this.sleep(500);    } catch (error) {  console.error(`Batch ${i + 1} failed after all retries:`, error.message);  results.push({  batch_number: i + 1,  success: false,  error: error.message,  users_attempted: batch.length,  retry_count: this.retryAttempts  });  }  }    return this.generateImportSummary(results);  }    async importBatchWithRetry(batch, batchNumber) {  let lastError;    for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {  try {  return await this.importBatch(batch, batchNumber, attempt);  } catch (error) {  lastError = error;    if (attempt < this.retryAttempts) {  const delay = this.retryDelay * Math.pow(2, attempt - 1); // Exponential backoff  console.log(`Batch ${batchNumber} attempt ${attempt} failed, retrying in ${delay}ms...`);  await this.sleep(delay);  } else {  console.error(`Batch ${batchNumber} failed after ${this.retryAttempts} attempts`);  }  }  }    throw lastError;  }    async importBatch(batch, batchNumber, attempt) {  const response = await axios.post(  `${this.baseUrl}/api/v2/users/bulk-import`,  {  users: batch,  upsert: true,  send_verification_email: false, // Don't send emails during migration  preserve_user_id: false, // Let MojoAuth generate new IDs  migration_mode: true // Enable migration-specific optimizations  },  {  headers: {  'Authorization': `Bearer ${this.apiKey}`,  'Content-Type': 'application/json',  'X-Migration-Source': 'stytch',  'X-Batch-Number': batchNumber.toString(),  'X-Retry-Attempt': attempt.toString()  },  timeout: 120000 // 2 minute timeout for large batches  }  );    return {  batch_number: batchNumber,  attempt_number: attempt,  success: true,  imported_count: response.data.imported_count,  updated_count: response.data.updated_count,  failed_count: response.data.failed_count,  errors: response.data.errors || [],  processing_time_ms: response.headers['x-processing-time'],  batch_size: batch.length  };  }    createBatches(array, batchSize) {  const batches = [];  for (let i = 0; i < array.length; i += batchSize) {  batches.push(array.slice(i, i + batchSize));  }  return batches;  }    sleep(ms) {  return new Promise(resolve => setTimeout(resolve, ms));  }    generateImportSummary(results) {  const summary = {  migration_type: 'stytch_bulk_import',  timestamp: new Date().toISOString(),    batch_statistics: {  total_batches: results.length,  successful_batches: results.filter(r => r.success).length,  failed_batches: results.filter(r => !r.success).length  },    user_statistics: {  total_attempted: results.reduce((sum, r) => sum + (r.batch_size || r.users_attempted || 0), 0),  total_imported: results.reduce((sum, r) => sum + (r.imported_count || 0), 0),  total_updated: results.reduce((sum, r) => sum + (r.updated_count || 0), 0),  total_failed: results.reduce((sum, r) => sum + (r.failed_count || 0), 0)  },    performance_metrics: {  average_batch_time: this.calculateAverageProcessingTime(results),  total_migration_time: this.calculateTotalMigrationTime(results),  throughput_per_minute: this.calculateThroughput(results)  },    errors: results.flatMap(r => r.errors || []),  failed_batches: results.filter(r => !r.success),    success_rate: null // Will be calculated below  };    // Calculate success rate  if (summary.user_statistics.total_attempted > 0) {  summary.success_rate = (  (summary.user_statistics.total_imported + summary.user_statistics.total_updated) /   summary.user_statistics.total_attempted  ) * 100;  }    console.log('Migration Summary:', summary);    return summary;  }    calculateAverageProcessingTime(results) {  const successfulResults = results.filter(r => r.success && r.processing_time_ms);  if (successfulResults.length === 0) return null;    const totalTime = successfulResults.reduce((sum, r) => sum + parseInt(r.processing_time_ms), 0);  return Math.round(totalTime / successfulResults.length);  }    calculateTotalMigrationTime(results) {  // This would need to be tracked externally or calculated from timestamps  return null;  }    calculateThroughput(results) {  // Calculate users imported per minute based on processing times  const successfulResults = results.filter(r => r.success && r.processing_time_ms);  if (successfulResults.length === 0) return null;    const totalUsers = successfulResults.reduce((sum, r) => sum + (r.imported_count + r.updated_count), 0);  const totalTimeMinutes = successfulResults.reduce((sum, r) => sum + parseInt(r.processing_time_ms), 0) / (1000 * 60);    return Math.round(totalUsers / totalTimeMinutes);  } }   // Execute bulk import async function executeBulkImport() {  try {  // Load validated user data  const users = JSON.parse(  fs.readFileSync('mojoauth_import_validated.json', 'utf8')  );    // Initialize importer  const importer = new MojoAuthBulkImporter(process.env.MOJOAUTH_API_KEY);    // Execute import  const results = await importer.importUsers(users);    // Save detailed results  fs.writeFileSync(  'stytch_migration_results.json',  JSON.stringify(results, null, 2)  );    console.log('Bulk import completed successfully!');  console.log(`Success rate: ${results.success_rate?.toFixed(2)}%`);    return results;    } catch (error) {  console.error('Bulk import failed:', error);  throw error;  } }   // Run if called directly if (require.main === module) {  executeBulkImport().catch(console.error); }   module.exports = { MojoAuthBulkImporter };

Just-In-Time (JIT) Migration

JIT migration provides seamless user experience by migrating users during their authentication attempts.

Stytch Webhook Integration

Configure Stytch webhooks to trigger MojoAuth account creation:

// Express.js service to handle Stytch webhooks for JIT migration const express = require('express'); const crypto = require('crypto'); const axios = require('axios'); const app = express();   app.use(express.json());   // Verify Stytch webhook signature function verifyStytchWebhook(payload, signature, secret) {  const expectedSignature = crypto  .createHmac('sha256', secret)  .update(payload)  .digest('base64');    return signature === expectedSignature; }   // Stytch webhook handler for JIT migration app.post('/webhooks/stytch-migration', async (req, res) => {  try {  const signature = req.headers['stytch-signature'];  const payload = JSON.stringify(req.body);    // Verify webhook authenticity  if (!verifyStytchWebhook(payload, signature, process.env.STYTCH_WEBHOOK_SECRET)) {  console.error('Invalid Stytch webhook signature');  return res.status(401).json({ error: 'Invalid signature' });  }    const { event_type, data } = req.body;    // Handle authentication success events for JIT migration  if (event_type === 'authentication_success') {  await handleAuthenticationSuccess(data);  }    res.status(200).json({ received: true });    } catch (error) {  console.error('Stytch webhook processing failed:', error);  res.status(500).json({ error: 'Webhook processing failed' });  } });   async function handleAuthenticationSuccess(authData) {  try {  const { user, session } = authData;    // Check if user already exists in MojoAuth  const existingUser = await checkMojoAuthUser(user.emails[0].email);    if (existingUser) {  console.log(`User already migrated: ${user.emails[0].email}`);  return;  }    // Get complete user profile from Stytch  const completeProfile = await getStytchUserProfile(user.user_id);    // Create user in MojoAuth  const mojoAuthUser = await createMojoAuthUser(completeProfile);    // Log successful JIT migration  console.log(`JIT Migration successful: ${user.emails[0].email} -> ${mojoAuthUser.user_id}`);    // Optional: Trigger welcome workflow  await triggerWelcomeWorkflow(mojoAuthUser);    } catch (error) {  console.error('JIT migration failed:', error);    // Log migration failure for retry  await logMigrationFailure(authData.user, error);  } }   async function checkMojoAuthUser(email) {  try {  const response = await axios.get(  `${process.env.MOJOAUTH_API_URL}/api/v2/users`,  {  params: { q: `email:"${email}"` },  headers: {  'Authorization': `Bearer ${process.env.MOJOAUTH_API_KEY}`  }  }  );    return response.data.users?.[0] || null;  } catch (error) {  if (error.response?.status === 404) {  return null;  }  throw error;  } }   async function getStytchUserProfile(userId) {  const stytch = require('stytch');  const client = new stytch.Client({  project_id: process.env.STYTCH_PROJECT_ID,  secret: process.env.STYTCH_SECRET_KEY,  env: stytch.envs.live  });    const response = await client.users.get({ user_id: userId });  return response.user; }   async function createMojoAuthUser(stytchUser) {  // Transform Stytch user to MojoAuth format  const mojoAuthUser = {  external_id: stytchUser.user_id,  email: stytchUser.emails[0].email,  email_verified: stytchUser.emails[0].verified,    given_name: stytchUser.name?.first_name,  family_name: stytchUser.name?.last_name,  name: stytchUser.name?.name || stytchUser.emails[0].email,    phone_number: stytchUser.phone_numbers?.[0]?.phone_number,  phone_verified: stytchUser.phone_numbers?.[0]?.verified || false,    app_metadata: {  migration_source: 'stytch_jit',  migration_date: new Date().toISOString(),  stytch_user_id: stytchUser.user_id,  migration_method: 'jit_webhook'  },    user_metadata: {  ...stytchUser.untrusted_metadata,  ...stytchUser.attributes  }  };    // Add social identities if present  if (stytchUser.oauth_registrations?.length > 0) {  mojoAuthUser.identities = stytchUser.oauth_registrations.map(oauth => ({  provider: oauth.provider_type,  user_id: oauth.provider_subject,  email: oauth.profile_picture_url,  connected_at: oauth.created_at  }));  }    const response = await axios.post(  `${process.env.MOJOAUTH_API_URL}/api/v2/users`,  mojoAuthUser,  {  headers: {  'Authorization': `Bearer ${process.env.MOJOAUTH_API_KEY}`,  'Content-Type': 'application/json',  'X-Migration-Type': 'stytch-jit'  }  }  );    return response.data; }   async function triggerWelcomeWorkflow(mojoAuthUser) {  // Optional: Trigger welcome email or onboarding workflow  console.log(`Welcome workflow triggered for: ${mojoAuthUser.email}`); }   async function logMigrationFailure(stytchUser, error) {  const failureLog = {  timestamp: new Date().toISOString(),  stytch_user_id: stytchUser.user_id,  email: stytchUser.emails?.[0]?.email,  error_message: error.message,  error_stack: error.stack,  retry_needed: true  };    // Save to failure log for later retry  const fs = require('fs');  const logFile = 'jit_migration_failures.jsonl';  fs.appendFileSync(logFile, JSON.stringify(failureLog) + '\n'); }   app.listen(3000, () => {  console.log('Stytch JIT migration service running on port 3000'); });

Frontend Integration Updates

Update your application's authentication flow to use MojoAuth:

Before (Stytch):

// Stytch React integration import { useStytch } from '@stytch/react';   function LoginComponent() {  const stytch = useStytch();    const handleLogin = async (email) => {  const response = await stytch.magicLinks.email.loginOrCreate({  email: email,  login_magic_link_url: 'https://yourapp.com/authenticate',  signup_magic_link_url: 'https://yourapp.com/authenticate'  });    console.log('Magic link sent:', response);  };    return (  <div>  <input   type="email"   placeholder="Enter your email"  onChange={(e) => setEmail(e.target.value)}  />  <button onClick={() => handleLogin(email)}>  Send Magic Link  </button>  </div>  ); }

After (MojoAuth):

// MojoAuth React integration import MojoAuth from 'mojoauth-web-sdk';   function LoginComponent() {  const [email, setEmail] = useState('');    const mojoauth = new MojoAuth({  apiKey: process.env.REACT_APP_MOJOAUTH_API_KEY,  source: [{ type: 'email', feature: 'magiclink' }]  });    const handleLogin = async (email) => {  try {  const response = await mojoauth.signIn({ email });  console.log('Magic link sent:', response);  } catch (error) {  console.error('Login failed:', error);  }  };    return (  <div>  <input   type="email"   placeholder="Enter your email"  value={email}  onChange={(e) => setEmail(e.target.value)}  />  <button onClick={() => handleLogin(email)}>  Send Magic Link  </button>  </div>  ); }

Post-Migration Validation

Comprehensive Migration Validation

// Post-migration validation and reconciliation const stytch = require('stytch'); const axios = require('axios'); const fs = require('fs');   class MigrationValidator {  constructor() {  this.stytchClient = new stytch.Client({  project_id: process.env.STYTCH_PROJECT_ID,  secret: process.env.STYTCH_SECRET_KEY,  env: stytch.envs.live  });    this.mojoAuthHeaders = {  'Authorization': `Bearer ${process.env.MOJOAUTH_API_KEY}`,  'Content-Type': 'application/json'  };  }    async validateCompleteMigration() {  console.log('Starting comprehensive migration validation...');    const validation = {  timestamp: new Date().toISOString(),  stytch_users: await this.getStytchUserStats(),  mojoauth_users: await this.getMojoAuthUserStats(),  user_matching: await this.validateUserMatching(),  authentication_flow: await this.testAuthenticationFlow(),  data_integrity: await this.validateDataIntegrity(),  performance_metrics: await this.measurePerformance()  };    validation.migration_completeness = this.calculateCompleteness(validation);    // Generate detailed report  fs.writeFileSync(  'migration_validation_complete.json',  JSON.stringify(validation, null, 2)  );    console.log('Migration validation completed:', validation.migration_completeness);  return validation;  }    async getStytchUserStats() {  let totalUsers = 0;  let cursor = null;  const authMethods = { email: 0, phone: 0, oauth: 0, webauthn: 0, totp: 0 };    do {  const response = await this.stytchClient.users.search({  limit: 200,  cursor: cursor  });    totalUsers += response.users.length;    // Analyze authentication methods  response.users.forEach(user => {  if (user.emails.length > 0) authMethods.email++;  if (user.phone_numbers.length > 0) authMethods.phone++;  if (user.oauth_registrations.length > 0) authMethods.oauth++;  if (user.webauthn_registrations.length > 0) authMethods.webauthn++;  if (user.totps.length > 0) authMethods.totp++;  });    cursor = response.has_more ? response.next_cursor : null;  } while (cursor);    return { total_users: totalUsers, auth_methods: authMethods };  }    async getMojoAuthUserStats() {  const response = await axios.get(  `${process.env.MOJOAUTH_API_URL}/api/v2/users/stats`,  { headers: this.mojoAuthHeaders }  );    return {  total_users: response.data.total_users,  verified_users: response.data.verified_users,  migrated_users: response.data.users_with_migration_source || 0  };  }    async validateUserMatching() {  const sampleSize = 100; // Validate a sample of users  const stytchUsers = await this.getSampleStytchUsers(sampleSize);    const matching = {  total_sampled: stytchUsers.length,  found_in_mojoauth: 0,  data_matches: 0,  mismatches: []  };    for (const stytchUser of stytchUsers) {  const email = stytchUser.emails[0]?.email;  if (!email) continue;    const mojoAuthUser = await this.findMojoAuthUser(email);    if (mojoAuthUser) {  matching.found_in_mojoauth++;    // Validate data consistency  const dataMatch = this.compareUserData(stytchUser, mojoAuthUser);  if (dataMatch.is_match) {  matching.data_matches++;  } else {  matching.mismatches.push({  stytch_id: stytchUser.user_id,  email: email,  differences: dataMatch.differences  });  }  }  }    matching.match_rate = (matching.found_in_mojoauth / matching.total_sampled) * 100;  matching.data_accuracy = (matching.data_matches / matching.found_in_mojoauth) * 100;    return matching;  }    async testAuthenticationFlow() {  // Test authentication with sample users  const testResults = {  email_otp_test: false,  magic_link_test: false,  oauth_test: false,  response_times: []  };    try {  // Test email OTP flow  const startTime = Date.now();    // This would need test user credentials  // const otpResponse = await mojoauth.signIn({   // email: '[email protected]',  // method: 'email_otp'  // });    testResults.response_times.push({  method: 'email_otp',  time_ms: Date.now() - startTime  });    testResults.email_otp_test = true;  } catch (error) {  console.error('Email OTP test failed:', error);  }    return testResults;  }    async validateDataIntegrity() {  // Check for common data integrity issues  const integrity = {  duplicate_emails: await this.findDuplicateEmails(),  orphaned_authenticators: await this.findOrphanedAuthenticators(),  invalid_formats: await this.validateDataFormats(),  missing_metadata: await this.checkMissingMetadata()  };    return integrity;  }    async measurePerformance() {  // Measure authentication performance  const metrics = {  average_response_time: null,  throughput: null,  error_rate: null  };    // This would involve load testing with actual authentication requests  return metrics;  }    calculateCompleteness(validation) {  const stytchTotal = validation.stytch_users.total_users;  const mojoAuthMigrated = validation.mojoauth_users.migrated_users;    return {  migration_percentage: (mojoAuthMigrated / stytchTotal) * 100,  data_accuracy: validation.user_matching.data_accuracy,  authentication_working: validation.authentication_flow.email_otp_test,  overall_score: null // Would be calculated based on weighted criteria  };  }    // Helper methods  async getSampleStytchUsers(limit) {  const response = await this.stytchClient.users.search({ limit });  return response.users;  }    async findMojoAuthUser(email) {  try {  const response = await axios.get(  `${process.env.MOJOAUTH_API_URL}/api/v2/users`,  {  params: { q: `email:"${email}"` },  headers: this.mojoAuthHeaders  }  );  return response.data.users?.[0];  } catch (error) {  return null;  }  }    compareUserData(stytchUser, mojoAuthUser) {  const differences = [];    // Compare email  const stytchEmail = stytchUser.emails[0]?.email;  if (stytchEmail !== mojoAuthUser.email) {  differences.push({ field: 'email', stytch: stytchEmail, mojoauth: mojoAuthUser.email });  }    // Compare name  const stytchName = stytchUser.name?.name;  if (stytchName && stytchName !== mojoAuthUser.name) {  differences.push({ field: 'name', stytch: stytchName, mojoauth: mojoAuthUser.name });  }    // Compare verification status  const stytchVerified = stytchUser.emails[0]?.verified;  if (stytchVerified !== mojoAuthUser.email_verified) {  differences.push({   field: 'email_verified',   stytch: stytchVerified,   mojoauth: mojoAuthUser.email_verified   });  }    return {  is_match: differences.length === 0,  differences: differences  };  }    async findDuplicateEmails() {  // Implementation to find duplicate emails in MojoAuth  return [];  }    async findOrphanedAuthenticators() {  // Implementation to find authenticators without associated users  return [];  }    async validateDataFormats() {  // Implementation to validate email and phone number formats  return { invalid_emails: 0, invalid_phones: 0 };  }    async checkMissingMetadata() {  // Implementation to check for users missing important metadata  return { users_missing_metadata: 0 };  } }   // Execute validation async function runPostMigrationValidation() {  const validator = new MigrationValidator();  const results = await validator.validateCompleteMigration();    console.log('Validation completed. Check migration_validation_complete.json for details.');  return results; }   if (require.main === module) {  runPostMigrationValidation().catch(console.error); }   module.exports = { MigrationValidator };

Timeline & Best Practices

Recommended Migration Timeline

PhaseDurationKey Activities
Assessment & Planning1-2 weeksEnvironment analysis, strategy selection, team preparation
Development & Testing2-3 weeksMigration scripts, JIT service, authentication flow updates
Pilot Migration1 weekSmall user group, validation, performance testing
Production Migration3-4 weeksPhased rollout based on strategy, monitoring, optimization
Validation & Cleanup1 weekData validation, performance tuning, Stytch decommission

Migration Best Practices

Security Considerations

  • API Security: Rotate all API keys after migration completion
  • Data Encryption: Encrypt all exported data and API communications
  • Access Controls: Limit migration script access to authorized personnel
  • Audit Logging: Maintain comprehensive logs of all migration activities
  • Backup Strategy: Keep secure backups of all original Stytch data

Performance Optimization

  • Batch Sizing: Optimize batch sizes based on API rate limits and response times
  • Parallel Processing: Use concurrent processing for large user bases
  • Rate Limiting: Implement proper rate limiting to avoid API throttling
  • Monitoring: Monitor system performance throughout migration process
  • Caching: Implement appropriate caching for frequently accessed data

User Experience

  • Communication Plan: Notify users about migration and any temporary limitations
  • Seamless Transition: Ensure passwordless authentication continues to work
  • Support Preparation: Prepare support team for migration-related questions
  • Rollback Plan: Have tested rollback procedures ready if needed
  • Feature Parity: Ensure all Stytch features are replicated in MojoAuth

Risk Mitigation

  • Phased Approach: Use staged rollouts for large user populations
  • Data Validation: Implement comprehensive data validation at each step
  • Error Handling: Build robust error handling and retry mechanisms
  • Monitoring & Alerting: Set up real-time monitoring for migration health
  • Contingency Planning: Prepare for various failure scenarios and responses

Support & Resources

Technical Support

Professional Services

  • Migration Services: Professional migration assistance for enterprise customers
  • Custom Development: Support for complex migration scenarios and customizations
  • Training & Onboarding: Comprehensive team training and best practices sessions
  • Performance Optimization: Post-migration performance tuning and optimization

Additional Resources


Next Steps:

  1. Complete your Stytch environment assessment using the provided scripts
  2. Choose your migration strategy based on your user base and requirements
  3. Set up a development environment to test the migration process
  4. Execute a pilot migration with a small user group
  5. Scale to full production migration with comprehensive monitoring

For complex migration scenarios or enterprise-scale deployments, contact MojoAuth's professional services team for dedicated migration assistance.