stack/.claude/CLAUDE-KNOWLEDGE.md
2026-05-22 16:28:43 -07:00

46 KiB

CLAUDE-KNOWLEDGE.md

This file contains knowledge learned while working on the codebase in Q&A format.

Q: How should GitHub Contents API request-body assertions be written in Stack Auth tests?

A: Prefer inline snapshots over individual field selectors. For request bodies that contain base64 file content, parse the JSON body, assert it is an object, decode the content field back to UTF-8, and snapshot the normalized call object so the test verifies the path, method, headers, branch, message, sha, and rendered file content together.

Q: How should Stack CLI GitHub source paths be stored?

A: Explicit stack config push --source github paths should be normalized as repo-relative paths before storing source metadata. Trim whitespace and strip leading ./, repeated ./, and leading / segments, matching the dashboard workflow generator's normalization for STACK_AUTH_CONFIG_PATH and workflow paths.

Q: How should Stack CLI code handle flags proven present by nearby validation?

A: Avoid non-null assertions even when an earlier missing-flags check proves presence. Use flags.foo ?? throwErr("Expected ...; this should have been caught by ...") so the type system receives a definite value and future refactors fail loudly with the violated assumption.

Q: How do anonymous users work in Stack Auth?

A: Anonymous users are a special type of user that can be created without any authentication. They have isAnonymous: true in the database and use different JWT signing keys with a role: 'anon' claim. Anonymous JWTs use a prefixed secret ("anon-" + audience) for signing and verification.

Q: How are anonymous user JWTs different from regular user JWTs?

A: Anonymous JWTs have:

  1. Different kid (key ID) - prefixed with "anon-" in the generation
  2. Different signing secret - uses getPerAudienceSecret with isAnonymous: true
  3. Contains role: 'anon' in the payload
  4. Must pass isAnonymous flag to both getPrivateJwk and getPublicJwkSet functions for proper verification

Q: What is the X-Stack-Allow-Anonymous-User header?

A: This header controls whether anonymous users are allowed to access an endpoint. When set to "true" (which is the default for client SDK calls), anonymous JWTs are accepted. When false or missing, anonymous users get an AnonymousAuthenticationNotAllowed error.

Q: How do you upgrade an anonymous user to a regular user?

A: When an anonymous user (identified by is_anonymous: true) signs up or signs in through any auth method (password, OTP, OAuth), instead of creating a new user, the system upgrades the existing anonymous user by:

  1. Setting is_anonymous: false
  2. Adding the authentication method (email, password, OAuth provider, etc.)
  3. Keeping the same user ID so old JWTs remain valid

Q: How do you access the current user in smart route handlers?

A: In smart route handlers, the user is accessed through fullReq.auth?.user not through the destructured auth parameter. The auth parameter only guarantees tenancy, while user is optional and needs to be accessed from the full request.

Q: How do user CRUD handlers work with parameters?

A: The adminUpdate and similar methods take parameters directly, not wrapped in a params object:

  • Correct: adminUpdate({ tenancy, user_id: "...", data: {...} })
  • Wrong: adminUpdate({ tenancy, params: { user_id: "..." }, data: {...} })

Q: What query parameter filters anonymous users in user endpoints?

A: The include_anonymous query parameter controls whether anonymous users are included in results:

  • Without parameter or include_anonymous=false: Anonymous users are filtered out
  • With include_anonymous=true: Anonymous users are included in results This applies to user list, get by ID, search, and team member endpoints.

Q: How does the JWKS endpoint handle anonymous keys?

A: The JWKS (JSON Web Key Set) endpoint at /.well-known/jwks.json:

  • By default: Returns only regular user signing keys
  • With ?include_anonymous=true: Returns both regular and anonymous user signing keys This allows systems that need to verify anonymous JWTs to fetch the appropriate public keys.

Q: What is the typical test command flow for Stack Auth?

A:

  1. pnpm typecheck - Check TypeScript compilation
  2. pnpm lint --fix - Fix linting issues
  3. pnpm test run <path> - Run specific tests (the run is important to avoid watch mode)
  4. Use -t "test name" to run specific tests by name

Q: How do E2E tests handle authentication in Stack Auth?

A: E2E tests use niceBackendFetch which automatically:

  • Sets x-stack-allow-anonymous-user: "true" for client access type
  • Includes project keys and tokens from backendContext.value
  • Handles auth tokens through the context rather than manual header setting

Q: What is the signature of a verification code handler?

A: The handler function in createVerificationCodeHandler receives 5 parameters:

async handler(tenancy, validatedMethod, validatedData, requestBody, currentUser)

Where:

  • tenancy - The tenancy object
  • validatedMethod - The validated method data (e.g., { email: "..." })
  • validatedData - The validated data object
  • requestBody - The raw request body
  • currentUser - The current authenticated user (if any)

Q: How does JWT key derivation work for anonymous users?

