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 Category | Stytch | MojoAuth |
|---|---|---|
| Authentication Methods | Email OTP, SMS OTP, Magic Links, OAuth | Email OTP, SMS OTP, Magic Links, Passkeys, WebAuthn, Biometrics |
| Pricing Model | Usage-based with tier restrictions | Predictable subscription with all features included |
| Customization | Limited UI customization | Complete white-labeling and custom branding |
| Enterprise Features | Available in higher tiers | Included across all plans |
| Multi-tenancy | Basic tenant separation | Advanced multi-brand/tenant management |
| Compliance | SOC 2, basic compliance | SOC 2, HIPAA, GDPR, industry-specific compliance |
| Global Deployment | US-centric with limited regions | Multi-region with data residency controls |
| Developer Tools | Good SDKs and documentation | Comprehensive SDKs, extensive docs, code examples |
Choosing the Right Migration Strategy
Select the optimal migration strategy based on your organization's characteristics:
| Organizational Characteristics | Recommended Strategy | Business Benefits |
|---|---|---|
| Active Passwordless Users (>80%) | Just-In-Time (JIT) | - Seamless passwordless continuity - No user re-enrollment - Progressive feature adoption |
| Mixed Authentication Methods | Hybrid Migration | - Prioritized passwordless user migration - Gradual traditional auth transition - Risk-distributed approach |
| Enterprise B2B Focus | Bulk with Validation | - Complete tenant data governance - Comprehensive audit trails - Enterprise security compliance |
| High-Volume Consumer Apps | Phased JIT Migration | - Load distribution - Performance optimization - User experience continuity |
| Regulatory Compliance Requirements | Bulk 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
| Phase | Duration | Key Activities |
|---|---|---|
| Assessment & Planning | 1-2 weeks | Environment analysis, strategy selection, team preparation |
| Development & Testing | 2-3 weeks | Migration scripts, JIT service, authentication flow updates |
| Pilot Migration | 1 week | Small user group, validation, performance testing |
| Production Migration | 3-4 weeks | Phased rollout based on strategy, monitoring, optimization |
| Validation & Cleanup | 1 week | Data 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
- MojoAuth Support: [email protected]
- Migration Assistance: Dedicated migration support team available
- Documentation: https://docs.mojoauth.com (opens in a new tab)
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:
- Complete your Stytch environment assessment using the provided scripts
- Choose your migration strategy based on your user base and requirements
- Set up a development environment to test the migration process
- Execute a pilot migration with a small user group
- 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.