Use this skill whenever anyone asks about migrating from Stytch to Descope — whether they're a developer doing it themselves or a technical lead evaluating the move. Triggers on: "how do I migrate from Stytch", "replace Stytch with Descope", "we're moving off Stytch", "Stytch to Descope", "switch from Stytch", "our app uses stytch / @stytch/nextjs / Stytch UI / Stytch SSO / Connected Apps / SCIM and we want to use Descope instead", or any question about Stytch features (Consumer authentication, Multi-tenant / B2B Authentication, Enterprise SSO, SCIM, Admin Portal, M2M Authentication, Connected Apps, Session Management, Fraud & Risk) 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.
68
84%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
This skill guides self-service migrations from Stytch 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. If the file view is truncated, partial, or cut off, continue reading with the appropriate offset until all lines have been loaded; do not proceed based on a partial read.
Stytch is not only an authentication provider — it is a broader identity platform spanning consumer authentication, multi-tenant/B2B authentication, organizations and members, enterprise SSO, SCIM, RBAC, JIT provisioning, MFA, session management, Admin Portal flows, fraud and risk protection, device fingerprinting, Protected Auth, machine-to-machine authentication, trusted auth tokens, and Connected Apps for OAuth/OIDC-based integrations and AI-agent access. A good migration first identifies which Stytch product surfaces are in use, then maps each one to the closest target feature or migration pattern. Expect Stytch migrations to vary more widely than a purely B2B auth migration, since a Stytch implementation may include consumer passwordless auth, enterprise-readiness features, fraud/risk infrastructure, and OAuth/OIDC connected-app workflows.
Primary references (both in this skill's directory):
references/implementation-nuances.md — verified migration patterns for each framework, Stytch 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 — 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 Stytch 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 Docs MCP is available by calling
search-descope-docs 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 Docs MCP is not installed.
This skill uses the Descope Docs MCP 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-mcp.descope.com/ (server URL:
https://docs-mcp.descope.com/mcp). 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?
search-descope-docs 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 — Stytch feature usage (use multiSelect: true):
iss), audience (aud), public keys or JWKS URL, and claim mappings. Flag as high complexity.After both calls, summarize findings and flag high-complexity items before proceeding to Step 0.5. The main high-complexity Stytch areas are typically SCIM, Enterprise SSO with JIT provisioning, RBAC tied to SSO or SCIM, Fraud & Risk/Device Fingerprinting, Protected Auth, Connected Apps, AI Agent/MCP authentication, Machine-to-Machine authentication, and Trusted Auth Tokens.
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_id, member_id, organization_id, organization_slug, roles, permissions, trusted_metadata, untrusted_metadata, or custom claims? These need a Descope JWT Template or Flow Custom Claims configured before equivalent reads will work.organization_id, member_id, sso_connection_id, scim_group_id, Connected App client IDs, or RBAC role_id / resource_id / action values in many places? The Stytch Organization → Descope Tenant remap ripples through SSO, SCIM, RBAC, JIT provisioning, Admin Portal replacement, Connected Apps, and membership checks — confirm the organization model before writing code.Deployment and risk
User, organization, and member migration (if they indicated existing users/orgs in Step 0)
user_id, member_id, organization_id, role_id, Connected App client IDs, and any other persisted identifiers.Gaps to flag immediately (don't ask — flag these proactively based on Step 0 answers)
aud enforcement.Console/Flow/Widget opportunities (flag before codebase analysis, then ask):
Summarize any blockers and Console/Flow/Widget opportunities before proceeding to codebase analysis.
Scan the codebase to map every auth touchpoint before writing the plan.
Stytch ships backend SDKs (Python, Go, Node, Ruby, Java/Kotlin/JVM), frontend SDKs (React, Next.js, Vanilla JS), and mobile SDKs (React Native, iOS Swift, Android Consumer SDK — a headless Kotlin Multiplatform library targeting Android). Adapt the file extensions below to whichever surfaces appear in the project.
Run these searches (adapt file extensions to the user's language and platform):
# Find all Stytch import / package sites (backend, frontend, mobile)
grep -rni "stytch\|@stytch\|stytchauth\|com\.stytch" \
--include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" \
--include="*.mjs" --include="*.cjs" --include="*.py" --include="*.go" \
--include="*.rb" --include="*.java" --include="*.kt" --include="*.kts" \
--include="*.swift" --include="*.gradle" --include="*.gradle.kts" \
--include="Podfile" --include="Gemfile" \
--exclude-dir=node_modules --exclude-dir=.next --exclude-dir=dist --exclude-dir=venv \
--exclude-dir=build --exclude-dir=.gradle \
. 2>/dev/null
# Find all Stytch env var references
grep -rn "STYTCH_\|stytch\." \
--include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" \
--include="*.py" --include="*.go" --include="*.rb" --include="*.java" --include="*.kt" \
--include="*.swift" --include="*.env*" --include="*.yml" --include="*.yaml" \
--include="Dockerfile" \
--exclude-dir=node_modules --exclude-dir=.next --exclude-dir=build \
. 2>/dev/null
# Find Stytch SDK surface + session / claim / org access patterns
# (things that may need a JWT Template or org→tenant remap)
# We match on ".sessions" etc to catch any variable name (e.g. stytch.sessions, stytchClient.sessions)
grep -rni "\.sessions\|\.b2b\|\.b2c_client\|\.otps\|\.magicLinks\|\.magic_links\|\.passwords\|\.oauth\|\.webauthn\|\.totps\|\.mfa\|\.m2m\|\.scim\|\.connected\|\.idp\|\.rbac\|\.discovery\|\.impersonation\|\.users\|\.organizations\|session_token\|session_jwt\|intermediate_session_token\|organization_id\|organization_slug\|member_id\|trusted_auth\|external_token\|custom_claims\|authenticateJwt\|authenticate(" \
--include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" \
--include="*.py" --include="*.go" --include="*.rb" --include="*.java" --include="*.kt" \
--include="*.swift" \
--exclude-dir=node_modules --exclude-dir=.next --exclude-dir=build \
. 2>/dev/null
# Find frontend / mobile session hooks and providers
grep -rn "StytchProvider\|StytchB2BProvider\|Products\|StytchB2B\|StytchLogin\|StytchHeadlessClient\|useStytch\|useStytchUser\|useStytchSession\|createStytchUIClient\|StytchConsumerSDK\|StytchClient\|StytchUI\|@stytch/nextjs\|@stytch/react\|@stytch/vanilla-js\|@stytch/react-native" \
--include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" \
--include="*.swift" --include="*.kt" \
--exclude-dir=node_modules --exclude-dir=.next --exclude-dir=build \
. 2>/dev/null
# Find B2B / enterprise / fraud / connected-app feature usage
grep -rn "scim\|saml\|sso\|adminPortal\|admin_portal\|discovery\|jit_provision\|connectedApp\|connected_app\|m2m\|client_credentials\|deviceFingerprint\|device_fingerprint\|protectedAuth\|protected_auth\|dfp\|webhook" \
--include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" \
--include="*.py" --include="*.go" --include="*.rb" --include="*.java" --include="*.kt" \
--include="*.swift" \
--exclude-dir=node_modules --exclude-dir=.next --exclude-dir=build \
. 2>/dev/null
# Check dependency manifests for Stytch packages
find . -maxdepth 4 \( \
-name "package.json" -o -name "go.mod" -o -name "requirements.txt" -o \
-name "Gemfile" -o -name "pom.xml" -o -name "build.gradle" -o -name "build.gradle.kts" -o \
-name "Podfile" -o -name "Podfile.lock" \
\) ! -path "*/node_modules/*" ! -path "*/build/*" \
-exec grep -l "stytch\|@stytch\|stytchauth\|com\.stytch" {} \;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 search-descope-docs or ask-question-about-descope
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, Stytch handles everything related to login: Stytch UI or frontend/mobile SDKs render the login experience, issue
session_token/session_jwt, and the backend SDK validates sessions on every request — routing B2B users to the right organization SSO connection when applicable. 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. Stytch Organizations become Descope Tenants.STYTCH_PROJECT_ID,STYTCH_SECRET, and the public token are replaced byDESCOPE_PROJECT_ID(andNEXT_PUBLIC_DESCOPE_PROJECT_IDfor the browser).Stytch features in use that need to carry over: [list in plain English, one clause each].
Tailor to triage findings.
For every Stytch touchpoint found in triage, produce a concrete, one-to-one mapping — Stytch 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 or mobile app
does: rendering the login/sign-up UI (a Descope Flow replaces Stytch UI, @stytch/react,
@stytch/nextjs, @stytch/vanilla-js, or mobile SDK login flows), initiating authentication,
holding the session on the client, refreshing the token, and reading the current user for UI
purposes. This replaces Stytch frontend/mobile providers (StytchProvider, useStytch,
useStytchUser, useStytchSession), headless client calls, 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 Stytch
server-side sessions.authenticate() / sessions.authenticateJwt() and route 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 Stytch backend SDK calls (stytch.sessions, stytch.b2b.*, stytch.m2m, etc.) and
every Stytch Management API call.For each file or area, name the Stytch 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 Stytch feature spans both sides — for example a Stytch UI or
mobile login flow (now a client Flow) plus per-request sessions.authenticateJwt() 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 Stytch session
authentication (sessions.authenticateJwt(), sessions.authenticate(), or frontend
useStytchSession()).
| File | What it does today | What changes |
|---|---|---|
lib/auth.ts:34 | Validates session_jwt via stytch.sessions.authenticateJwt(); returns user_id, organization_id, roles | Rewritten to return Descope authInfo; a thin adapter layer preserves the shape callers expect |
middleware.ts:12 | Reads stytch_session cookie and blocks unauthenticated requests app-wide | Updated to validate Descope DS/DSR cookies via Descope session validation; logic is identical, SDK call changes |
Login / auth UI (2 files) — These render Stytch UI or run headless Stytch client flows (magic links, OTP, OAuth, passkeys, B2B discovery). Descope replaces this with an embedded Flow component (or hosted Flow); token exchange and callback routes change shape.
| File | What it does today | What changes |
|---|---|---|
app/login/page.tsx | Renders <StytchLogin> or useStytch() headless flow | Replaced with <Descope flowId="..."> (or hosted Flow); onSuccess wires the Descope session client-side |
app/api/authenticate/route.ts | Exchanges token / session_token from Stytch callback | Deleted or rewritten — most flows complete client-side in Descope; verify any server-side exchange against the framework section |
Cover all functional groupings (B2B org/member management, SCIM webhooks, M2M token issuance, Connected Apps, mobile SDK auth, etc., when present). End with: "Total: N files. Estimated code-change effort: N–N hours."
For each Stytch 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 (Stytch Organizations → Descope Tenants) Stytch multi-tenant auth is built around Organizations and Members. A Stytch Organization represents a tenant/customer in the application, and a Member is a user's account within that Organization. Organization-scoped configuration can include SSO connections, SCIM, JIT provisioning, approved auth methods, MFA policies, RBAC behavior, custom metadata, and Connected Apps settings. Descope has the same core concept, called Tenants. In most migrations, map one Stytch
organization_idto one Descope tenant ID.Most code that handles Stytch Organizations is management/admin code that passes a Stytch
organization_idto B2B APIs — for example, loading an organization, updating organization settings, managing members, assigning roles, configuring SSO, or configuring SCIM. That becomes Descope tenant/user management code that passes a Descope tenant ID to the relevant tenant, user, SSO, SCIM, or RBAC operation. This is mostly by-ID management work, not token parsing.The main request-time difference is the session shape. In Stytch B2B, the authenticated session is tied to a specific Organization and returns fields such as
member_session.organization_id,member_session.organization_slug,member_session.roles, thememberobject, and theorganizationobject. In Descope, tenant membership and tenant-scoped roles/permissions are read from the validated session/JWT and should ideally be checked with SDK helpers such asvalidateTenantRoles(...)orvalidateTenantPermissions(...)rather than by manually parsing claims.Confirm the Organization→Tenant mapping first, since it ripples into SSO, SCIM, JIT provisioning, RBAC, Admin Portal replacement, Connected Apps, and any application database tables that store
organization_id. Also confirm whether Stytch Members can belong to multiple Organizations and whether the app supports organization switching, because that determines whether the Descope migration needs tenant selection, active-tenant handling, or separate tenant-scoped login routes.
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 |
|---|---|---|
STYTCH_PROJECT_ID | DESCOPE_PROJECT_ID | Your unique Stytch project ID. Descope uses a Project ID for the same purpose. |
STYTCH_SECRET | DESCOPE_MANAGEMENT_KEY | Backend secret used to securely authenticate Stytch API requests. Descope session validation uses only the Project ID; a Management Key is needed only for server-side user/tenant/SSO/SCIM administration. |
NEXT_PUBLIC_STYTCH_PUBLIC_TOKEN | NEXT_PUBLIC_DESCOPE_PROJECT_ID | Stytch's frontend-facing token for initializing client SDKs. Descope uses the same value as DESCOPE_PROJECT_ID, exposed to the browser for the login Flow component (Next.js and other frontend frameworks). |
Connected Apps client_id (e.g. STYTCH_CONNECTED_APP_CLIENT_ID or similar) | DESCOPE_INBOUND_APP_CLIENT_ID | Only if the app uses Stytch Connected Apps as an OAuth/OIDC client — replace with the Descope Inbound App client ID (Console → Inbound Apps). First-party Connected Apps clients map to Federated Apps instead and do not use this variable. For confidential Inbound Apps, also add DESCOPE_INBOUND_APP_CLIENT_SECRET. |
Follow with: "Net change: 2-3 variables removed, 1–4 added (Connected Apps env vars only if applicable). No secrets need to be rotated on the Stytch 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:
Consideration: Organization-to-tenant mapping affects almost every B2B feature Stytch 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.
Consideration: SCIM is a lifecycle system, not just a user import Stytch's SCIM 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 workflow and re-point it at Descope before cutover.
Consideration: Admin Portal UI should not automatically become custom code If the app uses the Stytch Admin Portal UI, 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.
Consideration: 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 trade-offs and considerations.
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.
localhost:3000, not http://localhost:3000/authenticate)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 trade-offs and considerations 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 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]
- Stytch 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)
- [ ] Approved Domains configured (domain only — e.g. `localhost:3000`, not `http://localhost:3000/authenticate`)
- [ ] JWT template configured
- [ ] Tenants created for each Stytch 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 · 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 Docs MCP is available, use ask-question-about-descope 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 '@stytch/\|from 'stytch'\|from \"stytch\"" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" .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. Stytch'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 Stytch'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 versionIf 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–8 — 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.Console → Project Settings → Security → Approved Domains.
Descope validates redirect URLs against this domain list — not full redirect URIs like Stytch.
Enter domain only: no http:///https://, no path.
localhost:3000 (include port)myapp.com or app.myapp.comDo not carry over Stytch callback URLs like http://localhost:3000/authenticate. Descope
embedded Flows complete auth client-side; there is no /authenticate route to whitelist. See
references/implementation-nuances.md → Approved Domains gotcha.
Stytch 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)Stytch metadata maps to Descope customAttributes, but the models are slightly different. Stytch stores arbitrary JSON in metadata fields, while Descope custom attributes should be pre-defined in the Console schema before setting them via the SDK.
Tenant custom attributes: map Stytch Organization trusted_metadata to Descope tenant customAttributes. Configure these in Console → Tenants → Custom Attributes tab → Create Attribute. User custom attributes: map Stytch Consumer User trusted_metadata and safe B2B Member trusted_metadata to Descope user custom attributes. Configure these in Console → Project → Custom Attributes.
| 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.
Stytch publishes three SDK families:
stytch-node (stytch), stytch-python (stytch), stytch-go, stytch-ruby, stytch-java (Java/Kotlin/JVM). These call the Stytch API and authenticate sessions server-side.@stytch/react, @stytch/nextjs, @stytch/vanilla-js. These render the login UI (Stytch UI or headless) and hold the client session.@stytch/react-native, the iOS Swift SDK, and the Android Consumer SDK (a headless Kotlin Multiplatform library targeting Android).The recipes below map each Stytch SDK to its Descope target — one section each. A Stytch app on a server framework not listed (Express, Flask, FastAPI, Rails, Spring) is using the underlying language Backend SDK (stytch-node, stytch-python, etc.), so map it via that SDK's section.
The framework recipes below are stubs listing the Stytch idioms that need mapping. Confirm the exact Stytch 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.
Frontend Stytch SDKs expose UI components and client session hooks; backend SDKs authenticate the session server-side. The mappings below apply across stacks:
<StytchLogin> / <StytchB2B>) or headless useStytch() login → embedded Descope Flow (<Descope flowId> / <descope-wc>) or hosted Flow, wiring onSuccessuseStytchSession() / useStytchUser() (client session access) → Descope useSession() / useUser() hooks, with useDescope() for actionsclient.sessions.authenticate() / client.sessions.authenticateJwt() → Descope backend validateSession() + an adapter returning the shape callers expectstytch_session, stytch_session_jwt) → Descope signed session JWT in DS / DSR cookiesclient.magicLinks.authenticate() / client.oauth.authenticate()) → removed/rewritten; Descope completes auth client-sideStytch SDK: stytch (stytch-node) → Descope @descope/node-sdk
stytch auth/session usage; add @descope/node-sdkclient.sessions.authenticateJwt() / client.sessions.authenticate() with custom middleware calling descopeClient.validateSession(sessionToken) against the DS cookie (parse the cookie yourself)Stytch SDK: stytch (stytch-python) → Descope descope Python SDK
descope Python SDKDS session token with descope_client.validate_session(session_token=session_token) (or validate against Descope's JWKS for a custom authorizer)Stytch SDK: stytch-go → Descope Go SDK github.com/descope/go-sdk
descope/go-sdkdescopeClient.Auth.ValidateSessionWithToken(ctx, token) returns (bool, *descope.Token, error)organization_id → 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)Stytch SDK: stytch-ruby → Descope Ruby SDK
DS session token with descope_client.validate_session(session_token: session_token) in your request lifecycleimplementation-nuances.md yet — follow the Node.js / Python backend patterns and verify against the Descope Ruby SDK.Stytch SDK: stytch-java (Java/Kotlin/JVM) → Descope descope-java
descope-javaDS token via a filter/interceptor: authenticationService.validateSessionWithToken(sessionToken) returns a TokenRead 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, 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().
Stytch SDK: @stytch/vanilla-js → Descope @descope/web-js-sdk + @descope/web-component
createStytchUIClient / StytchHeadlessClient) session access → @descope/web-js-sdk (getSessionToken(), isJwtExpired(), refresh())<descope-wc project-id flow-id> web component, listening for success / error eventssdk.logout() + clear stored tokens/cookiesStytch SDK: @stytch/react → Descope @descope/react-sdk
<StytchProvider> → Descope <AuthProvider projectId><StytchLogin>) / headless useStytch() login → embedded <Descope flowId> component, wiring onSuccessuseStytchSession() / useStytchUser() → Descope useSession() + useUser() hooks, with useDescope() for actionsvalidateSession() from client code; that runs only on the server.sdk.logout() via useDescope() hookStytch SDK: @stytch/nextjs → Descope @descope/nextjs-sdk + @descope/node-sdk
@stytch/nextjs → @descope/nextjs-sdk + @descope/node-sdk<StytchProvider> → Descope AuthProvider (takes projectId; must use NEXT_PUBLIC_ prefix)client.sessions.authenticateJwt() → session() (server); client useStytchSession() / useStytchUser() → useSession() / useUser()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.Stytch SDK: @stytch/react-native → Descope @descope/react-native-sdk
Stytch iOS Swift SDK → Descope descope-swift
Stytch Android Consumer SDK (headless Kotlin Multiplatform) → Descope descope-kotlin
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 Stytch references in non-code files after updating source files.
.env.example / .env.template / .env.sample# REMOVE
STYTCH_PROJECT_ID=
STYTCH_SECRET=
STYTCH_PUBLIC_TOKEN=
NEXT_PUBLIC_STYTCH_PUBLIC_TOKEN=
# ADD
DESCOPE_PROJECT_ID= # Console → Project Settings
NEXT_PUBLIC_DESCOPE_PROJECT_ID= # Next.js / frontend — same value as above
DESCOPE_MANAGEMENT_KEY= # Console → Company → Management Keys (replaces STYTCH_SECRET for admin APIs)Run grep -ir "STYTCH" to find all env var references — .env.example, Docker, CI, shell scripts.
Search all .md files for Stytch references. At minimum, update:
Check Dockerfile, docker-compose.yml, .github/workflows/, and any CI config for
STYTCH_* 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 Stytch 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, Inbound App, Console configuration, or tenant 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.
Stytch Consumer Auth handles B2C sign-in — hosted/prebuilt UI, frontend SDK flows, backend API flows, users, sessions, and methods (OAuth/social, magic links, OTP, passwords, passkeys/WebAuthn, mobile biometrics, MFA/TOTP, crypto wallet). Descope maps these to Flows, authentication methods, Users, session validation, and JWT Templates / custom claims.
| Stytch | Descope |
|---|---|
| Stytch UI / prebuilt login UI | Descope Flows |
| Frontend SDK auth flows | Descope frontend SDK + Flow component |
| Backend API-driven auth | Descope backend SDK / API auth methods when Flows are not sufficient |
| OAuth/social login | Descope OAuth/social login methods |
| Email magic links | Descope Magic Link / Enchanted Link |
| Email/SMS/WhatsApp OTP | Descope OTP methods |
| Passwords | Descope Passwords |
| Passkeys / WebAuthn | Descope Passkeys |
| TOTP / MFA | Descope MFA / TOTP / Flow conditions |
| Stytch User object | Descope User |
| Stytch session token / session JWT | Descope session token / JWT + backend session validation |
| Stytch custom claims / session metadata | Descope JWT Templates or Custom Claims action |
Prefer Flows for the user journey; use custom SDK/API calls only when Flows cannot express the requirement. Confirm which Stytch methods are enabled, whether Stytch or custom UI is used, and whether backend routes call Stytch APIs directly. Effort: Low–Medium for straightforward B2C auth; higher with custom session claims, MFA branching, or nonstandard factors.
Stytch B2B authentication is built around Organizations and Members. Descope maps this model most closely to Tenants and Users associated with tenants. A Stytch Organization usually becomes a Descope Tenant, while a Stytch Member usually becomes a Descope User with tenant membership, roles, permissions, and tenant-specific attributes.
| Stytch | Descope |
|---|---|
| Organization | Tenant |
| Member | User associated with a tenant |
| Organization ID | Tenant ID |
| Organization metadata | Tenant customAttributes |
| Member metadata | User custom attributes or tenant-specific user metadata |
| Organization-specific auth settings | Tenant settings + Flow logic + SSO configuration |
| Member invitations | Invitation Flow / management SDK flow |
| Organization discovery | Tenant discovery / tenant selection / domain-based routing |
| Org-specific login | Tenant-specific login route, tenant slug, or tenant Flow input |
| Organization session exchange / org switching | Active tenant selection and tenant-aware session claims |
| Members belonging to multiple Organizations | Users belonging to multiple tenants |
Confirm the one-Stytch-Organization-to-one-Descope-Tenant mapping before writing code. This mapping
ripples into SSO, SCIM, RBAC, JIT provisioning, sessions, custom claims, and domain routing. Also
check whether the application treats organization_id as an authorization boundary, a billing
boundary, a data partition key, or all three. Effort: Medium — conceptually clean, but application
code often assumes Stytch's Organization/Member object shapes.
Stytch Organizations and Members are not just data objects; they may drive onboarding, invitations, membership updates, deactivation, organization switching, metadata, and tenant-specific access controls. In Descope, model these workflows using Tenants, Users, tenant membership, roles, permissions, and optionally Flows or management SDK calls for lifecycle operations.
| Stytch | Descope |
|---|---|
| Create/update Organization | Create/update Tenant |
| Create/update Member | Create/update User and tenant association |
| Organization metadata | Tenant custom attributes |
| Member metadata | User custom attributes / tenant-specific user attributes |
| Member invite | Invite/onboarding Flow or management SDK |
| Member deactivate/delete | User deactivation, tenant removal, or tenant-role removal |
| Organization allowed auth methods | Tenant settings + Flow conditions |
| Organization-specific MFA policy | Tenant-aware MFA logic in Flows |
Ask whether Organization and Member data is synchronized into the app database, whether the app reads Stytch as the source of truth, and whether lifecycle changes trigger webhooks. Effort: Medium — especially if membership state is mirrored in the application database.
Stytch Enterprise SSO maps to Descope tenant-level SSO. In Stytch, SSO connections are associated with Organizations. In Descope, SSO is configured per Tenant, with support for SAML/OIDC providers, domain-based routing, SSO Setup Suite, and multiple SSO providers per tenant when needed.
Preferred approach — SSO Setup Suite: before migrating any Stytch SSO management code, 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, Microsoft Entra ID, Google Workspace, etc.) and can reduce engineering involvement for new enterprise customer onboarding.
Multiple SSO configurations per tenant. If a single Stytch customer has multiple SSO connections, or if the old Stytch model used multiple Organizations to represent one customer with multiple IdPs, do not blindly create multiple Descope Tenants. First decide whether the customer should become one Descope Tenant with multiple SSO configurations.
| Stytch | Descope |
|---|---|
| Organization SSO connection | Tenant SSO configuration |
| SAML SSO | Descope SAML SSO |
| OIDC SSO | Descope OIDC SSO |
| Organization-specific SSO routing | Tenant routing / SSO domain routing |
| Multi-Organization SSO behavior | Tenant design + active tenant/session model review |
| Customer-admin SSO setup | SSO Setup Suite |
| SSO claim/group role assignment | SSO attribute mapping / group-to-role mapping |
| Programmatic SSO connection management | Descope Management API / SDK, if self-service is not used |
Use AskUserQuestion to ask two things here:
For runtime login, prefer Descope's SSO-specific login path rather than generic social OAuth logic. The exact SDK method names differ by language/framework, so verify against the Descope MCP server before writing implementation code. Rule of thumb: tenant/enterprise SSO should use Descope's tenant-level SSO configuration; social login should use OAuth/social auth methods. Effort: Medium
Stytch SCIM maps to Descope SCIM provisioning. Treat this as a continuing provisioning pipeline, not a one-time import — enterprise directories keep pushing create, update, group, and deprovisioning events after cutover.
| Stytch SCIM | Descope |
|---|---|
| SCIM endpoint per Organization | Descope SCIM endpoint / token per tenant |
| User create/update/deactivate | Tenant user provisioning lifecycle |
| Groups | External groups / group-to-role mapping |
| SCIM group-to-role assignment | SCIM or SSO group mapping to Descope roles |
| Deprovisioning | User deactivation / tenant access removal behavior |
| SCIM tokens | Tenant-scoped SCIM-compatible access keys |
| SCIM webhooks / downstream sync handlers | Descope events, audit logs, webhooks, or app sync code |
Identify every connected directory, which IdPs are used, whether groups are synced, whether groups map to roles, and what happens when a user is removed from a group. Pay special attention to whether Stytch deprovisioning revoked sessions immediately, removed membership, changed roles, or only updated status. Effort: Medium–High — lifecycle, groups, deprovisioning, and role mapping can be subtle.
Stytch Admin Portal provides customer-admin workflows for managing enterprise configuration such as SSO, SCIM, organization settings, members, and related admin tasks. Do not default to rebuilding these screens as custom code.
| Stytch Admin Portal area | Descope replacement |
|---|---|
AdminPortalMemberManagement | User Management Widget |
| Member search/update/invite | User Management Widget or Management SDK/API |
| Member role assignment | User Management Widget; Role Management Widget if tenant admins manage roles |
AdminPortalOrgSettings | Tenant Profile Widget for tenant name, custom attributes, domains, and SSO enforcement |
| Organization auth method/JIT settings | Flow logic, tenant settings/custom attributes, or custom Management SDK/API UI |
AdminPortalSSO | SSO Setup Suite |
AdminPortalSCIM | SSO Setup Suite SCIM configuration |
| Custom member management UI | Prefer User Management Widget; otherwise Management SDK/API |
| Custom organization management UI | Prefer Tenant Profile Widget; otherwise Management SDK/API |
Ask which Stytch Admin Portal workflows are actually used today. If a Descope Widget or SSO Setup Suite covers the workflow, prefer that over custom migration code. Effort: Medium — may remove custom code, but generated portal-link workflows need replacement.
Stytch RBAC combines Resources, Actions, Permissions, and Roles — a Permission is a resource_id +
action pair (e.g. documents:read, employees:update), grouped into Roles assigned to Members. Stytch evaluates via
frontend SDK resource/action checks or backend session/JWT calls with organization_id,
resource_id, and action. Descope has Roles and Permissions too, but permissions are strings, not
first-class Resource + Action objects — encode each Stytch pair as a consistent permission string
(resource.action or resource:action).
Descope supports project- and tenant-level roles and permissions. Stytch defines its RBAC Policy once at the project level (shared role/resource catalog); roles are assigned per Organization with no per-org policy divergence. Stytch's only org-scoped feature is implicit assignment (auto-grant a project role by email domain) — tenant-specific assignment, not definition. Default migration: map Stytch role definitions to Descope project-level roles, then assign users in the relevant tenant.
| Stytch | Descope |
|---|---|
| Resource | Encoded in permission string |
| Action | Encoded in permission string |
| Permission = Resource + Action | Permission |
| Role | Role |
| Project-level RBAC policy | Project-level role/permission catalog |
| Role definition in RBAC policy | Usually project-level role |
| Member role assignment inside an Organization | User role assignment in a tenant |
| Same user has different roles in different Organizations | Same user has different roles in different tenants |
| Tenant-specific/custom role catalog | Use Descope tenant-level roles only if this behavior actually exists in the app |
Confirm whether roles gate UI only or backend auth too; whether roles/permissions appear in tokens; whether the app stores assignments locally; and whether SSO/SCIM mappings are source of truth. Effort: Medium for normal RBAC; higher if mixed with Connected Apps scopes or app-defined resource authorization.
If the Stytch app has authorization beyond RBAC — relationship-based or per-resource checks such as project membership, document ownership, workspace hierarchy, or shared/delegated access — do not assume a plain RBAC migration covers it. This maps to Descope ReBAC/FGA (only when the model truly depends on relationships between entities) or stays in the application database.
See references/implementation-nuances.md → Authorization beyond RBAC → Descope ReBAC for the
decision guide, an example schema, the recommended-approach table, and effort estimate.
Stytch JIT auto-adds Members to Organizations from auth context — main paths: email-domain JIT, SSO Connection JIT, and OAuth-tenant JIT. Trusted Auth Tokens have a separate JIT option that can create Members or Organizations from external JWTs. Invitations are a distinct onboarding path, not JIT.
Descope supports tenant association, self-provisioning domains, domain-based SSO routing, SSO-driven JIT, SCIM, and Flow-based tenant/user logic. Preserve whichever Stytch provisioning model is configured; do not assume the customer picks only one.
Important: for each tenant, identify the source of truth for membership and role assignment — JIT, SCIM, invitations, manual admin membership, Trusted Auth Tokens, or app-side onboarding. SCIM and JIT can coexist, but mixed sources without clear precedence cause duplicate accounts, unexpected tenant access, missed deprovisioning, or role confusion. Confirm which paths are enabled per Organization in Stytch.
| Stytch | Descope |
|---|---|
| Email-domain JIT provisioning | Tenant self-provisioning domains / Flow logic |
email_allowed_domains | Tenant domains / self-provisioning domains |
| SSO Connection JIT provisioning | SSO JIT provisioning / tenant association |
sso_jit_provisioning | Tenant SSO provisioning behavior |
| OAuth-tenant JIT provisioning | Custom Flow / Connector / app-side tenant logic |
| Allowed GitHub, Slack, or HubSpot tenants | Custom tenant association logic if still required |
| Member created on first login | User associated with tenant during login |
| JIT role assignment from SSO claims | SSO group/attribute mapping to roles |
| Trusted Auth Token JIT | JWT Bearer |
| Email invitations | Separate invite/admin onboarding flow, not JIT |
| JIT plus SCIM | Preserve both if both are configured |
Effort: Medium — low for email-domain JIT only; higher with SSO/SCIM role assignment, Trusted Auth Tokens, OAuth-tenant membership, or custom onboarding.
Stytch MFA and step-up authentication can involve OTPs, TOTP, passkeys/WebAuthn, passwords, OAuth, magic links, and organization-specific MFA requirements. Descope maps this to MFA methods and conditional Flow logic.
Use Flows for MFA whenever possible because MFA is usually part of the user journey, not just a backend API call. Flow conditions can branch based on user state, tenant context, risk signals, completed auth methods, or sensitive actions.
| Stytch | Descope |
|---|---|
| SMS/email OTP MFA | Descope OTP MFA |
| TOTP MFA | Descope Authenticator Apps / TOTP |
| Passkey/WebAuthn as MFA or step-up | Descope Passkeys / WebAuthn |
| Organization-specific MFA policy | Tenant-aware Flow condition |
| Step-up for sensitive actions | Step-up Flow or backend-triggered reauth pattern |
| Risk-based MFA | Flow condition using risk signals / fingerprinting |
| Recovery codes / fallback behavior | Confirm support and design fallback explicitly |
Ask whether MFA is required globally, per organization, per role, per risk level, or only for sensitive actions. Effort: Low–Medium unless MFA is deeply customized or risk-based.
Stytch sessions may use session_token, session_jwt, intermediate sessions, cookies, custom claims,
organization context, and session revocation. Descope sessions should be validated with the appropriate
backend SDK/session validation path, and claims should be shaped with JWT Templates or Flow Custom
Claims where appropriate.
| Stytch | Descope |
|---|---|
session_token | Descope session token |
session_jwt | Descope JWT |
| Intermediate sessions | Flow-driven intermediate state / MFA / step-up handling |
| Organization context in session | Tenant claims / active tenant context |
| Custom claims | JWT Templates or Custom Claims action |
| Session revocation | Descope session/user logout or revocation pattern |
| Cookie-based sessions | Descope SDK cookie/session configuration |
| Backend session authentication | Descope backend session validation |
Search the codebase for direct reads of Stytch session fields, token claims, organization/session exchange calls, and middleware that assumes Stytch-specific token shapes. Effort: Medium — token differences often affect middleware, API routes, and frontend hydration.
See references/implementation-nuances.md → Attack protection: Stytch Fraud & Risk → Descope Flow-based security for connector mappings (Arkose, reCAPTCHA, Fingerprint, Have I Been Pwned, AbuseIPDB), Flow branching guidance, and fraud/KYC connector docs. Ask whether Stytch verdicts are monitoring-only or actually gate login. Effort: Medium–High only when verdicts affect production login outcomes.
Stytch Connected Apps enables a Stytch-powered application to act as an OAuth/OIDC Authorization Server for first-party apps, third-party integrations, desktop apps, CLI tools, AI agents, MCP clients, and other clients that need scoped access to user data.
Do not map every Stytch Connected Apps client to Descope Inbound Apps. Stytch distinguishes first-party from third-party clients; Descope splits the equivalent workloads across two identity-federation features:
| Stytch Connected Apps client | Descope equivalent | Purpose |
|---|---|---|
| First-party client | Federated Apps | SSO across apps you own — Descope acts as the IdP so users authenticate once and access multiple connected applications without signing in again to each |
| Third-party client | Inbound Apps | OAuth/OIDC authorization server — external clients obtain scoped tokens to access your Resources, with consent and permission management |
Public vs. confidential applies only to Inbound Apps (Stytch third-party clients). Confidential clients are server-side apps that can securely store a client secret; public clients (SPAs, mobile apps, CLI tools) cannot store secrets and must use PKCE.
| Stytch Connected Apps (first-party) | Descope Federated Apps |
|---|---|
| First-party OAuth/OIDC client | Federated App (SAML or OIDC SSO connection) |
| Known first-party app | Federated App registration and callback URL |
| SSO across owned applications | Descope as IdP; users sign in once across connected apps |
| Session / ID token for owned apps | Federated App OIDC token or SSO session |
| Stytch Connected Apps (third-party) | Descope Inbound Apps |
|---|---|
| OAuth/OIDC Authorization Server | Inbound Apps authorization server (/oauth2/v1/apps/*) |
| Public client + PKCE | Public Inbound App / PKCE-capable flow |
| Confidential client | Confidential Inbound App with client secret |
| Authorization Code flow | Inbound App Authorization Code flow |
| Refresh tokens | Inbound App refresh token support (confidential clients use client secret; public clients do not) |
| ID tokens | OIDC ID tokens |
| Access tokens | Descope-issued scoped access tokens |
| Consent screen | Inbound App consent / consent management |
| Custom scopes | Resources and scopes |
| RBAC-backed scopes | Role/scope/resource mapping review |
| Token revocation | Inbound App token revocation |
| Dynamic Client Registration | DCR / Agentic Identity Hub client registration, if needed |
This is a high-complexity migration if real external clients depend on the current Stytch issuer, JWKS, token claims, scopes, refresh token lifetimes, consent records, or callback URLs. Inventory every client, redirect URI, grant type, scope, token audience, and resource server before writing code. Effort: High when third-party clients or AI agents are already in production.
Stytch can use Connected Apps for AI agents, MCP clients, CLI tools, and agentic integrations that need delegated OAuth/OIDC access. Descope has Agentic Identity Hub, MCP server configuration, Inbound Apps, resources/scopes, client registration, and token issuance patterns for these use cases.
Do not treat AI/MCP auth as a generic OAuth migration without review. Agentic flows often require clear resource scopes, dynamic client registration, token lifetimes, consent design, and organization-level controls.
| Stytch AI / MCP pattern | Descope |
|---|---|
| Connected App for AI agent | Agentic Identity Hub client |
| MCP client authorization | MCP Server authorization / Inbound App |
| Dynamic Client Registration | DCR / CIMD / known client registration |
| Agent scopes | Resource scopes / policies |
| Agent consent | Inbound App consent |
| CLI or desktop app client | Public client + PKCE |
| Organization-level agent control | Tenant-aware policy / scope / consent design |
Ask whether Stytch is acting as the OAuth provider for agents, whether the app exposes MCP tools, and whether external agents already store refresh tokens. Effort: Medium–High — flag for dedicated review.
Stytch M2M authentication uses M2M clients, client credentials, access tokens, scopes, custom claims, and secret rotation for service-to-service authentication.
Default mapping: For most Stytch M2M use cases, use Resources + Inbound Apps + Policies — not Access Keys. Stytch defines scopes on the application itself; in Descope each protected API is a Resource (its identifier becomes the token aud) with an OAuth scope catalog, an Inbound App is the confidential OAuth client, and a Policy grants that client specific scopes on that Resource via the client_credentials grant (no consent screen).
client_credentials) to the Resource scopes it needs.| Stytch M2M | Descope (default) |
|---|---|
| M2M client | Confidential Inbound App |
| Client ID / client secret | Inbound App client ID + secret |
| Client credentials flow | Inbound App client_credentials grant via Policy |
| M2M scopes | Resource scopes granted by Policy |
| Token audience | Resource identifier (aud) |
| Custom claims | JWT Template on Inbound App |
| Secret rotation | Inbound App client secret rotation |
Use Access Keys only when the service needs a Descope-issued JWT without OAuth scope or audience enforcement — a simpler internal service-auth pattern, not a scoped API access model.
Ask which services use M2M credentials, which APIs they call, what scopes and audiences they enforce, and whether downstream APIs validate scope and aud. Effort: Medium — often straightforward with Resources + Policies, but production services require careful secret rotation and rollout.
Stytch webhooks and event logs may be used to synchronize users, organizations, members, sessions, SCIM lifecycle events, fraud decisions, or Connected Apps consent/token events into the application. Descope can use audit events, webhook connectors, generic HTTP connectors, and audit/troubleshooting connectors depending on the use case.
| Stytch | Descope |
|---|---|
| Webhook endpoint + signing secret | Descope webhook/HTTP connector + signature validation |
| User events | Descope user/audit events |
| Organization/Member events | Tenant/user events or app-side lifecycle sync |
| SCIM lifecycle events | Descope SCIM provisioning events / audit events |
| Fraud/Risk events | Flow branch + audit/webhook/logging connector |
| Connected App consent/token events | Inbound App consent/token event review |
| Event log streaming | Audit & Troubleshooting connectors |
| Compliance logs | Audit Webhook Connector / log destination connector |
Search the codebase for Stytch webhook handlers and event-name switches. Update event names, signature validation, payload parsing, retry behavior, and downstream side effects. Identify which events are business-critical before cutover. Effort: Medium.
After mapping confirmed Stytch features, summarize findings and flag high-complexity items before proceeding to Step 0.5. The main high-complexity Stytch areas are:
Descope session JWTs contain sub, amr, drn, tenants, roles, permissions, and dct by
default. They do not contain email, name, or picture. Stytch returns profile fields on the
member/user object (and may carry them as custom claims in the session_jwt), so code that reads
those fields off the token or session response 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 Stytch's
organization_id — 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.
Stytch issues two session representations — an opaque session_token (validated by a network call
to Stytch) and a session_jwt (a short-lived JWT validated locally), typically stored in the
stytch_session and stytch_session_jwt cookies. Descope collapses this into one signed session
JWT in the DS cookie (refresh in DSR). Code that stores, reads, or validates either Stytch
session cookie must be replaced with Descope session validation (validateSession()), which returns
decoded JWT claims. There is no opaque-token-vs-JWT distinction to maintain in Descope.
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 Stytch organization_id (and an SSO connection_id) is
management/admin code — it becomes a Descope tenant ID passed to management.tenant.* /
management.user.* calls. Only request-time code that read the organization off the Stytch 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 organization_id
reads and sort them into these two buckets — by-ID management calls vs. session reads — before
updating.
Descope ships no drop-in auth-middleware package. Whatever validates Stytch sessions today — e.g. a
Next.js middleware.ts calling sessions.authenticateJwt(), or Stytch's session helpers — becomes
~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.If SCIM is in use, re-point the SCIM pipeline at Descope before cutover.
Stytch apps register full callback URLs (e.g. http://localhost:3000/authenticate). Descope uses
Approved Domains (Console → Project Settings → Security) — domain only, no protocol, no path.
For local dev: localhost:3000, not http://localhost:3000/authenticate. Descope embedded
Flows complete auth client-side; there is no /authenticate route to whitelist.
See references/implementation-nuances.md → Cookie names: DS and DSR → Split-origin (separate SPA + API) gotcha for the full explanation, Go code example, and fix (manage DS/DSR yourself with dev-friendly cookie attributes).
Management.User().Load* (not Auth.MyTenants) to list a user's tenantsAuth.MyTenants requires exactly one of a dct flag or an explicit ids list and errors with
E011004 ("should get only 1 of dct / ids") if you pass neither — it cannot enumerate "all of the
user's tenants." To list every tenant a user belongs to (with names + roles), validate the session
to get the user ID, then call Management.User().LoadByUserID(userId) and read UserTenants.
Magic-link tokens are single-use and short-lived. E062504 ("Token expired … or already used")
almost always means: a stale link from an earlier email, a corporate email scanner that
pre-clicked the link, or a page reload of /authenticate re-submitting a consumed token.
Always test with a fresh link, clicked once, to an inbox you control.
Calling w.WriteHeader(...) and then a JSON responder that also writes a status produces
http: superfluous response.WriteHeader call. Let one place own the status. For SDK methods that
write a redirect to the ResponseWriter (e.g. OAuth().SignUpOrIn), on error just log — don't then
emit a second body.
Run the app and verify it works — don't just hand over a checklist.
grep -rni "@stytch\|stytch\|com\.stytch" \
--include="*.ts" --include="*.tsx" --include="*.js" --include="*.py" --include="*.go" \
--include="*.rb" --include="*.java" --include="*.kt" --include="*.swift" \
--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 tidynpx tsc --noEmit # TypeScript
go build ./... # Go
mvn compile -q # Java/Maven
./gradlew compileJava compileKotlin # Java/Gradle
dotnet build # .NETDo not proceed until compilation exits with zero errors.
If compilation fails, diagnose by error message:
Cannot find module '@stytch/...' (or stytch) → stale import; re-run Phase 0Property 'X' does not exist on type '...' → wrapper built against the Stytch session/member response shape; re-derive from the Descope authInfo shape'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 Stytch shapes, or a
test validates JWT claims that are now missing (e.g., email without a JWT Template), or a test
still uses organization_id 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/meecho "<DS_cookie_value>" | cut -d'.' -f2 | base64 -d 2>/dev/null | python3 -m json.toolCheck 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, 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.d888111
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.