A: The JWT signing/verification uses a multi-step key derivation process:

  1. Secret Derivation: getPerAudienceSecret() creates a derived secret from:
    • Base secret (STACK_SERVER_SECRET)
    • Audience (usually project ID)
    • Optional "anon-" prefix for anonymous users
  2. Kid Generation: getKid() creates a key ID from:
    • Base secret (STACK_SERVER_SECRET)
    • "kid" string with optional "anon-" prefix
    • Takes only first 12 characters of hash
  3. Key Generation: Private/public keys are generated from the derived secret

Q: What is the JWT signing and verification flow?

A: Signing (signJWT):

  1. Derive secret: getPerAudienceSecret(audience, STACK_SERVER_SECRET, isAnonymous)
  2. Generate kid: getKid(STACK_SERVER_SECRET, isAnonymous)
  3. Create private key from derived secret
  4. Sign JWT with kid in header and role in payload

Verification (verifyJWT):

  1. Decode JWT without verification to read the role
  2. Check if role === 'anon' to determine if it's anonymous
  3. Derive secret with same parameters as signing
  4. Generate kid with same parameters as signing
  5. Create public key set and verify JWT

Q: What makes anonymous JWTs different from regular JWTs?

A: Anonymous JWTs have:

  1. Different derived secret: Uses "anon-" prefix in secret derivation
  2. Different kid: Uses "anon-" prefix resulting in different key ID
  3. Role field: Contains role: 'anon' in the payload
  4. Verification requirements: Requires allowAnonymous: true flag to be verified

Q: How do you debug JWT verification issues?

A: Common debugging steps:

  1. Check that the X-Stack-Allow-Anonymous-User header is set to "true"
  2. Verify the JWT has role: 'anon' in its payload
  3. Ensure the same secret derivation parameters are used for signing and verification
  4. Check that the kid in the JWT header matches the expected kid
  5. Verify that allowAnonymous flag is passed through the entire call chain

Q: What is the difference between getPrivateJwk and getPrivateJwkFromDerivedSecret?

A:

  • getPrivateJwk(secret, isAnonymous): Takes a base secret, may derive it internally, generates kid
  • getPrivateJwkFromDerivedSecret(derivedSecret, kid): Takes an already-derived secret and pre-calculated kid The second is used internally for the actual JWT signing flow, while the first is for backward compatibility and special cases like IDP.

Q: How does the JWT verification process work with jose?

A: The jose.jwtVerify function:

  1. Extracts the kid from the JWT header
  2. Looks for a key with matching kid in the provided JWK set
  3. Uses that key to verify the JWT signature
  4. If no matching kid is found, verification fails with an error

Q: What causes UNPARSABLE_ACCESS_TOKEN errors?

A: This error occurs when JWT verification fails in decodeAccessToken. Common causes:

  1. Kid mismatch - the kid in the JWT header doesn't match any key in the JWK set
  2. Wrong secret derivation - using different parameters for signing vs verification
  3. JOSEError thrown during jose.jwtVerify due to invalid signature or key mismatch

OAuth Flow and Validation

Q: Where does OAuth redirect URL validation happen in the flow?

A: The validation happens in the callback endpoint (/api/v1/auth/oauth/callback/[provider_id]/route.tsx), not in the authorize endpoint. The authorize endpoint just stores the redirect URL and redirects to the OAuth provider. The actual validation occurs when the OAuth provider calls back, and the oauth2-server library validates the redirect URL.

Q: How do you test OAuth flows that should fail?

A: Use Auth.OAuth.getMaybeFailingAuthorizationCode() instead of Auth.OAuth.getAuthorizationCode(). The latter expects success (status 303), while the former allows you to test failure cases. The failure happens at the callback stage with a 400 status and specific error message.

Q: What error is thrown for invalid redirect URLs in OAuth?

A: The callback endpoint returns a 400 status with the message: "Invalid redirect URI. The URL you are trying to redirect to is not trusted. If it should be, add it to the list of trusted domains in the Stack Auth dashboard."

Wildcard Pattern Implementation

Q: How do you handle ** vs * precedence in regex patterns?

A: Use a placeholder approach to prevent ** from being corrupted when replacing *:

const doubleWildcardPlaceholder = '\x00DOUBLE_WILDCARD\x00';
regexPattern = regexPattern.replace(/\*\*/g, doubleWildcardPlaceholder);
regexPattern = regexPattern.replace(/\*/g, '[^.]*');
regexPattern = regexPattern.replace(new RegExp(doubleWildcardPlaceholder, 'g'), '.*');

Q: Why can't you use new URL() with wildcard domains?

A: Wildcard characters (* and **) are not valid in URLs and will cause parsing errors. For wildcard domains, you need to manually parse the URL components instead of using the URL constructor.

Q: How do you validate URLs with wildcards?

A: Extract the hostname pattern manually and use matchHostnamePattern():

const protocolEnd = domain.baseUrl.indexOf('://');
const protocol = domain.baseUrl.substring(0, protocolEnd + 3);
const afterProtocol = domain.baseUrl.substring(protocolEnd + 3);
const pathStart = afterProtocol.indexOf('/');
const hostnamePattern = pathStart === -1 ? afterProtocol : afterProtocol.substring(0, pathStart);

Testing Best Practices

Q: How should you run multiple independent test commands?

