Add a Better Auth-backed migration layer for Stack Auth#1403
Add a Better Auth-backed migration layer for Stack Auth#1403mantrakp04 wants to merge 2 commits intodevfrom
Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughA new Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryThis PR introduces a new
Confidence Score: 3/5Not 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 Important Files Changed
Sequence DiagramsequenceDiagram
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
Prompt To Fix All With AIFix 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 |
| 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({})); | ||
| } |
There was a problem hiding this 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.
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.| function withMembershipMetadata(body: JsonObject): JsonObject { | ||
| return body; | ||
| } |
There was a problem hiding this 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.
| 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.| 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"), | ||
| }, | ||
| }; | ||
| } |
There was a problem hiding this 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.
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.| "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" |
There was a problem hiding this 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.
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.There was a problem hiding this comment.
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/migrationspackage 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; |
| 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, | ||
| })); |
| 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 }; |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (2)
packages/migrations/src/types.ts (2)
82-88: ⚡ Quick win
StackMigrationPlan.membershipsis structurally inconsistent withusersandteams.
usersandteamswrap their payload in a namedbodyfield (StackUserCreateBody/StackTeamCreateBody), butmembershipsinlines the fields directly without abodywrapper 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
StackImportOptionsfields 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
📒 Files selected for processing (18)
claude/CLAUDE-KNOWLEDGE.mddocs-mintlify/docs.jsondocs-mintlify/guides/faq.mdxdocs-mintlify/guides/going-further/migrations.mdxdocs/content/docs/(guides)/faq.mdxpackages/migrations/README.mdpackages/migrations/eslint.config.mjspackages/migrations/package.jsonpackages/migrations/scripts/test-clerk-to-stack.tspackages/migrations/src/adapters/better-auth.test.tspackages/migrations/src/adapters/better-auth.tspackages/migrations/src/cli.tspackages/migrations/src/core.tspackages/migrations/src/index.tspackages/migrations/src/stack-api.tspackages/migrations/src/types.tspackages/migrations/tsconfig.jsonpackages/migrations/tsdown.config.ts
| rules: { | ||
| ...tsPlugin.configs.recommended.rules, | ||
| "@typescript-eslint/no-explicit-any": "error", | ||
| }, |
There was a problem hiding this comment.
🧩 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 -100Repository: stack-auth/stack-auth
Length of output: 956
🏁 Script executed:
cat -n packages/migrations/eslint.config.mjs | head -30Repository: 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.jsonRepository: 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 -5Repository: 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 -100Repository: 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 -50Repository: 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 -80Repository: 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 -100Repository: 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 -150Repository: 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 -100Repository: 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 2Repository: 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 -100Repository: 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 -10Repository: 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.tsRepository: 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 -20Repository: 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 -20Repository: 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 -50Repository: 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 .rulesRepository: 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:
- 1: https://github.com/typescript-eslint/typescript-eslint/blob/v8.58.1/docs/packages/TypeScript_ESLint.mdx
- 2: https://v8--typescript-eslint.netlify.app/users/configs
- 3: https://tessl.io/registry/tessl/npm-typescript-eslint--eslint-plugin/8.42.0/files/docs/plugin-configuration.md
- 4: https://typescript-eslint.io/blog/announcing-typescript-eslint-v8
- 5: https://typescript-eslint.io/users/configs/
- 6: https://github.com/typescript-eslint/typescript-eslint/blob/v6/packages/eslint-plugin/src/configs/all.ts
- 7: https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/src/index.ts
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 @@ | |||
| { | |||
There was a problem hiding this comment.
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.
| "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" | ||
| } |
There was a problem hiding this comment.
🧩 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 -10Repository: 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"
doneRepository: 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.jsonRepository: 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 howstack-sharedandstack-schandle this—they omit it entirely and let.jsdefault 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
.cjsfiles for CJS builds (requires changingoutExtensionsincreateJsLibraryTsupConfig).
🤖 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.
| await persistence.flushToStackAuth({ | ||
| apiUrl: "http://localhost:8102", | ||
| projectId: process.env.STACK_PROJECT_ID!, | ||
| secretServerKey: process.env.STACK_SECRET_SERVER_KEY!, | ||
| }); | ||
| ``` |
There was a problem hiding this comment.
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.
| 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({})); |
There was a problem hiding this comment.
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.
| 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.
| primaryEmail: string | null, | ||
| primaryEmailVerified: boolean, |
There was a problem hiding this comment.
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 emailAt 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.
| 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.
Summary
@stackframe/migrationspackage for provider migrations into Stack Authuser,account,organization, andmemberrecords into Stack Auth users, OAuth accounts, teams, and membershipsTesting
pnpm -C packages/migrations typecheckpnpm -C packages/migrations testpnpm -C packages/migrations lintpnpm -C docs-mintlify lintSummary by CodeRabbit
New Features
Documentation