Skip to content

Add a Better Auth-backed migration layer for Stack Auth#1403

Draft
mantrakp04 wants to merge 2 commits intodevfrom
feat/migrations
Draft

Add a Better Auth-backed migration layer for Stack Auth#1403
mantrakp04 wants to merge 2 commits intodevfrom
feat/migrations

Conversation

@mantrakp04
Copy link
Copy Markdown
Collaborator

@mantrakp04 mantrakp04 commented May 1, 2026

Summary

  • Add a new public @stackframe/migrations package for provider migrations into Stack Auth
  • Treat Better Auth as the normalization layer and capture its model writes through Stack Auth-backed persistence
  • Map captured user, account, organization, and member records into Stack Auth users, OAuth accounts, teams, and memberships
  • Document the migration flow and add tests for the capture and import plan

Testing

  • pnpm -C packages/migrations typecheck
  • pnpm -C packages/migrations test
  • pnpm -C packages/migrations lint
  • pnpm -C docs-mintlify lint

Summary by CodeRabbit

  • New Features

    • Added user migration capabilities for importing users from external auth providers (specifically Better Auth) into Stack Auth, including support for credentials, OAuth accounts, organizations, and team memberships.
  • Documentation

    • Added comprehensive migration guide with setup and entity mapping details.
    • Updated FAQ to include dedicated migration instructions.

Copilot AI review requested due to automatic review settings May 1, 2026 22:58
@mintlify
Copy link
Copy Markdown

mintlify Bot commented May 1, 2026

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
stackauth-docs 🔴 Failed May 1, 2026, 10:58 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stack-auth-hosted-components Error Error May 4, 2026 4:45pm
stack-backend Error Error May 4, 2026 4:45pm
stack-dashboard Error Error May 4, 2026 4:45pm
stack-demo Error Error May 4, 2026 4:45pm
stack-docs Error Error May 4, 2026 4:45pm
stack-preview-backend Error Error May 4, 2026 4:45pm
stack-preview-dashboard Error Error May 4, 2026 4:45pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 1, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 601f7dda-422f-4e0b-ac8c-e9f5f13001d2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

A new @stackframe/migrations package is introduced with Better Auth migration support, enabling users to normalize and import auth data from Better Auth into Stack Auth. The package includes a persistence adapter, Stack API integration, type definitions, and CLI tooling, alongside documentation updates explaining the migration workflow.

Changes

Cohort / File(s) Summary
Documentation & Knowledge Base
claude/CLAUDE-KNOWLEDGE.md, docs-mintlify/docs.json, docs-mintlify/guides/..., docs/content/docs/(guides)/faq.mdx
Updated FAQ guidance to reference the new @stackframe/migrations package for provider migrations and added a dedicated "Migrations" guide explaining Better Auth-to-Stack Auth data transfer and entity mapping.
Package Configuration
packages/migrations/package.json, packages/migrations/tsconfig.json, packages/migrations/tsdown.config.ts, packages/migrations/eslint.config.mjs
Introduced workspace package manifest with CLI binary and dual ESM/CJS exports, TypeScript configuration with strict checking, ESLint flat config targeting TypeScript files, and tsdown build settings.
Better Auth Migration Adapter
packages/migrations/src/adapters/better-auth.ts, packages/migrations/src/adapters/better-auth.test.ts
Implemented in-memory Better Auth persistence adapter supporting create/read/update/delete operations, snapshot generation converting Better Auth records to Stack-compatible format, plan building, and Stack Auth flushing with password-hash validation and provider ID remapping.
Stack API & Core Migration Logic
packages/migrations/src/stack-api.ts, packages/migrations/src/core.ts, packages/migrations/src/types.ts, packages/migrations/src/index.ts
Added Stack API import client with authenticated requests, migration plan builder transforming snapshots into Stack creation payloads, comprehensive type definitions for external auth data structures, and barrel exports consolidating public API.
CLI & Documentation
packages/migrations/src/cli.ts, packages/migrations/README.md
Created CLI help output and root package README documenting the migration workflow, Better Auth usage patterns, and entity mapping.
Example Scripts
packages/migrations/scripts/test-clerk-to-stack.ts
Added test script demonstrating user seeding via Clerk API, persistence adapter integration, plan building, and Stack Auth import with environment-based configuration.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client Code
    participant BAAdapter as Better Auth<br/>Persistence Adapter
    participant CoreMigration as Migration<br/>Core
    participant StackAPI as Stack Auth<br/>API
    participant StackDB as Stack<br/>Database

    Client->>BAAdapter: Write users/accounts/orgs<br/>(via Better Auth logic)
    BAAdapter->>BAAdapter: Store in memory<br/>(in-memory records)
    
    Client->>CoreMigration: buildStackMigrationPlan()<br/>(from snapshot)
    BAAdapter->>CoreMigration: snapshot() converts<br/>Better Auth records
    CoreMigration->>CoreMigration: Build users/teams/memberships<br/>with Stack payloads
    CoreMigration-->>Client: Return StackMigrationPlan
    
    Client->>StackAPI: importPlanToStackAuth()<br/>(config + plan)
    StackAPI->>StackAPI: Parse & validate entities
    StackAPI->>StackDB: POST /users
    StackAPI->>StackDB: POST /teams
    StackDB-->>StackAPI: Return created IDs
    StackAPI->>StackDB: POST /memberships<br/>(using resolved IDs)
    StackDB-->>StackAPI: Confirm membership
    StackAPI-->>Client: Return userIdMap & teamIdMap
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • BilalG1
  • aadesh18

Poem

🐰 A hop through migrations we gleefully make,
Better Auth paths for Stack's sake,
With persistence in memory and plans pristine,
Data flows smooth to Stack's domain so keen!
Migrations burrow into the database deep! 🌰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: introduction of a Better Auth-backed migration layer for Stack Auth, which is the primary focus of this changeset.
Description check ✅ Passed The description provides a clear summary of changes, testing procedures, and aligns with the PR objectives. It covers the main deliverables and testing commands.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/migrations

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 1, 2026

Greptile Summary