A: Use parallel execution by batching tool calls together:

// Good - runs in parallel
const [result1, result2] = await Promise.all([
  niceBackendFetch("/endpoint1"),
  niceBackendFetch("/endpoint2")
]);

// In E2E tests, the framework handles this automatically when you
// batch multiple tool calls in a single response

Q: What's the correct way to update project configuration in E2E tests?

A: Use the /api/v1/internal/config/override/environment endpoint with PATCH method and admin access token:

await niceBackendFetch("/api/v1/internal/config/override/environment", {
  method: "PATCH",
  accessType: "admin",
  headers: {
    'x-stack-admin-access-token': adminAccessToken,
  },
  body: {
    config_override_string: JSON.stringify({
      'domains.trustedDomains.name': { baseUrl: '...', handlerPath: '...' }
    }),
  },
});

Code Organization

Q: Where does domain validation logic belong?

A: Core validation functions (isValidHostnameWithWildcards, matchHostnamePattern) belong in the shared utils package (packages/stack-shared/src/utils/urls.tsx) so they can be used by both frontend and backend.

Q: How do you simplify validation logic with wildcards?

A: Replace wildcards with valid placeholders before validation:

const normalizedDomain = domain.replace(/\*+/g, 'wildcard-placeholder');
url = new URL(normalizedDomain); // Now this won't throw

Debugging E2E Tests

Q: What does "ECONNREFUSED" mean in E2E tests?

A: The backend server isn't running. Make sure to start the backend with pnpm dev before running E2E tests.

Q: How do you debug which stage of OAuth flow is failing?

A: Check the error location:

  • Authorize endpoint (307 redirect) - Initial request succeeded
  • Callback endpoint (400 error) - Validation failed during callback
  • Token endpoint (400 error) - Validation failed during token exchange

Q: How should connected-account OAuth access-token refresh errors be classified?

A: In apps/backend/src/oauth/providers/base.tsx, invalid/revoked refresh-token provider errors such as invalid_grant return Result.error({ type: "invalid-refresh-token", ... }) so access-token-helpers.tsx can invalidate that stored refresh token and try another. Transient provider/network failures such as openid-client RPError: outgoing request timed out after 3500ms return Result.error({ type: "temporarily-unavailable", cause }); the connected-account helper converts that to OAuthProviderTemporarilyUnavailable without invalidating the refresh token. Expected refresh outcomes should be represented in the provider return type instead of thrown as known/status errors. Refresh requests use a 6s openid-client HTTP timeout and retry transient failures once. If a retry sees invalid_grant after an ambiguous transient failure, keep treating it as temporarily unavailable rather than invalidating the refresh token, because the first request may have reached the provider and rotated the token before our client timed out. Sentry should capture non-revocation refresh issues (temporary provider failure, invalid client, unexpected) with provider id/class, attempts, retry count, ambiguity state, final cause, and all provider errors seen during attempts; normal revoked/expired refresh tokens should not be reported.

Git and Development Workflow

Q: How should you format git commit messages in this project?

A: Use a HEREDOC to ensure proper formatting:

git commit -m "$(cat <<'EOF'
Commit message here.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
EOF
)"

Q: What commands should you run before considering a task complete?

A: Always run:

  1. pnpm test run <relevant-test-files> - Run tests
  2. pnpm lint - Check for linting errors
  3. pnpm typecheck - Check for TypeScript errors

Common Pitfalls

Q: Why might imports get removed after running lint --fix?

A: ESLint may remove "unused" imports. Always verify your changes after auto-fixing, especially if you're using imports in a way ESLint doesn't recognize (like in test expectations).

Q: What's a common linting error in test files?

A: Missing newline at end of file. ESLint requires files to end with a newline character.

Q: How do you handle TypeScript errors about missing exports?

A: Double-check that you're only importing what's actually exported from a module. The error "Module declares 'X' locally, but it is not exported" means you're trying to import something that isn't exported.

Project Transfer Implementation

Q: How do I add a new API endpoint to the internal project?

A: Create a new route file in /apps/backend/src/app/api/latest/internal/ using the createSmartRouteHandler pattern. Internal endpoints should check auth.project.id === "internal" and throw KnownErrors.ExpectedInternalProject() if not.

Q: How do team permissions work in Stack Auth?

A: Team permissions are defined in /apps/backend/src/lib/permissions.tsx. The permission team_admin (not $team_admin) is a normal permission that happens to be defined by default on the internal project. Use ensureUserTeamPermissionExists to check if a user has a specific permission.

Q: How do I check team permissions in the backend?

A: Use ensureUserTeamPermissionExists from /apps/backend/src/lib/request-checks.tsx. Example:

await ensureUserTeamPermissionExists(prisma, {
  tenancy: internalTenancy,
  teamId: teamId,
  userId: userId,
  permissionId: "team_admin",
  errorType: "required",
  recursive: true,
});

Q: How do I add new functionality to the admin interface?

A: Don't use server actions. Instead, implement the endpoint functions on the admin-app and admin-interface. Add methods to the AdminProject class in the SDK packages that call the backend API endpoints.

