skills/workos-to-descope/SKILL.md
Use this skill whenever anyone asks about migrating from WorkOS to Descope — whether they're a developer doing it themselves or a technical lead evaluating the move. Triggers on: "how do I migrate from WorkOS", "replace WorkOS with Descope", "we're moving off WorkOS", "WorkOS to Descope", "switch from WorkOS", "our app uses @workos-inc/node / @workos-inc/authkit-nextjs / AuthKit / WorkOS SSO / Directory Sync / SCIM and we want to use Descope instead", or any question about WorkOS features (AuthKit, Organizations, Enterprise SSO, Directory Sync/SCIM, Admin Portal, RBAC, FGA, Audit Logs, Radar, Pipes) in the context of Descope. Works for any language or framework with a Descope SDK. Always use this skill before producing migration guidance — do not rely on memory alone.
npx skillsauth add descope/skills workos-to-descopeInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill guides self-service migrations from WorkOS to Descope. It runs in three parts:
MIGRATION-PLAN.md for the user to reviewDo not collapse these parts or skip ahead. The plan must be reviewed before code changes begin.
WorkOS is not only an authentication provider — it is a B2B/enterprise-readiness platform spanning authentication, organizations, enterprise SSO, SCIM/directory sync, RBAC, FGA, audit logs, connected accounts, admin setup flows, and security controls. A good migration first identifies which WorkOS features are in use, then maps each one to the closest Descope feature or migration pattern. Expect WorkOS migrations to be more B2B-enterprise heavy than a typical consumer-auth migration.
Primary references (both in this skill's directory):
references/implementation-nuances.md — verified migration patterns for each framework, WorkOS feature-to-Descope mappings, and known gotchasreferences/flows-and-widgets.md — Descope terminology/lingo, Flow structure and templates, Widgets, SSO Setup Suite, Console-vs-code decision guideConsole-first. Before recommending SDK code for any user-facing auth feature, check whether the Console, a Flow, a Widget, or the SSO Setup Suite covers the use case. Engineers integrate once (SDK setup + session validation). All subsequent auth evolution — new methods, MFA changes, UI updates, tenant SSO onboarding — should happen in the Console without code deployments. See references/flows-and-widgets.md → Console vs. Code.
Ask, don't assume. At any design decision point — embed Flows vs. OIDC compatibility, Flow vs. custom code, Widget vs. custom page, MFA inline vs. separate enrollment, programmatic SSO vs. SSO Setup Suite, one-Organization-to-one-Tenant mapping — use AskUserQuestion rather than proceeding with an assumption. The cost of a wrong assumption compounds across 20+ files, and the WorkOS Organization → Descope Tenant mapping in particular ripples into SSO, SCIM, RBAC, and domain routing. Uncertainty about architecture or intent is always worth a question.
MCP over memory. When the Descope MCP Server is available (confirmed in Part 1), use docs_ask_question to verify every SDK method name, option shape, and return type before writing it. Do not fall back to "verify the exact method name in the SDK type declarations" as a hedge — just verify it directly.
Before doing anything else, check whether the Descope MCP Server is available by calling
docs_search with a simple query (e.g., "session validation").
If the tool is available: proceed to Part 2 immediately.
If the tool is not available, show this message and use AskUserQuestion to ask whether
they want to install it first:
Descope MCP is not installed.
This skill uses the Descope MCP server to look up current API signatures, SDK methods, and feature availability during migration. Without it, guidance is based on static training data, which may be stale and can produce SDK calls that don't exist.
You can install it in a few minutes at https://docs.descope.com/mcp/mcp-server (server URL:
https://mcp.descope.com). It significantly improves the accuracy of the migration output — especially for SDK lookups and flow-specific configuration.Would you like to install the MCP before we continue, or proceed without it?
docs_search again before proceeding.Do not proceed to Part 2 until this step is resolved.
Part 2 has two sub-steps:
MIGRATION-PLAN.md, and pause for reviewAskUserQuestion)Use the AskUserQuestion tool to gather the information below. Do not infer answers
from memory, prior conversations, or assumptions — even if you think you know.
The migration path differs based on these answers; getting them wrong wastes the user's
time and produces incorrect guidance.
Do not proceed to Step 0.5 until the user has answered.
First AskUserQuestion call (up to 4 questions):
Second AskUserQuestion call — WorkOS feature usage (use multiSelect: true):
After both calls, summarize findings and flag high-complexity items (Directory Sync/SCIM, FGA, Pipes, MCP Auth/Connect, Vault) before proceeding to Step 0.5.
AskUserQuestion)These questions surface blockers the framework doesn't expose. Ask even the ones you think
you know. Use AskUserQuestion before proceeding to codebase analysis.
Batch into calls of up to 4 questions. Skip questions that are clearly inapplicable given Step 0 answers (e.g., skip user migration planning if they said they're starting fresh).
Access and credentials
Codebase scope
user.email, claims.organization_id, role/permissions)? These need a JWT Template configured before they'll work.organizationId, connectionId, or directoryId in many places? The WorkOS Organization → Descope Tenant remap ripples through SSO, SCIM, RBAC, and membership checks — confirm the org model before writing code.Deployment and risk
User and organization migration (if they indicated existing users/orgs in Step 0)
Gaps to flag immediately (don't ask — flag these proactively based on Step 0 answers)
Console/Flow/Widget opportunities (flag before codebase analysis, then ask):
Summarize any blockers and Console/Flow opportunities before proceeding to codebase analysis.
Scan the codebase to map every auth touchpoint before writing the plan.
Run these searches (adapt file extensions to the user's language):
# Find all WorkOS / AuthKit import sites.
grep -rni "workos\|authkit" \
--include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" \
--include="*.mjs" --include="*.cjs" --include="*.py" --include="*.go" \
--include="*.rb" --include="*.php" --include="*.java" --include="*.kt" \
--include="*.cs" --include="*.ex" --include="*.exs" --include="*.rs" \
--exclude-dir=node_modules --exclude-dir=.next --exclude-dir=dist --exclude-dir=venv \
. 2>/dev/null
# Find all WorkOS env var references
grep -rn "WORKOS_\|workos\." \
--include="*.ts" --include="*.tsx" --include="*.js" --include="*.py" --include="*.go" \
--include="*.env*" --include="*.yml" --include="*.yaml" --include="Dockerfile" \
--exclude-dir=node_modules --exclude-dir=.next \
. 2>/dev/null
# Find WorkOS SDK surface + claim / token / org access patterns (things that may need a JWT Template or org→tenant remap)
grep -rn "workos.userManagement\|workos.organizations\|workos.sso\|workos.directorySync\|workos.auditLogs\|workos.fga\|workos.widgets\|workos.events\|workos.webhooks\|workos.pipes\|workos.portal\|workos.organizationDomains\|workos.featureFlags\|workos.types\|workos.mfa\|workos.authorization\|workos.vault\|organizationId\|organization_id\|orgId\|connectionId\|connection_id\|directoryId\|directory_id\|roleSlug\|permission" \
--include="*.ts" --include="*.tsx" --include="*.js" --include="*.py" --include="*.go" \
--exclude-dir=node_modules --exclude-dir=.next \
. 2>/dev/null
# Find protected route / session access declarations
grep -rn "authkitMiddleware\|withAuth\|getUser\|ensureSignedIn\|getSignInUrl\|getSession\|isAuthenticated\|require_session\|@login_required\|authMiddleware" \
--include="*.ts" --include="*.tsx" --include="*.js" --include="*.py" --include="*.go" \
--exclude-dir=node_modules --exclude-dir=.next \
. 2>/dev/null
# Find B2B / enterprise feature usage (SSO, SCIM, audit, admin portal, security)
grep -rn "scim\|saml\|sso\|auditLog\|audit_log\|adminPortal\|portalLink\|radar\|pipes" \
--include="*.ts" --include="*.tsx" --include="*.js" --include="*.py" --include="*.go" \
--exclude-dir=node_modules --exclude-dir=.next \
. 2>/dev/null
# Check package.json / go.mod / requirements.txt for WorkOS dependencies
find . -maxdepth 3 \( -name "package.json" -o -name "go.mod" -o -name "requirements.txt" \) \
! -path "*/node_modules/*" -exec grep -l "workos" {} \;
For each hit, record:
Read package.json (or equivalent) for the exact framework version — this affects async
behavior (Next.js 15 vs 14) and SDK compatibility.
If the Descope Docs MCP is available, use docs_search or docs_ask_question
to verify current SDK method names for anything you plan to reference in the plan.
Write MIGRATION-PLAN.md to the working directory using the triage answers and codebase
analysis.
Two audiences: the engineer needs enough technical detail to execute; the PM or tech lead needs scope, risk, and timeline without decoding jargon. Use plain English. Explain technical terms on first use. Open each section with a sentence summarizing what it means before presenting tables or evidence. Say what breaks if a risk is missed, not just that it exists. Pair complexity labels with time estimates; skew toward the lower bound — SDK swaps and mechanical rewrites are usually faster than they look, and repetitive files in a group after the first go much faster. Group execution into phases so parallel vs. sequential work is clear.
The plan must include these sections, in this order:
2–3 sentences: what's being replaced, what replaces it, and the recommended approach with a one-sentence rationale. Add one sentence on what doesn't change — user-facing login behavior, sessions, organizations, and existing accounts are preserved.
Include a Migration at a Glance table:
| | | | -------------------------------- | ------------------------------------------------------------------- | | Approach | Full native migration | | Files changing | N source files across N areas | | Console setup | N configuration steps before launch | | User impact | No re-login required / Users will need to log in once after cutover | | Estimated engineering effort | N–N hours | | Biggest risk | One sentence naming the highest-complexity item |
Prose (not a table) describing what each part of the system does today and what it does after. Example:
Today, WorkOS handles everything related to login: AuthKit shows the login UI, issues tokens and sealed sessions, validates them on every request, and routes enterprise users to the right SSO connection. After this migration, Descope takes over all of those responsibilities. The login UI becomes a Descope Flow embedded in the app. Session validation moves to the Descope SDK. WorkOS Organizations become Descope Tenants. The WorkOS API key, client ID, redirect URI, and cookie password are replaced by a single Descope Project ID.
WorkOS features in use that need to carry over: [list in plain English, one clause each].
Tailor to triage findings.
For every WorkOS touchpoint found in triage, produce a concrete, one-to-one mapping — WorkOS construct → the exact Descope SDK and method that replaces it — and state explicitly whether that replacement runs in the client SDK or the backend SDK, and why. Use this division of responsibility:
@descope/web-js-sdk, @descope/react-sdk, @descope/nextjs-sdk client
components, or the <descope-wc> web component) — everything the user's browser/app does:
rendering the login/sign-up UI (a Descope Flow replaces AuthKit's hosted or redirect login),
initiating authentication, holding the session on the client, refreshing the token, and reading
the current user for UI purposes. This replaces AuthKit's UI, the redirect cycle, and any
client-side session access. It uses only the public Project ID — never a Management Key.@descope/node-sdk, descope (Python), github.com/descope/go-sdk, etc.) —
everything the server does: validating the session JWT on every request (replacing WorkOS
server-side withAuth() / middleware), checking roles and permissions, and — with a Management
Key — all administrative operations done by ID (user and tenant CRUD, role/permission definitions,
SSO/SCIM configuration, ReBAC). This replaces WorkOS server-side validation and every WorkOS
Management API call.For each file or area, name the WorkOS call, the Descope SDK that replaces it, which side it runs on,
and the reason (e.g. "session validation must stay server-side because the validation/Management key
cannot ship to the browser"). When one WorkOS feature spans both sides — for example AuthKit login
(now a client Flow) plus per-request withAuth() validation (now the backend SDK) — split it into
its client half and its backend half so the reader sees exactly what moves where, and why each piece
belongs on that side.
Open with the scope count (e.g., "11 files across 4 areas"). Group by area, not file path. Each group gets a sentence on what it does and what changes.
Session handling (3 files) — These files read and validate the current user's login state. They'll be updated to use the Descope session SDK instead of WorkOS AuthKit.
| File | What it does today | What changes |
| ------------------ | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| lib/auth.ts:34 | Returns WorkOS session via withAuth() with user, organizationId, role | Rewritten to return Descope authInfo; a thin adapter layer preserves the shape callers expect |
| middleware.ts:12 | authkitMiddleware() blocks unauthenticated requests app-wide | Updated to call Descope session validation; logic is identical, SDK call changes |
Login / logout routes (2 files) — These handle the AuthKit redirect-based login flow. Descope replaces this with an embedded UI component (or hosted Flow); the redirect cycle changes.
| File | What it does today | What changes |
| ----------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------- |
| app/callback/route.ts | AuthKit OAuth callback handler | Deleted or rewritten — Descope handles this client-side; verify the replacement against the framework section |
Cover all functional groupings. End with: "Total: N files. Estimated code-change effort: N–N hours."
For each WorkOS feature confirmed in triage, write a short paragraph: what it's trying to accomplish, the best Descope approach for that goal, what's different, and what action is required. The best approach may be a Flow, Widget, SSO Setup Suite, or Console configuration rather than a direct SDK equivalent — reason about the intent, not just the API surface. Only recommend SDK code when programmatic control is genuinely required. Example:
Multi-tenancy (WorkOS Organizations → Descope Tenants) WorkOS Organizations group users by company and scope SSO, SCIM, roles, and domain policies. Descope has the same concept, called Tenants. Most code that handles organizations is management/admin code that passes a WorkOS
organizationIdto the API — that simply becomes a Descope tenant ID passed todescopeClient.management.tenant.*/management.user.*calls (load a tenant, create one, add/remove membership, scope roles). This is by-ID work, not token parsing. The only place a tenant shows up as a claim is request-time session reads: WorkOS's flatorganizationId(fromwithAuth()) becomes Descope's nestedtenantsobject (plusdctfor the active tenant), which you read off the validated session — ideally via SDK helpers likevalidateTenantRoles(authInfo, tenantId, [...])rather than parsing claims by hand. Confirm the org→tenant mapping first, since it ripples into SSO, SCIM, and RBAC. Effort: Medium (1–2 hours of code changes). Confirm the data migration path for orgs first.
Only include confirmed features.
Some Descope behavior is configured in the console, not in code. List every item that must be set up before the app works, as checkboxes with a plain description of what it is, why it's needed, and roughly how long it takes. Group into "Required before any testing" and "Required before production":
Required before any testing:
sign-up-or-in flow
works for most apps and requires no customization to start.Required before production:
Diff table with plain-English notes for each removal and addition:
| Remove | Add | Why |
| ------------------------ | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| WORKOS_API_KEY | — | WorkOS authenticates server-side calls with a secret API key. Descope uses a Project ID (+ optional Management Key) instead. |
| WORKOS_CLIENT_ID | — | WorkOS identifies the AuthKit client. Descope uses a Project ID. |
| WORKOS_REDIRECT_URI | — | AuthKit's OAuth callback URL, configured in console. Descope's embedded Flow doesn't require a server redirect URI in the same way. |
| WORKOS_COOKIE_PASSWORD | — | Used by AuthKit to encrypt/seal the session cookie. Descope issues a signed session JWT instead; no sealing password needed. |
| — | DESCOPE_PROJECT_ID | The single identifier for the Descope project. Replaces all of the above. |
| — | NEXT_PUBLIC_DESCOPE_PROJECT_ID | Same value, exposed to the browser for the login component (Next.js only). |
| — | DESCOPE_MANAGEMENT_KEY | Only needed if the app manages users, roles, tenants, or SSO/SCIM server-side. |
Follow with: "Net change: 4 variables removed, 1–3 added. No secrets need to be rotated on the WorkOS side — those credentials stop being used."
Prose strategy first, then steps. Start with: "X existing users across Y organizations need to be in Descope before cutover." Describe:
End with a brief checklist of the migration steps at the level a PM can track:
Things that could affect timeline, user experience, or scope. Write each in plain English with three parts: what it is, what breaks if it's ignored, and what to do. Format each as a named callout:
Risk: Organization-to-tenant mapping affects almost every B2B feature WorkOS Organizations should usually map to Descope Tenants. If this mapping is wrong, SSO, SCIM, roles, permissions, domain routing, and user membership checks may all break. Action: Confirm the organization model before writing migration code.
Risk: SCIM is a lifecycle system, not just a user import Directory Sync may create, update, suspend, and delete users or group memberships continuously. A one-time import is not enough if enterprise directories keep syncing after cutover. Action: Identify every SCIM/directory workflow and re-point it at Descope before cutover.
Risk: Admin Portal workflows should not automatically become custom code If the app uses the WorkOS Admin Portal, the Descope equivalent may be the SSO Setup Suite or a Widget rather than a custom settings page. Action: Ask whether tenant admins currently self-configure SSO/SCIM/domain verification.
Risk: Audit logs can silently disappear The app may keep working after migration even if audit logging is broken — creating compliance and enterprise-customer issues. Action: Set up Descope audit/event forwarding before production cutover.
Risk: User profile data won't appear after login until a token template is configured Descope session tokens don't include name, email, or profile photo by default. Any UI that displays user information will show blank values after migration until the token template is set up in the Descope console. This is a one-time configuration step, not a code change. Action: Configure the token template before running any tests. Estimated time: 10 minutes.
Include only applicable risks.
Open with one sentence: phases run in sequence; steps within a phase can run in parallel. Then labeled phases, each with a time estimate:
Phase 1 — Console Setup (~30–60 minutes, no code required) Can be done by any team member with Descope console access, in parallel with other work.
sign-up-or-in to start)Phase 2 — Code Changes (~X–Y hours, 1 engineer) Work through files in the order listed. Run a compile check after each group.
.env.example and CI config (15 min)withAuth() usage (30 min)tenantId; request-time session reads use tenants/dct (varies)Phase 3 — User & Organization Migration (~1–2 hours, includes dry run) Run against dev/staging first. Do not run against production until Phase 4 passes.
Phase 4 — Testing (~30–45 minutes)
Phase 5 — Production Cutover
Total estimated engineering effort: N–N hours across N engineers. Blocking dependencies: (list anything on the critical path — console access, SCIM re-point, etc.)
After writing MIGRATION-PLAN.md, stop and tell the user:
MIGRATION-PLAN.mdhas been written to your working directory. It maps every auth touchpoint found, lists what needs Console setup before the first test, and calls out risks that could affect the timeline.Take a look before we start making changes. When you're ready to proceed, say so.
Do not proceed to Part 3 unless the user confirms.
Execute the plan in MIGRATION-PLAN.md Execution Plan order. Follow the detailed guidance below
for each step.
Context can be lost between turns. These rules keep the migration coherent.
Step 3.0 — Create MIGRATION-STATE.md before touching any code.
Write MIGRATION-STATE.md to the working directory from the template below. It's the
source of truth for migration state — keep it current throughout execution.
# Migration State
_Last updated: [timestamp of last completed step]_
## Project Context
- Framework: [e.g., Next.js 14, Express + React]
- Language: [TypeScript / Python / Go]
- Package manager: [npm / yarn / pnpm / pip / etc.]
- Migration path: [Path A: OIDC compat / Path B: Full native]
- Migration goal: [Full cutover / Phased / Evaluating]
## Triage Answers
- Existing users: [Yes — N users / No — greenfield]
- Existing organizations: [Yes — N orgs → tenants / No]
- Password migration needed: [Yes / No]
- WorkOS features in use: [comma-separated list]
- Multiple environments: [Yes: dev/staging/prod / No]
- Zero-downtime required: [Yes / No]
## Files Inventory
_All files that need to change. Update status after each step._
| File | Change | Status |
|---|---|---|
| `app/callback/route.ts` | Delete/rewrite | ⬜ Pending |
| `lib/auth.ts` | Rewrite session helper | ⬜ Pending |
| `middleware.ts` | Update session check | ⬜ Pending |
## Console Setup Checklist
- [ ] Descope project created — Project ID: (fill in when done)
- [ ] JWT template configured
- [ ] Tenants created for each WorkOS Organization: (list)
- [ ] Roles created: (list roles)
- [ ] SSO connections / SSO Setup Suite configured: (list)
- [ ] Social providers configured: (list providers)
## Decisions Log
_Non-obvious decisions made during migration — preserves rationale if context is lost._
_(none yet)_
## Current Phase
Phase 1 — Console Setup (not started)
## Next Action
Complete console setup per MIGRATION-PLAN.md before making any code changes.
## Blockers
_(none)_
Rule 1 — Re-read before every turn.
At the start of every execution turn, re-read MIGRATION-PLAN.md and MIGRATION-STATE.md
before writing any code or making any decision.
Rule 2 — Verify context before every code change.
If the framework, migration path, triage answers, or next step aren't clear from the conversation, re-read both files before proceeding. Then output a context line:
Migration context: Next.js 14 · Path B · Phase 2, step 3/8 · Next: rewrite lib/auth.ts
If this line can't be filled in accurately, re-read the files first.
Rule 3 — Update MIGRATION-STATE.md immediately after each step.
Mark the file done in the Files Inventory, update "Current Phase" and "Next Action", and append any non-obvious decision to the Decisions Log. Do this before the next step.
Run before generating any import, wrapper type, or helper. Skipping produces code that compiles but fails at runtime.
1. Verify SDK exports before writing any import.
When the Descope MCP server is available, use docs_ask_question to confirm the exact method name, option shape, and return type before writing any SDK call. This is faster and more reliable than reading type declarations. Do not write a method name and add a hedge like "verify the exact name" — just verify it.
When the Descope MCP server is unavailable: resolve the package's type declarations (node_modules/<pkg>/dist/types/ or its package.json types field) and confirm the exact exported name and signature. For Go, run go doc. For Python, check the SDK stubs.
Prefer local node_modules/ over GitHub when reading type declarations. Installed packages reflect the exact version in use. If the Descope package isn't installed yet, install it first, then read local type declarations. Only fall back to GitHub if the package can't be installed in the current environment.
This applies to every SDK call you write, not just the first import. Field names on
option objects, hook return shapes (useDescope() returns the SDK directly, not { sdk }),
and subpath exports (/client vs root) differ just as often.
1a. After rewriting any module, grep for remaining imports of the removed package.
grep -r "from '@workos-inc/" --include="*.ts" --include="*.tsx" .
Add remaining hits to the work list.
2. Derive wrapper types from the actual return type. Read the function's declared return type and build the wrapper to match. WorkOS's field names, nesting, and flags differ — don't infer from them.
3. Check dependency versions before generating framework-specific code.
For Next.js: cookies() and headers() from next/headers are synchronous in v14 and
async in v15. Read package.json (or go.mod, requirements.txt) first.
4. When making a helper async, propagate to all callers immediately.
In TypeScript, async on a shared utility silently breaks callers that omit await. Grep
for all call sites of the changed function and update them in the same pass. The cascade can
span 10–20 files.
5. Verify published package versions before writing to package.json or running npm install.
Don't reuse WorkOS's version number or rely on training data for versions. Before writing any
install command:
npm view @descope/node-sdk version
npm view @descope/nextjs-sdk version
If npm is unavailable, leave the version as "latest" and flag it.
Several steps require Descope Console setup that can't be done in code. The app compiles without them but won't work at runtime.
Use AskUserQuestion to ask whether they already have a Project ID and working Flow. If
yes, skip to verifying items 5–7 — these are easy to miss even for existing projects.
P (e.g. P2abc123...).NEXT_PUBLIC_DESCOPE_PROJECT_ID. For all server-side SDKs, it's DESCOPE_PROJECT_ID.Required for: user management API, role/permission management, tenant operations, SSO/SCIM configuration, ReBAC (FGA), Outbound Apps. If the app does any server-side user, tenant, SSO, or SCIM management, they need this.
DESCOPE_MANAGEMENT_KEY. Treat like a secret — never expose client-side.A Flow is the auth UI sequence. Reference it by Flow ID in the web component.
references/flows-and-widgets.md → Flows.references/implementation-nuances.md → MFA section.references/implementation-nuances.md → Social login / SSO section.WorkOS AuthKit tokens may include profile fields; Descope tokens do not by default.
{"email": "{{user.email}}", "name": "{{user.name}}", "picture": "{{user.picture}}"}token.email
will get undefined after migration.Descope roles are referenced by name, not by ID. They must be created manually in the Console before the code that assigns them will work.
admin, member)WorkOS Organization metadata and User metadata map to Descope customAttributes.
Pre-define them in the Console schema before setting them via the SDK.
metadata): Console → Tenants → Custom Attributes tab → Create Attribute| Variable | Where to get it | Used by |
| -------------------------------- | ----------------------------------- | ------------------------------------------- |
| DESCOPE_PROJECT_ID | Console → Project Settings | All server-side SDKs |
| NEXT_PUBLIC_DESCOPE_PROJECT_ID | Same value as above | Next.js AuthProvider (client-side) |
| DESCOPE_MANAGEMENT_KEY | Console → Company → Management Keys | Management SDK, SSO/SCIM, Outbound Apps API |
Before migrating custom profile pages, user management pages, role assignment UI, or admin
SSO/SCIM setup pages, ask whether a Descope Widget or the SSO Setup Suite covers the use case.
See references/flows-and-widgets.md → Widgets.
After completing console setup: Update MIGRATION-STATE.md — check off each completed
item in the Console Setup Checklist, record the Project ID in the file, and set Next Action
to the first code change step.
WorkOS publishes exactly two SDK families (per the WorkOS SDKs page):
workos-node, workos-go, workos-ruby, workos-rust, workos-python, workos-php, workos-php-laravel, workos-kotlin (Java), workos-dotnet (.NET). These call the WorkOS API and hold server-side session helpers.authkit-js, authkit-react, authkit-nextjs, authkit-remix, authkit-react-router, authkit-tanstack-start. These handle the login UI + session.The recipes below cover exactly these SDKs — one section each — annotated with the matching Descope target. Do not infer other frameworks (e.g. Express, Flask, FastAPI); a WorkOS app using those is using the underlying language Backend SDK (workos-node, workos-python, etc.), so map it via that SDK's section.
The framework recipes below are stubs listing the WorkOS idioms that need mapping. Confirm the exact WorkOS SDK surface for the user's stack and the matching Descope SDK calls via the Descope MCP or local type declarations before generating any code. Do not ship code from these stubs without verification.
Read references/implementation-nuances.md in two passes before writing any code:
offset to jump directly) — read only the section matching the user's stack.When a new framework is added to the file, add it to this list.
These idioms are AuthKit-JS-specific. Backend-SDK apps (Python, Go, Ruby, PHP, Java, .NET) don't
have these helpers — they instead call the SDK directly (e.g. workos.userManagement.authenticateWithCode(...)
for the code exchange and workos.userManagement.loadSealedSession(...) for session access), which map
to Descope session validation + the hosted/embedded Flow the same way.
withAuth() / getUser() (AuthKit session access) → Descope session validation + an adapter returning the shape callers expectauthkitMiddleware() → Descope session-validation middlewareWORKOS_COOKIE_PASSWORD) → Descope signed session JWT in DS/DSR cookiesgetSignInUrl() / hosted AuthKit redirect → embedded Descope Flow component (or hosted Flow), wiring onSuccessWorkOS SDK: workos-node (@workos-inc/node) → Descope @descope/node-sdk
@workos-inc/node auth/session usage; add @descope/node-sdkDS session token via custom middleware calling descopeClient.validateSession() (parse the cookie yourself)workos-go → Descope Go SDK github.com/descope/go-sdkdescope/go-sdkdescopeClient.Auth.ValidateSessionWithToken(ctx, token) returns (bool, *descope.Token, error)organizationId → a Descope tenant ID: pass it to management calls (descopeClient.Management.Tenant() / user-tenant association); at request time read tenant context off the returned *descope.Token (token.GetTenants(), or the dct claim for the active tenant)WorkOS SDK: workos-ruby → Descope Ruby SDK
DS session token via the Descope Ruby SDK in your request lifecycleimplementation-nuances.md yet — follow the Node.js / Python backend patterns and verify against the Descope Ruby SDK.WorkOS SDK: workos-rust → No Descope Rust SDK; validate via Descope JWKS + Management REST API
DS session JWT directly against Descope's JWKS endpoint (https://api.descope.com/v2/keys/<project_id>) using a standard JWT library.WorkOS SDK: workos-python → Descope descope Python SDK
descope Python SDKDS session token with descope_client.validate_session(session_token) (or validate against Descope's JWKS for a custom authorizer)WorkOS SDK: workos-php → Descope PHP SDK
DS token via the Descope PHP SDK in your request lifecycleWorkOS SDK: workos-php-laravel → Descope PHP SDK (no Descope Laravel-specific SDK)
DS token in Laravel middlewareWorkOS SDK: workos-kotlin (Java/Kotlin) → Descope descope-java
descope-javaDS token via a filter/interceptorWorkOS SDK: workos-dotnet → Descope descope-dotnet
descope-dotnetDS token in middleware / a custom auth handlerRead the session the framework-native way — never hand-parse the JWT on the client. On front-end pages and components, get auth state from the Descope hooks:
useSession()for the session token and auth status,useUser()for the user profile, anduseDescope()for actions likelogout(). Do not manually decode the session token or pull claims out of it in client code. Server-side session validation —session()in@descope/nextjs-sdk/server,validateSession()in@descope/node-sdk(or the other backend SDKs) — belongs only in backend routes, loaders, middleware, and API handlers, never in a rendered client component. This matters most with the React SDK, where it's tempting to crack open the raw token in a component instead of callinguseUser()/useSession().
WorkOS SDK: authkit-js → Descope @descope/web-js-sdk + @descope/web-component
createClient() / authkit.getUser() / getAccessToken() → @descope/web-js-sdk (getSessionToken(), isJwtExpired(), refresh())<descope-wc project-id flow-id> web component, listening for success / error eventssdk.logout() + clear stored tokens/cookiesWorkOS SDK: authkit-react → Descope @descope/react-sdk
<AuthKitProvider> → Descope <AuthProvider projectId>useAuth() (user/session/loading) → useSession() + useUser() hooksuseSession() (token + isAuthenticated) and useUser() (profile), with useDescope() for actions. Never decode the session token by hand in a component, and never call backend validateSession() from client code; that runs only on the server.signIn() / hosted redirect → embedded <Descope flowId> component, wiring onSuccesssdk.logout() via useDescope() hookWorkOS SDK: authkit-nextjs → Descope @descope/nextjs-sdk + @descope/node-sdk
authkit-nextjs → @descope/nextjs-sdk + @descope/node-sdk<AuthKitProvider> → Descope AuthProvider (takes projectId; must use NEXT_PUBLIC_ prefix)withAuth() / useAuth() → session() (server) + useSession() / useUser() (client)authkitMiddleware() → Descope authMiddleware(options)sdk.logout() via useDescope() hook + clear cookies (two-step)session() from @descope/nextjs-sdk/server is server-only; useSession()/useUser() from @descope/nextjs-sdk/client are client-only. Using session() in a client component compiles but throws at runtime. Verify exact exports before writing imports.WorkOS SDK: authkit-remix → Descope @descope/react-sdk + @descope/web-js-sdk (no Descope Remix SDK)
authkitLoader / authLoader() loaders → custom Remix loaders that validate the session token (@descope/web-js-sdk / @descope/node-sdk) and gate routesgetSignInUrl() → embedded Descope component (@descope/react-sdk) or hosted FlowDS/DSR cookies in an action + sdk.logout()WorkOS SDK: authkit-react-router → Descope @descope/react-sdk
authkit-react-router is the React Router 7+ port): loader-based session checks → custom loaders + Descope session validationsdk.logout() + cookie clearWorkOS SDK: authkit-tanstack-start → Descope @descope/react-sdk / @descope/web-js-sdk (no Descope TanStack SDK)
sdk.logout() + cookie clearDescope exposes standard OIDC endpoints. If the app uses a generic OIDC client library pointed at WorkOS, it can point at Descope's OIDC issuer instead with minimal code changes.
First classify the current integration — the OIDC endpoints only matter for the hosted-page case. Before considering Path A, determine how the app authenticates today:
Don't recommend Path A until you've confirmed the app uses a standard OIDC/OAuth client against the hosted page.
Many WorkOS apps use AuthKit's own SDK rather than a generic OIDC client, in which case Path A may not apply cleanly. Confirm whether the app uses a standard OIDC client before recommending this path. Verify the Descope OIDC endpoint table below against current docs.
| Endpoint | Descope |
| ------------- | ------------------------------------------------------------- |
| Issuer | https://api.descope.com |
| Authorization | https://api.descope.com/oauth2/v1/authorize |
| Token | https://api.descope.com/oauth2/v1/token |
| UserInfo | https://api.descope.com/oauth2/v1/userinfo |
| JWKS | https://api.descope.com/__ProjectID__/.well-known/jwks.json |
Good for: Teams that want to swap the IdP first, then refactor to Descope-native SDKs later. Preserves existing OIDC client code.
Caveats: Claim shapes differ, token lifetimes may differ, and WorkOS organization-scoped login / SSO must be rebuilt in Descope regardless of path.
For B2B apps using WorkOS Organizations: Path A preserves only a fraction of the work; the management SDK, org/tenant-scoped login, SSO/SCIM setup, and claim mapping require full migration regardless. Path A savings are minimal for B2B workloads — account for this when estimating effort.
After completing framework code changes: Update MIGRATION-STATE.md — mark each
modified file as Done in the Files Inventory, update Current Phase and Next Action, and
log any non-obvious decisions made (adapter types kept, async cascade scope, etc.).
Scan for WorkOS references in non-code files after updating source files.
.env.example / .env.template / .env.sample# REMOVE
WORKOS_API_KEY=
WORKOS_CLIENT_ID=
WORKOS_REDIRECT_URI=
WORKOS_COOKIE_PASSWORD=
# ADD
DESCOPE_PROJECT_ID= # Console → Project Settings
NEXT_PUBLIC_DESCOPE_PROJECT_ID= # Next.js only — same value as above
DESCOPE_MANAGEMENT_KEY= # Console → Company → Management Keys (only if using management SDK)
Run grep -r "WORKOS" to find all env var references — .env.example, Docker, CI, shell scripts.
Search all .md files for WorkOS references. At minimum, update:
Check Dockerfile, docker-compose.yml, .github/workflows/, and any CI config for
WORKOS_* env var declarations. Update them to DESCOPE_*.
When the migration includes a setup or seed script (e.g., scripts/bootstrap.mjs, scripts/seed.ts), split it into two parts:
MIGRATION-PLAN.md.management.role.create()), tenant creation, access key provisioning, SSO/SCIM config. Preserve these as a Node.js/Python script using the Descope Management SDK.After completing non-code file updates: Update MIGRATION-STATE.md — mark env files,
README, and CI config done in the Files Inventory, and advance Next Action.
For each WorkOS feature confirmed in triage, write a short paragraph: what it accomplishes, the best Descope approach for that goal, what's different, and what action is required. Reason about intent, not just the API surface — the best approach may be a Flow, Widget, SSO Setup Suite, or Console configuration rather than a direct SDK equivalent. Only recommend SDK/API code when programmatic control is genuinely required, and verify every method name against the Descope MCP server before writing it. Include only confirmed features.
WorkOS AuthKit handles login UI, authentication methods, users, sessions, and enterprise login routing. Descope splits these responsibilities across a Flow (UI + methods), session validation (SDK), Users/Tenants, and JWT Templates (profile claims).
| WorkOS | Descope |
| ---------------------------------------------------------------- | ---------------------------------------------------------------- |
| Hosted AuthKit login UI | Descope Flows |
| withAuth() / getUser() session access | validateSession() + adapter returning the shape callers expect |
| AuthKit user object | Descope User (profile fields via JWT Template) |
| Auth method config (password, social, passkeys, MFA, magic auth) | Methods toggled in Console + added as Flow steps |
Use Flows for the user-facing journey whenever possible; write custom SDK calls only when Flows cannot express the requirement. Ask which auth methods are enabled before recommending details. Effort: Low–Medium (mostly SDK/UI/session swap; token differences matter).
organizationId → a Descope tenant ID (the argument you pass to management.tenant.* / management.user.* calls). At request time the tenant also appears in the session as the nested tenants object ({ tenantId: { roles, permissions } }) plus dct for the active tenant.sso.start(tenantId, ...) for a known tenant). Use when each customer
has a dedicated login path rather than a shared email-entry screen.metadata → tenant customAttributes (pre-define in the Console schema)Confirm the one-Organization-to-one-Tenant mapping before writing code — it ripples into SSO, SCIM,
RBAC, and domain routing. Effort: Medium (clean conceptually; most organizationId usage is
management calls that take a tenant ID, with a smaller set of session reads that change shape).
Preferred approach — SSO Setup Suite: before migrating any management-SDK SSO calls, ask whether the no-code SSO Setup Suite removes the need for that code. It guides tenant admins through per-tenant SAML/OIDC setup with IdP-specific instructions (Okta, Azure AD, Google Workspace, etc.) — no engineering involvement for new tenant onboarding.
Multiple SSO configurations per tenant. Descope supports more than one SSO/IdP configuration on a
single tenant: each tenant has a Default SSO Configuration plus optional additional named SSO
configurations (Console or API/SDK). At login, Descope selects the right IdP by domain-based
routing (e.g. @acme.com → Acme's Okta, @globex.com → Globex's Azure AD), a tenant-specific
login URL, an explicit SSO configuration ID, or Flow logic. SCIM provisioning can be scoped per SSO
configuration, and each configuration can have its own SSO Setup Suite link. See
references/flows-and-widgets.md and Descope Multi-SSO.
Use AskUserQuestion to ask two things here:
references/flows-and-widgets.md → SSO Setup Suite.SDK path (when programmatic SSO is needed):
| WorkOS | Descope |
| --------------------------- | ------------------------------------------------- |
| SAML connection (sso.*) | management.ssoApplication.createSamlApplication |
| OIDC connection | management.ssoApplication.createOidcApplication |
| Per-Organization connection | Per-tenant SSO (Console → SSO or Management SDK) |
(Verify exact method names against the Descope MCP server.) Ask whether SSO is configured by internal engineers or by customer admins. Effort: Medium — setup recreated per tenant.
Runtime login calls — always use sso.start / sso.exchange, never the OAuth flow. When code
initiates an enterprise SSO login (the equivalent of WorkOS's sso.getAuthorizationUrl() +
sso.getProfileAndToken()), call the Descope SDK's sso.start(tenant, redirectUrl, ...) to begin
the flow and sso.exchange(code) to complete it. Use these regardless of the IdP's underlying
protocol — even when the tenant's SSO is configured in Descope as OIDC/OAuth — because sso.start
resolves the tenant-level SSO configuration (the correct IdP, domain-based routing, and
connection settings) for you. Do not reach for the generic oauth.start / oauth.exchange
functions for enterprise SSO: those drive project-level social/OAuth providers and will not apply a
tenant's SSO config. Rule of thumb: tenant/enterprise SSO → sso.*; social or generic OAuth login →
oauth.*.
Don't rebuild provider-specific SSO UI — let tenant config do the routing. Especially when the
app uses the backend SDKs, do not migrate or recreate any per-IdP login UI (separate "Sign in
with Okta" / "Sign in with Azure AD" buttons, provider-picker screens, etc.), regardless of which
SSO provider the WorkOS code names. In Descope the IdP is defined as tenant SSO configuration,
and a single sso.start call resolves it automatically via home realm discovery — either
domain-based routing (email domain or SSO domain on the tenant config) or an explicit tenant
slug (tenant ID/name hardcoded in source). So the login surface just collects an email (or targets a
known tenant) and calls
sso.start; Descope selects the correct IdP from config. Keep the UI generic and push all
provider-specific details into Console/tenant configuration.
WorkOS Directory Sync maps to Descope SCIM provisioning. Treat this as a continuing pipeline, not a one-time import — enterprise directories keep pushing create/update/suspend/delete events after cutover, so every directory must be re-pointed at Descope before cutover or provisioning silently breaks.
| WorkOS Directory Sync | Descope |
| ------------------------------------------ | ---------------------------------------- |
| SCIM endpoint + bearer token per directory | Descope SCIM endpoint + token per tenant |
| Directory user create/update/deprovision | Tenant user provisioning lifecycle |
| Directory groups | Group → role mapping in Descope |
| dsync.* / directory webhooks | Descope provisioning events / connectors |
Identify every directory, whether groups are synced, and whether groups map to roles. Effort: Medium–High (lifecycle, groups, deprovisioning, and role mapping can be subtle).
WorkOS Admin Portal is a hosted self-serve UI where customer IT admins configure SSO, Directory Sync, and domain verification. Do not default to rebuilding it as custom code.
portal.generateLink(...)) → SSO Setup Suite hosted/embedded flow or Tenant Profile WidgetAsk which admin workflows are hosted by WorkOS today before choosing a replacement. Effort: Medium — may remove custom code, but portal-link workflows need replacement.
WorkOS roles come in two scopes, and they map to Descope's two scopes:
tenantId to management.role.create(...) (it surfaces per-tenant when you check roles on the session, e.g. validateTenantRoles).| WorkOS | Descope |
| --------------------------------------------------------- | ------------------------------------------------ |
| role | role |
| permission | permission |
| Environment-level role (applies across all organizations) | Project-level role (applies across all tenants) |
| Organization-scoped role ("custom role") | Tenant-scoped role |
| roleSlug reference | Descope role name (not ID) |
| IdP group → role mapping | Group-to-role mapping (SSO Configuration / SCIM) |
SDK: descopeClient.management.role.create(name, description, permissionNames, tenantId) (verify
the exact function name depending on the language sdk). Pass tenantId to create a tenant-level
role (the equivalent of a WorkOS organization-scoped/custom role); omit it for a project-level
role (the equivalent of a WorkOS environment-level role). Roles must exist in the Console before
assignment. Check whether each WorkOS role is environment- or organization-scoped and where checks
happen (middleware, API routes, DB queries, frontend). Effort: Medium.
Authorization model must be translated and validated. Schema translation example:
# WorkOS FGA
Resource type: project
Parent: workspace
Permissions:
- project:view
- project:edit
- project:delete
Roles:
- project-viewer
- project:view
- project-editor
- project:view
- project:edit
# Descope ReBAC DSL
type document
relation owner: user
relation viewer: user
permission can_view: owner | viewer
Descope ReBAC schema DSL — use this syntax to author the Descope ReBAC schema:
Syntax Description Example
--------------------------------- ------------------- ----------------------------
type <name> Define a type type user
relation <name>: <type> Define a relation relation owner: user
| Union (OR) operator user | group
# Relation reference Group#member
. Traverse relation parent.owner
permission <name>: <expression> Define a permission permission can_edit: owner
| Operation | WorkOS FGA | Descope ReBAC |
| -------------- | ------------------------- | ----------------------------------------------------- |
| Write relation | fga.writeWarrant({...}) | descopeClient.management.fga.createRelations([...]) |
| Check | fga.check({...}) | descopeClient.management.fga.check([...]) |
(Verify exact WorkOS and Descope shapes against current docs.) Identify resources, relationships/privileges, where checks run, and any hierarchical inheritance. Effort: High — require a dedicated model review.
WorkOS Audit Logs map to Descope audit events, the Audit Webhook Connector, or other connectors depending on the use case. Determine whether the app writes events to WorkOS, reads them back, shows them to customer admins, or requires them for compliance. Effort: Medium.
Mechanism difference (read this first): WorkOS Radar is a dashboard toggle layered on top of AuthKit — it collects device-fingerprint signals and automatically blocks / challenges / notifies based on the actions you enable, with no app code. Descope has no single equivalent toggle. Instead you reproduce Radar's behavior by adding Descope's built-in fingerprinting/risk signals to your Flow and branching on them. So "configuring Radar" becomes "designing the Flow."
Descope surfaces risk signals as riskInfo inside a Flow. riskInfo.botDetected and
riskInfo.riskScore require adding a Fingerprint / Assess action immediately after the
login/signup screen; riskInfo.impossibleTravel and riskInfo.trustedDevice do not. For stronger
detection, layer in fraud/CAPTCHA connectors (reCAPTCHA Enterprise, Turnstile, Telesign,
Fingerprint, Forter, Sardine).
| Radar action | What it does in WorkOS | Descope equivalent |
| ------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Block | Auth fails even with valid credentials | Flow conditional after Fingerprint Assess: on high riskInfo.riskScore / riskInfo.botDetected, branch to a deny/failure screen and end the Flow without issuing a session |
| Challenge | Sends an email (or SMS) OTP step-up | Risk-based step-up in the Flow: branch the high-risk path into an OTP/MFA step or a CAPTCHA connector (reCAPTCHA / Turnstile) before continuing |
| Notify | Sends an informational email to user/admin; sign-in still proceeds | Compose it: on the risk branch, fire an email/messaging connector or an outbound webhook (or rely on Descope audit events) to alert the user/admin, while letting the Flow continue |
Detection mapping (verify current signal names against docs):
riskInfo.botDetected (needs Fingerprint Assess) + CAPTCHA connectorsriskInfo.impossibleTravelriskInfo.trustedDevice (invert: untrusted = unrecognized)riskInfo.riskScore thresholds, connectors, or custom Flow conditions. Flag any of these in use for dedicated design.Ask whether each Radar detection is set to block, challenge, or notify, and whether app logic depends on its decisions (vs. pure config). Effort: Medium — Console/Flow configuration rather than app code, but the decisioning must be rebuilt in the Flow, not simply toggled on.
WorkOS Pipes (connected third-party accounts with OAuth token storage/refresh) maps to Descope Outbound Apps.
Users connect accounts client-side:
sdk.outbound.connect(appId, { redirectURL, scopes })
Fetch stored tokens server-side:
POST https://api.descope.com/v1/mgmt/outbound/app/user/token
Authorization: Bearer {projectId}:{managementKey}
Body: { "appId": "google-calendar", "userId": "U2abc...", "scopes": [...] }
Ask which providers are connected, where tokens are used (including AI agents / background jobs), and whether users must reconnect accounts or tokens can be migrated. Effort: Medium.
| WorkOS | Descope |
| ---------------------------------------------------- | ------------------------------------------------- |
| Webhook endpoint + signing secret | Descope webhook/connector + signature validation |
| user.created / organization.* / dsync.* events | Corresponding Descope events / connector triggers |
Search the codebase for webhook handlers; update event names, signature/validation logic, and payload handling. Identify which event types are business-critical. Effort: Medium.
WorkOS Domain Verification maps most closely to Descope’s tenant SSO domain verification, where the customer proves ownership of their email domain with a DNS TXT record. Descope Custom Domains are separate: they configure a CNAME such as auth.example.com so Descope authentication endpoints, cookies, OAuth callbacks, and related URLs can use your own domain. a custom auth domain. Effort: Low–Medium.
WorkOS Widgets (org switching, Directory Sync setup, SSO setup, domain verification, audit log
streaming, API keys) should be evaluated against Descope Widgets. If a Descope Widget covers the
workflow, prefer it over custom migration code. See references/flows-and-widgets.md → Widgets.
May map to Descope Inbound Apps, OAuth app patterns, or custom MCP authorization. Do not generate implementation code until the exact WorkOS usage is understood. Determine whether WorkOS is acting as an OAuth provider, an OAuth client, or both. Effort: Medium–High — flag for dedicated review.
WorkOS Vault and EKM (encrypting/storing/controlling access to sensitive data) may have no direct Descope identity equivalent. Flag it separately and ask whether it is part of the identity migration or a separate secrets/data-security effort. Do not present it as an SDK swap.
WorkOS Feature Flags are usually not part of an auth migration. If they are used for access control, some behavior may map to Descope roles/permissions, but general feature flagging should be treated as out of scope.
Descope session JWTs contain sub, amr, drn, tenants, roles, permissions, and dct by
default. They do not contain email, name, or picture. WorkOS AuthKit tokens may expose
some profile fields, so code that reads them directly will break after migration.
dct and tenants only matter when you read a user's tenant context from their session at
request time — not for tenant administration, which is done by tenant ID through
management.tenant.* / management.user.*. When you do read the session, dct (Descope Current
Tenant) is a flat string holding the active tenant ID — the direct equivalent of WorkOS's
organizationId — and tenants is a keyed object ({ [tenantId]: { roles, permissions } }) for
per-tenant roles/permissions. Prefer the SDK's role/permission helpers (e.g.
validateTenantRoles(authInfo, tenantId, [...])) over reading these claims by hand; reach for dct
when you only need the active tenant ID.
Action required: Configure a JWT Template in the Descope Console to add email,
name, and any other profile fields the app reads from the token.
WorkOS AuthKit uses an encrypted/sealed session cookie protected by WORKOS_COOKIE_PASSWORD.
Descope issues a signed session JWT in the DS cookie (refresh in DSR). The sealing password is
no longer needed, and code that unseals/inspects the WorkOS cookie must be replaced with Descope
session validation (validateSession()), which returns decoded JWT claims.
descopeClient.logout(refreshToken) to invalidate server-sideDS and DSR cookiesSkipping either step leaves a broken state.
Descope session tokens have no aud claim by default. Apps that rely on audience-scoped API access
must (1) configure a custom aud claim in JWT Templates and (2) pass audience to
validateSession() on the backend.
Most code that references a WorkOS organizationId (and connectionId / directoryId) is
management/admin code — it becomes a Descope tenant ID passed to management.tenant.* /
management.user.* calls. Only request-time code that read the org off the WorkOS session changes
shape: Descope exposes the active tenant as dct and membership as the nested tenants object,
read off the validated session (ideally via SDK helpers). Grep for all organizationId reads and
sort them into these two buckets — by-ID management calls vs. session reads — before updating.
Forward the Descope session JWT (DS cookie) as Authorization: Bearer <DS> to API servers. There
is one session token; downstream services validate it with validateSession().
Descope has no authkitMiddleware() equivalent package. The middleware is ~20 lines of custom code
that reads the DS cookie and calls validateSession().
cookies() and headers() Are Async in Next.js 15cookies() and headers() from next/headers return a Promise in Next.js 15+. Before
generating any server-side helper that reads cookies:
package.json for the Next.js version.await cookies() and mark the containing function async.When a shared utility becomes async, TypeScript accepts await on non-Promises without
error — so callers that forget await silently return a Promise object. Always grep for
all call sites of any utility you make async and update them in the same pass.
If Directory Sync is in use, re-point the SCIM pipeline at Descope before cutover. A one-time user import leaves provisioning broken the moment the directory pushes its next change.
WorkOS: WORKOS_API_KEY, WORKOS_CLIENT_ID, WORKOS_REDIRECT_URI, WORKOS_COOKIE_PASSWORD (4+).
Descope: DESCOPE_PROJECT_ID only (+ DESCOPE_MANAGEMENT_KEY for management ops).
Run the app and verify it works — don't just hand over a checklist.
grep -r "@workos-inc\|workos\|authkit" \
--include="*.ts" --include="*.tsx" --include="*.js" --include="*.py" --include="*.go" \
--exclude-dir=node_modules --exclude-dir=.next --exclude-dir=dist \
.
If this returns any results, stop and fix them before proceeding.
npm install # or: pip install -r requirements.txt / go mod tidy
npx tsc --noEmit # TypeScript
go build ./... # Go
mvn compile -q # Java/Maven
./gradlew compileJava compileKotlin # Java/Gradle
dotnet build # .NET
Do not proceed until compilation exits with zero errors.
If compilation fails, diagnose by error message:
Cannot find module '@workos-inc/...' → stale import; re-run Phase 0Property 'X' does not exist on type 'AuthenticationInfo' → wrapper built against WorkOS shape; re-derive'await' expression is not allowed in synchronous contexts → async cascade gapObject is possibly 'undefined' on session fields → add null check or early returnnpm run dev # or: python main.py / go run . / flask run / etc.
npm test # or: pytest / go test ./... / etc.
Auth-related test failures usually mean: a mock or fixture still uses WorkOS shapes, or a
test validates JWT claims that are now missing (e.g., email without a JWT Template), or a test
still uses organizationId where the code now passes a Descope tenant ID (management calls) or
reads dct/tenants off the validated session.
# Root path
curl -s -o /dev/null -w "%{http_code}" http://localhost:<port>/
# Unauthenticated protected route (expect 302 or 401)
curl -s -o /dev/null -w "%{http_code}" http://localhost:<port>/dashboard
# Login page loads Descope component
curl -s http://localhost:<port>/login | grep -i "descope"
# Invalid token → 401
curl -s -H "Cookie: DS=invalid_token" http://localhost:<port>/api/me
echo "<DS_cookie_value>" | cut -d'.' -f2 | base64 -d 2>/dev/null | python3 -m json.tool
Check that email, name, and any other expected claims (including dct/tenants for B2B) are present.
## Test Results
**Server startup:** ✅ Started successfully on port 3000
**Existing tests:** ✅ 12 passed / ❌ 2 failed (list failures)
**Unauthenticated /dashboard:** ✅ 302 → /login
**Unauthenticated /api/protected:** ✅ 401
**Login page loads Descope component:** ✅
**JWT claims (email, name, dct):** ✅ Present / ❌ Missing — JWT Template not yet configured
**Blockers before going live:**
- [ ] (list anything that failed or needs manual action)
Do not proceed to Step 6 until ALL of the following are true:
Every migration produces a MIGRATION-SUMMARY.md covering what was done, manual setup
remaining, and behavioral differences that matter before production.
Write a numbered migration guide in Markdown, scoped to the user's stack. Use code snippets and direct doc links. Always include the MIGRATION-SUMMARY.md deliverable (Step 6).
For complex migrations (Directory Sync/SCIM, FGA, Pipes, MCP Auth), flag the high-effort items explicitly with estimated complexity (Low/Medium/High) so the user can plan.
references/implementation-nuances.md — Verified migration patterns, code-level diffs, and edge
cases for several frameworks.development
Use when building React "Bring Your Own Screen" (BYOS) custom UI on top of a Descope flow — takes exported flow JSONs, extracts the real interaction IDs and outputs, generates BYOS components that match hosted parity, and avoids the rediscovery-the-hard-way failure modes (silent form rejection, shared screen-name collisions, anonymous-session stickiness, nested-form hydration errors, wrong form keys, dead-end buttons, missing OAuth provider field).
development
Use this skill whenever anyone asks about migrating from Okta Customer Identity Service (CIS) to Descope — whether they're a developer doing it themselves or a technical lead evaluating the move. Triggers on: "how do I migrate from Okta", "replace Okta CIS with Descope", "we're moving off Okta", "Okta to Descope", "switch from Okta", "our app uses okta-auth-js / @okta/okta-react / @okta/okta-angular / @okta/oidc-middleware / okta-jwt-verifier and we want to use Descope instead", or any question about Okta CIS features (Sign-On Policies, Authorization Servers, Authenticators, Identity Providers, Log Streams, Service Apps, scp claim) in the context of Descope. Works for any language or framework with a Descope SDK. Always use this skill before producing migration guidance — do not rely on memory alone.
development
Set up and manage Descope projects with Terraform. Use when configuring authentication infrastructure as code, managing environments, creating roles/permissions, setting up connectors, or deploying Descope project configurations.
testing
Author, edit, or apply a Descope FGA schema using the ReBAC/ABAC DSL. Use this skill whenever the user asks to create a new FGA schema, modify an existing one, add types/relations/permissions/conditions, review an authorization model, or apply schema changes to a Descope project. Trigger even if the user says things like "set up authorization", "define roles and permissions", "add team-based access", "make this endpoint check FGA", or "update my authz model" — these almost always mean an FGA schema change.