This PR introduces a new @stackframe/migrations package that uses Better Auth as a normalization layer for migrating users, OAuth accounts, organizations, and memberships from other auth providers into Stack Auth. The architecture is clean — an in-memory adapter captures Better Auth model writes, a core module builds a typed migration plan, and a separate HTTP layer flushes that plan to Stack Auth's REST API.

  • P1 — Membership roles silently dropped: withMembershipMetadata in stack-api.ts is an unimplemented stub that returns its argument unchanged, and it is called with an empty object {}. The role and metadata captured in the plan are never forwarded to the Stack Auth team-membership endpoint, so every imported member arrives with the default role regardless of their original role.
  • P2 — Date object handling: Better Auth's internal adapter contract typically passes createdAt/updatedAt as Date instances; optionalString will throw for these values. The tests pass ISO strings directly, masking the issue in CI.
  • P2 — Missing devDependencies: eslint, @typescript-eslint/eslint-plugin, and @typescript-eslint/parser are used by eslint.config.mjs but absent from devDependencies.

Confidence Score: 3/5

Not safe to merge as-is — the role-drop bug means memberships are imported with incorrect permissions on every migration run.

A P1 defect causes membership roles computed correctly in the migration plan to be unconditionally discarded when POSTing to Stack Auth. This is a present bug on the changed code path that silently produces wrong data for every user with a non-default org role.

packages/migrations/src/stack-api.ts — the withMembershipMetadata stub and the membership import loop need fixing before merge.

Important Files Changed