Q: How do I use TeamSwitcher component in the dashboard?

A: Import TeamSwitcher from @stackframe/stack and use it like:

<TeamSwitcher
  triggerClassName="w-full"
  teamId={selectedTeamId}
  onChange={async (team) => {
    setSelectedTeamId(team.id);
  }}
/>

Q: How do I write E2E tests for backend endpoints?

A: Import it from helpers (not vitest), and set up the project context inside each test:

import { describe } from "vitest";
import { it } from "../../../../../../helpers";
import { Auth, Project, backendContext, niceBackendFetch, InternalProjectKeys } from "../../../../../backend-helpers";

it("test name", async ({ expect }) => {
  backendContext.set({ projectKeys: InternalProjectKeys });
  await Project.createAndSwitch({ config: { magic_link_enabled: true } });
  // test logic
});

Q: Where is project ownership stored in the database?

A: Projects have an ownerTeamId field in the Project model (see /apps/backend/prisma/schema.prisma). This links to a team in the internal project.

Q: What's the difference between ensureTeamMembershipExists and ensureUserTeamPermissionExists?

A: ensureTeamMembershipExists only checks if a user is a member of a team. ensureUserTeamPermissionExists checks if a user has a specific permission (like team_admin) within that team. The latter also calls ensureTeamMembershipExists internally.

Q: How do I handle errors in the backend API?

A: Use KnownErrors from @stackframe/stack-shared for standard errors (e.g., KnownErrors.ProjectNotFound()). For custom errors, use StatusError from @stackframe/stack-shared/dist/utils/errors with an HTTP status code and message.

Q: What's the pattern for TypeScript schema validation in API routes?

A: Use yup schemas from @stackframe/stack-shared/dist/schema-fields. Don't use regular yup imports. Example:

import { yupObject, yupString, yupNumber } from "@stackframe/stack-shared/dist/schema-fields";

A: Projects belong to teams via the ownerTeamId field. Teams exist within the internal project. Users can be members of multiple teams and have different permissions in each team.

Q: How do I properly escape quotes in React components to avoid lint errors?

A: Use template literals with backticks instead of quotes in JSX text content:

<Typography>{`Text with "quotes" inside`}</Typography>

Q: What auth headers are needed for internal API calls?

A: Internal API calls need:

  • X-Stack-Access-Type: 'server'
  • X-Stack-Project-Id: 'internal'
  • X-Stack-Secret-Server-Key: <server key>
  • Either X-Stack-Auth: Bearer <token> or a session cookie

Q: How do I reload the page after a successful action in the dashboard?

A: Use window.location.reload() after the action completes. This ensures the UI reflects the latest state from the server.

Q: What's the file structure for API routes in the backend?

A: Routes follow Next.js App Router conventions in /apps/backend/src/app/api/latest/. Each route has a route.tsx file that exports HTTP method handlers (GET, POST, etc.).

Q: How do I get all teams a user is a member of in the dashboard?

A: Use user.useTeams() where user is from useUser({ or: 'redirect', projectIdMustMatch: "internal" }).

Q: What's the difference between client and server access types?

A: Client access type is for frontend applications and has limited permissions. Server access type is for backend operations and requires a secret key. Admin access type is for dashboard operations with full permissions.

Q: How to avoid TypeScript "unnecessary conditional" errors when checking auth.user?

A: If the schema defines auth.user as .defined(), TypeScript knows it can't be null, so checking if (!auth.user) causes a lint error. Remove the check or adjust the schema if the field can be undefined.

Q: What to do when TypeScript can't find module '@stackframe/stack' declarations?

A: This happens when packages haven't been built yet. Run these commands in order:

pnpm clean && pnpm i && pnpm codegen && pnpm build:packages

Then restart the dev server. This rebuilds all packages and generates the necessary TypeScript declarations.

Q: How is backwards compatibility for the offer→product rename handled in the payments purchase APIs?

A: API v1 requests are routed through the v2beta1 migration. The migration wraps the latest handlers, accepts legacy offer_id/offer_inline request fields, translates product-related errors back to the old offer error codes/messages, and augments responses (like validate-code) with offer/conflicting_group_offers aliases alongside the new product fields. Newer API versions keep the product-only contract.

Q: How does the Stack Auth template dev tool decide whether to iframe the dashboard?

A: The dev tool always iframes the Dashboard tab and provides an "Open in New Tab" escape hatch for auth or framing issues.

Q: What's the reliable way to run targeted tests across backend, dashboard, stack-shared, and e2e at once?

A: Run from the monorepo root with explicit file paths: pnpm test run "<path1>" "<path2>" .... This works even when individual packages do not define a local test script. Also avoid passing an extra run argument to package-level test scripts that already execute vitest run.

Q: What's the new Authorization header format for Stack token forwarding?

A: Use getAuthorizationHeader(), which returns Bearer stackauth_<base64(getAuthJson())>. The payload encodes both accessToken and refreshToken, and request-like token stores should parse this format first, with legacy x-stack-auth remaining as a backward-compatible fallback.

Q: What RequestLike header shapes are supported by tokenStore overrides?

