workos-migrate-other-services
WorkOS Migration: Other Services
Step 1: Fetch Documentation (BLOCKING)
STOP. Do not proceed until complete.
WebFetch: https://workos.com/docs/migrate/other-services
The migration guide is the source of truth. If this skill conflicts with the guide, follow the guide.
Step 2: Pre-Migration Assessment (Decision Tree)
Answer these questions to determine your migration path:
Can you export password hashes?
|
+-- YES --> Which algorithm?
| |
| +-- bcrypt/scrypt/pbkdf2/argon2/ssha/firebase-scrypt
| | --> Import passwords during user creation (Step 5)
| |
| +-- Other algorithm
| --> Trigger password reset flow (Step 6)
|
+-- NO --> Options:
|
+-- Security policy prevents export
| --> Trigger password reset flow (Step 6)
|
+-- Users use social auth (Google/Microsoft)
| --> Configure OAuth providers (Step 7)
|
+-- Remove passwords entirely
--> Skip password steps, use Magic Auth
Critical: WorkOS supports password hash IMPORT even if your source system doesn't export them. The source system limitation (cannot export) is NOT a WorkOS limitation.
Step 3: User Signup Strategy (MUST CHOOSE ONE)
During migration, new users signing up create a data consistency problem. Choose a strategy:
Migration timeline + user tolerance?
|
+-- Small app OR short migration window (< 24 hours)
| --> Strategy A: Disable signups during migration
|
+-- Large app OR critical path OR long migration (days/weeks)
--> Strategy B: Dual-write strategy
Strategy A: Disable Signups (Simpler)
Pattern:
- Schedule maintenance window
- Add feature flag to disable signup endpoints
- Export all users (Step 4)
- Import into WorkOS (Step 5)
- Switch auth to WorkOS
- Remove feature flag
Pros: Guarantees consistency, simpler implementation
Cons: User disruption during window
Strategy B: Dual-Write (Complex)
Pattern:
- Add WorkOS user creation to existing signup flow
- Store WorkOS user ID alongside local user record
- Mirror ALL user updates (email, password) to WorkOS
- Export historical users (Step 4)
- Import into WorkOS (Step 5) — skip existing users
- Switch auth to WorkOS
- Remove dual-write logic
Pros: No downtime, gradual migration
Cons: More code complexity, must handle sync failures
Critical for dual-write: If user updates email/password after dual-write begins but BEFORE migration completes, you MUST update WorkOS or that user will have stale data.
Document your choice before proceeding:
echo "STRATEGY_CHOICE=A" >> migration.log # or B
Step 4: Export User Data
Export from your data store. Required fields per user:
Minimum required:
- Email address (primary key for WorkOS matching)
- First name / Last name (optional but recommended)
Optional but valuable:
- Email verification status (prevents re-verification)
- Password hash + algorithm (if available)
- OAuth provider linkages (e.g., "signed up via Google")
- WorkOS user ID (if using dual-write strategy)
Export to JSON format:
[
{
"email": "user@example.com",
"firstName": "Jane",
"lastName": "Doe",
"emailVerified": true,
"passwordHash": "$2a$10$...",
"passwordAlgorithm": "bcrypt",
"oauthProvider": "google"
}
]
Verify export completeness:
# Count exported users
jq 'length' users_export.json
# Check for required fields
jq '.[] | select(.email == null or .email == "")' users_export.json
# Should return empty - if not, fix missing emails
Step 5: Import Users into WorkOS
Use Create User API for each exported user:
CRITICAL: Preserve the WorkOS user ID returned in response — you will need it for your user table.
// Example import loop (NOT production code - add retries/rate limiting)
for (const user of exportedUsers) {
const response = await workos.users.createUser({
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
emailVerified: user.emailVerified,
// If you have password hash:
password: user.passwordHash,
passwordHashType: user.passwordAlgorithm, // bcrypt/scrypt/pbkdf2/etc
});
// CRITICAL: Store this mapping
await saveWorkOSMapping(user.localId, response.id);
}
Dual-write users: Check if user already exists in WorkOS (they have a WorkOS ID in your DB). If yes, skip creation.
Error handling during import:
409 Conflict(user exists) — if dual-write, log and skip; if not, investigate400 Bad Request(invalid hash format) — check algorithm name matches WorkOS supported list429 Rate Limit— implement exponential backoff
Verify import:
# Count users in WorkOS (via Admin Portal or API)
curl -H "Authorization: Bearer $WORKOS_API_KEY" \
"https://api.workos.com/user_management/users" | jq '.data | length'
# Compare to export count
diff <(jq 'length' users_export.json) <(echo "$WORKOS_COUNT")
Step 6: Password Reset Flow (If No Hashes)
If you cannot export password hashes, trigger password reset for each user:
Timing decision:
When to trigger resets?
|
+-- During import (Step 5)
| --> User gets reset email immediately
| --> Pros: Done upfront
| --> Cons: May confuse users before cutover
|
+-- After cutover (post Step 8)
--> Trigger on first login attempt
--> Pros: Less confusing, on-demand
--> Cons: Adds complexity to login flow
Triggering reset:
// Option 1: Batch trigger during import
await workos.users.sendPasswordResetEmail({
email: user.email,
});
// Option 2: Lazy trigger on first login
// In your login handler:
if (loginFailed && userExistsInWorkOS && !hasWorkOSPassword) {
await workos.users.sendPasswordResetEmail({ email });
return "Check your email to set a new password";
}
Critical: Communicate to users BEFORE cutover that they will need to reset passwords. Sudden reset emails cause support tickets.
Step 7: Social Auth Configuration (If Applicable)
If your users sign in via Google/Microsoft/GitHub OAuth:
For each provider:
- Go to WorkOS Dashboard → Authentication → Social Connections
- Configure provider client ID/secret (see integrations docs for provider-specific steps)
- Set redirect URI to match your WorkOS callback
Email matching behavior:
- WorkOS links OAuth sign-ins to existing users by email address
- If email from provider matches imported user email → automatic link
- If email verified by provider (e.g., @gmail.com from Google) → no re-verification
- If email NOT verified by provider → WorkOS may require verification (check Dashboard settings)
Test OAuth linking:
# Before cutover, test with a dummy account:
# 1. Import user with email test@example.com
# 2. Sign in via OAuth with same email
# 3. Verify WorkOS links to existing user (check user ID matches)
Step 8: Switch Authentication to WorkOS
This is the cutover point. After this step, all auth flows use WorkOS.
Code changes required:
- Login endpoint — replace existing auth with WorkOS sign-in redirect
- Signup endpoint — replace with WorkOS sign-up redirect (or disable if strategy A)
- Session management — replace session tokens with WorkOS session cookies
- Logout endpoint — call WorkOS sign-out
Verification before cutover:
# Ensure all users imported
[ $(jq 'length' users_export.json) -eq $WORKOS_USER_COUNT ] || echo "MISMATCH"
# Ensure OAuth providers configured (if applicable)
grep "oauth_provider_configured=true" migration.log
# Ensure dual-write logic handles sync (if strategy B)
grep "dual_write_tested=true" migration.log
Deploy auth changes:
- Use feature flag or deploy during low-traffic window
- Monitor login success rate immediately after deploy
- Have rollback plan ready (restore old auth endpoints)
Step 9: Post-Migration Cleanup
After cutover is stable (24-48 hours):
If dual-write strategy:
- Remove dual-write code from signup/update flows
- Remove WorkOS user ID sync logic
Database cleanup:
- Archive old password hashes (do NOT delete immediately — keep for rollback)
- Archive old session tokens
- Keep WorkOS user ID mapping permanently
Monitoring:
# Check login success rate
# Expected: >95% success after migration settles
# Check password reset volume
# Expected: Spike if no hashes imported, then decline
# Check support tickets for auth issues
# Expected: Temporary increase, then return to baseline
Verification Checklist (ALL MUST PASS)
Run these checks to confirm migration success:
# 1. User count matches export
EXPORT_COUNT=$(jq 'length' users_export.json)
WORKOS_COUNT=$(curl -H "Authorization: Bearer $WORKOS_API_KEY" \
"https://api.workos.com/user_management/users" | jq '.data | length')
[ "$EXPORT_COUNT" -eq "$WORKOS_COUNT" ] || echo "FAIL: User count mismatch"
# 2. Test user can log in via WorkOS
# Manual test: Sign in as test user, verify session works
# 3. OAuth linking works (if applicable)
# Manual test: Sign in via Google with existing user email, verify links
# 4. Password reset flow works (if no hashes)
# Manual test: Request password reset, verify email received
# 5. Application builds without old auth code
npm run build | grep -i "auth.*error" && echo "FAIL: Auth code errors"
Error Recovery
"User already exists" during import (409 Conflict)
Root cause: User created by dual-write before batch import
Fix:
// During import, check for existing user
try {
await workos.users.createUser({...});
} catch (error) {
if (error.code === 'user_already_exists') {
// Dual-write user - skip creation, fetch existing ID
const existing = await workos.users.listUsers({ email: user.email });
await saveWorkOSMapping(user.localId, existing.data[0].id);
}
}
"Invalid password hash" during import (400 Bad Request)
Root cause: Hash algorithm name mismatch
Fix: Verify algorithm name matches WorkOS supported list:
bcrypt(NOTbcrypt2orblowfish)scrypt(NOTscrypt-js)firebase-scrypt(Firebase-specific variant)pbkdf2(specify rounds/key length in docs)argon2(specify variant in docs)ssha(salted SHA)
Check WebFetched docs for exact parameter format.
"Email not verified" blocking login
Root cause: Email verification enabled in WorkOS, user imported with emailVerified: false
Fix:
// Option 1: Update user after import
await workos.users.updateUser(userId, { emailVerified: true });
// Option 2: Trigger verification email
await workos.users.sendVerificationEmail({ userId });
Prevention: Set emailVerified: true during import if your old system verified emails.
OAuth sign-in creates duplicate user
Root cause: Email address mismatch between OAuth provider and imported user
Fix:
- Check: OAuth provider email matches imported user email exactly (case-sensitive)
- Check: User imported BEFORE OAuth sign-in attempt (timing issue)
- If mismatch, manually merge users via WorkOS Admin Portal
Dual-write sync failures
Root cause: Race condition — user updated between dual-write and migration
Fix:
// Add sync verification step after import
for (const user of dualWriteUsers) {
const workosUser = await workos.users.getUser(user.workosId);
if (workosUser.email !== user.email) {
// Stale data - update WorkOS
await workos.users.updateUser(user.workosId, { email: user.email });
}
}
High volume of password reset requests post-migration
Root cause: Users don't remember passwords OR hashes weren't imported
Fix:
- Check: Password hashes were actually imported (verify API responses logged success)
- Check: Users received pre-migration communication about potential resets
- If expected, monitor volume and ensure email delivery is working
Support response template: "We've upgraded our authentication system. Please reset your password using the email link."
Related Skills
workos-authkit-nextjs— Integrate WorkOS auth in Next.js after migrationworkos-authkit-react— Client-side auth UI for React appsworkos-api-authkit— Direct API usage for custom auth flowsworkos-magic-link— Alternative to passwords for passwordless auth
More from workos/skills
workos
Use when the user asks for a WorkOS docs URL, term, or dashboard field (Sign-in endpoint, initiate_login_uri, Redirect URI, `WORKOS_*` env vars), or is implementing, debugging, or migrating WorkOS — AuthKit, SSO/SAML, Directory Sync, RBAC, FGA, MFA, Vault, Audit Logs, Admin Portal, Pipes (Connected Apps), Feature Flags, Radar (bot/fraud detection), webhooks, Custom Domains, or migrating from Auth0, Clerk, Cognito, Firebase, Supabase, Stytch, Descope, or Better Auth. Also triggers on @workos-inc/* imports.
595workos-widgets
Use when the user is implementing, embedding, or debugging a WorkOS Widget — specifically the User Management, User Profile, Admin Portal SSO Connection, or Admin Portal Domain Verification widgets. Handles the full stack — detecting the frontend (Next.js, React, React Router, TanStack Start, Vite, SvelteKit), generating access tokens via the backend SDK in use (Node, Python, Go, Ruby, PHP, Java, .NET), and wiring up the widget component correctly per the bundled OpenAPI spec. Also use when code imports from @workos-inc/widgets or the user pastes <UserManagement /> or <UserProfile /> JSX.
270workos-authkit-nextjs
Integrate WorkOS AuthKit with Next.js App Router (13+). Server-side rendering required.
64workos-authkit-base
Architectural reference for WorkOS AuthKit integrations. Fetch README first for implementation details.
42workos-authkit-react
Integrate WorkOS AuthKit with React single-page applications. Client-side only authentication. Use when the project is a React SPA without Next.js or React Router.
33workos-authkit-tanstack-start
Integrate WorkOS AuthKit with TanStack Start applications. Full-stack TypeScript with server functions. Use when project uses TanStack Start, @tanstack/start, or vinxi.
28