Makes the repo's AI-agent setup tool-agnostic: instructions live in
`AGENTS.md` files, skills live in `.agents/skills/`, and Claude Code,
Codex, Cursor, and Copilot all read the same sources. Also sweeps the
skills for stale and duplicated content while everything was being
moved.
**Changed:**
- Every `CLAUDE.md` (root, `apps/studio`, `apps/docs`, `apps/kb`) is now
a one-line `@AGENTS.md` import; the content moved verbatim into an
`AGENTS.md` beside it. The root one moved from `.claude/CLAUDE.md` to
the repo root for consistency.
- All skills now live in `.agents/skills/`; `.claude/skills` is a single
symlink to it (replacing the old mix of real dirs and per-skill
symlinks). Path references in `.coderabbit.yaml`, code comments, and
docs updated to match.
- `.github/copilot-instructions.md` keeps only the review policy and
points at `AGENTS.md` + `.agents/skills/`. Copilot code review reads
those natively now, so the per-topic
`.github/instructions/*.instructions.md` files were duplicates of the
skills.
- Stale skill content fixed: `studio-queries` imported a toast library
Studio doesn't use, `telemetry-standards` and `studio-testing` used
import paths that don't resolve, `safe-sql-execution` cited a boundary
test that doesn't exist, the ask-the-docs references described an
`AiPrompt` mechanism that was replaced by the ID-keyed registry, plus a
handful of wrong paths, a self-contradicting `waitForTimeout` rule, an
invalid Playwright signature, and a ConfigCat flag described as PostHog.
- `studio-error-handling` now explains when to use `AlertError` (the
default) vs `ErrorMatcher`.
**Added:**
- `apps/docs/AGENTS.md` (docs test requirements, from the old Cursor
rule)
- `studio-shortcuts` skill (from the old Copilot instruction file,
verified against the current registry)
- `ask-the-docs/reference/graphql-endpoint.md` and
`search-embeddings.md` (from the old Cursor rules, with the missing
resolver/registration/codegen steps filled in)
- Feature-flag measurement section in `telemetry-standards`
**Removed:**
- `.cursor/` (rules folded in as above; skill symlinks no longer needed)
and `.cursorignore`
- `.github/instructions/` (8 files)
- `vercel-composition-patterns/AGENTS.md` – a 946-line verbatim
concatenation of its own `rules/` directory, and a nested `AGENTS.md`
that agents could auto-load as repo instructions
- `edit-the-docs/reference/structure-and-flow.md` – word-for-word copy
of the skill's own Phase 2 text
## To test
- `readlink .claude/skills` → `../.agents/skills`, and `ls
.claude/skills/copywriting/SKILL.md` resolves
- Open a Claude Code session at the repo root and in `apps/studio` – the
imported `AGENTS.md` content should load as before
- `git diff master --stat -M` shows the skill moves as 100% renames
(content unchanged except the listed fixes)
- Spot-check a fixed claim, e.g. `import { toast } from 'sonner'` in
`studio-queries`, or the `logs.all` ESLint rule cited in
`clickhouse-logs-queries/references/codebase-integration.md`
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **Documentation**
- Expanded guidance for documentation workflows, GraphQL resources,
search, ClickHouse logs, React forms, Studio testing, shortcuts,
telemetry, accessibility, copywriting, and composition patterns.
- Clarified local testing, linting, build workflows, error handling, and
AI coding agent usage.
- Added contributor guidance for the knowledge base, documentation, and
Studio areas.
- **Chores**
- Consolidated agent instructions and skill references.
- Removed obsolete editor-specific guidance, duplicate links, and
superseded documentation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
50 KiB
TanStack Start migration — route checklist
Temporary tracking doc. Delete once migration is done.
Runtime model — Next.js and TanStack Start run side-by-side
Throughout this migration both runtimes coexist in the same workspace:
- The Next.js pages router (
pages/...) and the TanStack route tree (routes/...) ship at the same time. The Vite/TanStack build is what we run today; the Next build (build:next/dev:nextscripts inapps/studio/package.json) stays alive as a fallback so we can bisect regressions and ship either runtime if needed. - Do not delete any
apps/studio/pages/...file during the per-route migration. Path A pages re-export theirpages/default export, so the Next file is load-bearing for both runtimes. Removing it breaks the Next build and breaks the TanStack route too. - Body-moves and
pages/...deletion happen only in the final cleanup pass, after every route is represented inroutes/...and we're ready to retire the Next runtime entirely. That's a separate, deliberate phase — not something to fold into individual route PRs. - Same rule for the Next compat shims (
apps/studio/compat/next/): they stay until the cleanup pass, regardless of how many routes have moved.
PR guardrail — mirror page edits into their route
Because both runtimes ship at once, any change to a pages/... file can
leave its routes/... mirror stale. A CodeRabbit path_instructions
rule (.coderabbit.yaml, scoped to apps/studio/pages/**) posts a
review reminder on every PR that touches a page, prompting the author to
check whether the corresponding route needs the same change. It's a
verify-not-block reminder: pure body edits on re-export (Path A) pages
propagate automatically, but layout/getLayout, staticData props,
withAuth, redirect-path, or brand-new-page changes must be mirrored by
hand. This guardrail is temporary scaffolding — remove it in the cleanup
pass when pages/** is deleted (tracked by FE-3106).
Strategy — minimum-diff re-export
The goal is to flip URL ownership to TanStack without rewriting page internals yet. For each page we pick one of two paths:
- Path A — re-export from
pages/(default). The TanStack route imports the page's default export fromapps/studio/pages/...and renders it inside a thin wrapper component used as the route'scomponent.getLayoutis dropped on the floor — the TanStack layout chain (pathless_app.tsx/_auth.tsx+ sibling-file layouts) handles wrapping instead. The page's Next-specific imports keep working via thecompat/next/shim. BecauseNextPageWithLayoutdeclares{ dehydratedState: any }as required props, passdehydratedState={undefined}in the wrapper. - Path B — direct component import. When the
pages/...file is essentiallyexport default SomeComponentre-exporting a component from elsewhere (typical for thin page wrappers), skip the middle-man and importSomeComponentdirectly in the TanStack route.
We still need to land the shared layouts up-front:
- Pathless layout routes (
_app.tsx,_auth.tsx) hold shared shells without contributing URL segments. - Sibling-file layouts:
segment.tsxnext to asegment/directory provides the layout with<Outlet/>for children in that directory (e.g._app/account.tsxwraps_app/account/me.tsx). Noroute.tsxfiles. - Each product layout (DatabaseLayout, AuthLayout, SQLEditorLayout, …) becomes one sibling-file layout.
Once every page is represented in routes/, we do a second pass to properly move the page body into the route file and delete pages/.... Path-tracking (A vs B) below tells us which pages still have live Next files we need to eliminate.
Other rules:
- New code uses native TanStack APIs directly (no
next/router, nonext/link). The Next compat shim stays in place for pages we re-export. withAuth()HOC → TanStackbeforeLoadon the containing route/layout. Apply this at shared-layout level where possible.- Never delete a
pages/...file mid-migration — both runtimes need to keep working. Body-moves and Next-file deletions are reserved for the cleanup pass at the very end, after every entry in this checklist is[x]. See "Runtime model" above. - Not migrated via this list:
pages/api/**(Next API routes — separate migration),_app.tsx,_document.tsx,_error,pages/org/_/[[...routeSlug]].tsx,pages/project/_/[[...routeSlug]].tsx(catch-alls — revisit at the end).
Legend
[ ]not started[~]in progress[x] Adone — re-exported frompages/...(Next file still exists, needs body-move later)[x] A→donedone & body moved — Next file deleted[x] Bdone — direct component import, no Next file involved (or Next file already deletable)
Shared layouts
These are the layout-only TanStack files. Most hold a single product layout component.
App shell (pathless)
routes/_app.tsx— AppLayout + DefaultLayout (readsdefaultLayoutHeaderTitle/hideMobileMenufrom leafstaticData)routes/_app/account.tsx— AccountLayout (readsaccountLayoutTitlefrom leafstaticData)routes/_app/org.tsx— OrganizationLayout (readsorgLayoutTitlefrom leafstaticData). Delta vs plan: placed at_app/org.tsx(wraps both/org/index and/org/$slug/*) instead of_app/org/$slug.tsx. PageLayout stays inline on/org/$slug/index.tsxsince only that one route uses it.routes/_app/new.tsx— skipped; only_app/new/index.tsxlives under _app (inlines WizardLayout).new/$slugis top-level (no AppLayout) so a sub-shell would not actually share state.routes/integrations/vercel.tsx— passthroughOutletonly (no shared window layout). Delta vs plan: placed at top-level rather than under_app/. All three Vercel leaves (install, marketplace choose-project, deploy-button new-project) render their ownInterstitialLayoutinline; the oldVercelIntegrationWindowLayoutwas removed.
Project shell
routes/project/$ref.tsx— DefaultLayout only. Delta vs plan: ProjectLayoutWithAuth omitted from the shell because product layouts (DatabaseLayout, AuthLayout, StorageLayout, …) already renderwithAuth(... ProjectLayout ...)internally — adding it here would double-wrap. The home page (/project/$ref/index.tsx) wraps itself inProjectLayoutWithAuthsince it has no product layout.routes/project/$ref/database.tsx— DatabaseLayout (readsdatabaseLayoutTitlefrom leafstaticData)routes/project/$ref/database/triggers.tsx— sub-shell withPageLayout+ permission gate + nav items, inlined fromDatabaseTriggersLayout. Delta vs plan: the existingDatabaseTriggersLayoutcomponent wraps<DatabaseLayout title="Triggers">internally, so re-using it inside the database.tsx shell would double-wrap. Inlined the inner part instead; the Next-side component is left untouched (still used by thepages/...files we re-export).routes/project/$ref/auth.tsx— AuthLayout (readsauthLayoutTitlefrom leafstaticData). Delta vs plan: shell honours askipAuthLayout: trueopt-out instaticDatafor leaves whose own body or sub-layout already wraps inAuthLayout(AuthProvidersLayout,AuthEmailsLayout,pages/.../auth/third-party.tsx) — without it those routes would double-wrap (which also doubleswithAuth+ProjectLayout).Delta vs plan: not landed. A unifiedroutes/project/$ref/auth/templates.tsx— AuthEmailsLayouttemplates.tsxsub-shell would forcetemplates/$templateId.tsx(which uses plainAuthLayout, notAuthEmailsLayout) into the wrong wrapping. Insteadtemplates/index.tsxandauth/smtp.tsxeach setskipAuthLayout: trueand wrap themselves inAuthEmailsLayout;templates/$templateId.tsxuses the standard auth shell withauthLayoutTitle: 'Emails'.routes/project/$ref/storage.tsx— StorageLayout + StorageBucketsLayout (readsstorageLayoutTitle, optionalskipStorageBucketsLayout,storageBucketsLayoutTitle,storageBucketsLayoutHideSubtitlefrom leafstaticData). Delta vs plan: the shell wraps in BOTH StorageLayout and StorageBucketsLayout by default — every storage page except bucket-detail pages uses both. Bucket-detail pages setskipStorageBucketsLayout: true./storage/s3usesstorageBucketsLayout{Title,HideSubtitle}to override the inner header.routes/project/$ref/realtime.tsx— RealtimeLayout (readsrealtimeLayoutTitlefrom leafstaticData)routes/project/$ref/functions.tsx— EdgeFunctionsLayout (readsfunctionsLayoutTitlefrom leafstaticData). HonoursskipFunctionsLayout: trueopt-out for the$functionSlugsubtree, whoseEdgeFunctionDetailsLayoutalready wrapsEdgeFunctionsLayoutinternally — same pattern as auth.tsx. Sub-shell atroutes/project/$ref/functions/$functionSlug.tsxprovidesEdgeFunctionDetailsLayoutfor all 5 slug leaves (readsedgeFunctionDetailsTitlefrom leaf staticData).routes/project/$ref/branches.tsx— BranchLayout only. Delta vs plan: the per-pagePageLayout(with different titles + primary/secondary actions) stays in each leaf. HoistedBranchesPageWrapperandMergeRequestsPageWrapperto top-level exports in their respectivepages/...files so the route files can import + re-use the same wrapping.routes/project/$ref/logs.tsx— LogsLayout (readslogsLayoutTitlefrom leaf staticData). HonoursskipLogsLayout: trueforlogs/index(page handles its own ProjectLayout-wrapped content for the UnifiedLogs / no-permission cases). Refactoredpages/.../logs/index.tsxto move the inline<DefaultLayout>intogetLayoutso it isn't duplicated when the TanStack project shell already provides DefaultLayout.routes/project/$ref/observability.tsx— ObservabilityLayout (readsobservabilityLayoutTitlefrom leaf staticData)routes/project/$ref/workers.tsx— WorkersLayout (readsworkersLayoutTitlefrom leafstaticData). Flag-gated:WorkersLayoutitself redirects to the project home whenuseFlag('workers')is off, so the shell needs no extra guard.routes/project/$ref/advisors.tsx— AdvisorsLayout (readsadvisorsLayoutTitlefrom leaf staticData). HonoursskipAdvisorsLayout: trueopt-out for the rules sub-shell, which provides its own AdvisorsLayout-less-DefaultLayout wrap. Scans whole match chain (same pattern as functions.tsx).routes/project/$ref/advisors/rules.tsx— sub-shell that inlines the inner body ofAdvisorRulesLayout(AdvisorsLayout + PageLayout with title/tabs/feature-preview badge), minus the outer DefaultLayout (already provided by the parent project shell). SetsskipAdvisorsLayout: trueon its own staticData. Delta vs plan: the existingAdvisorRulesLayoutcomponent wraps in DefaultLayout + AdvisorsLayout internally, so reusing it as-is would double-wrap both. Inlined the inner part; the Next-side component is untouched.routes/project/$ref/settings.tsx— SettingsLayout (readssettingsLayoutTitlefrom leaf staticData). HonoursskipSettingsLayout: trueforsettings/api(redirect-only page). Adds a sub-shell atroutes/project/$ref/settings/api-keys.tsxprovidingApiKeysLayoutfor both api-keys leaves;jwt/indexwraps inJWTKeysLayoutinline sincejwt/legacydoesn't share it.routes/project/$ref/integrations.tsx— ProjectIntegrationsLayout (no staticData; all 4 leaves share identical layout). Layout iswithAuth(({ children }) => <ProjectLayout>{children}</ProjectLayout>), so the shell just wraps<Outlet />once.routes/project/$ref/sql.tsx— EditorBaseLayout + SQLEditorLayout. Twin of editor.tsx; all four leaves share identical layout props so the shell hardcodes them (nostaticDataoverrides). EditorBaseLayout wraps in ProjectLayoutWithAuth; SQLEditorLayout adds its ownwithAuthHOC but no extra ProjectLayout — same shape as the table editor (auth check runs twice but no double render).routes/project/$ref/editor.tsx— EditorBaseLayout + TableEditorLayout. All three leaves share identical layout props so the shell hardcodes them (nostaticDataoverrides). EditorBaseLayout wraps inProjectLayoutWithAuthinternally; TableEditorLayout's happy path is just a fragment + side-effect (banner) and only wraps inProjectLayoutWithAuthon its no-permission branch — same as Next, no double-wrap in normal use.
Auth shell (pathless)
routes/_auth.tsx— AuthenticationLayout
Pages
App shell — /account/*
- A
routes/_app/account/me.tsx←pages/account/me.tsx - A
routes/_app/account/security.tsx←pages/account/security.tsx - A
routes/_app/account/audit.tsx←pages/account/audit.tsx - A
routes/_app/account/tokens/index.tsx←pages/account/tokens.tsx - A
routes/_app/account/tokens/scoped.tsx←pages/account/tokens/scoped.tsx
App shell — /org/$slug/*
- A
routes/_app/org/$slug/index.tsx←pages/org/[slug]/index.tsx - A
routes/_app/org/$slug/apps.tsx←pages/org/[slug]/apps.tsx - A
routes/_app/org/$slug/audit.tsx←pages/org/[slug]/audit.tsx - A
routes/_app/org/$slug/audit-log-drains.tsx←pages/org/[slug]/audit-log-drains.tsx(wraps in OrganizationSettingsLayout inline) - A
routes/_app/org/$slug/billing.tsx←pages/org/[slug]/billing.tsx - A
routes/_app/org/$slug/documents.tsx←pages/org/[slug]/documents.tsx - A
routes/_app/org/$slug/general.tsx←pages/org/[slug]/general.tsx - A
routes/_app/org/$slug/integrations.tsx←pages/org/[slug]/integrations.tsx - A
routes/_app/org/$slug/security.tsx←pages/org/[slug]/security.tsx - A
routes/_app/org/$slug/sso.tsx←pages/org/[slug]/sso.tsx - A
routes/_app/org/$slug/team.tsx←pages/org/[slug]/team.tsx - A
routes/_app/org/$slug/usage.tsx←pages/org/[slug]/usage.tsx - A
routes/_app/org/$slug/private-apps/index.tsx←pages/org/[slug]/private-apps/index.tsx - A
routes/_app/org/$slug/webhooks/index.tsx←pages/org/[slug]/webhooks/index.tsx - A
routes/_app/org/$slug/webhooks/$endpointId.tsx←pages/org/[slug]/webhooks/[endpointId].tsx - A
routes/_app/org/index.tsx←pages/org/index.tsx(redirect)
App shell — top-level pages
- A
routes/_app/organizations.tsx←pages/organizations.tsx(page default already withAuth-wrapped; PageLayout wraps body) routes/_app/new/index.tsx←pages/new/index.tsx(inlines WizardLayout; setsdefaultLayoutHeaderTitle: 'New organization'+hideMobileMenu: trueon staticData). Delta vs plan: no_app/new.tsxsub-shell —new/$slugdoesn't fit under _app and uses a different inner wrapper (PageLayout), so a shared shell wouldn't share anything.- A
routes/new/$slug.tsx←pages/new/[slug].tsxDelta vs plan: placed at top-level rather than under_app/— Next getLayout omits AppLayout and uses PageLayout (not WizardLayout) inside DefaultLayout, so leaf inlines the full DefaultLayout + PageLayout wrap itself. - A
routes/aws-marketplace-onboarding.tsx←pages/aws-marketplace-onboarding.tsxDelta vs plan: placed at root rather than under_app/— page uses its ownLinkAwsMarketplaceLayoutand doesn't wantAppLayout+DefaultLayoutwrapping. - A
routes/claim-project.tsx←pages/claim-project.tsxDelta vs plan: placed at root rather than under_app/— page uses its own<Head>+<main>layout and doesn't wantAppLayout+DefaultLayoutwrapping. - A
routes/join.tsx←pages/join.tsxDelta vs plan: placed at root rather than under_app/— page uses a centered-div layout and doesn't wantAppLayout+DefaultLayoutwrapping. routes/_app/support/new.tsx←pages/support/new.tsx(setshideMobileMenu: truestaticData; existing page iswithAuth-wrapped so no beforeLoad migration needed yet)routes/_app/support/link.tsx←pages/support/link.tsx
App shell — integrations
- A
routes/integrations/vercel/install.tsx←pages/integrations/vercel/install.tsx - A
routes/integrations/vercel/$slug/marketplace/choose-project.tsx←pages/integrations/vercel/[slug]/marketplace/choose-project.tsx - A
routes/integrations/vercel/$slug/deploy-button/new-project.tsx←pages/integrations/vercel/[slug]/deploy-button/new-project.tsx - A
routes/integrations/github/authorize.tsx←pages/integrations/github/authorize.tsxDelta vs plan: placed at top-level rather than under_app/— Next page has no getLayout (renders bare), so adding AppLayout/DefaultLayout via _app would be a behaviour change.
Project shell — home
- A
routes/project/$ref/index.tsx←pages/project/[ref]/index.tsx(route wraps inProjectLayoutWithAuthitself — see shell delta above) routes/project/$ref/merge.tsx←pages/project/[ref]/merge.tsx(leaf wraps body inProjectLayoutWithAuth; parentproject/$ref.tsxshell provides DefaultLayout)routes/project/$ref/explorer.tsx— converted from a leaf into a shell (ExplorerLayout+Outlet) to host the new/explorer/notebook/$idleaf; parentproject/$ref.tsxshell still provides DefaultLayout.
Project shell — /api/*
routes/project/$ref/api/index.tsx←pages/project/[ref]/api/index.tsx(redirect-only page; no extra wrap needed beyond the parent DefaultLayout shell)
Project shell — /database/*
- A
routes/project/$ref/database/schemas.tsx←pages/project/[ref]/database/schemas.tsx - A
routes/project/$ref/database/extensions.tsx←pages/project/[ref]/database/extensions.tsx - A
routes/project/$ref/database/functions.tsx←pages/project/[ref]/database/functions.tsx - A
routes/project/$ref/database/indexes.tsx←pages/project/[ref]/database/indexes.tsx - A
routes/project/$ref/database/migrations.tsx←pages/project/[ref]/database/migrations.tsx - A
routes/project/$ref/database/policies.tsx←pages/project/[ref]/database/policies.tsx - A
routes/project/$ref/database/roles.tsx←pages/project/[ref]/database/roles.tsx - A
routes/project/$ref/database/settings.tsx←pages/project/[ref]/database/settings.tsx - A
routes/project/$ref/database/types.tsx←pages/project/[ref]/database/types.tsx - A
routes/project/$ref/database/column-privileges.tsx←pages/project/[ref]/database/column-privileges.tsx - A
routes/project/$ref/database/tables/index.tsx←pages/project/[ref]/database/tables/index.tsx - A
routes/project/$ref/database/tables/$id.tsx←pages/project/[ref]/database/tables/[id].tsx - A
routes/project/$ref/database/publications/index.tsx←pages/project/[ref]/database/publications/index.tsx - A
routes/project/$ref/database/publications/$id.tsx←pages/project/[ref]/database/publications/[id].tsx - A
routes/project/$ref/database/replication/index.tsx←pages/project/[ref]/database/replication/index.tsx - A
routes/project/$ref/database/replication/$pipelineId.tsx←pages/project/[ref]/database/replication/[pipelineId].tsx - A
routes/project/$ref/database/replication/replica/$replicaId.tsx←pages/project/[ref]/database/replication/replica/[replicaId].tsx(redirects to Infrastructure) - A
routes/project/$ref/database/triggers/index.tsx←pages/project/[ref]/database/triggers/index.tsx - A
routes/project/$ref/database/triggers/data.tsx←pages/project/[ref]/database/triggers/data.tsx(sub-shell atdatabase/triggers.tsxprovides PageLayout + nav, parent shell provides DatabaseLayout) - A
routes/project/$ref/database/triggers/event.tsx←pages/project/[ref]/database/triggers/event.tsx(same as data) - A
routes/project/$ref/database/backups/pitr.tsx←pages/project/[ref]/database/backups/pitr.tsx - A
routes/project/$ref/database/backups/restore-to-new-project.tsx←pages/project/[ref]/database/backups/restore-to-new-project.tsx - A
routes/project/$ref/database/backups/scheduled.tsx←pages/project/[ref]/database/backups/scheduled.tsx
Project shell — /auth/*
- A
routes/project/$ref/auth/overview.tsx←pages/project/[ref]/auth/overview.tsx - A
routes/project/$ref/auth/users.tsx←pages/project/[ref]/auth/users.tsx - A
routes/project/$ref/auth/providers.tsx←pages/project/[ref]/auth/providers.tsx(setsskipAuthLayout: true, wraps inAuthProvidersLayoutdirectly) - A
routes/project/$ref/auth/mfa.tsx←pages/project/[ref]/auth/mfa.tsx - A
routes/project/$ref/auth/hooks.tsx←pages/project/[ref]/auth/hooks.tsx - A
routes/project/$ref/auth/smtp.tsx←pages/project/[ref]/auth/smtp.tsx(setsskipAuthLayout: true, wraps inAuthEmailsLayoutdirectly) - A
routes/project/$ref/auth/sessions.tsx←pages/project/[ref]/auth/sessions.tsx - A
routes/project/$ref/auth/passkeys.tsx←pages/project/[ref]/auth/passkeys.tsx - A
routes/project/$ref/auth/performance.tsx←pages/project/[ref]/auth/performance.tsx - A
routes/project/$ref/auth/protection.tsx←pages/project/[ref]/auth/protection.tsx - A
routes/project/$ref/auth/rate-limits.tsx←pages/project/[ref]/auth/rate-limits.tsx - A
routes/project/$ref/auth/third-party.tsx←pages/project/[ref]/auth/third-party.tsx(setsskipAuthLayout: true— page body inlines<AuthProvidersLayout>which already wraps<AuthLayout>) - A
routes/project/$ref/auth/oauth-apps.tsx←pages/project/[ref]/auth/oauth-apps.tsx - A
routes/project/$ref/auth/oauth-server.tsx←pages/project/[ref]/auth/oauth-server.tsx - A
routes/project/$ref/auth/url-configuration.tsx←pages/project/[ref]/auth/url-configuration.tsx - A
routes/project/$ref/auth/audit-logs.tsx←pages/project/[ref]/auth/audit-logs.tsx - A
routes/project/$ref/auth/templates/index.tsx←pages/project/[ref]/auth/templates/index.tsx(setsskipAuthLayout: true, wraps inAuthEmailsLayoutdirectly) - A
routes/project/$ref/auth/templates/$templateId.tsx←pages/project/[ref]/auth/templates/[templateId].tsx(authLayoutTitle: 'Emails'— page uses plainAuthLayout, notAuthEmailsLayout)
Project shell — /storage/*
- A
routes/project/$ref/storage/s3.tsx←pages/project/[ref]/storage/s3.tsx - A
routes/project/$ref/storage/files/index.tsx←pages/project/[ref]/storage/files/index.tsx - A
routes/project/$ref/storage/files/policies.tsx←pages/project/[ref]/storage/files/policies.tsx - A
routes/project/$ref/storage/files/settings.tsx←pages/project/[ref]/storage/files/settings.tsx - A
routes/project/$ref/storage/files/buckets/$bucketId.tsx←pages/project/[ref]/storage/files/buckets/[bucketId].tsx(setsskipStorageBucketsLayout: true) - A
routes/project/$ref/storage/analytics/index.tsx←pages/project/[ref]/storage/analytics/index.tsx - A
routes/project/$ref/storage/analytics/buckets/$bucketId.tsx←pages/project/[ref]/storage/analytics/buckets/[bucketId].tsx(setsskipStorageBucketsLayout: true) - A
routes/project/$ref/storage/vectors/index.tsx←pages/project/[ref]/storage/vectors/index.tsx - A
routes/project/$ref/storage/vectors/buckets/$bucketId.tsx←pages/project/[ref]/storage/vectors/buckets/[bucketId].tsx(setsskipStorageBucketsLayout: true)
Project shell — /realtime/*
- A
routes/project/$ref/realtime/inspector.tsx←pages/project/[ref]/realtime/inspector.tsx - A
routes/project/$ref/realtime/policies.tsx←pages/project/[ref]/realtime/policies.tsx - A
routes/project/$ref/realtime/settings.tsx←pages/project/[ref]/realtime/settings.tsx
Project shell — /workers/*
- A
routes/project/$ref/workers/index.tsx←pages/project/[ref]/workers/index.tsx - A
routes/project/$ref/workers/$name.tsx←pages/project/[ref]/workers/[name].tsx - A
routes/project/$ref/workers/secrets.tsx←pages/project/[ref]/workers/secrets.tsx
Project shell — /functions/*
- A
routes/project/$ref/functions/index.tsx←pages/project/[ref]/functions/index.tsx(route wraps in exportedEdgeFunctionsIndexPageWrapperfor the inline PageHeader + actions) - A
routes/project/$ref/functions/new.tsx←pages/project/[ref]/functions/new.tsx - A
routes/project/$ref/functions/secrets.tsx←pages/project/[ref]/functions/secrets.tsx(route wraps in exportedSecretsPageWrapper) - A
routes/project/$ref/functions/$functionSlug/index.tsx←pages/project/[ref]/functions/[functionSlug]/index.tsx - A
routes/project/$ref/functions/$functionSlug/code.tsx←pages/project/[ref]/functions/[functionSlug]/code.tsx - A
routes/project/$ref/functions/$functionSlug/details.tsx←pages/project/[ref]/functions/[functionSlug]/details.tsx - A
routes/project/$ref/functions/$functionSlug/invocations.tsx←pages/project/[ref]/functions/[functionSlug]/invocations.tsx - A
routes/project/$ref/functions/$functionSlug/logs.tsx←pages/project/[ref]/functions/[functionSlug]/logs.tsx
Project shell — /branches/*
- A
routes/project/$ref/branches/index.tsx←pages/project/[ref]/branches/index.tsx(route wraps in exportedBranchesPageWrapperto preserve the page'sPageLayout+ Create-branch action) - A
routes/project/$ref/branches/merge-requests.tsx←pages/project/[ref]/branches/merge-requests.tsx(route wraps in exportedMergeRequestsPageWrapper)
Project shell — /logs/*
- A
routes/project/$ref/logs/index.tsx←pages/project/[ref]/logs/index.tsx(setsskipLogsLayout: true; page handles its own ProjectLayout; DefaultLayout moved to page'sgetLayoutso Next still wraps it) - A
routes/project/$ref/logs/auth-logs.tsx←pages/project/[ref]/logs/auth-logs.tsx - A
routes/project/$ref/logs/cron-logs.tsx←pages/project/[ref]/logs/cron-logs.tsx - A
routes/project/$ref/logs/dedicated-pooler-logs.tsx←pages/project/[ref]/logs/dedicated-pooler-logs.tsx - A
routes/project/$ref/logs/edge-functions-logs.tsx←pages/project/[ref]/logs/edge-functions-logs.tsx - A
routes/project/$ref/logs/edge-logs.tsx←pages/project/[ref]/logs/edge-logs.tsx - A
routes/project/$ref/logs/multigres-logs.tsx←pages/project/[ref]/logs/multigres-logs.tsx - A
routes/project/$ref/logs/pg-upgrade-logs.tsx←pages/project/[ref]/logs/pg-upgrade-logs.tsx - A
routes/project/$ref/logs/pgcron-logs.tsx←pages/project/[ref]/logs/pgcron-logs.tsx - A
routes/project/$ref/logs/pooler-logs.tsx←pages/project/[ref]/logs/pooler-logs.tsx - A
routes/project/$ref/logs/postgres-logs.tsx←pages/project/[ref]/logs/postgres-logs.tsx - A
routes/project/$ref/logs/postgrest-logs.tsx←pages/project/[ref]/logs/postgrest-logs.tsx - A
routes/project/$ref/logs/realtime-logs.tsx←pages/project/[ref]/logs/realtime-logs.tsx - A
routes/project/$ref/logs/replication-logs.tsx←pages/project/[ref]/logs/replication-logs.tsx - A
routes/project/$ref/logs/storage-logs.tsx←pages/project/[ref]/logs/storage-logs.tsx - A
routes/project/$ref/logs/explorer/index.tsx←pages/project/[ref]/logs/explorer/index.tsx - A
routes/project/$ref/logs/explorer/recent.tsx←pages/project/[ref]/logs/explorer/recent.tsx - A
routes/project/$ref/logs/explorer/saved.tsx←pages/project/[ref]/logs/explorer/saved.tsx - A
routes/project/$ref/logs/explorer/templates.tsx←pages/project/[ref]/logs/explorer/templates.tsx
Project shell — /observability/*
- A
routes/project/$ref/observability/index.tsx←pages/project/[ref]/observability/index.tsx - A
routes/project/$ref/observability/$id.tsx←pages/project/[ref]/observability/[id].tsx - A
routes/project/$ref/observability/auth.tsx←pages/project/[ref]/observability/auth.tsx - A
routes/project/$ref/observability/database.tsx←pages/project/[ref]/observability/database.tsx - A
routes/project/$ref/observability/api-overview.tsx←pages/project/[ref]/observability/api-overview.tsx - A
routes/project/$ref/observability/connections.tsx←pages/project/[ref]/observability/connections.tsx - A
routes/project/$ref/observability/edge-functions.tsx←pages/project/[ref]/observability/edge-functions.tsx - A
routes/project/$ref/observability/postgrest.tsx←pages/project/[ref]/observability/postgrest.tsx - A
routes/project/$ref/observability/query-insights.tsx←pages/project/[ref]/observability/query-insights.tsx - A
routes/project/$ref/observability/query-performance.tsx←pages/project/[ref]/observability/query-performance.tsx - A
routes/project/$ref/observability/realtime.tsx←pages/project/[ref]/observability/realtime.tsx - A
routes/project/$ref/observability/storage.tsx←pages/project/[ref]/observability/storage.tsx
Project shell — /advisors/*
- A
routes/project/$ref/advisors/health.tsx←pages/project/[ref]/advisors/health.tsx - A
routes/project/$ref/advisors/performance.tsx←pages/project/[ref]/advisors/performance.tsx - A
routes/project/$ref/advisors/security.tsx←pages/project/[ref]/advisors/security.tsx - A
routes/project/$ref/advisors/rules/performance.tsx←pages/project/[ref]/advisors/rules/performance.tsx - A
routes/project/$ref/advisors/rules/security.tsx←pages/project/[ref]/advisors/rules/security.tsx
Project shell — /settings/*
- A
routes/project/$ref/settings/general.tsx←pages/project/[ref]/settings/general.tsx - A
routes/project/$ref/settings/addons.tsx←pages/project/[ref]/settings/addons.tsx - A
routes/project/$ref/settings/api.tsx←pages/project/[ref]/settings/api.tsx(setsskipSettingsLayout: true— page is a useEffect redirect) - A
routes/project/$ref/settings/dashboard.tsx←pages/project/[ref]/settings/dashboard.tsx - A
routes/project/$ref/settings/code-configuration.tsx←pages/project/[ref]/settings/code-configuration.tsx - A
routes/project/$ref/settings/infrastructure/index.tsx←pages/project/[ref]/settings/infrastructure.tsx - A
routes/project/$ref/settings/infrastructure/replica/$replicaId.tsx←pages/project/[ref]/settings/infrastructure/replica/[replicaId].tsx - A
routes/project/$ref/settings/integrations.tsx←pages/project/[ref]/settings/integrations.tsx - A
routes/project/$ref/settings/log-drains.tsx←pages/project/[ref]/settings/log-drains.tsx - A
routes/project/$ref/settings/api-keys/index.tsx←pages/project/[ref]/settings/api-keys/index.tsx(underapi-keys.tsxsub-shell with ApiKeysLayout) - A
routes/project/$ref/settings/api-keys/legacy.tsx←pages/project/[ref]/settings/api-keys/legacy.tsx(underapi-keys.tsxsub-shell) - A
routes/project/$ref/settings/billing/usage.tsx←pages/project/[ref]/settings/billing/usage.tsx - A
routes/project/$ref/settings/jwt/index.tsx←pages/project/[ref]/settings/jwt/index.tsx(wraps in JWTKeysLayout inline) - A
routes/project/$ref/settings/jwt/legacy.tsx←pages/project/[ref]/settings/jwt/legacy.tsx - A
routes/project/$ref/settings/webhooks/index.tsx←pages/project/[ref]/settings/webhooks/index.tsx - A
routes/project/$ref/settings/webhooks/$endpointId.tsx←pages/project/[ref]/settings/webhooks/[endpointId].tsx
Project shell — /integrations/*
routes/project/$ref/integrations/index.tsx←pages/project/[ref]/integrations/index.tsxroutes/project/$ref/integrations/$id/index.tsx←pages/project/[ref]/integrations/[id]/index.tsxroutes/project/$ref/integrations/$id/$pageId/index.tsx←pages/project/[ref]/integrations/[id]/[pageId]/index.tsxroutes/project/$ref/integrations/$id/$pageId/$childId/index.tsx←pages/project/[ref]/integrations/[id]/[pageId]/[childId]/index.tsx
Project shell — /sql/*
- A
routes/project/$ref/sql/index.tsx←pages/project/[ref]/sql/index.tsx - A
routes/project/$ref/sql/$id.tsx←pages/project/[ref]/sql/[id].tsx - A
routes/project/$ref/sql/templates.tsx←pages/project/[ref]/sql/templates.tsx - A
routes/project/$ref/sql/examples.tsx←pages/project/[ref]/sql/examples.tsx
Project shell — /editor/*
- A
routes/project/$ref/editor/index.tsx←pages/project/[ref]/editor/index.tsx - A
routes/project/$ref/editor/$id.tsx←pages/project/[ref]/editor/[id].tsx - A
routes/project/$ref/editor/new.tsx←pages/project/[ref]/editor/new.tsx
Project shell — /explorer/*
- A
routes/project/$ref/explorer/index.tsx←pages/project/[ref]/explorer/index.tsx - A
routes/project/$ref/explorer/notebook/$id.tsx←pages/project/[ref]/explorer/notebook/[id].tsx - A
routes/project/$ref/explorer/chat/$id.tsx←pages/project/[ref]/explorer/chat/[id].tsx - A
routes/project/$ref/explorer/query/$id.tsx←pages/project/[ref]/explorer/query/[id].tsx
Auth shell — /sign-in, /sign-up, etc.
- A
routes/_auth/sign-in.tsx←pages/sign-in.tsx - A
routes/_auth/sign-up.tsx←pages/sign-up.tsx - A
routes/_auth/sign-in-sso.tsx←pages/sign-in-sso.tsx - A
routes/_auth/sign-in-partner.tsx←pages/sign-in-partner.tsx - A
routes/_auth/sign-in-mfa.tsx←pages/sign-in-mfa.tsx(page inlines SignInLayout) - A
routes/_auth/forgot-password.tsx←pages/forgot-password.tsx - A
routes/_auth/forgot-password-mfa.tsx←pages/forgot-password-mfa.tsx(page inlines ForgotPasswordLayout) - A
routes/_auth/reset-password.tsx←pages/reset-password.tsx(page default already withAuth-wrapped) - A
routes/_auth/cli/login.tsx←pages/cli/login.tsx(page inlines APIAuthorizationLayout, withAuth) - A
routes/_auth/partners/stripe/projects/login.tsx←pages/partners/stripe/projects/login.tsx(page inlines APIAuthorizationLayout, withAuth)
Standalone (no shared shell)
- B
routes/index.tsx— redirect-only root route. Mirrors the Next.jsredirects()rules innext.config.ts: platform sends users to/org(or/new/new-projectwhen deep-linked with?next=new-project), self-hosted sends them to/project/default. Follow-up: the redirect targets currently usehref(full reload) because they were on the Next side when this was written; switch totonow that all of them live in the TanStack tree. - A
routes/authorize.tsx←pages/authorize.tsx(APIAuthorizationLayout) - A
routes/redeem.tsx←pages/redeem.tsx(RedeemCreditsLayout) - A
routes/logout.tsx←pages/logout.tsx - A
routes/maintenance.tsx←pages/maintenance.tsx - A
routes/verify-email.tsx←pages/verify-email.tsx
Error pages (handled at root)
- A
__root.tsx— wirednotFoundComponenttopages/404.tsx __root.tsx— wirederrorComponenttopages/500.tsx. Mirrors the in-treereact-error-boundarySentry capture (scope.setTag('routerErrorComponent', true)) so router-level errors (loader/component-render failures before the in-tree boundary mounts) still report.pages/_error.jsxstays load-bearing under Next but isn't reached at runtime under TanStack — it's the pages-router catch-all that has no TanStack equivalent.
API routes
Strategy — shim + re-export. compat/next/api.ts exposes
toWebHandler(nextHandler) that adapts a (req, res) => … Next.js handler
into a TanStack Start Web-fetch handler. Each routes/api/... file imports
the default export from pages/api/..., wraps with toWebHandler, and
registers via createFileRoute(...).server.handlers. apiWrapper and
apiAuthenticate stay untouched — they run inside the shim, seeing a
NextApiRequest-shaped req and a proxy res.
Path conventions — pages/api/foo/[bar]/baz.ts → routes/api/foo/$bar/baz.ts;
pages/api/foo/[[...slug]].ts → routes/api/foo/$.ts.
Shim coverage. The proxy req / res cover both the buffered and
streaming patterns that pages-router handlers use:
- Buffered responses —
res.status/setHeader/json/send/write/endaccumulate into a singleResponsebody when the handler returns. - Streaming responses —
res.writeHead(status, headers?)(orres.flushHeaders()) flips the proxy into streaming mode: a WebReadableStreamopens, buffered chunks flush into it, subsequentres.write(chunk)enqueues live,res.end()closes it.finalize()returns theResponsewhile the handler keeps pushing chunks. This is what makesresult.pipeUIMessageStreamToResponse(res, …)(AI SDK) stream token-by-token to the browser. - Client abort — Web
Request.signalis plumbed through asreq.on('close' | 'aborted', …). AI handlers that wireabortController.abort()off those events keep working. - EventEmitter surface —
req.on/once/off/emit(eventsclose/abortedare real; other names accepted but no-op).res.on/etc. are no-op stubs so pipe helpers attachingdrain/close/errorlisteners don't crash. - Body parsing — JSON and
application/x-www-form-urlencodedparsed toreq.body; everything else is the raw text. Multipart inbound is not implemented — no studio handler reads multipart in.
Two routes still bypass the shim because they're easier to write Web-natively from scratch:
routes/api/v1/projects/$ref/functions/$slug/body.ts— multipart streaming OUT (artifact download). Builds theResponsebody as aReadableStream; each artifact file converts viaReadable.toWeb(createReadStream(...))and pulls chunk-by-chunk into the stream.routes/api/mcp/index.ts— uses MCP SDK'sWebStandardStreamableHTTPServerTransport(handleRequest(request)returns aResponsedirectly).pages/api/ai/docs.tswas already edge-runtime / Web-Response native — direct re-export, no shim involved.
Tracking below is coarse — each bullet is a pages/api/** subtree. Check off
once every file in the subtree has a routes/api/** counterpart. Expand into
per-file items only when a subtree has special cases.
routes/api/get-ip-address.ts— canary port (validates the shim)routes/api/**— root-level simple endpoints (check-cname,cli-release-version,enabled-features-overrides,generate-attachment-url,get-deployment-commit,get-utc-time,status-override)routes/api/ai/**— AI endpoints (docs.tsdirect-ported as Web-native)routes/api/connect/**routes/api/content/**routes/api/edge-functions/**routes/api/integrations/**routes/api/platform/**(60 files, scripted port)routes/api/v1/**— exceptbody.ts(streaming rewrite)routes/api/v1/projects/$ref/functions/$slug/body.ts— Web-streams rewrite. Returns aResponsewhose body is aReadableStream; each artifact file is converted viaReadable.toWeb(createReadStream(...))and pulled chunk-by-chunk into the multipart stream. Skips theapiWrappersincegetFunctionsArtifactStorealready asserts self-hosted mode (the pages-routerwithAuthwas a no-op outsideIS_PLATFORM).routes/api/mcp/index.ts— usesWebStandardStreamableHTTPServerTransportfrom@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js. Takes a WebRequest, returns aResponsedirectly — no shim needed. Query parsing pulled fromrequest.url's search params; headers passed through unchanged.routes/api/incident-banner.ts,routes/api/incident-status.ts— App Router routes underapp/api/**(already Web-native, direct re-export)
Compat shim surface (compat/next/)
The Next compat shims stay alive as long as any pages/... file is
load-bearing. Listed here so the cleanup PR knows what to delete /
inline.
router.ts—useRouter()for hook callers (TanStackuseRouter+useLocation+useMatches+useParams+useSearchglued together), plus adefaultexport (SingletonRoutershape) for the one module-scopeimport router from 'next/router'consumer (Support/DiscordCTACard) that readsrouter.basePathoutside React.router.pathnamestrips the trailing slash TanStack appends to index routes (without it,router.pathname.split('/')[3]returns''instead ofundefinedfor index pages and the project sidebar's active-route check breaks)._router-events.ts— adaptsrouter.events.on(event, handler)ontorouter.subscribe(tsEvent, …). Forwards Next's(url, { shallow })args. MapsrouteChangeStart/routeChangeComplete/beforeHistoryChange/hashChangeStart/hashChangeComplete. Known gap: Next's throw-from-routeChangeStart-to-cancel pattern isn't supportable —subscribeis fire-and-forget.usePreventNavigationOnUnsavedChangesrelies on it and needs migrating to TanStack'suseBlockerseparately.api.ts—toWebHandler(nextHandler). See API routes → Shim coverage above.link.tsx,navigation.ts,dynamic.tsx,image.tsx,legacy/image.tsx,script.tsx,head.tsx,server.ts— comprehensive drop-in replacements for thenext/*modules studio imports. All bundled viavite.config.ts'snextCompat()plugin (alias) +ssr.noExternal: [/^next(\/|$)/]so the shims always win over the real Next packages.
Build / bundler workarounds
vite.config.ts carries two classes of build-time guard that exist
purely because of how Rolldown chunks our specific dependency graph.
They should be revisited (and ideally lifted) once the migration is
done.
manualChunks pins
Pin shared library code into dedicated chunks so per-component chunks
can't import from a chunk that (transitively) imports them back —
chunk-level cycles surface in the browser as
TypeError: <name> is not a function at module-load time.
class-variance-authority— entry #1 in CIRCULAR_IMPORTS.md.lucide-react— keeps Lucide icons from being per-icon-split into chunks that importcreateLucideIconback from theuichunk (folder-open-<hash>.jswas the canary).react-vendor(react + react-dom + scheduler + jsx-runtime) — pinned beforelucide-reactso Rolldown doesn't suck React into the lucide chunk for CJS interop and shift live-bindings across the rest of the graph (Alert-<hash>.jswas the canary).
All three are documented in CIRCULAR_IMPORTS.md — slated for a
follow-up structural fix in packages/ui so the pins can be lifted.
assertNoChunkCycles build plugin
Vite plugin that runs Tarjan's SCC on the emitted chunk graph in
generateBundle and fails the build if any unknown chunk cycle exists.
The pre-existing CVA cycle is allowlisted by chunk basename
(KNOWN_CHUNK_CYCLES constant) so the build still passes; any new
cycle blocks the build with a message pointing at CIRCULAR_IMPORTS.md.
Keep this plugin even after migration — it's not a Next-related shim, it's general protection against this entire class of bug. Just clear the allowlist when the underlying cycle is gone.
@sentry/nextjs → @sentry/react alias
resolve.alias in vite.config.ts rewrites the bare @sentry/nextjs
import to compat/sentry-nextjs.ts, which re-exports @sentry/react
(the same-version package @sentry/nextjs wraps on the client) plus
explicit stand-ins for the Next-only APIs (captureRouterTransitionStart,
captureRequestError, withSentryConfig).
Why: @sentry/nextjs's client entry imports
next/dist/shared/lib/constants, whose module scope evaluates
...(process?.features?.typescript ? ['next.config.mts'] : []).
Optional chaining does not guard an undeclared process identifier,
so every built client chunk containing it (table editor was the canary)
threw ReferenceError: process is not defined at module load. Dev was
unaffected (dev pipeline shims process), so it only surfaced in the
production/test build.
The alias also made the previous @sentry/nextjs SSR workarounds
(ssr.noExternal entry + ssr.optimizeDeps.include) obsolete — the id
is rewritten before SSR resolution, and @sentry/react ships real ESM.
App source keeps importing @sentry/nextjs so the Next build
(build:next) is untouched; drop the alias + shim together with the
Next build when the migration is done (switch imports to
@sentry/react directly).
GraphiQL Monaco workers: setup-workers/webpack → setup-workers/vite
App source (GraphiQLTab.tsx) imports graphiql/setup-workers/webpack,
which registers MonacoEnvironment.getWorker using
new Worker(new URL('monaco-editor/...', import.meta.url)) — the URL form
webpack/turbopack rewrites at build time. Vite doesn't rewrite bare module
specifiers inside new URL(..., import.meta.url), so under the TanStack
build the worker URLs 404'd and Monaco fell back to running the json,
editorWorkerService and graphql workers on the main thread ("Could not
create web worker(s). Falling back to loading web worker code in main
thread" in the console).
The graphiqlViteWorkers plugin in vite.config.ts resolves that import
to graphiql's own setup-workers/vite variant (same three workers via
Vite ?worker imports) in client builds; SSR resolution is untouched. The
import specifier stays .../webpack in app source so the Next build keeps
working. The whole setup-workers chain is also in optimizeDeps.exclude —
the Rolldown dep optimizer can't load ?worker ids
(UNLOADABLE_DEPENDENCY), so the modules go through the normal transform
pipeline where Vite's built-in worker plugin handles them. Drop the plugin
and the exclude, and switch the import to graphiql/setup-workers/vite,
when the Next build goes away.
Raw-text imports: *.md + public/deno/*.d.ts (rawTextLoader)
Next's raw-loader rules (next.config.ts turbopack.rules) serve *.md
files and the Deno typings public/deno/edge-runtime.d.ts /
public/deno/lib.deno.d.ts as JS modules whose default export is the
file's text. The rawTextLoader plugin in vite.config.ts mirrors that
for the Vite pipeline:
*.md— plaintransform(used bystatic-data/integrations/*/overview.mdvia the literal-import registry instatic-data/integrations/overviews.ts).- The two Deno
.d.tsfiles (used bycomponents/ui/AIEditoras Monaco extra libs for edge-function editors) — an exact-specifier allowlist resolved to\0-virtual ids and served from aloadhook. They can't go throughtransform: Rolldown's native dep scanner skips JS plugin hooks and hard-fails parsing TS declaration syntax (get stdin(): ...;) as runtime TS, which killed dependency pre-bundling wholesale. The previous/* @vite-ignore */hack kept the scanner away but also meant the imports failed at runtime, silently dropping Deno type hints in the TanStack build. Do NOT widen the allowlist to*.d.ts— hijacking declaration-file resolution globally would corrupt packages that ship.d.tsnext to their JS. Theas stringcasts on the import specifiers inAIEditor/index.tsxkeep tsc from resolving the.d.tsfiles as declaration files (TS2846) while erasing to plain literals both bundlers statically analyze.
Other build-side migration changes
pnpm-workspace.yamlcatalog now includes@tanstack/react-router,@tanstack/react-start,@tanstack/react-tableso studio and library stay aligned.react-queryis not in the catalog yet — three consumers (studio, docs, library) sit on different 5.x ranges and unifying them is a separate decision.NODE_OPTIONS=--max-old-space-size=8192is set on the studiodevscript — Vite's Rolldown-RC frontend hits the default 4 GB ceiling when chewing through studio's module graph in watch mode.
Deferred / revisit
landed aspages/org/_/[[...routeSlug]].tsxroutes/org.[_].tsx+routes/org.[_].$.tsx. Naming delta: path-as-filename form (notroutes/org/[_]/index.tsx) because the index-file form trips a router-generator bug atgetRouteNodes.js:132— when anindex.tsxhas a bracket-escaped parent segment,originalRoutePathgets wiped wholesale and the escape info is lost, so_gets stripped as pathless. The path-as-filename form keeps the last segment non-index and avoids the bug branch entirely. Next page accepts either Next-stylerouteSlug(string[]) or TanStack-style_splat(string) and normalises to the array shape.landed aspages/project/_/[[...routeSlug]].tsxroutes/project.[_].tsx+routes/project.[_].$.tsx. Same naming-delta rationale as the org catch-alls above.
Cleanup checklist (after every pages/... file is gone)
- Switch
routes/index.tsxredirects fromhreftoto— all targets now live in the TanStack tree. - Migrate
usePreventNavigationOnUnsavedChangesfromrouter.events.on('routeChangeStart', …)(throw-to-cancel pattern) to TanStack'suseBlocker. - Drop the
_splat/routeSlugnormalisation block frompages/org/_/[[...routeSlug]].tsx+pages/project/_/[[...routeSlug]].tsx(only there to keep both runtimes mounting the same body). - Remove
RouteValidationWrapper+next/routercompat shim usage from__root.tsx. - Remove
compat/next/directory entirely once nonext/*import remains in workspace source. - Lift
manualChunkspins (class-variance-authority,lucide-react,react-vendor) once the structural fix inpackages/uilands — see CIRCULAR_IMPORTS.md. KeepassertNoChunkCycles; just clearKNOWN_CHUNK_CYCLES. - Delete
pages/_app.tsx,pages/_document.tsx,pages/_error.jsx,pages/500.tsx,pages/404.tsx(Next-only catch-alls; TanStack equivalents on__root.tsx). - Drop the
dev:next/build:next/start:nextscripts fromapps/studio/package.jsononce we're committed to TanStack. - Remove the
apps/studio/pages/**path_instructionsguardrail entry from.coderabbit.yaml(added in FE-3423; remove it as part of this FE-3106 cleanup) — it's only useful while both runtimes coexist. - Remove the "TanStack Start migration" section from
apps/studio/AGENTS.md— it only applies while both runtimes coexist. - Delete this file.