A: RequestLike accepts both { headers: { get(name): string | null } } and { headers: Record<string, string | null> }. Header lookup is case-insensitive for record-style headers, and supports authorization, x-stack-auth, and cookie.

Q: Which env var should emulator onboarding URLs use for dashboard port?

A: Use EMULATOR_DASHBOARD_PORT (default 26700) or explicit STACK_LOCAL_EMULATOR_DASHBOARD_URL. Do not derive emulator URLs from NEXT_PUBLIC_STACK_PORT_PREFIX, because that points to the host dev environment ports (e.g. 92xx) rather than the emulator host-forwarded ports.

Q: Why does PATCH /api/v1/internal/projects/current fail in local emulator when updating only onboarding_state?

A: createOrUpdateProjectWithLegacyConfig always called overrideEnvironmentConfigOverride, even when there were zero config override keys to apply. In local emulator mode, environment config overrides are intentionally blocked, so this threw Environment configuration overrides cannot be changed in the local emulator and returned 500. The fix is to skip overrideEnvironmentConfigOverride unless configOverrideOverride has at least one key.

Q: Why might local emulator UI changes in apps/dashboard not appear immediately at localhost:26700?

A: The QEMU local emulator serves the dashboard from the Docker image bundled inside the VM, not from the host repo's live source tree. Source edits in apps/dashboard are reflected in lint/typecheck/tests immediately, but you need an updated emulator image/runtime to see the visual change on 26700.

Q: Why can local emulator onboarding break with ParseError on non-.ts config files (e.g. test-config.untracked)?

A: The emulator writes TypeScript-style config source (import type ... and config: StackConfig) and later evaluates it with Jiti. If the filename has a non-TS extension, Jiti may parse it as plain JS and fail. Fix by evaluating unknown extensions as TypeScript (use a .ts eval filename fallback) and add regression coverage for non-.ts config paths.

Q: How should docs fetch the canonical AI setup prompt text?

A: Expose an unauthenticated backend endpoint at /api/v1/setup-prompt that returns getSdkSetupPrompt("ai-prompt", { tanstackQuery: false }) as plain text and sets Cache-Control: public, max-age=60. Mintlify docs should fetch https://api.stack-auth.com/api/v1/setup-prompt directly when docs and API are on different origins.

Q: Can Mintlify snippets import other snippets?

A: No. Keep snippet logic inline within each snippet file; avoid snippet-to-snippet imports. For setup prompt fetching, point directly to https://api.stack-auth.com/api/v1/setup-prompt when docs run on a different origin/port than the API.

Q: How does /api/v1/ai/query/generate reject invalid AI tool names?

A: Invalid tools entries are rejected by requestBodySchema in apps/backend/src/lib/ai/schema.ts via yupString().oneOf(TOOL_NAMES), so the endpoint returns a structured SCHEMA_ERROR object mentioning body.tools[n] rather than a custom "Invalid tool names" string from handler logic.

Q: Why did the internal metrics E2E snapshots need to change in April 2026?

A: The /api/v1/internal/metrics response now intentionally includes analytics_overview.daily_anonymous_visitors_fallback, analytics_overview.anonymous_visitors_fallback, and active_users_by_country. Those additions are reflected in packages/stack-shared/src/interface/admin-metrics.ts and the backend route, so the E2E snapshots must include them instead of treating them as regressions.

Q: Why can environment config override writes fail with a product/product-line customer type warning after creating a preview project?

A: The environment override endpoint validates the new environment override against the rendered branch config. Preview dummy payments data must therefore be internally coherent: products assigned to a product line need the same customerType as that product line, otherwise unrelated environment patches can fail with warnings like Product "growth" has customer type "user" but its product line "workspace" has customer type "team".

Q: How do you keep the Stack Auth dev tool from reopening automatically after navigation or reload?

A: Treat isOpen as mount-local state in packages/template/src/dev-tool/dev-tool-core.ts: load persisted preferences with isOpen: false, and save state back to localStorage with isOpen: false so tab/size preferences persist without reopening the panel on the next mount.

Q: How should the Stack Auth dev tool indicator avoid being hidden by other dev indicators?

A: Do not dynamically reflow around framework indicators; that makes pointer interaction brittle. Keep the trigger anchored to its saved corner and give .sdt-trigger a max practical z-index (2147483647) so the Stack indicator renders above Next/Turbo overlays.

Q: How should Stack Auth dev tool trigger movement feel?

A: Dragging should remain instant/direct, but programmatic moves like snap-to-corner after drag, resize reposition, and post-measurement correction should use a short snappy left/top transition. In dev-tool-core.ts, toggle a dedicated animation class only for those programmatic updates and remove it shortly after.

Q: How do you prevent duplicate Stack Auth dev tool indicators from multiple package/module instances?

A: createDevTool in packages/template/src/dev-tool/dev-tool-core.ts should register a browser-wide singleton instance on window with an idempotent cleanup function, call any previous global cleanup before mounting, and remove leftover #__stack-dev-tool-root nodes as a fallback for older instances that did not register cleanup.

Q: How should the Stack Auth dev tool handle Dashboard tab sizing?

