workos-migrate-descope
WorkOS Migration: Descope
Step 1: Fetch Documentation (BLOCKING)
STOP. Do not proceed until complete.
WebFetch: https://workos.com/docs/migrate/descope
The migration guide is the source of truth. If this skill conflicts with the guide, follow the guide.
Step 2: Pre-Flight Validation
WorkOS Environment
- Confirm WorkOS Dashboard access at
dashboard.workos.com - Confirm environment variables exist:
WORKOS_API_KEY- starts withsk_WORKOS_CLIENT_ID- starts withclient_
WorkOS SDK
Check package.json contains @workos-inc/node dependency.
Verify: SDK package exists in node_modules before continuing.
Descope Data Access
Decision point:
Do you need to migrate passwords?
|
+-- YES --> Contact Descope support for password export
| (Backend APIs don't expose password hashes)
| Result: CSV file with hashes + algorithm name
|
+-- NO --> Use Descope Management API for user export only
(GET /v1/mgmt/user/search endpoint)
Critical: Descope support ticket for passwords MUST be filed early — data transfers take time.
Step 3: Export User Data from Descope
Without Passwords
Use Descope Management SDK to fetch users:
import DescopeClient from '@descope/node-sdk';
const descopeClient = DescopeClient({ projectId: DESCOPE_PROJECT_ID });
async function exportUsers() {
const users = await descopeClient.management.user.searchAll();
return users;
}
Export fields needed for WorkOS:
emailgivenNamefamilyNameverifiedEmailtenants(for organization membership)
With Passwords
Wait for CSV from Descope support containing:
- All user fields above
password_hashcolumn- Hash algorithm used (bcrypt, argon2, or pbkdf2)
Verify: CSV received and algorithm documented before proceeding to import.
Step 4: Import Users to WorkOS
Field Mapping (REQUIRED)
Descope --> WorkOS Create User API
----------------------------------------------
email --> email
givenName --> first_name
familyName --> last_name
verifiedEmail --> email_verified
Basic User Creation (No Passwords)
import { WorkOS } from '@workos-inc/node';
const workos = new WorkOS(process.env.WORKOS_API_KEY);
async function importUser(descopeUser) {
const user = await workos.userManagement.createUser({
email: descopeUser.email,
firstName: descopeUser.givenName,
lastName: descopeUser.familyName,
emailVerified: descopeUser.verifiedEmail,
});
return user;
}
With Password Hashes (If Available)
Critical: Determine hash algorithm from Descope support response.
Hash algorithm from Descope?
|
+-- bcrypt --> password_hash_type: 'bcrypt'
|
+-- argon2 --> password_hash_type: 'argon2'
|
+-- pbkdf2 --> password_hash_type: 'pbkdf2'
Import with password hash:
async function importUserWithPassword(descopeUser, passwordHash, hashType) {
const user = await workos.userManagement.createUser({
email: descopeUser.email,
firstName: descopeUser.givenName,
lastName: descopeUser.familyName,
emailVerified: descopeUser.verifiedEmail,
passwordHash: passwordHash,
passwordHashType: hashType, // 'bcrypt', 'argon2', or 'pbkdf2'
});
return user;
}
Rate Limiting (IMPORTANT)
WorkOS Create User API is rate-limited. For large migrations, implement batching:
async function batchImport(users, batchSize = 100, delayMs = 1000) {
for (let i = 0; i < users.length; i += batchSize) {
const batch = users.slice(i, i + batchSize);
await Promise.all(batch.map(importUser));
if (i + batchSize < users.length) {
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
}
See: https://workos.com/docs/reference/rate-limits for current limits.
Step 5: Migrate Social Auth Users (Optional)
If Descope users signed in via Google, Microsoft, or other OAuth providers:
Configure OAuth Providers in WorkOS
- Navigate to WorkOS Dashboard → Authentication → Social Connections
- For each provider used in Descope, add client credentials
- See provider-specific guides:
- Google:
https://workos.com/docs/integrations/google-oauth - Microsoft:
https://workos.com/docs/integrations/microsoft-oauth
- Google:
Automatic Linking
WorkOS automatically links social sign-ins to existing users by email address match.
Critical: Email addresses from OAuth providers MUST match WorkOS user emails.
Email Verification Behavior
Provider verification status?
|
+-- Known verified (gmail.com via Google) --> No extra verification
|
+-- Unknown status --> User may need to verify email in WorkOS
Check WorkOS environment settings → Authentication → Email Verification to see current policy.
Step 6: Migrate Organizations
Export Descope Tenants
const descopeClient = DescopeClient({ projectId: DESCOPE_PROJECT_ID });
async function exportTenants() {
const tenants = await descopeClient.management.tenant.loadAll();
return tenants;
}
Create WorkOS Organizations
Field Mapping:
Descope Tenant --> WorkOS Organization
-----------------------------------------
name --> name
id --> external_id
Why external_id: Preserves Descope tenant ID for reference during migration.
async function importOrganization(descopeTenant) {
const org = await workos.organizations.createOrganization({
name: descopeTenant.name,
externalId: descopeTenant.id, // Descope tenant ID
});
return org;
}
Verify: Map of Descope tenant ID → WorkOS organization ID for membership step.
Step 7: Create Organization Memberships
Extract User-Tenant Associations from Descope
When exporting users (Step 3), the tenants field contains tenant associations:
descopeUser.tenants // Array of tenant IDs user belongs to
Create Memberships in WorkOS
async function createMembership(workosUserId, workosOrgId, roleSlug?) {
const membership = await workos.userManagement.createOrganizationMembership({
userId: workosUserId,
organizationId: workosOrgId,
roleSlug: roleSlug, // Optional: see RBAC section
});
return membership;
}
async function migrateMemberships(descopeUsers, tenantToOrgMap, userIdMap) {
for (const descopeUser of descopeUsers) {
const workosUserId = userIdMap[descopeUser.userId];
for (const tenantId of descopeUser.tenants) {
const workosOrgId = tenantToOrgMap[tenantId];
await createMembership(workosUserId, workosOrgId);
}
}
}
RBAC Migration (Optional)
If using Descope roles:
-
Define roles in WorkOS Dashboard:
- Navigate to
dashboard.workos.com/environment/roles-and-permissions - Create roles matching Descope role definitions
- Note the
slugfor each role
- Navigate to
-
Assign roles during membership creation:
- Pass
roleSlugparameter tocreateOrganizationMembership() - Map Descope role names to WorkOS role slugs
- Pass
Verification Checklist (ALL MUST PASS)
Run these checks to confirm migration success:
# 1. Verify WorkOS SDK installed
npm list @workos-inc/node
# 2. Check environment variables
echo $WORKOS_API_KEY | grep -q "^sk_" && echo "PASS" || echo "FAIL"
# 3. Verify users imported (replace COUNT with expected number)
curl -X GET "https://api.workos.com/user_management/users" \
-H "Authorization: Bearer $WORKOS_API_KEY" \
| jq '.data | length' | grep -q "COUNT" && echo "PASS" || echo "FAIL"
# 4. Verify organizations created
curl -X GET "https://api.workos.com/organizations" \
-H "Authorization: Bearer $WORKOS_API_KEY" \
| jq '.data | length'
# 5. Verify memberships created for a sample user
curl -X GET "https://api.workos.com/user_management/organization_memberships?user_id=USER_ID" \
-H "Authorization: Bearer $WORKOS_API_KEY" \
| jq '.data | length'
Do not mark complete until all checks pass.
Error Recovery
"Invalid password_hash_type" during user import
Root cause: Mismatch between Descope's hash algorithm and WorkOS parameter.
Fix:
- Check Descope support email for exact algorithm name
- Valid types:
'bcrypt','argon2','pbkdf2'(exact string match) - If Descope used variant (e.g., argon2id), try base type (
'argon2') - Consult:
https://workos.com/docs/migrate/descopefor supported formats
"User already exists" error
Root cause: Duplicate email in WorkOS.
Fix:
- Check if user was previously imported
- Use Update User API instead of Create User API
- Query existing users first:
workos.userManagement.listUsers({ email: 'user@example.com' })
Rate limit exceeded (429 response)
Root cause: Too many API calls in short period.
Fix:
- Implement exponential backoff in batch import
- Reduce batch size (default: 100 → 50)
- Increase delay between batches (default: 1s → 2s)
- Check current limits:
https://workos.com/docs/reference/rate-limits
Social auth user not auto-linked
Root cause: Email mismatch between Descope and WorkOS user.
Fix:
- Verify
emailfield matches exactly (case-sensitive) - Check
email_verifiedis true in WorkOS user - Confirm OAuth provider returns same email as WorkOS user record
Organization membership fails with "User not found"
Root cause: User ID mapping incorrect or user not yet created.
Fix:
- Verify
userIdMapcontains Descope user ID → WorkOS user ID - Check user import completed before membership creation
- Query WorkOS to confirm user exists:
workos.userManagement.getUser(userId)
Missing tenant data in Descope export
Root cause: Descope user search doesn't include tenant associations by default.
Fix:
- Use Descope Management API with tenant expansion
- Alternatively, query each user individually to get full tenant list
- Check Descope docs:
https://docs.descope.com/management/user-management
Password import fails silently (users can't log in)
Root cause: Hash format mismatch or corrupted hash string.
Fix:
- Verify password hash is complete (no truncation in CSV)
- Test with single user before bulk import
- Confirm hash type matches what Descope support provided
- If still failing, consider password reset flow instead of hash import
Related Skills
- workos-authkit-nextjs - Implement AuthKit after migration
- workos-api-authkit - AuthKit API reference
- workos-api-organization - Organization management API
- workos-rbac - Role-based access control setup
- workos-migrate-other-services - Generic migration patterns
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-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.
28workos-authkit-react-router
Integrate WorkOS AuthKit with React Router applications. Supports v6 and v7 (Framework, Data, Declarative modes). Use when project uses react-router, react-router-dom, or mentions React Router authentication.
27