Filename Overview
packages/migrations/src/stack-api.ts Implements the HTTP layer for flushing plans to Stack Auth; contains a P1 bug where withMembershipMetadata is an unfinished stub called with {}, causing membership role and metadata to be silently dropped.
packages/migrations/src/adapters/better-auth.ts In-memory Better Auth persistence adapter that captures model writes; may throw at snapshot time if Better Auth passes Date objects for createdAt/updatedAt instead of ISO strings.
packages/migrations/src/core.ts Snapshot-to-plan transformation logic; correctly maps users, teams, and memberships including role and metadata — the role loss happens downstream in stack-api.ts, not here.
packages/migrations/src/types.ts Type definitions for the public API surface; well-structured with no issues.
packages/migrations/package.json Package manifest missing eslint and @typescript-eslint/* devDependencies used by eslint.config.mjs; currently works only via monorepo hoisting.
packages/migrations/src/adapters/better-auth.test.ts Unit tests covering capture, plan building, unsupported hash handling, and CRUD ops; tests use manually-crafted ISO strings which masks the Date object issue.
packages/migrations/scripts/test-clerk-to-stack.ts Integration/smoke test script for Clerk → Stack migration; seeds test users via Clerk API and flushes them through the new persistence layer.
docs-mintlify/guides/going-further/migrations.mdx New migration guide documenting the Better Auth adapter, password hash handling, and provider ID mapping.

Sequence Diagram

sequenceDiagram
    participant Src as Source Provider
    participant BA as Better Auth Migration Script
    participant Adapter as BetterAuthStackPersistence
    participant Core as buildStackMigrationPlan
    participant API as Stack Auth REST API

    Src->>BA: Raw user/org export
    BA->>Adapter: adapter.create model=user
    BA->>Adapter: adapter.create model=account
    BA->>Adapter: adapter.create model=organization
    BA->>Adapter: adapter.create model=member
    Adapter->>Core: snapshot()
    Core-->>Adapter: StackMigrationPlan
    Adapter->>API: POST /api/v1/users
    API-->>Adapter: stackUserId
    Adapter->>API: POST /api/v1/teams
    API-->>Adapter: stackTeamId
    Adapter->>API: POST /api/v1/team-memberships - role dropped
    API-->>Adapter: 200 OK
    Adapter-->>BA: StackImportResult
Loading

Fix All in Claude Code Fix All in Cursor Fix All in Codex

Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
packages/migrations/src/stack-api.ts:66-76
**Membership role and metadata are silently dropped on import**

`withMembershipMetadata` is an unfinished stub that returns its argument unchanged, and it is called with an empty object `{}`. This means every team membership is posted to the Stack Auth API without the `role` or `metadata` that was computed in the migration plan. Any organization member imported as `"admin"` or another custom role will arrive as the default role with no metadata — the data is captured correctly in `StackMigrationPlan` but then discarded at the actual API call on line 75.

### Issue 2 of 4
packages/migrations/src/stack-api.ts:49-51
`withMembershipMetadata` is a stub that ignores its argument. It should be removed and the membership's `role` and `metadata` should be forwarded in the request body so they are not silently dropped.

```suggestion
function withMembershipMetadata(role: string | null, metadata: JsonObject): JsonObject {
  return {
    ...(role != null ? { role } : {}),
    ...metadata,
  };
}
```

### Issue 3 of 4
packages/migrations/src/adapters/better-auth.ts:89-97
**`createdAt`/`updatedAt` will throw when Better Auth passes `Date` objects**

Better Auth's internal adapter contract passes `createdAt` and `updatedAt` as JavaScript `Date` instances (not ISO strings). `optionalString` will throw for any non-null, non-string value. The unit tests pass because they manually write ISO strings, but a real Better Auth migration runner calling this adapter will hit this at snapshot time. Consider calling `.toISOString()` when the value is a Date before the string check.

### Issue 4 of 4
packages/migrations/package.json:40-47
**Missing `eslint` and `@typescript-eslint/*` devDependencies**

`eslint.config.mjs` imports `@typescript-eslint/eslint-plugin` and `@typescript-eslint/parser`, but neither `eslint` nor either `@typescript-eslint` package is listed in `devDependencies`. The lint script works today only because pnpm hoists these from sibling packages in the monorepo. Any consumer running `pnpm -C packages/migrations lint` in isolation, or a future CI step that installs only this package, will fail with module-not-found errors.

Reviews (1): Last reviewed commit: "Add Better Auth to Stack Auth migration ..." | Re-trigger Greptile

Comment on lines +66 to +76
for (const membership of plan.memberships) {
const stackUserId = userIdMap.get(membership.externalUserId);
if (stackUserId == null) {
throw new Error(`External membership ${membership.externalMembershipId} references missing user ${membership.externalUserId}`);
}
const stackTeamId = teamIdMap.get(membership.externalOrganizationId);
if (stackTeamId == null) {
throw new Error(`External membership ${membership.externalMembershipId} references missing organization ${membership.externalOrganizationId}`);
}
await stackFetch(config, `/api/v1/team-memberships/${stackTeamId}/${stackUserId}`, "POST", withMembershipMetadata({}));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Membership role and metadata are silently dropped on import

withMembershipMetadata is an unfinished stub that returns its argument unchanged, and it is called with an empty object {}. This means every team membership is posted to the Stack Auth API without the role or metadata that was computed in the migration plan. Any organization member imported as "admin" or another custom role will arrive as the default role with no metadata — the data is captured correctly in StackMigrationPlan but then discarded at the actual API call on line 75.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/migrations/src/stack-api.ts
Line: 66-76

Comment:
**Membership role and metadata are silently dropped on import**

`withMembershipMetadata` is an unfinished stub that returns its argument unchanged, and it is called with an empty object `{}`. This means every team membership is posted to the Stack Auth API without the `role` or `metadata` that was computed in the migration plan. Any organization member imported as `"admin"` or another custom role will arrive as the default role with no metadata — the data is captured correctly in `StackMigrationPlan` but then discarded at the actual API call on line 75.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Cursor Fix in Codex

Comment on lines +49 to +51
function withMembershipMetadata(body: JsonObject): JsonObject {
return body;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 withMembershipMetadata is a stub that ignores its argument. It should be removed and the membership's role and metadata should be forwarded in the request body so they are not silently dropped.

Suggested change
function withMembershipMetadata(body: JsonObject): JsonObject {
return body;
}
function withMembershipMetadata(role: string | null, metadata: JsonObject): JsonObject {
return {
...(role != null ? { role } : {}),
...metadata,
};
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/migrations/src/stack-api.ts
Line: 49-51

Comment:
`withMembershipMetadata` is a stub that ignores its argument. It should be removed and the membership's `role` and `metadata` should be forwarded in the request body so they are not silently dropped.

```suggestion
function withMembershipMetadata(role: string | null, metadata: JsonObject): JsonObject {
  return {
    ...(role != null ? { role } : {}),
    ...metadata,
  };
}
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Cursor Fix in Codex

Comment on lines +89 to +97
function createdUpdatedMetadata(record: BetterAuthPersistenceRecord): JsonObject {
return {
better_auth: {
id: record.id,
created_at: optionalString(record.createdAt, "createdAt", "record"),
updated_at: optionalString(record.updatedAt, "updatedAt", "record"),
},
};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 createdAt/updatedAt will throw when Better Auth passes Date objects

Better Auth's internal adapter contract passes createdAt and updatedAt as JavaScript Date instances (not ISO strings). optionalString will throw for any non-null, non-string value. The unit tests pass because they manually write ISO strings, but a real Better Auth migration runner calling this adapter will hit this at snapshot time. Consider calling .toISOString() when the value is a Date before the string check.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/migrations/src/adapters/better-auth.ts
Line: 89-97

Comment:
**`createdAt`/`updatedAt` will throw when Better Auth passes `Date` objects**

Better Auth's internal adapter contract passes `createdAt` and `updatedAt` as JavaScript `Date` instances (not ISO strings). `optionalString` will throw for any non-null, non-string value. The unit tests pass because they manually write ISO strings, but a real Better Auth migration runner calling this adapter will hit this at snapshot time. Consider calling `.toISOString()` when the value is a Date before the string check.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Cursor Fix in Codex

Comment on lines +40 to +47
"devDependencies": {
"@types/node": "20.17.6",
"rimraf": "^6.0.1",
"tsdown": "^0.20.3",
"typescript": "5.9.3",
"vitest": "^1.6.0"
},
"packageManager": "pnpm@10.23.0"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing eslint and @typescript-eslint/* devDependencies

eslint.config.mjs imports @typescript-eslint/eslint-plugin and @typescript-eslint/parser, but neither eslint nor either @typescript-eslint package is listed in devDependencies. The lint script works today only because pnpm hoists these from sibling packages in the monorepo. Any consumer running pnpm -C packages/migrations lint in isolation, or a future CI step that installs only this package, will fail with module-not-found errors.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/migrations/package.json
Line: 40-47

Comment:
**Missing `eslint` and `@typescript-eslint/*` devDependencies**

`eslint.config.mjs` imports `@typescript-eslint/eslint-plugin` and `@typescript-eslint/parser`, but neither `eslint` nor either `@typescript-eslint` package is listed in `devDependencies`. The lint script works today only because pnpm hoists these from sibling packages in the monorepo. Any consumer running `pnpm -C packages/migrations lint` in isolation, or a future CI step that installs only this package, will fail with module-not-found errors.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Cursor Fix in Codex

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new @stackframe/migrations package to support migrating external auth provider data into Stack Auth by capturing Better Auth–shaped persistence writes and importing them via Stack Auth’s server REST API, with accompanying docs updates.

Changes:

  • Introduces @stackframe/migrations package scaffolding (build/lint/typecheck/test config) plus a small CLI help entrypoint.
  • Implements Better Auth persistence capture → snapshot/plan builder → Stack Auth import (users, teams, team-memberships).
  • Adds documentation (Mintlify + existing docs FAQ) describing the migration flow and options (bcrypt-only import, provider id mapping), plus unit tests for the Better Auth adapter.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/migrations/tsdown.config.ts Builds the new package with the shared tsdown library config.
packages/migrations/tsconfig.json TypeScript configuration for the new package.
packages/migrations/src/types.ts Public types for snapshots, plans, and Stack API create bodies.
packages/migrations/src/core.ts Snapshot → Stack migration plan builder (user/team/membership mapping).
packages/migrations/src/stack-api.ts Stack Auth REST importer for a migration plan.
packages/migrations/src/adapters/better-auth.ts In-memory Better Auth-shaped persistence adapter + snapshot/flush helpers.
packages/migrations/src/adapters/better-auth.test.ts Vitest coverage for capture and plan-building behavior.
packages/migrations/src/index.ts Public exports for the package API surface.
packages/migrations/src/cli.ts Minimal CLI help output entrypoint.
packages/migrations/scripts/test-clerk-to-stack.ts Manual script for a Clerk → Stack migration smoke test via the adapter.
packages/migrations/package.json New package metadata, exports map, and scripts.
packages/migrations/eslint.config.mjs Package-local ESLint setup for TS sources.
packages/migrations/README.md Package README explaining Better Auth-based migrations.
docs/content/docs/(guides)/faq.mdx Adds mention of @stackframe/migrations for migrations.
docs-mintlify/guides/going-further/migrations.mdx New migrations guide for Mintlify docs.
docs-mintlify/guides/faq.mdx Updates FAQ to link to the new migrations guide.
docs-mintlify/docs.json Adds the migrations guide to the “Going further” navigation.
claude/CLAUDE-KNOWLEDGE.md Adds internal knowledge entry describing the migration architecture.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

"repository": "https://github.com/stack-auth/stack-auth",
"description": "Migration utilities for moving users, organizations, and auth data from other auth providers to Stack Auth.",
"main": "dist/index.js",
"type": "module",
return pushRecord(getTable(input.model), input.model, input.data);
},
async findOne(input) {
return [...getTable(input.model).values()].find((record) => matchesWhere(record, input.where)) ?? null;
Comment on lines +50 to +56
const oauthProviders = user.oauthAccounts
.sort((a, b) => `${a.providerId}:${a.accountId}`.localeCompare(`${b.providerId}:${b.accountId}`))
.map((account) => ({
id: getProviderId(account.providerId, options.providerIdMap),
account_id: account.accountId,
email: account.email,
}));
Comment on lines +53 to +78
export async function importPlanToStackAuth(config: StackApiConfig, plan: StackMigrationPlan): Promise<StackImportResult> {
const userIdMap = new Map<string, string>();
for (const user of plan.users) {
const response = await stackFetch(config, "/api/v1/users", "POST", user.body);
userIdMap.set(user.externalUserId, readIdFromStackResponse(response, "/api/v1/users"));
}

const teamIdMap = new Map<string, string>();
for (const team of plan.teams) {
const response = await stackFetch(config, "/api/v1/teams", "POST", team.body);
teamIdMap.set(team.externalOrganizationId, readIdFromStackResponse(response, "/api/v1/teams"));
}

for (const membership of plan.memberships) {
const stackUserId = userIdMap.get(membership.externalUserId);
if (stackUserId == null) {
throw new Error(`External membership ${membership.externalMembershipId} references missing user ${membership.externalUserId}`);
}
const stackTeamId = teamIdMap.get(membership.externalOrganizationId);
if (stackTeamId == null) {
throw new Error(`External membership ${membership.externalMembershipId} references missing organization ${membership.externalOrganizationId}`);
}
await stackFetch(config, `/api/v1/team-memberships/${stackTeamId}/${stackUserId}`, "POST", withMembershipMetadata({}));
}

return { userIdMap, teamIdMap };
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
packages/migrations/src/types.ts (2)

82-88: ⚡ Quick win

StackMigrationPlan.memberships is structurally inconsistent with users and teams.

users and teams wrap their payload in a named body field (StackUserCreateBody / StackTeamCreateBody), but memberships inlines the fields directly without a body wrapper or a named type. This inconsistency makes the plan shape surprising and prevents callers from referring to the membership payload type independently.

♻️ Proposed refactor
+export type StackMembershipCreateBody = {
+  role: string | null,
+  metadata: JsonObject,
+};

 export type StackMigrationPlan = {
   ...
   memberships: {
     externalMembershipId: string,
     externalUserId: string,
     externalOrganizationId: string,
-    role: string | null,
-    metadata: JsonObject,
+    body: StackMembershipCreateBody,
   }[],
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/migrations/src/types.ts` around lines 82 - 88,
StackMigrationPlan.memberships is inconsistent with users and teams because it
inlines membership fields instead of using a wrapped payload type; update
memberships to mirror users/teams by introducing a named payload type (e.g.,
StackMembershipCreateBody) and make memberships an array of objects with a body
field of that type (i.e., memberships: { body: StackMembershipCreateBody, /*
other metadata if needed */ }[]), and replace the current inline shape
(externalMembershipId, externalUserId, externalOrganizationId, role, metadata)
with the new StackMembershipCreateBody so callers can reference the membership
payload type independently and keep shape consistent with StackUserCreateBody
and StackTeamCreateBody.