A: Keep the user's default panel width/height in state for normal tabs, but apply a transient fullscreen class while the active tab is dashboard. The fullscreen class should override fixed dimensions and hide resize handles, then remove itself and restore the saved default dimensions when any other tab is selected.

Q: How should the Stack Auth dev tool animate Dashboard fullscreen transitions?

A: Add a short-lived geometry animation class only around tab-driven switches into or out of Dashboard fullscreen. Animate width, height, right, bottom, and radius for the mode change, then remove the class so manual dragging/resizing remains direct and does not lag.

Q: Should the Stack Auth dev tool Dashboard tab be gated on local emulator mode?

A: No. The Dashboard tab should always render the dashboard URL in an iframe inside the dev tool, and should also show an "Open in New Tab" link so users can escape iframe/auth/framing issues without losing the embedded view.

Q: How should the Dashboard iframe use space in the Stack Auth dev tool?

A: In Dashboard fullscreen mode, the panel should cover the full viewport with no inset or rounded frame, and the iframe should fill all available content space. Put auxiliary actions like "Open in New Tab" in a top-edge overlay below the tab bar so they do not reserve layout height from the iframe.

Q: How do you maximize iframe tabs inside the Stack Auth dev tool panel?

A: Mark Docs/Dashboard panes with an iframe-specific class, remove the normal 16px tab-pane padding, hide pane overflow, and give the iframe container explicit width: 100% and height: 100%. Keep toolbar actions as absolute overlays so they do not reduce iframe layout space.

Q: How should docs access work in the Stack Auth dev tool?

A: Docs should not be an iframe-backed tab. Keep docs as a top-bar external link to https://docs.stack-auth.com with an up-right arrow icon, and migrate any persisted activeTab: "docs" value back to overview.

Q: How should the Stack Auth dev tool Console tab handle large log volumes?

A: Keep the full log history in the shared log store, but render only the newest 100 entries initially. When the log scroll area nears the bottom, increase the visible count by another 100 and rerender. The Console tab should be a single logs view with Copy, Export, and Clear buttons in the header rather than nested Logs/Config subtabs.

Q: How should the Stack Auth dev tool Customize page detail show page metadata?

A: Avoid repeated page-type badges like Handler in page tiles or detail headers. Keep actionable badges such as Outdated, show the compact route path next to the page title, use Open for the page action, and phrase the customization prompt as "Want to customize this page? Paste this prompt into your coding agent." with a Copy prompt button below it.

Q: How should the Stack Auth dev tool Customize page Open action behave?

A: The page detail Open action should be a taller button matching the redirect code chip height, include a small up-right arrow icon, and always open the selected page in a new tab.

Q: How should the Stack Auth dev tool Support tab be structured?

A: Do not keep top-level Feedback/Feature Request subtabs unless the backend supports them. The Support tab should mount the feedback form directly, show Discord/Email/GitHub links at the top, keep only Feedback and Bug Report choices in the form, and use a Submit button with a right-arrow icon after the text.

Q: Which tab should the Stack Auth dev tool show when opened?

A: Opening the dev tool should always reset activeTab to overview before creating the panel. Other preferences like size can persist, but each fresh open should start on Overview instead of the last-used tab.

Q: How should Stack Auth dev tool PR review comments around Overview and trigger behavior be handled?

A: Keep trigger corner resolution on-screen even in tiny viewports by snapping to bounded edge positions, remove unused trigger-position helpers, treat browser fetch TypeErrors such as Safari's Load failed as best-effort Overview hydration errors, replace auth-method skeletons with a fallback on every load failure, and compute the Auth method active checklist row from loaded project config instead of hard-coding it as passing.

Q: Why can pnpm run dev fail with ERR_MODULE_NOT_FOUND for @stackframe/stack/dist/esm/index.js during OpenAPI docs generation?

A: Root dev starts the OpenAPI docs watcher at the same time as package dev watchers. If a package dev script removes dist before tsdown --watch recreates it, the docs generator can import apps/backend/src/stack.tsx while @stackframe/stack's ESM entrypoint is temporarily missing. Package watch scripts should update dist in place, and eager generators should wait for package imports to resolve before running.

Q: How do SDK source tests replace the compile-time client version sentinel?

A: packages/template/vitest.config.ts installs a Vite transform plugin for Vitest that replaces STACK_COMPILE_TIME_CLIENT_PACKAGE_VERSION_SENTINEL with js <package-name>@<version> from the local package.json. Keep the plugin in packages/template so pnpm pre/scripts/generate-sdks.ts propagates it to packages/js, packages/react, and packages/stack; otherwise tests importing common.ts throw Client version was not replaced before test collection.

Q: How does the Mintlify apps sidebar filter stay in sync with theme changes?

A: docs-mintlify/apps-sidebar-filter.js injects the Apps filter with inline styles, so the MutationObserver must reapply applySidebarAppsFilterTheme when an existing input is found. Theme detection should handle both html.dark and data-theme="dark" signals.

Q: How should StackAssertionError preserve an underlying thrown error?

A: Pass the underlying error as the cause property in the second argument. The StackAssertionError constructor only forwards cause into ErrorOptions, so storing a caught error under an error property captures it as ordinary metadata instead of preserving the error cause chain.

Q: How does the local QEMU emulator expose host-side control channels?

A: docker/local-emulator/qemu/run-emulator.sh daemonizes QEMU with a QMP monitor socket at $EMULATOR_RUN_DIR/vm/monitor.sock, a QEMU guest agent socket at $EMULATOR_RUN_DIR/vm/qga.sock, and serial output redirected to $EMULATOR_RUN_DIR/vm/serial.log. The default user networking forwards only Stack-facing service ports, not SSH.

Q: Where should remote development environment local state live?

A: Use ~/.stack/dev-envs.json on macOS/Linux and %LOCALAPPDATA%\Stack Auth\dev-envs.json on Windows for local remote-development-environment state. The CLI and local dashboard both read this file; it stores the local dashboard bearer secret, anonymous refresh token, and config-path-to-project credential mappings with owner-only permissions.

Q: How should stack dev run the local dashboard in a published CLI?

A: The CLI cannot depend on apps/dashboard source being present or run next dev. Package a Next.js standalone dashboard build into packages/stack-cli/dist/dashboard, copy it to a writable runtime directory next to the RDE state file, replace dashboard STACK_ENV_VAR_SENTINEL_* values for that launch, and run the standalone apps/dashboard/server.js with node, HOSTNAME=127.0.0.1, PORT=26700, and NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT=true.

Q: Where should the RDE local dashboard self-shutdown lifecycle start?

A: Start the RDE lifecycle from the dashboard server startup path (apps/dashboard/src/instrumentation.ts) when NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT=true, not lazily after session registration. Keep the shutdown timer idempotent and use a short initial empty-session grace period so failed session registration still exits instead of leaving an orphaned standalone dashboard process. Once a CLI session has been explicitly closed, the dashboard can skip the startup grace and exit on the next shutdown tick if no sessions or operations remain.

Q: How should the dashboard Stack app be split for the local remote development environment?

A: Keep apps/dashboard/src/stack/client.tsx as the root StackProvider app and handler app so the local RDE dashboard can boot without STACK_SECRET_SERVER_KEY. Put StackServerApp in apps/dashboard/src/stack/server.tsx, inherit from the client app, and only import it from server-only routes that are not needed in local RDE.

Q: How should stack dev handle a local RDE dashboard outage while the child command is still running?

A: Keep it in the existing heartbeat path. If the heartbeat cannot reach the local dashboard and the dashboard session has passed the 5-second stability window, restart the standalone dashboard and re-register the RDE session; otherwise throw to avoid restart loops.

Q: How should the local RDE dashboard authenticate in the browser without exposing refresh tokens?

A: The browser should fetch only a short-lived access token from the local RDE auth endpoint, install it into the memory token store with an empty refresh token, and refresh it by calling the local endpoint before expiry. Shared session logic must allow access-token-only sessions to read a still-valid access token; otherwise the SDK treats the session as absent and may redirect or create a separate anonymous user.

Q: Why can stack dev fail to register an RDE session with ECONNREFUSED against localhost?

A: The RDE dashboard does server-side SDK calls from Node. If the backend is configured as http://localhost:<port>, Node may resolve or probe loopback differently than the browser; normalize exact localhost API base URLs to 127.0.0.1 in the CLI. If the backend process is actually down, the dashboard log will still show ECONNREFUSED 127.0.0.1:<port> and the dev server needs to be restarted.

Q: How should Stack CLI --config-file options interpret paths?

A: --config-file should point directly to a regular config file. Do not treat an existing directory as a shortcut for stack.config.ts inside it; reject directories with a clear error instead. stack config pull may default to ./stack.config.ts when the flag is omitted, but an explicitly provided directory is still invalid.

Q: How should RDE PR-review fixes handle the local dashboard and CLI lifecycle?

A: Use the shared RDE browser security helper for browser-local endpoints, mark bearer-token responses Cache-Control: private, no-store, and return 400 for malformed local endpoint JSON. stack dev should fail loudly if a bundled dashboard sentinel has no environment value, validate session response shapes at runtime before using env, recover from HTTP heartbeat failures the same way as network heartbeat failures, and make heartbeat shutdown interruptible so child-process exit is not delayed by the full heartbeat interval.

Q: How should local RDE endpoints trust browser origins and API base URLs?

A: Browser-only RDE endpoints should accept only the exact local dashboard origin derived from the dashboard env/state, such as http://127.0.0.1:26700, and reject arbitrary localhost subdomains like evil.localhost. CLI bearer endpoints should require the bearer secret and a loopback host but should not use broad localhost origins as trust signals. RDE session registration should accept only https://api.stack-auth.com, the exact API base URL passed into the local dashboard by the CLI, or exact custom URLs from a STACK_-prefixed allowlist.

Q: How should the Stack CLI depend on the dashboard RDE standalone build in CI?

A: Do not invoke a nested turbo run build:rde-standalone from packages/stack-cli's build script. When the outer CI is already running turbo run build, that nested Turbo process can start apps/dashboard's Next build while the outer graph is also building it, causing .next/lock failures. Model the dependency in turbo.json instead with @stackframe/stack-cli#build depending on @stackframe/dashboard#build:rde-standalone, and let the CLI script only run tsdown plus runtime asset copying.

Q: How should local RDE/browser health endpoints handle active state and origins?

A: Browser-only RDE endpoints should require RDE to be enabled, a local dashboard state entry with a non-empty secret, loopback host, exact dashboard origin, and same-origin/none fetch metadata. Development-environment health checks should not trust broad localhost origins; reject origins like evil.localhost and only allow the exact expected dashboard origins (or no Origin header for same-origin polling).

Q: How should development-environment project creation seed environment config?

A: Seed the normal initial environment config before marking the project as isDevelopmentEnvironment=true. Existing development-environment projects should continue to reject environment config override writes, but creation needs to populate defaults like RBAC permissions, password sign-in, and installed apps first; otherwise the write guard throws during setup/restart-deps.

Q: What can cause React error #185 immediately on dashboard load?

A: React error #185 is a maximum update depth error. In the dashboard root, useSyncExternalStore snapshot getters must return cached referentially stable values. Returning a fresh object such as { status: "healthy" } from getSnapshot on every call can make React think the external store changed on every render and loop immediately. Use module-level constants for stable snapshots.

Q: How should client-side OAuth callback and nested cross-domain auth avoid racing session consumers?

A: Track startup auth transitions as pending client-app promises and make _getSession/react-like _useSession wait for them when using the default persistent token store. Auth-transition code that needs to inspect the current session should explicitly call _getSession(..., { awaitPendingAuthResolutions: false }) instead of relying on a global reentrancy flag.

Q: When should hosted OAuth callback handling auto-start on a client app page?

A: Only auto-start hosted OAuth callback handling when the current URL has code and state and the matching stack-oauth-outer-${state} verifier cookie exists. Generic code/state or errorCode/message query parameters are not Stack-owned enough to run callback processing automatically on every hosted app page.

Q: Should built-with hosted handler domains be manually configured as trusted domains?

A: No. Treat the hosted handler origin for the project, such as https://<project-id>.built-with-stack-auth.com or the origin derived from NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE, as an implicit trusted redirect domain on both client and backend validation paths. The hosted template must put {projectId} in the hostname so every project has its own origin; path-based templates like https://host/{projectId}/{hostedPath} are not safe for implicit origin trust.

Q: How should post-auth redirects mint cross-domain auth codes right after sign-in?

A: When a sign-in/sign-up/OAuth callback immediately redirects through the cross-domain authorize endpoint, pass the freshly returned { accessToken, refreshToken } as an override token store into the redirect path. Do not rely on browser cookies having already flushed; otherwise the request can pair a new access token with an old refresh token and fail the backend refresh-token/session consistency check.

Q: How should mixed-token cross-domain auth regressions be tested?

A: Add a source-level template test that creates a client app with a stale persistent token store, calls _createCrossDomainAuthRedirectUrl with overrideTokenStoreInit, and patches only sendClientRequest on the real client interface so session creation remains intact. The assertion should inspect the session passed to the interface and require the fresh refresh token, proving the cross-domain authorize request does not use stale persisted tokens.

Q: When should cross-domain auth call captureError?

A: Do not call captureError for normal cross-domain auth failures such as stale/deleted cookies, untrusted redirect URLs, invalid or mismatched refresh tokens, missing handoff params, or interrupted auth flows. Reserve captureError for states that definitely imply a Stack developer mistake; ordinary auth failures should just return or throw their normal user-facing errors.

Q: How should the npm publish workflow create the post-publish dev version bump?

A: The workflow needs a full checkout using the fine-grained NPM_PUBLISH_VERSION_UPDATE_PR_PAT secret. It then fetches origin/dev, checks out dev, creates a non-interactive patch changeset, runs pnpm changeset version, copies the generated packages/template/package.json version line back into packages/template/package-template.json, and commit/pushes chore: update package versions. Because direct pushes to dev are blocked by repository rules requiring PRs and the all-good status check, the PAT's owning user or bot account must be added to the ruleset bypass list with "Always allow" rather than "For pull requests only".

Q: How should the Mintlify docs homepage reuse the generated setup prompt?

A: Import generatedSetupPromptText from docs-mintlify/snippets/home-prompt-island.jsx in docs-mintlify/index.mdx, render it directly in a <pre><code>{generatedSetupPromptText}</code></pre> block, and keep the home copy button wired to that imported value. Clipboard failures can happen when the browser document is not focused, so the button should surface the actual error text instead of only saying "Copy failed".

Q: Where should Mintlify docs for restricted users live?

A: Put restricted-user docs at docs-mintlify/guides/apps/authentication/restricted-users.mdx and register the page in the Authentication group in docs-mintlify/docs.json. The page should cover includeRestricted: true, user.isRestricted, user.restrictedReason, anonymous users being restricted by definition, and JWKS include_restricted=true for services that intentionally accept restricted-user tokens.