91-94: ⚡ Quick win

StackImportOptions fields lack documentation.

Both fields need JSDoc explaining their semantics:

  • providerIdMap: it's unclear what keys/values it maps (e.g. external provider ID → Stack Auth provider ID?).
  • unsupportedPasswordHashAction: the behaviour when the field is omitted (undefined) is not documented; callers have no way to know the default without reading the implementation.
📄 Proposed addition
 export type StackImportOptions = {
+  /**
+   * Maps an external OAuth provider ID to the corresponding Stack Auth provider ID.
+   * Keys are the provider IDs found in `ExternalOAuthAccount.providerId`;
+   * values are the Stack Auth provider IDs they should be imported as.
+   */
   providerIdMap?: Map<string, string>,
+  /**
+   * What to do when a user has a password hash in an unsupported format.
+   * - `"error"` (default): throw and abort the migration.
+   * - `"omit"`: silently drop the password hash and continue.
+   */
   unsupportedPasswordHashAction?: "error" | "omit",
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/migrations/src/types.ts` around lines 91 - 94, Add JSDoc to the
StackImportOptions type and its fields: document StackImportOptions, then add
JSDoc for providerIdMap explaining the direction and meaning of the Map
keys/values (e.g. external provider ID string -> internal Stack Auth provider ID
string) and any expectations about formats/when to use it; add JSDoc for
unsupportedPasswordHashAction describing the two allowed values ("error" |
"omit") and explicitly state the default behavior when the property is undefined
(copy the actual default from the implementation), so callers know what happens
when they omit the field; reference the StackImportOptions type and its
providerIdMap and unsupportedPasswordHashAction members when making these edits.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/migrations/eslint.config.mjs`:
- Around line 24-27: The current spread of tsPlugin.configs.recommended.rules is
wrong for flat config because tsPlugin.configs.recommended is an array (so
.rules is undefined); replace this approach by importing and using the
typescript-eslint "wrapper" flat config export (e.g., import { configs as
tsEsLintConfigs } from "typescript-eslint" or directly import the flat-config
export documented by typescript-eslint) and merge its rules into your rules
object instead of spreading tsPlugin.configs.recommended.rules; also add
"typescript-eslint" to packages/migrations devDependencies in package.json so
the wrapper is available.

In `@packages/migrations/package.json`:
- Line 1: CI is failing because pnpm-lock.yaml is missing new specifiers for the
packages/migrations package; run pnpm install from the repository root (do not
use --frozen-lockfile) to regenerate/update pnpm-lock.yaml so it includes
packages/migrations, then commit the updated pnpm-lock.yaml and package.json
changes; verify the packages/migrations package.json is present and correct
before committing.
- Around line 6-38: The package currently sets "type": "module" while building
CommonJS .js outputs via tsdown, causing ERR_REQUIRE_ESM; fix by choosing one
approach and applying it consistently: either remove the "type": "module" key
from package.json so .js files are treated as CJS (matches
stack-shared/stack-sc), or keep "type": "module" and delete the "main" field and
all "require" export conditions under "exports" to make the package ESM-only, or
update the tsdown/tsup config (see createJsLibraryTsupConfig and its
outExtensions setting) so CJS artifacts are emitted as .cjs and adjust
"main"/"exports" to point to .cjs require entries.

In `@packages/migrations/README.md`:
- Around line 26-31: Update the example call to persistence.flushToStackAuth so
the apiUrl is not pointing to localhost; replace "http://localhost:8102" with
the production Stack Auth URL or a neutral placeholder (for example
"https://api.stack-auth.com" or "https://<STACK_AUTH_API_URL>") so users copying
the snippet won't accidentally use a local dev URL.

In `@packages/migrations/src/stack-api.ts`:
- Line 75: The URL path currently interpolates raw IDs into the template string;
update the call to stackFetch so path segments are URI-encoded: use
encodeURIComponent(stackTeamId) and encodeURIComponent(stackUserId) when
building the path in the await stackFetch(...) call (the rest of the call
including withMembershipMetadata({}) remains unchanged) so the route becomes
safe for any ID characters.

In `@packages/migrations/src/types.ts`:
- Around line 6-7: The two fields primaryEmail and primaryEmailVerified in
packages/migrations/src/types.ts are inconsistent; update the type to couple
them so verified cannot be true when primaryEmail is null (for example replace
the separate fields with a discriminated union like a "hasPrimaryEmail" branch
vs "noPrimaryEmail" branch, or make primaryEmailVerified nullable/optional so it
can only be true when primaryEmail is a string), and add a JSDoc invariant on
the exported type/interface (referencing primaryEmail and primaryEmailVerified)
to document the constraint if you prefer not to encode it fully in the type
system.

---

Nitpick comments:
In `@packages/migrations/src/types.ts`:
- Around line 82-88: StackMigrationPlan.memberships is inconsistent with users
and teams because it inlines membership fields instead of using a wrapped
payload type; update memberships to mirror users/teams by introducing a named
payload type (e.g., StackMembershipCreateBody) and make memberships an array of
objects with a body field of that type (i.e., memberships: { body:
StackMembershipCreateBody, /* other metadata if needed */ }[]), and replace the
current inline shape (externalMembershipId, externalUserId,
externalOrganizationId, role, metadata) with the new StackMembershipCreateBody
so callers can reference the membership payload type independently and keep
shape consistent with StackUserCreateBody and StackTeamCreateBody.
- Around line 91-94: Add JSDoc to the StackImportOptions type and its fields:
document StackImportOptions, then add JSDoc for providerIdMap explaining the
direction and meaning of the Map keys/values (e.g. external provider ID string
-> internal Stack Auth provider ID string) and any expectations about
formats/when to use it; add JSDoc for unsupportedPasswordHashAction describing
the two allowed values ("error" | "omit") and explicitly state the default
behavior when the property is undefined (copy the actual default from the
implementation), so callers know what happens when they omit the field;
reference the StackImportOptions type and its providerIdMap and
unsupportedPasswordHashAction members when making these edits.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 79486cad-b955-4aba-b637-408420ecd65a

📥 Commits

Reviewing files that changed from the base of the PR and between d2f2fb0 and f29ce37.

📒 Files selected for processing (18)
  • claude/CLAUDE-KNOWLEDGE.md
  • docs-mintlify/docs.json
  • docs-mintlify/guides/faq.mdx
  • docs-mintlify/guides/going-further/migrations.mdx
  • docs/content/docs/(guides)/faq.mdx
  • packages/migrations/README.md
  • packages/migrations/eslint.config.mjs
  • packages/migrations/package.json
  • packages/migrations/scripts/test-clerk-to-stack.ts
  • packages/migrations/src/adapters/better-auth.test.ts
  • packages/migrations/src/adapters/better-auth.ts
  • packages/migrations/src/cli.ts
  • packages/migrations/src/core.ts
  • packages/migrations/src/index.ts
  • packages/migrations/src/stack-api.ts
  • packages/migrations/src/types.ts
  • packages/migrations/tsconfig.json
  • packages/migrations/tsdown.config.ts

Comment on lines +24 to +27
rules: {
...tsPlugin.configs.recommended.rules,
"@typescript-eslint/no-explicit-any": "error",
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the `@typescript-eslint/eslint-plugin` version installed in the workspace
cat package.json | grep -i "typescript-eslint" || true
rg --type json '"@typescript-eslint' packages/migrations/package.json || true

# Find the workspace root package.json to check the installed version
rg --type json '"@typescript-eslint/eslint-plugin"' package.json || true

# Also check what shape configs.recommended has in the installed package
fd -g 'package.json' node_modules/@typescript-eslint/eslint-plugin --max-depth=1 --exec cat {}

Repository: stack-auth/stack-auth

Length of output: 3510


🏁 Script executed:

#!/bin/bash

# Check the actual structure of configs.recommended in the installed package
node -e "
const plugin = require('node_modules/@typescript-eslint/eslint-plugin');
console.log('Type of plugin.configs:', typeof plugin.configs);
console.log('Type of plugin.configs.recommended:', typeof plugin.configs.recommended);
console.log('Is array:', Array.isArray(plugin.configs.recommended));
console.log('Value of .rules:', plugin.configs.recommended?.rules);
console.log('JSON:', JSON.stringify(plugin.configs.recommended, null, 2));
" 2>&1 | head -100

Repository: stack-auth/stack-auth

Length of output: 956


🏁 Script executed:

cat -n packages/migrations/eslint.config.mjs | head -30

Repository: stack-auth/stack-auth

Length of output: 936


🏁 Script executed:

# Check packages/migrations/package.json to see if `@typescript-eslint` is listed
cat packages/migrations/package.json

Repository: stack-auth/stack-auth

Length of output: 1250


🏁 Script executed:

# Check the type definitions for `@typescript-eslint/eslint-plugin`
find node_modules/@typescript-eslint/eslint-plugin -name "*.d.ts" | head -5

Repository: stack-auth/stack-auth

Length of output: 47


🏁 Script executed:

# Check the dist/index.js to see how configs are exported in v8.56.1
head -200 node_modules/@typescript-eslint/eslint-plugin/dist/index.js | tail -100

Repository: stack-auth/stack-auth

Length of output: 327


🏁 Script executed:

# Also check if there's a specific export for the configs shape
rg "configs" node_modules/@typescript-eslint/eslint-plugin/index.d.ts -A 5 | head -50

Repository: stack-auth/stack-auth

Length of output: 191


🏁 Script executed:

# Check what ClassicConfig.Config structure looks like
rg "ClassicConfig" node_modules/@typescript-eslint/eslint-plugin/index.d.ts -A 10 | head -80

Repository: stack-auth/stack-auth

Length of output: 330


🏁 Script executed:

# Also check for any imports or definitions of ClassicConfig
rg "interface.*Config|type.*Config" node_modules/@typescript-eslint/eslint-plugin/index.d.ts -B 2 -A 5 | head -100

Repository: stack-auth/stack-auth

Length of output: 47


🏁 Script executed:

# Check the ClassicConfig.Config definition in `@typescript-eslint/utils`
rg "namespace ClassicConfig|interface.*ClassicConfig|class ClassicConfig" node_modules/@typescript-eslint/utils/dist/ts-eslint -A 20 | head -150

Repository: stack-auth/stack-auth

Length of output: 158


🏁 Script executed:

# Check if Config has a rules property
rg "rules" node_modules/@typescript-eslint/utils/dist/ts-eslint -B 2 -A 2 | grep -A 5 -B 5 "ClassicConfig\|Config.*rules" | head -100

Repository: stack-auth/stack-auth

Length of output: 158


🏁 Script executed:

# Find where ClassicConfig.Config is defined
fd "index.d.ts" node_modules/@typescript-eslint/utils --max-depth 2

Repository: stack-auth/stack-auth

Length of output: 190


🏁 Script executed:

# Check the main index.d.ts for `@typescript-eslint/utils`
cat node_modules/@typescript-eslint/utils/index.d.ts | head -100

Repository: stack-auth/stack-auth

Length of output: 146


🏁 Script executed:

# Also check what's in the utils dist folder
find node_modules/@typescript-eslint/utils -name "*.d.ts" | head -10

Repository: stack-auth/stack-auth

Length of output: 138


🏁 Script executed:

# View the full eslint-plugin index.d.ts to see complete structure
cat node_modules/@typescript-eslint/eslint-plugin/index.d.ts

Repository: stack-auth/stack-auth

Length of output: 344


🏁 Script executed:

# Check if there are any comments or warnings in the actual config files
rg "configs\|recommended" node_modules/@typescript-eslint/eslint-plugin --type d.ts | head -20

Repository: stack-auth/stack-auth

Length of output: 94


🏁 Script executed:

# Search for any error handling or runtime behavior in the package
find node_modules/@typescript-eslint/eslint-plugin -name "*.d.ts" -exec cat {} \;

Repository: stack-auth/stack-auth

Length of output: 47


🏁 Script executed:

# Check if the eslint.config.mjs would actually work by looking at workspace package.json
cat package.json | grep -A 30 "devDependencies"

Repository: stack-auth/stack-auth

Length of output: 992


🏁 Script executed:

# Verify if packages/migrations can access root dependencies
ls -la packages/migrations/node_modules 2>&1 | head -20

Repository: stack-auth/stack-auth

Length of output: 145


🏁 Script executed:

# Check if there's a monorepo setup (pnpm-workspace or similar)
cat pnpm-workspace.yaml 2>/dev/null || cat package.json | grep -A 5 "workspaces" || echo "No workspace config found"

Repository: stack-auth/stack-auth

Length of output: 207


🏁 Script executed:

# Try to run eslint to see if the config actually works
cd packages/migrations && npx eslint --version && npx eslint . --max-warnings 0 2>&1 | head -50

Repository: stack-auth/stack-auth

Length of output: 73


🏁 Script executed:

# Also check the web for `@typescript-eslint` v8 documentation on configs structure
# to confirm if ClassicConfig.Config has .rules

Repository: stack-auth/stack-auth

Length of output: 47


🌐 Web query:

@typescript-eslint/eslint-plugin v8 configs.recommended structure ClassicConfig

💡 Result:

In @typescript-eslint/eslint-plugin v8 (as of v8.58.1), the configs.recommended is primarily an array of FlatConfig objects for ESLint's flat config format (eslint.config.* files). It enables recommended rules for code correctness without requiring type information, disables conflicting core ESLint rules, and automatically sets up the @typescript-eslint parser and plugin. For legacy ESLint RC configs (.eslintrc.*), the plugin exports configs.recommended as a ClassicConfig.Config object (Linter.LinterConfig in ESLint terms), which has this structure: interface ClassicConfig { extends?: string[]; plugins?: string[]; rules?: Record<string, RuleConfig>; // e.g. { '@typescript-eslint/no-explicit-any': 'error' } parser?: string; parserOptions?: ParserOptions; env?: Record<string, boolean>; globals?: Record<string, boolean>; } Usage example in legacy config: module.exports = { parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint'], extends: ['@typescript-eslint/recommended'], // or plugin.configs.recommended directly }; The exact rules in v8 recommended include (from diff/changes): '@typescript-eslint/ban-ts-comment', '@typescript-eslint/no-array-constructor', '@typescript-eslint/no-duplicate-enum-values', '@typescript-eslint/no-empty-object-type', '@typescript-eslint/no-explicit-any', etc. (full list in generated configs source). See configs/recommended.ts for exact contents (generated file). Primary focus in v8 docs is flat configs: export default tseslint.configs.recommended;

Citations:


tsPlugin.configs.recommended.rules is undefined in flat config format — recommended rules are silently skipped.

In @typescript-eslint/eslint-plugin v8, when used in an ESLint flat config file (eslint.config.mjs), configs.recommended is an array of FlatConfig objects, not an object with a .rules property. Spreading ...tsPlugin.configs.recommended.rules on an array returns undefined, silently disabling all recommended rules. Only the @typescript-eslint/no-explicit-any override will apply.

Use the typescript-eslint package wrapper for proper flat config support:

🔧 Suggested fix for flat config compatibility
-import tsPlugin from "@typescript-eslint/eslint-plugin";
-import tsParser from "@typescript-eslint/parser";
+import tseslint from "typescript-eslint";
 import { fileURLToPath } from "node:url";

 const tsconfigRootDir = fileURLToPath(new URL(".", import.meta.url));

-export default [
-  {
-    ignores: ["dist/**"],
-  },
-  {
-    files: ["src/**/*.ts"],
-    languageOptions: {
-      parser: tsParser,
-      parserOptions: {
-        project: "./tsconfig.json",
-        sourceType: "module",
-        tsconfigRootDir,
-      },
-    },
-    plugins: {
-      "@typescript-eslint": tsPlugin,
-    },
-    rules: {
-      ...tsPlugin.configs.recommended.rules,
-      "@typescript-eslint/no-explicit-any": "error",
-    },
-  },
-];
+export default tseslint.config(
+  { ignores: ["dist/**"] },
+  {
+    files: ["src/**/*.ts"],
+    extends: [...tseslint.configs.recommended],
+    languageOptions: {
+      parserOptions: {
+        project: "./tsconfig.json",
+        tsconfigRootDir,
+      },
+    },
+    rules: {
+      "@typescript-eslint/no-explicit-any": "error",
+    },
+  },
+);

This requires adding typescript-eslint to packages/migrations/package.json devDependencies.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/migrations/eslint.config.mjs` around lines 24 - 27, The current
spread of tsPlugin.configs.recommended.rules is wrong for flat config because
tsPlugin.configs.recommended is an array (so .rules is undefined); replace this
approach by importing and using the typescript-eslint "wrapper" flat config
export (e.g., import { configs as tsEsLintConfigs } from "typescript-eslint" or
directly import the flat-config export documented by typescript-eslint) and
merge its rules into your rules object instead of spreading
tsPlugin.configs.recommended.rules; also add "typescript-eslint" to
packages/migrations devDependencies in package.json so the wrapper is available.

@@ -0,0 +1,48 @@
{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

CI pipeline is blocked: update pnpm-lock.yaml.

Both CI jobs fail because pnpm-lock.yaml does not include the new packages/migrations package specifiers. Run pnpm install (without --frozen-lockfile) from the repo root and commit the updated lockfile.

🧰 Tools
🪛 GitHub Actions: Runs E2E API Tests

[error] 1-1: Lockfile mismatch: specifiers in pnpm-lock.yaml do not match specifiers in package.json for /packages/migrations.

🪛 GitHub Actions: Runs E2E API Tests with custom port prefix

[error] 1-1: Dependency specifiers differ from pnpm-lock.yaml, causing frozen-lockfile installation to fail in CI. Suggested fix: run pnpm install (without --frozen-lockfile) to update pnpm-lock.yaml.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/migrations/package.json` at line 1, CI is failing because
pnpm-lock.yaml is missing new specifiers for the packages/migrations package;
run pnpm install from the repository root (do not use --frozen-lockfile) to
regenerate/update pnpm-lock.yaml so it includes packages/migrations, then commit
the updated pnpm-lock.yaml and package.json changes; verify the
packages/migrations package.json is present and correct before committing.

Comment on lines +6 to +38
"main": "dist/index.js",
"type": "module",
"bin": {
"stack-migrate": "./dist/cli.js"
},
"scripts": {
"build": "rimraf dist && tsdown",
"dev": "tsdown --watch",
"lint": "eslint .",
"typecheck": "tsc --noEmit",
"test": "vitest run"
},
"files": [
"README.md",
"dist",
"CHANGELOG.md",
"LICENSE"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": {
"default": "./dist/index.js"
},
"default": "./dist/esm/index.js"
},
"./better-auth": {
"types": "./dist/adapters/better-auth.d.ts",
"require": {
"default": "./dist/adapters/better-auth.js"
},
"default": "./dist/esm/adapters/better-auth.js"
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the shared js-library tsdown config to understand CJS output format
fd -g 'js-library.ts' configs/tsdown --exec cat {}

# Check how other packages in the monorepo structure their CJS exports
rg --type json '"require"' packages/stack-shared/package.json packages/stack-sc/package.json 2>/dev/null | head -30

# Look for .cjs output in any existing dist directories
fd -g '*.cjs' packages --max-depth=4 | head -10

Repository: stack-auth/stack-auth

Length of output: 2656


🏁 Script executed:

#!/bin/bash
# Get full exports section from sibling packages
echo "=== stack-shared/package.json exports ==="
jq '.exports' packages/stack-shared/package.json

echo -e "\n=== stack-sc/package.json exports ==="
jq '.exports' packages/stack-sc/package.json

echo -e "\n=== migrations/package.json exports ==="
jq '.exports' packages/migrations/package.json

# Check if any .cjs files actually exist in dist directories
echo -e "\n=== Checking for actual .cjs files ==="
find packages/stack-shared/dist packages/stack-sc/dist packages/migrations/dist -name "*.cjs" 2>/dev/null || echo "No .cjs files found"

# Check the actual file extensions in dist directories
echo -e "\n=== File types in stack-shared dist ==="
ls -la packages/stack-shared/dist/ 2>/dev/null | grep -E '\.(js|cjs)$' | head -5 || echo "dist not available"

Repository: stack-auth/stack-auth

Length of output: 1846


🏁 Script executed:

#!/bin/bash
# Verify "type": "module" in all three packages
echo "=== Type field in package.json files ==="
for pkg in migrations stack-shared stack-sc; do
  echo "$pkg: $(jq '.type' packages/$pkg/package.json 2>/dev/null || echo 'N/A')"
done

# Check if there are any actual require() calls in test files or documentation
echo -e "\n=== Looking for require() usage patterns ==="
rg "require\(" packages/migrations --type ts --type js -A 2 | head -30

# Check if there are any CommonJS test setup files (e.g., vitest.config.js)
echo -e "\n=== Build/test config files ==="
ls -la packages/migrations/ | grep -E '(vitest|vite|tsconfig|jest)'

# Check package.json main field for all three
echo -e "\n=== Main field in all packages ==="
for pkg in migrations stack-shared stack-sc; do
  main=$(jq '.main' packages/$pkg/package.json 2>/dev/null)
  echo "$pkg main: $main"
done

Repository: stack-auth/stack-auth

Length of output: 417


🏁 Script executed:

#!/bin/bash
# Check migrations package purpose - is it supposed to be ESM-only?
echo "=== migrations package.json key fields ==="
jq '{name, description, type, main, exports}' packages/migrations/package.json

# Check if migrations is meant to be a CLI-only tool
echo -e "\n=== Migrations bin field ==="
jq '.bin' packages/migrations/package.json

# See if there are CommonJS test imports for migrations
echo -e "\n=== Test imports in migrations ==="
rg "import.*migrations|from.*migrations" packages --type ts --type tsx --type js | head -20

# Check the git history or comments around "type": "module" in migrations
echo -e "\n=== migrations/package.json first 20 lines ==="
head -20 packages/migrations/package.json

Repository: stack-auth/stack-auth

Length of output: 1473


Remove "type": "module" or drop the "require" exports — current configuration causes ERR_REQUIRE_ESM at runtime.

With "type": "module" enabled, Node.js treats all .js files as ESM. The tsdown config outputs CJS to dist/*.js (not .cjs), so any require() call will fail:

Error [ERR_REQUIRE_ESM]: require() of ES Module ... not supported.

The tsdown CJS format is hardcoded to outExtensions: { js: '.js' }, so changing exports to .cjs won't help—the files won't exist.

Instead, choose one approach:

  • Option A: Remove "type": "module" from package.json (matches how stack-shared and stack-sc handle this—they omit it entirely and let .js default to CJS).
  • Option B: Keep "type": "module" but remove the "main" field and all "require" export conditions (ESM-only package).
  • Option C: Modify the tsdown config to output .cjs files for CJS builds (requires changing outExtensions in createJsLibraryTsupConfig).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/migrations/package.json` around lines 6 - 38, The package currently
sets "type": "module" while building CommonJS .js outputs via tsdown, causing
ERR_REQUIRE_ESM; fix by choosing one approach and applying it consistently:
either remove the "type": "module" key from package.json so .js files are
treated as CJS (matches stack-shared/stack-sc), or keep "type": "module" and
delete the "main" field and all "require" export conditions under "exports" to
make the package ESM-only, or update the tsdown/tsup config (see
createJsLibraryTsupConfig and its outExtensions setting) so CJS artifacts are
emitted as .cjs and adjust "main"/"exports" to point to .cjs require entries.

Comment on lines +26 to +31
await persistence.flushToStackAuth({
apiUrl: "http://localhost:8102",
projectId: process.env.STACK_PROJECT_ID!,
secretServerKey: process.env.STACK_SECRET_SERVER_KEY!,
});
```
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

apiUrl in the example should use the production URL (or a placeholder), not localhost.

The docs guide (migrations.mdx) uses "https://api.stack-auth.com", but this README—published to npm—shows "http://localhost:8102". Users copying the snippet for a real migration could leave the wrong URL in place.

📝 Suggested fix
 await persistence.flushToStackAuth({
-  apiUrl: "http://localhost:8102",
+  apiUrl: "https://api.stack-auth.com",
   projectId: process.env.STACK_PROJECT_ID!,
   secretServerKey: process.env.STACK_SECRET_SERVER_KEY!,
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await persistence.flushToStackAuth({
apiUrl: "http://localhost:8102",
projectId: process.env.STACK_PROJECT_ID!,
secretServerKey: process.env.STACK_SECRET_SERVER_KEY!,
});
```
await persistence.flushToStackAuth({
apiUrl: "https://api.stack-auth.com",
projectId: process.env.STACK_PROJECT_ID!,
secretServerKey: process.env.STACK_SECRET_SERVER_KEY!,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/migrations/README.md` around lines 26 - 31, Update the example call
to persistence.flushToStackAuth so the apiUrl is not pointing to localhost;
replace "http://localhost:8102" with the production Stack Auth URL or a neutral
placeholder (for example "https://api.stack-auth.com" or
"https://<STACK_AUTH_API_URL>") so users copying the snippet won't accidentally
use a local dev URL.

if (stackTeamId == null) {
throw new Error(`External membership ${membership.externalMembershipId} references missing organization ${membership.externalOrganizationId}`);
}
await stackFetch(config, `/api/v1/team-memberships/${stackTeamId}/${stackUserId}`, "POST", withMembershipMetadata({}));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use encodeURIComponent() for path-segment IDs — coding guideline.

The template literal `/api/v1/team-memberships/${stackTeamId}/${stackUserId}` interpolates IDs directly. While Stack Auth-issued IDs are currently safe, the guideline requires encodeURIComponent() for URL construction.

✏️ Proposed fix (applies after the major issue above is resolved)
-    await stackFetch(config, `/api/v1/team-memberships/${stackTeamId}/${stackUserId}`, "POST", ...);
+    await stackFetch(config, `/api/v1/team-memberships/${encodeURIComponent(stackTeamId)}/${encodeURIComponent(stackUserId)}`, "POST", ...);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await stackFetch(config, `/api/v1/team-memberships/${stackTeamId}/${stackUserId}`, "POST", withMembershipMetadata({}));
await stackFetch(config, `/api/v1/team-memberships/${encodeURIComponent(stackTeamId)}/${encodeURIComponent(stackUserId)}`, "POST", withMembershipMetadata({}));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/migrations/src/stack-api.ts` at line 75, The URL path currently
interpolates raw IDs into the template string; update the call to stackFetch so
path segments are URI-encoded: use encodeURIComponent(stackTeamId) and
encodeURIComponent(stackUserId) when building the path in the await
stackFetch(...) call (the rest of the call including withMembershipMetadata({})
remains unchanged) so the route becomes safe for any ID characters.

Comment on lines +6 to +7
primaryEmail: string | null,
primaryEmailVerified: boolean,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

primaryEmailVerified: boolean is incoherent when primaryEmail is null.

When primaryEmail is null there is no address to verify, yet the type permits (and requires) a boolean value. This allows callers to construct objects with contradictory state (e.g. primaryEmail: null, primaryEmailVerified: true) without any type error.

Consider coupling the two fields or narrowing the type:

🛡️ Proposed fix
-  primaryEmail: string | null,
-  primaryEmailVerified: boolean,
+  primaryEmail: string | null,
+  /** Must be `false` when `primaryEmail` is `null`. */
+  primaryEmailVerified: boolean,

Or, if the project prefers encoding the constraint in the type:

-  primaryEmail: string | null,
-  primaryEmailVerified: boolean,
+  primaryEmail: string,
+  primaryEmailVerified: boolean,
+  // use externalId-only records for users without an email

At minimum, add a JSDoc invariant so implementors can't set primaryEmailVerified: true on a null email silently.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
primaryEmail: string | null,
primaryEmailVerified: boolean,
primaryEmail: string | null,
/** Must be `false` when `primaryEmail` is `null`. */
primaryEmailVerified: boolean,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/migrations/src/types.ts` around lines 6 - 7, The two fields
primaryEmail and primaryEmailVerified in packages/migrations/src/types.ts are
inconsistent; update the type to couple them so verified cannot be true when
primaryEmail is null (for example replace the separate fields with a
discriminated union like a "hasPrimaryEmail" branch vs "noPrimaryEmail" branch,
or make primaryEmailVerified nullable/optional so it can only be true when
primaryEmail is a string), and add a JSDoc invariant on the exported
type/interface (referencing primaryEmail and primaryEmailVerified) to document
the constraint if you prefer not to encode it fully in the type system.

- Introduced the AuthDataMigrationJob model in the Prisma schema to manage authentication migration jobs.
- Created SQL migration scripts to establish the AuthDataMigrationJob table with necessary constraints and indexes.
- Implemented API routes for creating, listing, retrieving, and retrying auth migration jobs.
- Added utility functions for handling encryption and decryption of migration credentials.
- Developed tests to validate the functionality and constraints of the new migration job model.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants