diff --git a/.prettierignore b/.prettierignore
index 8c41d148ff5..66e4d58fa92 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -22,6 +22,9 @@ apps/**/.turbo
apps/docs/CONTRIBUTING.md
apps/docs/__generated__
apps/design-system/__registry__
+# TanStack Router auto-generated route tree; the file header explicitly
+# says to exclude it from formatters.
+apps/studio/routeTree.gen.ts
packages/icons/__registry__
packages/icons/src/icons/*.ts
apps/ui-library/__registry__
diff --git a/apps/design-system/package.json b/apps/design-system/package.json
index 5f8269c8af0..089349e2223 100644
--- a/apps/design-system/package.json
+++ b/apps/design-system/package.json
@@ -18,7 +18,7 @@
},
"dependencies": {
"@hookform/resolvers": "^3.1.1",
- "@tanstack/react-table": "^8.21.3",
+ "@tanstack/react-table": "catalog:",
"contentlayer2": "0.4.6",
"common": "workspace:*",
"date-fns": "^2.30.0",
diff --git a/apps/docs/package.json b/apps/docs/package.json
index bf086e3a985..2dd0afcbd89 100644
--- a/apps/docs/package.json
+++ b/apps/docs/package.json
@@ -62,7 +62,7 @@
"@octokit/plugin-retry": "^8.1.0",
"@sentry/nextjs": "catalog:",
"@supabase/supabase-js": "catalog:",
- "@tanstack/react-query": "^5.13.4",
+ "@tanstack/react-query": "~5.83.0",
"acorn": "^8.11.3",
"acorn-typescript": "^1.4.13",
"ai-commands": "workspace:*",
diff --git a/apps/studio/.gitignore b/apps/studio/.gitignore
index 39577a49f19..55e8f54894a 100644
--- a/apps/studio/.gitignore
+++ b/apps/studio/.gitignore
@@ -15,6 +15,8 @@ next-env.d.ts
# production
/build
+/dist
+/.output
# misc
.DS_Store
diff --git a/apps/studio/TANSTACK_MIGRATION.md b/apps/studio/TANSTACK_MIGRATION.md
new file mode 100644
index 00000000000..2d49fd90c13
--- /dev/null
+++ b/apps/studio/TANSTACK_MIGRATION.md
@@ -0,0 +1,514 @@
+# 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:next` scripts
+ in `apps/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 their `pages/` 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 in `routes/...` 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.
+
+**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 from `apps/studio/pages/...` and renders it inside a thin wrapper component used as the route's `component`. `getLayout` is 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 the `compat/next/` shim. Because `NextPageWithLayout` declares `{ dehydratedState: any }` as required props, pass `dehydratedState={undefined}` in the wrapper.
+- **Path B — direct component import**. When the `pages/...` file is essentially `export default SomeComponent` re-exporting a component from elsewhere (typical for thin page wrappers), skip the middle-man and import `SomeComponent` directly 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.tsx` next to a `segment/` directory provides the layout with `` for children in that directory (e.g. `_app/account.tsx` wraps `_app/account/me.tsx`). No `route.tsx` files.
+- 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`, no `next/link`). The Next compat shim stays in place for pages we re-export.
+- `withAuth()` HOC → TanStack `beforeLoad` on 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] A` done — re-exported from `pages/...` (Next file still exists, needs body-move later)
+- `[x] A→done` done & body moved — Next file deleted
+- `[x] B` done — 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)
+
+- [x] `routes/_app.tsx` — AppLayout + DefaultLayout (reads `defaultLayoutHeaderTitle`/`hideMobileMenu` from leaf `staticData`)
+- [x] `routes/_app/account.tsx` — AccountLayout (reads `accountLayoutTitle` from leaf `staticData`)
+- [x] `routes/_app/org.tsx` — OrganizationLayout (reads `orgLayoutTitle` from leaf `staticData`). **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.tsx` since only that one route uses it.
+- [x] `routes/_app/new.tsx` — skipped; only `_app/new/index.tsx` lives under \_app (inlines WizardLayout). `new/$slug` is top-level (no AppLayout) so a sub-shell would not actually share state.
+- [x] `routes/integrations/vercel.tsx` — VercelIntegrationWindowLayout. **Delta vs plan:** placed at top-level rather than under `_app/` — Next getLayout for all three leaves wraps only in VercelIntegrationWindowLayout, no AppLayout/DefaultLayout.
+
+### Project shell
+
+- [x] `routes/project/$ref.tsx` — DefaultLayout only. **Delta vs plan:** ProjectLayoutWithAuth omitted from the shell because product layouts (DatabaseLayout, AuthLayout, StorageLayout, …) already render `withAuth(... ProjectLayout ...)` internally — adding it here would double-wrap. The home page (`/project/$ref/index.tsx`) wraps itself in `ProjectLayoutWithAuth` since it has no product layout.
+- [x] `routes/project/$ref/database.tsx` — DatabaseLayout (reads `databaseLayoutTitle` from leaf `staticData`)
+- [x] `routes/project/$ref/database/triggers.tsx` — sub-shell with `PageLayout` + permission gate + nav items, inlined from `DatabaseTriggersLayout`. **Delta vs plan:** the existing `DatabaseTriggersLayout` component wraps `` 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 the `pages/...` files we re-export).
+- [x] `routes/project/$ref/auth.tsx` — AuthLayout (reads `authLayoutTitle` from leaf `staticData`). **Delta vs plan:** shell honours a `skipAuthLayout: true` opt-out in `staticData` for leaves whose own body or sub-layout already wraps in `AuthLayout` (`AuthProvidersLayout`, `AuthEmailsLayout`, `pages/.../auth/third-party.tsx`) — without it those routes would double-wrap (which also doubles `withAuth` + `ProjectLayout`).
+- ~~`routes/project/$ref/auth/templates.tsx` — AuthEmailsLayout~~ **Delta vs plan: not landed.** A unified `templates.tsx` sub-shell would force `templates/$templateId.tsx` (which uses plain `AuthLayout`, not `AuthEmailsLayout`) into the wrong wrapping. Instead `templates/index.tsx` and `auth/smtp.tsx` each set `skipAuthLayout: true` and wrap themselves in `AuthEmailsLayout`; `templates/$templateId.tsx` uses the standard auth shell with `authLayoutTitle: 'Emails'`.
+- [x] `routes/project/$ref/storage.tsx` — StorageLayout + StorageBucketsLayout (reads `storageLayoutTitle`, optional `skipStorageBucketsLayout`, `storageBucketsLayoutTitle`, `storageBucketsLayoutHideSubtitle` from leaf `staticData`). **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 set `skipStorageBucketsLayout: true`. `/storage/s3` uses `storageBucketsLayout{Title,HideSubtitle}` to override the inner header.
+- [x] `routes/project/$ref/realtime.tsx` — RealtimeLayout (reads `realtimeLayoutTitle` from leaf `staticData`)
+- [x] `routes/project/$ref/functions.tsx` — EdgeFunctionsLayout (reads `functionsLayoutTitle` from leaf `staticData`). Honours `skipFunctionsLayout: true` opt-out for the `$functionSlug` subtree, whose `EdgeFunctionDetailsLayout` already wraps `EdgeFunctionsLayout` internally — same pattern as auth.tsx. Sub-shell at `routes/project/$ref/functions/$functionSlug.tsx` provides `EdgeFunctionDetailsLayout` for all 5 slug leaves (reads `edgeFunctionDetailsTitle` from leaf staticData).
+- [x] `routes/project/$ref/branches.tsx` — BranchLayout only. **Delta vs plan:** the per-page `PageLayout` (with different titles + primary/secondary actions) stays in each leaf. Hoisted `BranchesPageWrapper` and `MergeRequestsPageWrapper` to top-level exports in their respective `pages/...` files so the route files can import + re-use the same wrapping.
+- [x] `routes/project/$ref/logs.tsx` — LogsLayout (reads `logsLayoutTitle` from leaf staticData). Honours `skipLogsLayout: true` for `logs/index` (page handles its own ProjectLayout-wrapped content for the UnifiedLogs / no-permission cases). Refactored `pages/.../logs/index.tsx` to move the inline `` into `getLayout` so it isn't duplicated when the TanStack project shell already provides DefaultLayout.
+- [x] `routes/project/$ref/observability.tsx` — ObservabilityLayout (reads `observabilityLayoutTitle` from leaf staticData)
+- [x] `routes/project/$ref/advisors.tsx` — AdvisorsLayout (reads `advisorsLayoutTitle` from leaf staticData). Honours `skipAdvisorsLayout: true` opt-out for the rules sub-shell, which provides its own AdvisorsLayout-less-DefaultLayout wrap. Scans whole match chain (same pattern as functions.tsx).
+- [x] `routes/project/$ref/advisors/rules.tsx` — sub-shell that inlines the inner body of `AdvisorRulesLayout` (AdvisorsLayout + PageLayout with title/tabs/feature-preview badge), minus the outer DefaultLayout (already provided by the parent project shell). Sets `skipAdvisorsLayout: true` on its own staticData. **Delta vs plan:** the existing `AdvisorRulesLayout` component wraps in DefaultLayout + AdvisorsLayout internally, so reusing it as-is would double-wrap both. Inlined the inner part; the Next-side component is untouched.
+- [x] `routes/project/$ref/settings.tsx` — SettingsLayout (reads `settingsLayoutTitle` from leaf staticData). Honours `skipSettingsLayout: true` for `settings/api` (redirect-only page). Adds a sub-shell at `routes/project/$ref/settings/api-keys.tsx` providing `ApiKeysLayout` for both api-keys leaves; `jwt/index` wraps in `JWTKeysLayout` inline since `jwt/legacy` doesn't share it.
+- [x] `routes/project/$ref/integrations.tsx` — ProjectIntegrationsLayout (no staticData; all 4 leaves share identical layout). Layout is `withAuth(({ children }) => {children})`, so the shell just wraps `` once.
+- [x] `routes/project/$ref/sql.tsx` — EditorBaseLayout + SQLEditorLayout. Twin of editor.tsx; all four leaves share identical layout props so the shell hardcodes them (no `staticData` overrides). EditorBaseLayout wraps in ProjectLayoutWithAuth; SQLEditorLayout adds its own `withAuth` HOC but no extra ProjectLayout — same shape as the table editor (auth check runs twice but no double render).
+- [x] `routes/project/$ref/editor.tsx` — EditorBaseLayout + TableEditorLayout. All three leaves share identical layout props so the shell hardcodes them (no `staticData` overrides). EditorBaseLayout wraps in `ProjectLayoutWithAuth` internally; TableEditorLayout's happy path is just a fragment + side-effect (banner) and only wraps in `ProjectLayoutWithAuth` on its no-permission branch — same as Next, no double-wrap in normal use.
+
+### Auth shell (pathless)
+
+- [x] `routes/_auth.tsx` — AuthenticationLayout
+
+---
+
+## Pages
+
+### App shell — `/account/*`
+
+- [x] A `routes/_app/account/me.tsx` ← `pages/account/me.tsx`
+- [x] A `routes/_app/account/security.tsx` ← `pages/account/security.tsx`
+- [x] A `routes/_app/account/audit.tsx` ← `pages/account/audit.tsx`
+- [x] A `routes/_app/account/tokens/index.tsx` ← `pages/account/tokens.tsx`
+- [x] A `routes/_app/account/tokens/scoped.tsx` ← `pages/account/tokens/scoped.tsx`
+
+### App shell — `/org/$slug/*`
+
+- [x] A `routes/_app/org/$slug/index.tsx` ← `pages/org/[slug]/index.tsx`
+- [x] A `routes/_app/org/$slug/apps.tsx` ← `pages/org/[slug]/apps.tsx`
+- [x] A `routes/_app/org/$slug/audit.tsx` ← `pages/org/[slug]/audit.tsx`
+- [x] A `routes/_app/org/$slug/billing.tsx` ← `pages/org/[slug]/billing.tsx`
+- [x] A `routes/_app/org/$slug/documents.tsx` ← `pages/org/[slug]/documents.tsx`
+- [x] A `routes/_app/org/$slug/general.tsx` ← `pages/org/[slug]/general.tsx`
+- [x] A `routes/_app/org/$slug/integrations.tsx` ← `pages/org/[slug]/integrations.tsx`
+- [x] A `routes/_app/org/$slug/security.tsx` ← `pages/org/[slug]/security.tsx`
+- [x] A `routes/_app/org/$slug/sso.tsx` ← `pages/org/[slug]/sso.tsx`
+- [x] A `routes/_app/org/$slug/team.tsx` ← `pages/org/[slug]/team.tsx`
+- [x] A `routes/_app/org/$slug/usage.tsx` ← `pages/org/[slug]/usage.tsx`
+- [x] A `routes/_app/org/$slug/private-apps/index.tsx` ← `pages/org/[slug]/private-apps/index.tsx`
+- [x] A `routes/_app/org/$slug/webhooks/index.tsx` ← `pages/org/[slug]/webhooks/index.tsx`
+- [x] A `routes/_app/org/$slug/webhooks/$endpointId.tsx` ← `pages/org/[slug]/webhooks/[endpointId].tsx`
+- [x] A `routes/_app/org/index.tsx` ← `pages/org/index.tsx` (redirect)
+
+### App shell — top-level pages
+
+- [x] A `routes/_app/organizations.tsx` ← `pages/organizations.tsx` (page default already withAuth-wrapped; PageLayout wraps body)
+- [x] `routes/_app/new/index.tsx` ← `pages/new/index.tsx` (inlines WizardLayout; sets `defaultLayoutHeaderTitle: 'New organization'` + `hideMobileMenu: true` on staticData). **Delta vs plan:** no `_app/new.tsx` sub-shell — `new/$slug` doesn't fit under \_app and uses a different inner wrapper (PageLayout), so a shared shell wouldn't share anything.
+- [x] A `routes/new/$slug.tsx` ← `pages/new/[slug].tsx` **Delta 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.
+- [x] A `routes/aws-marketplace-onboarding.tsx` ← `pages/aws-marketplace-onboarding.tsx` **Delta vs plan:** placed at root rather than under `_app/` — page uses its own `LinkAwsMarketplaceLayout` and doesn't want `AppLayout` + `DefaultLayout` wrapping.
+- [x] A `routes/claim-project.tsx` ← `pages/claim-project.tsx` **Delta vs plan:** placed at root rather than under `_app/` — page uses its own `` + `` layout and doesn't want `AppLayout` + `DefaultLayout` wrapping.
+- [x] A `routes/join.tsx` ← `pages/join.tsx` **Delta vs plan:** placed at root rather than under `_app/` — page uses a centered-div layout and doesn't want `AppLayout` + `DefaultLayout` wrapping.
+- [x] `routes/_app/support/new.tsx` ← `pages/support/new.tsx` (sets `hideMobileMenu: true` staticData; existing page is `withAuth`-wrapped so no beforeLoad migration needed yet)
+- [x] `routes/_app/support/link.tsx` ← `pages/support/link.tsx`
+
+### App shell — integrations
+
+- [x] A `routes/integrations/vercel/install.tsx` ← `pages/integrations/vercel/install.tsx`
+- [x] A `routes/integrations/vercel/$slug/marketplace/choose-project.tsx` ← `pages/integrations/vercel/[slug]/marketplace/choose-project.tsx`
+- [x] A `routes/integrations/vercel/$slug/deploy-button/new-project.tsx` ← `pages/integrations/vercel/[slug]/deploy-button/new-project.tsx`
+- [x] A `routes/integrations/github/authorize.tsx` ← `pages/integrations/github/authorize.tsx` **Delta 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
+
+- [x] A `routes/project/$ref/index.tsx` ← `pages/project/[ref]/index.tsx` (route wraps in `ProjectLayoutWithAuth` itself — see shell delta above)
+- [x] `routes/project/$ref/merge.tsx` ← `pages/project/[ref]/merge.tsx` (leaf wraps body in `ProjectLayoutWithAuth`; parent `project/$ref.tsx` shell provides DefaultLayout)
+
+### Project shell — `/api/*`
+
+- [x] `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/*`
+
+- [x] A `routes/project/$ref/database/schemas.tsx` ← `pages/project/[ref]/database/schemas.tsx`
+- [x] A `routes/project/$ref/database/extensions.tsx` ← `pages/project/[ref]/database/extensions.tsx`
+- [x] A `routes/project/$ref/database/functions.tsx` ← `pages/project/[ref]/database/functions.tsx`
+- [x] A `routes/project/$ref/database/indexes.tsx` ← `pages/project/[ref]/database/indexes.tsx`
+- [x] A `routes/project/$ref/database/migrations.tsx` ← `pages/project/[ref]/database/migrations.tsx`
+- [x] A `routes/project/$ref/database/roles.tsx` ← `pages/project/[ref]/database/roles.tsx`
+- [x] A `routes/project/$ref/database/settings.tsx` ← `pages/project/[ref]/database/settings.tsx`
+- [x] A `routes/project/$ref/database/types.tsx` ← `pages/project/[ref]/database/types.tsx`
+- [x] A `routes/project/$ref/database/column-privileges.tsx` ← `pages/project/[ref]/database/column-privileges.tsx`
+- [x] A `routes/project/$ref/database/tables/index.tsx` ← `pages/project/[ref]/database/tables/index.tsx`
+- [x] A `routes/project/$ref/database/tables/$id.tsx` ← `pages/project/[ref]/database/tables/[id].tsx`
+- [x] A `routes/project/$ref/database/publications/index.tsx` ← `pages/project/[ref]/database/publications/index.tsx`
+- [x] A `routes/project/$ref/database/publications/$id.tsx` ← `pages/project/[ref]/database/publications/[id].tsx`
+- [x] A `routes/project/$ref/database/replication/index.tsx` ← `pages/project/[ref]/database/replication/index.tsx`
+- [x] A `routes/project/$ref/database/replication/$pipelineId.tsx` ← `pages/project/[ref]/database/replication/[pipelineId].tsx`
+- [x] A `routes/project/$ref/database/replication/replica/$replicaId.tsx` ← `pages/project/[ref]/database/replication/replica/[replicaId].tsx`
+- [x] A `routes/project/$ref/database/triggers/index.tsx` ← `pages/project/[ref]/database/triggers/index.tsx`
+- [x] A `routes/project/$ref/database/triggers/data.tsx` ← `pages/project/[ref]/database/triggers/data.tsx` (sub-shell at `database/triggers.tsx` provides PageLayout + nav, parent shell provides DatabaseLayout)
+- [x] A `routes/project/$ref/database/triggers/event.tsx` ← `pages/project/[ref]/database/triggers/event.tsx` (same as data)
+- [x] A `routes/project/$ref/database/backups/pitr.tsx` ← `pages/project/[ref]/database/backups/pitr.tsx`
+- [x] A `routes/project/$ref/database/backups/restore-to-new-project.tsx` ← `pages/project/[ref]/database/backups/restore-to-new-project.tsx`
+- [x] A `routes/project/$ref/database/backups/scheduled.tsx` ← `pages/project/[ref]/database/backups/scheduled.tsx`
+
+### Project shell — `/auth/*`
+
+- [x] A `routes/project/$ref/auth/overview.tsx` ← `pages/project/[ref]/auth/overview.tsx`
+- [x] A `routes/project/$ref/auth/users.tsx` ← `pages/project/[ref]/auth/users.tsx`
+- [x] A `routes/project/$ref/auth/policies.tsx` ← `pages/project/[ref]/auth/policies.tsx`
+- [x] A `routes/project/$ref/auth/providers.tsx` ← `pages/project/[ref]/auth/providers.tsx` (sets `skipAuthLayout: true`, wraps in `AuthProvidersLayout` directly)
+- [x] A `routes/project/$ref/auth/mfa.tsx` ← `pages/project/[ref]/auth/mfa.tsx`
+- [x] A `routes/project/$ref/auth/hooks.tsx` ← `pages/project/[ref]/auth/hooks.tsx`
+- [x] A `routes/project/$ref/auth/smtp.tsx` ← `pages/project/[ref]/auth/smtp.tsx` (sets `skipAuthLayout: true`, wraps in `AuthEmailsLayout` directly)
+- [x] A `routes/project/$ref/auth/sessions.tsx` ← `pages/project/[ref]/auth/sessions.tsx`
+- [x] A `routes/project/$ref/auth/passkeys.tsx` ← `pages/project/[ref]/auth/passkeys.tsx`
+- [x] A `routes/project/$ref/auth/performance.tsx` ← `pages/project/[ref]/auth/performance.tsx`
+- [x] A `routes/project/$ref/auth/protection.tsx` ← `pages/project/[ref]/auth/protection.tsx`
+- [x] A `routes/project/$ref/auth/rate-limits.tsx` ← `pages/project/[ref]/auth/rate-limits.tsx`
+- [x] A `routes/project/$ref/auth/third-party.tsx` ← `pages/project/[ref]/auth/third-party.tsx` (sets `skipAuthLayout: true` — page body inlines `` which already wraps ``)
+- [x] A `routes/project/$ref/auth/oauth-apps.tsx` ← `pages/project/[ref]/auth/oauth-apps.tsx`
+- [x] A `routes/project/$ref/auth/oauth-server.tsx` ← `pages/project/[ref]/auth/oauth-server.tsx`
+- [x] A `routes/project/$ref/auth/url-configuration.tsx` ← `pages/project/[ref]/auth/url-configuration.tsx`
+- [x] A `routes/project/$ref/auth/audit-logs.tsx` ← `pages/project/[ref]/auth/audit-logs.tsx`
+- [x] A `routes/project/$ref/auth/templates/index.tsx` ← `pages/project/[ref]/auth/templates/index.tsx` (sets `skipAuthLayout: true`, wraps in `AuthEmailsLayout` directly)
+- [x] A `routes/project/$ref/auth/templates/$templateId.tsx` ← `pages/project/[ref]/auth/templates/[templateId].tsx` (`authLayoutTitle: 'Emails'` — page uses plain `AuthLayout`, not `AuthEmailsLayout`)
+
+### Project shell — `/storage/*`
+
+- [x] A `routes/project/$ref/storage/s3.tsx` ← `pages/project/[ref]/storage/s3.tsx`
+- [x] A `routes/project/$ref/storage/files/index.tsx` ← `pages/project/[ref]/storage/files/index.tsx`
+- [x] A `routes/project/$ref/storage/files/policies.tsx` ← `pages/project/[ref]/storage/files/policies.tsx`
+- [x] A `routes/project/$ref/storage/files/settings.tsx` ← `pages/project/[ref]/storage/files/settings.tsx`
+- [x] A `routes/project/$ref/storage/files/buckets/$bucketId.tsx` ← `pages/project/[ref]/storage/files/buckets/[bucketId].tsx` (sets `skipStorageBucketsLayout: true`)
+- [x] A `routes/project/$ref/storage/analytics/index.tsx` ← `pages/project/[ref]/storage/analytics/index.tsx`
+- [x] A `routes/project/$ref/storage/analytics/buckets/$bucketId.tsx` ← `pages/project/[ref]/storage/analytics/buckets/[bucketId].tsx` (sets `skipStorageBucketsLayout: true`)
+- [x] A `routes/project/$ref/storage/vectors/index.tsx` ← `pages/project/[ref]/storage/vectors/index.tsx`
+- [x] A `routes/project/$ref/storage/vectors/buckets/$bucketId.tsx` ← `pages/project/[ref]/storage/vectors/buckets/[bucketId].tsx` (sets `skipStorageBucketsLayout: true`)
+
+### Project shell — `/realtime/*`
+
+- [x] A `routes/project/$ref/realtime/inspector.tsx` ← `pages/project/[ref]/realtime/inspector.tsx`
+- [x] A `routes/project/$ref/realtime/policies.tsx` ← `pages/project/[ref]/realtime/policies.tsx`
+- [x] A `routes/project/$ref/realtime/settings.tsx` ← `pages/project/[ref]/realtime/settings.tsx`
+
+### Project shell — `/functions/*`
+
+- [x] A `routes/project/$ref/functions/index.tsx` ← `pages/project/[ref]/functions/index.tsx` (route wraps in exported `EdgeFunctionsIndexPageWrapper` for the inline PageHeader + actions)
+- [x] A `routes/project/$ref/functions/new.tsx` ← `pages/project/[ref]/functions/new.tsx`
+- [x] A `routes/project/$ref/functions/secrets.tsx` ← `pages/project/[ref]/functions/secrets.tsx` (route wraps in exported `SecretsPageWrapper`)
+- [x] A `routes/project/$ref/functions/$functionSlug/index.tsx` ← `pages/project/[ref]/functions/[functionSlug]/index.tsx`
+- [x] A `routes/project/$ref/functions/$functionSlug/code.tsx` ← `pages/project/[ref]/functions/[functionSlug]/code.tsx`
+- [x] A `routes/project/$ref/functions/$functionSlug/details.tsx` ← `pages/project/[ref]/functions/[functionSlug]/details.tsx`
+- [x] A `routes/project/$ref/functions/$functionSlug/invocations.tsx` ← `pages/project/[ref]/functions/[functionSlug]/invocations.tsx`
+- [x] A `routes/project/$ref/functions/$functionSlug/logs.tsx` ← `pages/project/[ref]/functions/[functionSlug]/logs.tsx`
+
+### Project shell — `/branches/*`
+
+- [x] A `routes/project/$ref/branches/index.tsx` ← `pages/project/[ref]/branches/index.tsx` (route wraps in exported `BranchesPageWrapper` to preserve the page's `PageLayout` + Create-branch action)
+- [x] A `routes/project/$ref/branches/merge-requests.tsx` ← `pages/project/[ref]/branches/merge-requests.tsx` (route wraps in exported `MergeRequestsPageWrapper`)
+
+### Project shell — `/logs/*`
+
+- [x] A `routes/project/$ref/logs/index.tsx` ← `pages/project/[ref]/logs/index.tsx` (sets `skipLogsLayout: true`; page handles its own ProjectLayout; DefaultLayout moved to page's `getLayout` so Next still wraps it)
+- [x] A `routes/project/$ref/logs/auth-logs.tsx` ← `pages/project/[ref]/logs/auth-logs.tsx`
+- [x] A `routes/project/$ref/logs/cron-logs.tsx` ← `pages/project/[ref]/logs/cron-logs.tsx`
+- [x] A `routes/project/$ref/logs/dedicated-pooler-logs.tsx` ← `pages/project/[ref]/logs/dedicated-pooler-logs.tsx`
+- [x] A `routes/project/$ref/logs/edge-functions-logs.tsx` ← `pages/project/[ref]/logs/edge-functions-logs.tsx`
+- [x] A `routes/project/$ref/logs/edge-logs.tsx` ← `pages/project/[ref]/logs/edge-logs.tsx`
+- [x] A `routes/project/$ref/logs/pg-upgrade-logs.tsx` ← `pages/project/[ref]/logs/pg-upgrade-logs.tsx`
+- [x] A `routes/project/$ref/logs/pgcron-logs.tsx` ← `pages/project/[ref]/logs/pgcron-logs.tsx`
+- [x] A `routes/project/$ref/logs/pooler-logs.tsx` ← `pages/project/[ref]/logs/pooler-logs.tsx`
+- [x] A `routes/project/$ref/logs/postgres-logs.tsx` ← `pages/project/[ref]/logs/postgres-logs.tsx`
+- [x] A `routes/project/$ref/logs/postgrest-logs.tsx` ← `pages/project/[ref]/logs/postgrest-logs.tsx`
+- [x] A `routes/project/$ref/logs/realtime-logs.tsx` ← `pages/project/[ref]/logs/realtime-logs.tsx`
+- [x] A `routes/project/$ref/logs/replication-logs.tsx` ← `pages/project/[ref]/logs/replication-logs.tsx`
+- [x] A `routes/project/$ref/logs/storage-logs.tsx` ← `pages/project/[ref]/logs/storage-logs.tsx`
+- [x] A `routes/project/$ref/logs/explorer/index.tsx` ← `pages/project/[ref]/logs/explorer/index.tsx`
+- [x] A `routes/project/$ref/logs/explorer/recent.tsx` ← `pages/project/[ref]/logs/explorer/recent.tsx`
+- [x] A `routes/project/$ref/logs/explorer/saved.tsx` ← `pages/project/[ref]/logs/explorer/saved.tsx`
+- [x] A `routes/project/$ref/logs/explorer/templates.tsx` ← `pages/project/[ref]/logs/explorer/templates.tsx`
+
+### Project shell — `/observability/*`
+
+- [x] A `routes/project/$ref/observability/index.tsx` ← `pages/project/[ref]/observability/index.tsx`
+- [x] A `routes/project/$ref/observability/$id.tsx` ← `pages/project/[ref]/observability/[id].tsx`
+- [x] A `routes/project/$ref/observability/auth.tsx` ← `pages/project/[ref]/observability/auth.tsx`
+- [x] A `routes/project/$ref/observability/database.tsx` ← `pages/project/[ref]/observability/database.tsx`
+- [x] A `routes/project/$ref/observability/api-overview.tsx` ← `pages/project/[ref]/observability/api-overview.tsx`
+- [x] A `routes/project/$ref/observability/edge-functions.tsx` ← `pages/project/[ref]/observability/edge-functions.tsx`
+- [x] A `routes/project/$ref/observability/postgrest.tsx` ← `pages/project/[ref]/observability/postgrest.tsx`
+- [x] A `routes/project/$ref/observability/query-insights.tsx` ← `pages/project/[ref]/observability/query-insights.tsx`
+- [x] A `routes/project/$ref/observability/query-performance.tsx` ← `pages/project/[ref]/observability/query-performance.tsx`
+- [x] A `routes/project/$ref/observability/realtime.tsx` ← `pages/project/[ref]/observability/realtime.tsx`
+- [x] A `routes/project/$ref/observability/storage.tsx` ← `pages/project/[ref]/observability/storage.tsx`
+
+### Project shell — `/advisors/*`
+
+- [x] A `routes/project/$ref/advisors/performance.tsx` ← `pages/project/[ref]/advisors/performance.tsx`
+- [x] A `routes/project/$ref/advisors/security.tsx` ← `pages/project/[ref]/advisors/security.tsx`
+- [x] A `routes/project/$ref/advisors/rules/performance.tsx` ← `pages/project/[ref]/advisors/rules/performance.tsx`
+- [x] A `routes/project/$ref/advisors/rules/security.tsx` ← `pages/project/[ref]/advisors/rules/security.tsx`
+
+### Project shell — `/settings/*`
+
+- [x] A `routes/project/$ref/settings/general.tsx` ← `pages/project/[ref]/settings/general.tsx`
+- [x] A `routes/project/$ref/settings/addons.tsx` ← `pages/project/[ref]/settings/addons.tsx`
+- [x] A `routes/project/$ref/settings/api.tsx` ← `pages/project/[ref]/settings/api.tsx` (sets `skipSettingsLayout: true` — page is a useEffect redirect)
+- [x] A `routes/project/$ref/settings/compute-and-disk.tsx` ← `pages/project/[ref]/settings/compute-and-disk.tsx`
+- [x] A `routes/project/$ref/settings/dashboard.tsx` ← `pages/project/[ref]/settings/dashboard.tsx`
+- [x] A `routes/project/$ref/settings/infrastructure.tsx` ← `pages/project/[ref]/settings/infrastructure.tsx`
+- [x] A `routes/project/$ref/settings/integrations.tsx` ← `pages/project/[ref]/settings/integrations.tsx`
+- [x] A `routes/project/$ref/settings/log-drains.tsx` ← `pages/project/[ref]/settings/log-drains.tsx`
+- [x] A `routes/project/$ref/settings/api-keys/index.tsx` ← `pages/project/[ref]/settings/api-keys/index.tsx` (under `api-keys.tsx` sub-shell with ApiKeysLayout)
+- [x] A `routes/project/$ref/settings/api-keys/legacy.tsx` ← `pages/project/[ref]/settings/api-keys/legacy.tsx` (under `api-keys.tsx` sub-shell)
+- [x] A `routes/project/$ref/settings/billing/usage.tsx` ← `pages/project/[ref]/settings/billing/usage.tsx`
+- [x] A `routes/project/$ref/settings/jwt/index.tsx` ← `pages/project/[ref]/settings/jwt/index.tsx` (wraps in JWTKeysLayout inline)
+- [x] A `routes/project/$ref/settings/jwt/legacy.tsx` ← `pages/project/[ref]/settings/jwt/legacy.tsx`
+- [x] A `routes/project/$ref/settings/webhooks/index.tsx` ← `pages/project/[ref]/settings/webhooks/index.tsx`
+- [x] A `routes/project/$ref/settings/webhooks/$endpointId.tsx` ← `pages/project/[ref]/settings/webhooks/[endpointId].tsx`
+
+### Project shell — `/integrations/*`
+
+- [x] `routes/project/$ref/integrations/index.tsx` ← `pages/project/[ref]/integrations/index.tsx`
+- [x] `routes/project/$ref/integrations/$id/index.tsx` ← `pages/project/[ref]/integrations/[id]/index.tsx`
+- [x] `routes/project/$ref/integrations/$id/$pageId/index.tsx` ← `pages/project/[ref]/integrations/[id]/[pageId]/index.tsx`
+- [x] `routes/project/$ref/integrations/$id/$pageId/$childId/index.tsx` ← `pages/project/[ref]/integrations/[id]/[pageId]/[childId]/index.tsx`
+
+### Project shell — `/sql/*`
+
+- [x] A `routes/project/$ref/sql/index.tsx` ← `pages/project/[ref]/sql/index.tsx`
+- [x] A `routes/project/$ref/sql/$id.tsx` ← `pages/project/[ref]/sql/[id].tsx`
+- [x] A `routes/project/$ref/sql/templates.tsx` ← `pages/project/[ref]/sql/templates.tsx`
+- [x] A `routes/project/$ref/sql/quickstarts.tsx` ← `pages/project/[ref]/sql/quickstarts.tsx`
+
+### Project shell — `/editor/*`
+
+- [x] A `routes/project/$ref/editor/index.tsx` ← `pages/project/[ref]/editor/index.tsx`
+- [x] A `routes/project/$ref/editor/$id.tsx` ← `pages/project/[ref]/editor/[id].tsx`
+- [x] A `routes/project/$ref/editor/new.tsx` ← `pages/project/[ref]/editor/new.tsx`
+
+### Auth shell — `/sign-in`, `/sign-up`, etc.
+
+- [x] A `routes/_auth/sign-in.tsx` ← `pages/sign-in.tsx`
+- [x] A `routes/_auth/sign-up.tsx` ← `pages/sign-up.tsx`
+- [x] A `routes/_auth/sign-in-sso.tsx` ← `pages/sign-in-sso.tsx`
+- [x] A `routes/_auth/sign-in-partner.tsx` ← `pages/sign-in-partner.tsx`
+- [x] A `routes/_auth/sign-in-mfa.tsx` ← `pages/sign-in-mfa.tsx` (page inlines SignInLayout)
+- [x] A `routes/_auth/forgot-password.tsx` ← `pages/forgot-password.tsx`
+- [x] A `routes/_auth/forgot-password-mfa.tsx` ← `pages/forgot-password-mfa.tsx` (page inlines ForgotPasswordLayout)
+- [x] A `routes/_auth/reset-password.tsx` ← `pages/reset-password.tsx` (page default already withAuth-wrapped)
+- [x] A `routes/_auth/cli/login.tsx` ← `pages/cli/login.tsx` (page inlines APIAuthorizationLayout, withAuth)
+- [x] A `routes/_auth/partners/stripe/projects/login.tsx` ← `pages/partners/stripe/projects/login.tsx` (page inlines APIAuthorizationLayout, withAuth)
+
+### Standalone (no shared shell)
+
+- [x] B `routes/index.tsx` — redirect-only root route. Mirrors the Next.js `redirects()` rules in `next.config.ts`: platform sends users to `/org` (or `/new/new-project` when deep-linked with `?next=new-project`), self-hosted sends them to `/project/default`. **Follow-up:** the redirect targets currently use `href` (full reload) because they were on the Next side when this was written; switch to `to` now that all of them live in the TanStack tree.
+- [x] A `routes/authorize.tsx` ← `pages/authorize.tsx` (APIAuthorizationLayout)
+- [x] A `routes/redeem.tsx` ← `pages/redeem.tsx` (RedeemCreditsLayout)
+- [x] A `routes/logout.tsx` ← `pages/logout.tsx`
+- [x] A `routes/maintenance.tsx` ← `pages/maintenance.tsx`
+
+### Error pages (handled at root)
+
+- [x] A `__root.tsx` — wired `notFoundComponent` to `pages/404.tsx`
+- [x] `__root.tsx` — wired `errorComponent` to `pages/500.tsx`. Mirrors the in-tree `react-error-boundary` Sentry capture (`scope.setTag('routerErrorComponent', true)`) so router-level errors (loader/component-render failures before the in-tree boundary mounts) still report. `pages/_error.jsx` stays 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`/
+ `end` accumulate into a single `Response` body when the handler returns.
+- **Streaming responses** — `res.writeHead(status, headers?)` (or
+ `res.flushHeaders()`) flips the proxy into streaming mode: a Web
+ `ReadableStream` opens, buffered chunks flush into it, subsequent
+ `res.write(chunk)` enqueues live, `res.end()` closes it. `finalize()`
+ returns the `Response` while the handler keeps pushing chunks. This is
+ what makes `result.pipeUIMessageStreamToResponse(res, …)` (AI SDK)
+ stream token-by-token to the browser.
+- **Client abort** — Web `Request.signal` is plumbed through as
+ `req.on('close' | 'aborted', …)`. AI handlers that wire
+ `abortController.abort()` off those events keep working.
+- **EventEmitter surface** — `req.on`/`once`/`off`/`emit` (events `close`
+ / `aborted` are real; other names accepted but no-op). `res.on`/etc.
+ are no-op stubs so pipe helpers attaching `drain`/`close`/`error`
+ listeners don't crash.
+- **Body parsing** — JSON and `application/x-www-form-urlencoded` parsed
+ to `req.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 the `Response` body as a
+ `ReadableStream`; each artifact file converts via
+ `Readable.toWeb(createReadStream(...))` and pulls chunk-by-chunk into
+ the stream.
+- `routes/api/mcp/index.ts` — uses MCP SDK's
+ `WebStandardStreamableHTTPServerTransport` (`handleRequest(request)`
+ returns a `Response` directly).
+- `pages/api/ai/docs.ts` was 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.
+
+- [x] `routes/api/get-ip-address.ts` — canary port (validates the shim)
+- [x] `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`)
+- [x] `routes/api/ai/**` — AI endpoints (`docs.ts` direct-ported as Web-native)
+- [x] `routes/api/connect/**`
+- [x] `routes/api/content/**`
+- [x] `routes/api/edge-functions/**`
+- [x] `routes/api/integrations/**`
+- [x] `routes/api/platform/**` (60 files, scripted port)
+- [x] `routes/api/v1/**` — except `body.ts` (streaming rewrite)
+- [x] `routes/api/v1/projects/$ref/functions/$slug/body.ts` — Web-streams rewrite. Returns a `Response` whose body is a `ReadableStream`; each artifact file is converted via `Readable.toWeb(createReadStream(...))` and pulled chunk-by-chunk into the multipart stream. Skips the `apiWrapper` since `getFunctionsArtifactStore` already asserts self-hosted mode (the pages-router `withAuth` was a no-op outside `IS_PLATFORM`).
+- [x] `routes/api/mcp/index.ts` — uses `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js`. Takes a Web `Request`, returns a `Response` directly — no shim needed. Query parsing pulled from `request.url`'s search params; headers passed through unchanged.
+- [x] `routes/api/incident-banner.ts`, `routes/api/incident-status.ts` — App
+ Router routes under `app/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 (TanStack `useRouter` +
+ `useLocation` + `useMatches` + `useParams` + `useSearch` glued
+ together), plus a `default` export (`SingletonRouter` shape) for the
+ one module-scope `import router from 'next/router'` consumer
+ (Support/DiscordCTACard) that reads `router.basePath` outside React.
+ `router.pathname` strips the trailing slash TanStack appends to index
+ routes (without it, `router.pathname.split('/')[3]` returns `''`
+ instead of `undefined` for index pages and the project sidebar's
+ active-route check breaks).
+- `_router-events.ts` — adapts `router.events.on(event, handler)` onto
+ `router.subscribe(tsEvent, …)`. Forwards Next's `(url, { shallow })`
+ args. Maps `routeChangeStart` / `routeChangeComplete` /
+ `beforeHistoryChange` / `hashChangeStart` / `hashChangeComplete`.
+ **Known gap:** Next's throw-from-`routeChangeStart`-to-cancel pattern
+ isn't supportable — `subscribe` is fire-and-forget.
+ `usePreventNavigationOnUnsavedChanges` relies on it and needs
+ migrating to TanStack's `useBlocker` separately.
+- `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 the `next/*` modules studio imports. All
+ bundled via `vite.config.ts`'s `nextCompat()` 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: 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 import `createLucideIcon` back from the `ui` chunk
+ (`folder-open-.js` was the canary).
+- `react-vendor` (react + react-dom + scheduler + jsx-runtime) — pinned
+ before `lucide-react` so Rolldown doesn't suck React into the
+ lucide chunk for CJS interop and shift live-bindings across the rest
+ of the graph (`Alert-.js` was 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.
+
+### Other build-side migration changes
+
+- `pnpm-workspace.yaml` catalog now includes `@tanstack/react-router`,
+ `@tanstack/react-start`, `@tanstack/react-table` so studio and
+ ui-library stay aligned. `react-query` is **not** in the catalog yet
+ — three consumers (studio, docs, ui-library) sit on different 5.x
+ ranges and unifying them is a separate decision.
+- `NODE_OPTIONS=--max-old-space-size=8192` is set on the studio
+ `dev` script — Vite's Rolldown-RC frontend hits the default 4 GB
+ ceiling when chewing through studio's module graph in watch mode.
+
+---
+
+## Deferred / revisit
+
+- ~~`pages/org/_/[[...routeSlug]].tsx`~~ landed as `routes/org.[_].tsx` + `routes/org.[_].$.tsx`. **Naming delta:** path-as-filename form (not `routes/org/[_]/index.tsx`) because the index-file form trips a router-generator bug at `getRouteNodes.js:132` — when an `index.tsx` has a bracket-escaped _parent_ segment, `originalRoutePath` gets 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-style `routeSlug` (string[]) or TanStack-style `_splat` (string) and normalises to the array shape.
+- ~~`pages/project/_/[[...routeSlug]].tsx`~~ landed as `routes/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.tsx` redirects from `href` to `to` — all targets
+ now live in the TanStack tree.
+- Migrate `usePreventNavigationOnUnsavedChanges` from `router.events.on('routeChangeStart', …)` (throw-to-cancel pattern) to TanStack's `useBlocker`.
+- Drop the `_splat` / `routeSlug` normalisation block from
+ `pages/org/_/[[...routeSlug]].tsx` + `pages/project/_/[[...routeSlug]].tsx` (only there to keep both runtimes mounting the same body).
+- Remove `RouteValidationWrapper` + `next/router` compat shim usage from `__root.tsx`.
+- Remove `compat/next/` directory entirely once no `next/*` import remains in workspace source.
+- Lift `manualChunks` pins (`class-variance-authority`, `lucide-react`, `react-vendor`) once the structural fix in `packages/ui` lands — see CIRCULAR_IMPORTS.md. Keep `assertNoChunkCycles`; just clear `KNOWN_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:next` scripts from `apps/studio/package.json` once we're committed to TanStack.
+- Delete this file.
diff --git a/apps/studio/components/interfaces/Database/Replication/Destinations.tsx b/apps/studio/components/interfaces/Database/Replication/Destinations.tsx
index 462cb4c7125..d6cc0d79330 100644
--- a/apps/studio/components/interfaces/Database/Replication/Destinations.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/Destinations.tsx
@@ -99,7 +99,17 @@ export const Destinations = () => {
isError: isDatabasesError,
isSuccess: isDatabasesSuccess,
} = useReadReplicasQuery({ projectRef }, { refetchInterval: statusRefetchInterval })
- const readReplicas = databases.filter((x) => x.identifier !== projectRef)
+ // Memoise so the array reference is stable across renders. Without this
+ // the polling useEffect below has an unstable dep, runs every render, and
+ // its `setStatusRefetchInterval(false)` churn keeps the parent re-rendering
+ // — which trips a latent ref-instability bug in @radix-ui/react-slot
+ // (`composeRefs` is called per render instead of `useComposedRefs`) and
+ // tanks the page with "Maximum update depth exceeded" via the Tooltip
+ // trigger refs.
+ const readReplicas = useMemo(
+ () => databases.filter((x) => x.identifier !== projectRef),
+ [databases, projectRef]
+ )
const hasReplicas = isDatabasesSuccess && readReplicas.length > 0
const filteredReplicas =
filterString.length === 0
diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/CancellationFlow.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/CancellationFlow.tsx
index b64817dc36d..c1b4288de28 100644
--- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/CancellationFlow.tsx
+++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/CancellationFlow.tsx
@@ -1,6 +1,6 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
-import React, { ComponentProps, useEffect, useMemo, useState } from 'react'
+import { ComponentProps, useEffect, useMemo, useState } from 'react'
import { DowngradeModal } from './DowngradeModal'
import { ExitSurveyModal } from './ExitSurveyModal'
diff --git a/apps/studio/components/ui/AIEditor/index.tsx b/apps/studio/components/ui/AIEditor/index.tsx
index 63076cff831..1fa7d3bb507 100644
--- a/apps/studio/components/ui/AIEditor/index.tsx
+++ b/apps/studio/components/ui/AIEditor/index.tsx
@@ -218,15 +218,17 @@ export const AIEditor = ({
if (language === 'javascript' || language === 'typescript') {
// The Deno libs are loaded as a raw text via raw-loader in next.config.ts. They're passed as raw text to the
- // Monaco editor.
- import('@/public/deno/edge-runtime.d.ts' as string)
+ // Monaco editor. The @vite-ignore stops Vite/Rolldown's dep scanner from following the dynamic import and
+ // trying to parse the .d.ts declarations as runtime modules — that crashes the scan in vite 8.0.13+ and
+ // cascades into SSR pre-bundling breakage.
+ import(/* @vite-ignore */ '@/public/deno/edge-runtime.d.ts' as string)
.then((module) => {
monaco.languages.typescript.typescriptDefaults.addExtraLib(module.default)
})
.catch((error) => {
console.error('Failed to load Deno edge-runtime typings:', error)
})
- import('@/public/deno/lib.deno.d.ts' as string)
+ import(/* @vite-ignore */ '@/public/deno/lib.deno.d.ts' as string)
.then((module) => {
monaco.languages.typescript.typescriptDefaults.addExtraLib(module.default)
})
diff --git a/apps/studio/data/utils/deployment-commit-query.ts b/apps/studio/data/utils/deployment-commit-query.ts
index 44df7f26534..8729bd57cfd 100644
--- a/apps/studio/data/utils/deployment-commit-query.ts
+++ b/apps/studio/data/utils/deployment-commit-query.ts
@@ -5,7 +5,15 @@ import { BASE_PATH } from '@/lib/constants'
import type { ResponseError, UseCustomQueryOptions } from '@/types'
export async function getDeploymentCommit() {
- const response = await fetchHandler(`${BASE_PATH}/api/get-deployment-commit`)
+ // `credentials: 'omit'` drops cookies for this one request — including the
+ // `__vdpl` skew-protection pin (TanStack, see router.tsx). With no pin cookie,
+ // Vercel's edge routes it to the LATEST deployment, so this check can detect a
+ // newer version even while the rest of the session stays pinned. The endpoint
+ // is public (no auth needed), and we keep the basePath URL so it still routes
+ // to studio in production (root `/api/*` there is the marketing site).
+ const response = await fetchHandler(`${BASE_PATH}/api/get-deployment-commit`, {
+ credentials: 'omit',
+ })
return (await response.json()) as { commitSha: string; commitTime: string }
}
diff --git a/apps/studio/hooks/use-check-latest-deploy.tsx b/apps/studio/hooks/use-check-latest-deploy.tsx
index f695da8f0b4..8e1d4a073a7 100644
--- a/apps/studio/hooks/use-check-latest-deploy.tsx
+++ b/apps/studio/hooks/use-check-latest-deploy.tsx
@@ -6,6 +6,7 @@ import { toast } from 'sonner'
import { Button, StatusIcon } from 'ui'
import { useDeploymentCommitQuery } from '@/data/utils/deployment-commit-query'
+import { BASE_PATH } from '@/lib/constants'
const DeployCheckToast = ({ id }: { id: string | number }) => {
const router = useRouter()
@@ -24,7 +25,16 @@ const DeployCheckToast = ({ id }: { id: string | number }) => {
-
+
)
diff --git a/apps/studio/lib/ai/util.ts b/apps/studio/lib/ai/util.ts
index 953f1e58227..21b47997f7e 100644
--- a/apps/studio/lib/ai/util.ts
+++ b/apps/studio/lib/ai/util.ts
@@ -32,7 +32,7 @@ export async function selectWeightedKey(
const keys = Object.keys(weights) as T[]
const encoder = new TextEncoder()
const data = encoder.encode(input)
- const hashBuffer = await crypto.subtle.digest('SHA-256', data)
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data as BufferSource)
// Use first 4 bytes (32 bit integer)
const hashInt = new DataView(hashBuffer).getUint32(0)
diff --git a/apps/studio/lib/hosted-api-allowlist.ts b/apps/studio/lib/hosted-api-allowlist.ts
new file mode 100644
index 00000000000..614d863ddc3
--- /dev/null
+++ b/apps/studio/lib/hosted-api-allowlist.ts
@@ -0,0 +1,37 @@
+// [Joshen] Allowlist of API endpoints supported in hosted (platform) mode.
+// Every other /api/* route must 404 in platform mode. Shared by the Next
+// middleware (proxy.ts) and the TanStack request middleware (start.ts) so
+// the list can't drift between the two frameworks while both run in parallel.
+export const HOSTED_SUPPORTED_API_URLS = [
+ '/ai/sql/generate-v4',
+ '/ai/sql/policy',
+ '/ai/feedback/rate',
+ '/ai/code/complete',
+ '/ai/sql/cron-v2',
+ '/ai/sql/title-v2',
+ '/ai/sql/filter-v1',
+ '/ai/onboarding/design',
+ '/ai/feedback/classify',
+ '/ai/docs',
+ '/ai/sql/parse-client-code',
+ '/get-ip-address',
+ '/get-utc-time',
+ '/get-deployment-commit',
+ '/check-cname',
+ '/edge-functions/test',
+ '/edge-functions/body',
+ '/generate-attachment-url',
+ '/incident-status',
+ '/incident-banner',
+ '/status-override',
+ '/api/integrations/stripe-sync',
+ '/content/graphql',
+ '/parse-query',
+]
+
+// `pathname` must be basePath-relative — Next's `nextUrl.pathname` already is,
+// and the TanStack guard strips BASE_PATH before calling. Entries are path
+// suffixes, so `endsWith` stays correct regardless.
+export function isHostedSupportedApiPath(pathname: string): boolean {
+ return HOSTED_SUPPORTED_API_URLS.some((url) => pathname.endsWith(url))
+}
diff --git a/apps/studio/lib/role-impersonation.ts b/apps/studio/lib/role-impersonation.ts
index 5d49df34485..0f6353f12b7 100644
--- a/apps/studio/lib/role-impersonation.ts
+++ b/apps/studio/lib/role-impersonation.ts
@@ -126,7 +126,7 @@ function encodeBase64Url(data: ArrayBuffer | Uint8Array | string): string {
function genKey(rawKey: string) {
return window.crypto.subtle.importKey(
'raw',
- encodeText(rawKey),
+ encodeText(rawKey) as BufferSource,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign', 'verify']
@@ -144,7 +144,7 @@ async function createToken(jwtPayload: object, key: string) {
await window.crypto.subtle.sign(
{ name: 'HMAC' },
await genKey(key),
- encodeText(headerAndPayload)
+ encodeText(headerAndPayload) as BufferSource
)
)
)
diff --git a/apps/studio/lib/telemetry.tsx b/apps/studio/lib/telemetry.tsx
index 9ae59ef7441..0b0cbb9358b 100644
--- a/apps/studio/lib/telemetry.tsx
+++ b/apps/studio/lib/telemetry.tsx
@@ -10,7 +10,7 @@ import { API_URL, IS_PLATFORM } from '@/lib/constants'
const getAnonId = async (id: string) => {
const encoder = new TextEncoder()
const data = encoder.encode(id)
- const hashBuffer = await crypto.subtle.digest('SHA-256', data)
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data as BufferSource)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const base64String = btoa(hashArray.map((byte) => String.fromCharCode(byte)).join(''))
diff --git a/apps/studio/next.config.ts b/apps/studio/next.config.ts
index bdaba9c3961..0f956022608 100644
--- a/apps/studio/next.config.ts
+++ b/apps/studio/next.config.ts
@@ -5,6 +5,12 @@ import { withSentryConfig } from '@sentry/nextjs'
import type { NextConfig } from 'next'
import { getCSP } from './csp'
+import {
+ getMaintenanceRedirects,
+ PLATFORM_REDIRECTS,
+ SELF_HOSTED_REDIRECTS,
+ SHARED_REDIRECTS,
+} from './redirects.shared'
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
@@ -61,423 +67,15 @@ const nextConfig = {
]
},
async redirects() {
+ // Rules live in `redirects.shared.ts` (shared with `vercel.ts`). Next
+ // auto-prepends `basePath` to source and destination on its own,
+ // except for the special `/` → basePath bounce below which opts out
+ // via `basePath: false`.
+ const isPlatform = process.env.NEXT_PUBLIC_IS_PLATFORM === 'true'
+ const maintenance = process.env.MAINTENANCE_MODE === 'true'
return [
- ...(process.env.NEXT_PUBLIC_IS_PLATFORM === 'true'
- ? [
- {
- source: '/',
- has: [
- {
- type: 'query' as const,
- key: 'next',
- value: 'new-project',
- },
- ],
- destination: '/new/new-project',
- permanent: false,
- },
- {
- source: '/',
- destination: '/org',
- permanent: false,
- },
- {
- source: '/register',
- destination: '/sign-up',
- permanent: false,
- },
- {
- source: '/signup',
- destination: '/sign-up',
- permanent: false,
- },
- {
- source: '/signin',
- destination: '/sign-in',
- permanent: false,
- },
- {
- source: '/login',
- destination: '/sign-in',
- permanent: false,
- },
- {
- source: '/log-in',
- destination: '/sign-in',
- permanent: false,
- },
- {
- source: '/project/:ref/building',
- destination: '/project/:ref',
- permanent: false,
- },
- ]
- : [
- {
- source: '/',
- destination: '/project/default',
- permanent: false,
- },
- {
- source: '/register',
- destination: '/project/default',
- permanent: false,
- },
- {
- source: '/signup',
- destination: '/project/default',
- permanent: false,
- },
- {
- source: '/signin',
- destination: '/project/default',
- permanent: false,
- },
- {
- source: '/login',
- destination: '/project/default',
- permanent: false,
- },
- {
- source: '/log-in',
- destination: '/project/default',
- permanent: false,
- },
- {
- source: '/project/:ref/building',
- destination: '/project/:ref',
- permanent: false,
- },
- ]),
- {
- source: '/project/:ref/auth',
- destination: '/project/:ref/auth/users',
- permanent: true,
- },
- {
- source: '/project/:ref/auth/advanced',
- destination: '/project/:ref/auth/performance',
- permanent: true,
- },
- {
- source: '/project/:ref/database',
- destination: '/project/:ref/database/tables',
- permanent: true,
- },
- {
- source: '/project/:ref/database/graphiql',
- destination: '/project/:ref/api/graphiql',
- permanent: true,
- },
- {
- source: '/project/:ref/storage',
- destination: '/project/:ref/storage/files',
- permanent: true,
- },
- {
- source: '/project/:ref/storage/buckets',
- destination: '/project/:ref/storage/files',
- permanent: true,
- },
- {
- source: '/project/:ref/storage/policies',
- destination: '/project/:ref/storage/files/policies',
- permanent: true,
- },
- {
- source: '/project/:ref/storage/buckets/:bucketId',
- destination: '/project/:ref/storage/files/buckets/:bucketId',
- permanent: true,
- },
- {
- permanent: true,
- source: '/project/:ref/settings/api-keys/new',
- destination: '/project/:ref/settings/api-keys',
- },
- {
- source: '/project/:ref/settings/storage',
- destination: '/project/:ref/storage/files/settings',
- permanent: true,
- },
- {
- source: '/project/:ref/storage/settings',
- destination: '/project/:ref/storage/files/settings',
- permanent: true,
- },
- {
- source: '/project/:ref/settings/database',
- destination: '/project/:ref/database/settings',
- permanent: true,
- },
- {
- source: '/project/:ref/settings',
- destination: '/project/:ref/settings/general',
- permanent: true,
- },
- {
- source: '/project/:ref/auth/settings',
- destination: '/project/:ref/auth/users',
- permanent: true,
- },
- {
- source: '/project/:ref/settings/billing/subscription',
- has: [
- {
- type: 'query',
- key: 'panel',
- value: 'subscriptionPlan',
- },
- ],
- destination: '/org/_/billing?panel=subscriptionPlan',
- permanent: true,
- },
- {
- source: '/project/:ref/settings/billing/subscription',
- has: [
- {
- type: 'query',
- key: 'panel',
- value: 'pitr',
- },
- ],
- destination: '/project/:ref/settings/addons?panel=pitr',
- permanent: true,
- },
- {
- source: '/project/:ref/settings/billing/subscription',
- has: [
- {
- type: 'query',
- key: 'panel',
- value: 'computeInstance',
- },
- ],
- destination: '/project/:ref/settings/compute-and-disk',
- permanent: true,
- },
- {
- source: '/project/:ref/settings/billing/subscription',
- has: [
- {
- type: 'query',
- key: 'panel',
- value: 'customDomain',
- },
- ],
- destination: '/project/:ref/settings/addons?panel=customDomain',
- permanent: true,
- },
- {
- source: '/project/:ref/settings/billing/subscription',
- destination: '/org/_/billing',
- permanent: true,
- },
- {
- permanent: true,
- source: '/project/:ref/settings/jwt/signing-keys',
- destination: '/project/:ref/settings/jwt',
- },
- {
- source: '/project/:ref/database/api-logs',
- destination: '/project/:ref/logs/edge-logs',
- permanent: true,
- },
- {
- source: '/project/:ref/database/postgres-logs',
- destination: '/project/:ref/logs/postgres-logs',
- permanent: true,
- },
- {
- source: '/project/:ref/database/postgrest-logs',
- destination: '/project/:ref/logs/postgrest-logs',
- permanent: true,
- },
- {
- source: '/project/:ref/database/pgbouncer-logs',
- destination: '/project/:ref/logs/pooler-logs',
- permanent: true,
- },
- {
- source: '/project/:ref/logs/pgbouncer-logs',
- destination: '/project/:ref/logs/pooler-logs',
- permanent: true,
- },
- {
- source: '/project/:ref/database/realtime-logs',
- destination: '/project/:ref/logs/realtime-logs',
- permanent: true,
- },
- {
- source: '/project/:ref/storage/logs',
- destination: '/project/:ref/logs/storage-logs',
- permanent: true,
- },
- {
- source: '/project/:ref/auth/logs',
- destination: '/project/:ref/logs/auth-logs',
- permanent: true,
- },
- {
- source: '/project/:ref/logs-explorer',
- destination: '/project/:ref/logs/explorer',
- permanent: true,
- },
- {
- source: '/project/:ref/sql/quickstarts',
- destination: '/project/:ref/sql/examples',
- permanent: true,
- },
- {
- source: '/org/:slug/settings',
- destination: '/org/:slug/general',
- permanent: true,
- },
- {
- source: '/project/:ref/settings/billing/update',
- destination: '/org/_/billing',
- permanent: true,
- },
- {
- source: '/project/:ref/settings/billing/update/free',
- destination: '/org/_/billing',
- permanent: true,
- },
- {
- source: '/project/:ref/settings/billing/update/pro',
- destination: '/org/_/billing',
- permanent: true,
- },
- {
- source: '/project/:ref/settings/billing/update/team',
- destination: '/org/_/billing',
- permanent: true,
- },
- {
- source: '/project/:ref/settings/billing/update/enterprise',
- destination: '/org/_/billing',
- permanent: true,
- },
- {
- permanent: true,
- source: '/project/:ref/reports/linter',
- destination: '/project/:ref/database/linter',
- },
- {
- permanent: true,
- source: '/project/:ref/reports',
- destination: '/project/:ref/observability',
- },
- {
- permanent: true,
- source: '/project/:ref/reports/:path*',
- destination: '/project/:ref/observability/:path*',
- },
- {
- permanent: true,
- source: '/project/:ref/query-performance',
- destination: '/project/:ref/observability/query-performance',
- },
- {
- permanent: true,
- source: '/project/:ref/advisors/query-performance',
- destination: '/project/:ref/observability/query-performance',
- },
- {
- permanent: true,
- source: '/project/:ref/database/query-performance',
- destination: '/project/:ref/observability/query-performance',
- },
- {
- permanent: true,
- source: '/project/:ref/auth/column-privileges',
- destination: '/project/:ref/database/column-privileges',
- },
- {
- permanent: true,
- source: '/project/:ref/database/linter',
- destination: '/project/:ref/database/security-advisor',
- },
- {
- permanent: true,
- source: '/project/:ref/database/security-advisor',
- destination: '/project/:ref/advisors/security',
- },
- {
- permanent: true,
- source: '/project/:ref/database/performance-advisor',
- destination: '/project/:ref/advisors/performance',
- },
- {
- permanent: true,
- source: '/project/:ref/database/webhooks',
- destination: '/project/:ref/integrations/webhooks/overview',
- },
- {
- permanent: true,
- source: '/project/:ref/database/wrappers',
- destination: '/project/:ref/integrations?category=wrapper',
- },
- {
- permanent: true,
- source: '/project/:ref/database/cron-jobs',
- destination: '/project/:ref/integrations/cron',
- },
- {
- permanent: true,
- source: '/project/:ref/api/graphiql',
- destination: '/project/:ref/integrations/graphiql',
- },
- {
- permanent: true,
- source: '/project/:ref/settings/vault/secrets',
- destination: '/project/:ref/integrations/vault/secrets',
- },
- {
- permanent: true,
- source: '/project/:ref/settings/vault/keys',
- destination: '/project/:ref/integrations/vault/keys',
- },
- {
- permanent: true,
- source: '/project/:ref/integrations/cron-jobs',
- destination: '/project/:ref/integrations/cron',
- },
- {
- permanent: true,
- source: '/project/:ref/settings/warehouse',
- destination: '/project/:ref/settings/general',
- },
- {
- permanent: true,
- source: '/project/:ref/settings/functions',
- destination: '/project/:ref/functions/secrets',
- },
- {
- source: '/org/:slug/invoices',
- destination: '/org/:slug/billing#invoices',
- permanent: true,
- },
- {
- source: '/projects',
- destination: '/organizations',
- permanent: false,
- },
- {
- source: '/project/:ref/settings/auth',
- destination: '/project/:ref/auth/providers',
- permanent: true,
- },
- {
- source: '/project/:ref/settings/api',
- destination: '/project/:ref/integrations/data_api/overview',
- permanent: false,
- },
- {
- source: '/project/:ref/api',
- destination: '/project/:ref/integrations/data_api/docs',
- permanent: false,
- },
-
+ ...(isPlatform ? PLATFORM_REDIRECTS : SELF_HOSTED_REDIRECTS),
+ ...SHARED_REDIRECTS,
...(process.env.NEXT_PUBLIC_BASE_PATH?.length
? [
{
@@ -488,22 +86,7 @@ const nextConfig = {
},
]
: []),
-
- ...(process.env.MAINTENANCE_MODE === 'true'
- ? [
- {
- source: '/((?!maintenance|img).*)', // Redirect all paths except /maintenance and /img
- destination: '/maintenance',
- permanent: false,
- },
- ]
- : [
- {
- source: '/maintenance',
- destination: '/',
- permanent: false,
- },
- ]),
+ ...getMaintenanceRedirects(maintenance),
]
},
async headers() {
diff --git a/apps/studio/package.json b/apps/studio/package.json
index 5c1c9f7e86b..156f4d7e111 100644
--- a/apps/studio/package.json
+++ b/apps/studio/package.json
@@ -6,9 +6,17 @@
"scripts": {
"preinstall": "npx only-allow pnpm",
"predev": "node scripts/clean-turbopack-cache.mjs",
- "dev": "next dev -p ${STUDIO_PORT:-8082}",
- "build": "next build && if [ \"$SKIP_ASSET_UPLOAD\" != \"1\" ]; then ./../../scripts/upload-static-assets.sh; fi",
- "start": "next start -p 8082",
+ "dev": "node scripts/dispatch.js dev",
+ "build": "node scripts/dispatch.js build",
+ "start": "node scripts/dispatch.js start",
+ "preview": "vite preview --mode ${MODE:-production} --port 8082",
+ "dev:next": "next dev -p ${STUDIO_PORT:-8082}",
+ "build:next": "next build && if [ \"$SKIP_ASSET_UPLOAD\" != \"1\" ]; then ./../../scripts/upload-static-assets.sh; fi",
+ "start:next": "next start -p 8082",
+ "dev:tanstack": "NODE_OPTIONS=--max-old-space-size=8192 vite dev --port ${STUDIO_PORT:-8082}",
+ "build:tanstack": "vite build --mode ${MODE:-production} && pnpm smoke:tanstack",
+ "smoke:tanstack": "node scripts/smoke-server.mjs",
+ "start:tanstack": "node scripts/serve.js",
"lint": "eslint .",
"lint:ratchet": "tsx scripts/ratchet-eslint-rules.ts --rule react-hooks/exhaustive-deps --rule import/no-anonymous-default-export --rule @tanstack/query/exhaustive-deps --rule @typescript-eslint/no-explicit-any --rule no-restricted-imports --rule no-restricted-exports --rule react/no-unstable-nested-components",
"clean": "rimraf node_modules tsconfig.tsbuildinfo .next .turbo",
@@ -54,7 +62,11 @@
"@monaco-editor/react": "^4.6.0",
"@next/bundle-analyzer": "16.2.3",
"@number-flow/react": "^0.3.2",
+ "@radix-ui/react-slider": "^1.1.2",
+ "@radix-ui/react-slot": "^1.2.0",
+ "@radix-ui/react-visually-hidden": "^1.2.3",
"@sentry/nextjs": "catalog:",
+ "@sentry/react": "^10.27.0",
"@std/path": "npm:@jsr/std__path@^1.0.8",
"@stripe/react-stripe-js": "6.1.0",
"@stripe/stripe-js": "9.1.0",
@@ -65,11 +77,16 @@
"@supabase/shared-types": "0.1.88",
"@supabase/sql-to-rest": "^0.1.6",
"@supabase/supabase-js": "catalog:",
- "@tanstack/react-hotkeys": "^0.9.1",
- "@tanstack/react-query": "^5.0.0",
- "@tanstack/react-query-devtools": "^5.0.0",
- "@tanstack/react-table": "^8.21.3",
- "@tanstack/react-virtual": "^3.13.12",
+ "@tanstack/react-devtools": "^0.10.3",
+ "@tanstack/react-hotkeys": "^0.10.0",
+ "@tanstack/react-query": "~5.83.0",
+ "@tanstack/react-query-devtools": "~5.90.2",
+ "@tanstack/react-router": "catalog:",
+ "@tanstack/react-router-devtools": "^1.166.13",
+ "@tanstack/react-router-ssr-query": "^1.166.12",
+ "@tanstack/react-start": "catalog:",
+ "@tanstack/react-table": "catalog:",
+ "@tanstack/react-virtual": "^3.13.24",
"@types/d3-geo": "^3.1.0",
"@uidotdev/usehooks": "^2.4.1",
"@vercel/functions": "^2.1.0",
@@ -154,6 +171,9 @@
"@graphql-typed-document-node/core": "^3.2.0",
"@radix-ui/react-use-escape-keydown": "^1.1.1",
"@smithy/property-provider": "^4.0.4",
+ "@tailwindcss/container-queries": "^0.1.1",
+ "@tailwindcss/vite": "4.2.4",
+ "@tanstack/devtools-vite": "^0.6.0",
"@testing-library/dom": "^10.0.0",
"@testing-library/jest-dom": "^6.6.0",
"@testing-library/react": "^16.0.0",
@@ -177,6 +197,7 @@
"@types/recharts": "^1.8.23",
"@types/sqlstring": "^2.3.0",
"@types/zxcvbn": "^4.4.1",
+ "@vercel/config": "^0.2.1",
"@vitejs/plugin-react": "catalog:",
"@vitest/coverage-v8": "catalog:",
"@vitest/ui": "catalog:",
diff --git a/apps/studio/proxy.ts b/apps/studio/proxy.ts
index 2ca590998b7..cfb19146dfe 100644
--- a/apps/studio/proxy.ts
+++ b/apps/studio/proxy.ts
@@ -1,44 +1,17 @@
import type { NextRequest } from 'next/server'
import { IS_PLATFORM } from '@/lib/constants'
+import { isHostedSupportedApiPath } from '@/lib/hosted-api-allowlist'
export const config = {
matcher: '/api/:function*',
}
-// [Joshen] Return 404 for all next.js API endpoints EXCEPT the ones we use in hosted:
-const HOSTED_SUPPORTED_API_URLS = [
- '/ai/sql/generate-v4',
- '/ai/sql/policy',
- '/ai/feedback/rate',
- '/ai/code/complete',
- '/ai/sql/cron-v2',
- '/ai/sql/title-v2',
- '/ai/sql/filter-v1',
- '/ai/onboarding/design',
- '/ai/feedback/classify',
- '/ai/docs',
- '/ai/sql/parse-client-code',
- '/get-ip-address',
- '/get-utc-time',
- '/get-deployment-commit',
- '/check-cname',
- '/edge-functions/test',
- '/edge-functions/body',
- '/generate-attachment-url',
- '/incident-status',
- '/incident-banner',
- '/status-override',
- '/api/integrations/stripe-sync',
- '/content/graphql',
- '/parse-query',
-]
-
+// Return 404 for all next.js API endpoints EXCEPT the ones we use in hosted.
+// The allowlist is shared with the TanStack guard (start.ts) — see
+// lib/hosted-api-allowlist.ts.
export function proxy(request: NextRequest) {
- if (
- IS_PLATFORM &&
- !HOSTED_SUPPORTED_API_URLS.some((url) => request.nextUrl.pathname.endsWith(url))
- ) {
+ if (IS_PLATFORM && !isHostedSupportedApiPath(request.nextUrl.pathname)) {
return Response.json(
{ success: false, message: 'Endpoint not supported on hosted' },
{ status: 404 }
diff --git a/apps/studio/redirects.shared.ts b/apps/studio/redirects.shared.ts
new file mode 100644
index 00000000000..a786fac35d3
--- /dev/null
+++ b/apps/studio/redirects.shared.ts
@@ -0,0 +1,424 @@
+// Shared redirect tables consumed by both `next.config.ts` (the Next.js
+// build) and `vercel.ts` (the TanStack/Vercel deploy). Defined once here
+// so the two configs don't drift.
+//
+// Shape mirrors Next's `redirects()` entry and Vercel's `Redirect` type
+// (they're identical — `source`, `destination`, `permanent`, optional
+// `has`). Basepath handling differs between the two: Next auto-prefixes,
+// Vercel doesn't — each consumer is responsible for that.
+
+export interface StudioRedirect {
+ source: string
+ destination: string
+ permanent: boolean
+ has?: Array<{ type: 'query'; key: string; value: string }>
+}
+
+export const PLATFORM_REDIRECTS: StudioRedirect[] = [
+ {
+ source: '/',
+ has: [{ type: 'query', key: 'next', value: 'new-project' }],
+ destination: '/new/new-project',
+ permanent: false,
+ },
+ { source: '/', destination: '/org', permanent: false },
+ { source: '/register', destination: '/sign-up', permanent: false },
+ { source: '/signup', destination: '/sign-up', permanent: false },
+ { source: '/signin', destination: '/sign-in', permanent: false },
+ { source: '/login', destination: '/sign-in', permanent: false },
+ { source: '/log-in', destination: '/sign-in', permanent: false },
+ { source: '/project/:ref/building', destination: '/project/:ref', permanent: false },
+]
+
+export const SELF_HOSTED_REDIRECTS: StudioRedirect[] = [
+ { source: '/', destination: '/project/default', permanent: false },
+ { source: '/register', destination: '/project/default', permanent: false },
+ { source: '/signup', destination: '/project/default', permanent: false },
+ { source: '/signin', destination: '/project/default', permanent: false },
+ { source: '/login', destination: '/project/default', permanent: false },
+ { source: '/log-in', destination: '/project/default', permanent: false },
+ { source: '/project/:ref/building', destination: '/project/:ref', permanent: false },
+]
+
+export const SHARED_REDIRECTS: StudioRedirect[] = [
+ { source: '/project/:ref/auth', destination: '/project/:ref/auth/users', permanent: true },
+ {
+ source: '/project/:ref/auth/advanced',
+ destination: '/project/:ref/auth/performance',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/database',
+ destination: '/project/:ref/database/tables',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/database/graphiql',
+ destination: '/project/:ref/api/graphiql',
+ permanent: true,
+ },
+ { source: '/project/:ref/storage', destination: '/project/:ref/storage/files', permanent: true },
+ {
+ source: '/project/:ref/storage/buckets',
+ destination: '/project/:ref/storage/files',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/storage/policies',
+ destination: '/project/:ref/storage/files/policies',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/storage/buckets/:bucketId',
+ destination: '/project/:ref/storage/files/buckets/:bucketId',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/settings/api-keys/new',
+ destination: '/project/:ref/settings/api-keys',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/settings/storage',
+ destination: '/project/:ref/storage/files/settings',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/storage/settings',
+ destination: '/project/:ref/storage/files/settings',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/settings/database',
+ destination: '/project/:ref/database/settings',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/settings',
+ destination: '/project/:ref/settings/general',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/auth/settings',
+ destination: '/project/:ref/auth/users',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/settings/billing/subscription',
+ has: [{ type: 'query', key: 'panel', value: 'subscriptionPlan' }],
+ destination: '/org/_/billing?panel=subscriptionPlan',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/settings/billing/subscription',
+ has: [{ type: 'query', key: 'panel', value: 'pitr' }],
+ destination: '/project/:ref/settings/addons?panel=pitr',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/settings/billing/subscription',
+ has: [{ type: 'query', key: 'panel', value: 'computeInstance' }],
+ destination: '/project/:ref/settings/compute-and-disk',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/settings/billing/subscription',
+ has: [{ type: 'query', key: 'panel', value: 'customDomain' }],
+ destination: '/project/:ref/settings/addons?panel=customDomain',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/settings/billing/subscription',
+ destination: '/org/_/billing',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/settings/jwt/signing-keys',
+ destination: '/project/:ref/settings/jwt',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/database/api-logs',
+ destination: '/project/:ref/logs/edge-logs',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/database/postgres-logs',
+ destination: '/project/:ref/logs/postgres-logs',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/database/postgrest-logs',
+ destination: '/project/:ref/logs/postgrest-logs',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/database/pgbouncer-logs',
+ destination: '/project/:ref/logs/pooler-logs',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/logs/pgbouncer-logs',
+ destination: '/project/:ref/logs/pooler-logs',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/database/realtime-logs',
+ destination: '/project/:ref/logs/realtime-logs',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/storage/logs',
+ destination: '/project/:ref/logs/storage-logs',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/auth/logs',
+ destination: '/project/:ref/logs/auth-logs',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/logs-explorer',
+ destination: '/project/:ref/logs/explorer',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/sql/quickstarts',
+ destination: '/project/:ref/sql/examples',
+ permanent: true,
+ },
+ { source: '/org/:slug/settings', destination: '/org/:slug/general', permanent: true },
+ {
+ source: '/project/:ref/settings/billing/update',
+ destination: '/org/_/billing',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/settings/billing/update/free',
+ destination: '/org/_/billing',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/settings/billing/update/pro',
+ destination: '/org/_/billing',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/settings/billing/update/team',
+ destination: '/org/_/billing',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/settings/billing/update/enterprise',
+ destination: '/org/_/billing',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/reports/linter',
+ destination: '/project/:ref/database/linter',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/reports',
+ destination: '/project/:ref/observability',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/reports/:path*',
+ destination: '/project/:ref/observability/:path*',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/query-performance',
+ destination: '/project/:ref/observability/query-performance',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/advisors/query-performance',
+ destination: '/project/:ref/observability/query-performance',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/database/query-performance',
+ destination: '/project/:ref/observability/query-performance',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/auth/column-privileges',
+ destination: '/project/:ref/database/column-privileges',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/database/linter',
+ destination: '/project/:ref/database/security-advisor',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/database/security-advisor',
+ destination: '/project/:ref/advisors/security',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/database/performance-advisor',
+ destination: '/project/:ref/advisors/performance',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/database/webhooks',
+ destination: '/project/:ref/integrations/webhooks/overview',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/database/wrappers',
+ destination: '/project/:ref/integrations?category=wrapper',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/database/cron-jobs',
+ destination: '/project/:ref/integrations/cron',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/api/graphiql',
+ destination: '/project/:ref/integrations/graphiql',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/settings/vault/secrets',
+ destination: '/project/:ref/integrations/vault/secrets',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/settings/vault/keys',
+ destination: '/project/:ref/integrations/vault/keys',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/integrations/cron-jobs',
+ destination: '/project/:ref/integrations/cron',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/settings/warehouse',
+ destination: '/project/:ref/settings/general',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/settings/functions',
+ destination: '/project/:ref/functions/secrets',
+ permanent: true,
+ },
+ { source: '/org/:slug/invoices', destination: '/org/:slug/billing#invoices', permanent: true },
+ { source: '/projects', destination: '/organizations', permanent: false },
+ {
+ source: '/project/:ref/settings/auth',
+ destination: '/project/:ref/auth/providers',
+ permanent: true,
+ },
+ {
+ source: '/project/:ref/settings/api',
+ destination: '/project/:ref/integrations/data_api/overview',
+ permanent: false,
+ },
+ {
+ source: '/project/:ref/api',
+ destination: '/project/:ref/integrations/data_api/docs',
+ permanent: false,
+ },
+]
+
+// The two maintenance-mode rules are mutually exclusive; pick by env at
+// build time. Same shape for Next and Vercel.
+export function getMaintenanceRedirects(maintenanceMode: boolean): StudioRedirect[] {
+ return maintenanceMode
+ ? [{ source: '/((?!maintenance|img).*)', destination: '/maintenance', permanent: false }]
+ : [{ source: '/maintenance', destination: '/', permanent: false }]
+}
+
+// ---------------------------------------------------------------------------
+// Runtime matcher — used by TanStack's `__root` beforeLoad so self-hosted
+// (and any request Vercel's edge didn't already intercept) still gets the
+// same redirect behaviour as the Next.js / Vercel deploys.
+//
+// The patterns above are written in Next/Vercel-flavoured syntax: `:name`
+// for a single segment, `:name*` for a trailing catch-all, literals
+// otherwise. That's all the redirect rules use, so a hand-rolled matcher
+// is simpler (and dependency-free) than pulling in path-to-regexp here.
+// ---------------------------------------------------------------------------
+
+type RedirectMatch = { destination: string; permanent: boolean }
+
+function matchPattern(pattern: string, pathname: string): Record | null {
+ const patternParts = pattern.split('/')
+ const pathParts = pathname.split('/')
+ const params: Record = {}
+
+ for (let i = 0; i < patternParts.length; i++) {
+ const p = patternParts[i]
+ if (p?.startsWith(':') && p.endsWith('*')) {
+ params[p.slice(1, -1)] = pathParts.slice(i).join('/')
+ return params
+ }
+ if (p?.startsWith(':')) {
+ const v = pathParts[i]
+ if (!v) return null
+ params[p.slice(1)] = decodeURIComponent(v)
+ continue
+ }
+ if (p !== pathParts[i]) return null
+ }
+ return patternParts.length === pathParts.length ? params : null
+}
+
+function substituteDestination(dest: string, params: Record): string {
+ return dest.replace(/:(\w+)\*?/g, (_, name) => params[name] ?? '')
+}
+
+function hasQueryMatches(
+ has: StudioRedirect['has'],
+ search: URLSearchParams | Record
+): boolean {
+ if (!has?.length) return true
+ const get = (k: string) =>
+ search instanceof URLSearchParams
+ ? search.get(k)
+ : Array.isArray(search[k])
+ ? (search[k] as string[])[0]
+ : (search[k] as string | undefined)
+ return has.every((h) => h.type === 'query' && get(h.key) === h.value)
+}
+
+export function matchRedirect(input: {
+ pathname: string
+ search: URLSearchParams | Record
+ isPlatform: boolean
+ maintenanceMode?: boolean
+}): RedirectMatch | null {
+ const { pathname, search, isPlatform, maintenanceMode = false } = input
+
+ // Maintenance mode handled inline — the maintenance-on rule uses a
+ // negative-lookahead regex source that the segment matcher above can't
+ // parse. Cheap to special-case here.
+ if (maintenanceMode) {
+ if (!pathname.startsWith('/maintenance') && !pathname.startsWith('/img')) {
+ return { destination: '/maintenance', permanent: false }
+ }
+ } else if (pathname === '/maintenance') {
+ return { destination: '/', permanent: false }
+ }
+
+ const ordered = [
+ ...(isPlatform ? PLATFORM_REDIRECTS : SELF_HOSTED_REDIRECTS),
+ ...SHARED_REDIRECTS,
+ ]
+ for (const rule of ordered) {
+ const params = matchPattern(rule.source, pathname)
+ if (!params) continue
+ if (!hasQueryMatches(rule.has, search)) continue
+ return {
+ destination: substituteDestination(rule.destination, params),
+ permanent: rule.permanent,
+ }
+ }
+ return null
+}
diff --git a/apps/studio/scripts/dispatch.js b/apps/studio/scripts/dispatch.js
new file mode 100644
index 00000000000..4ca2f43f3c5
--- /dev/null
+++ b/apps/studio/scripts/dispatch.js
@@ -0,0 +1,64 @@
+#!/usr/bin/env node
+// Dispatch a top-level npm script (dev/build/start) to either the next- or
+// tanstack-flavoured variant based on STUDIO_FRAMEWORK. We parse the env files
+// (via the shared scripts/lib/env.js parser) and pull out only
+// STUDIO_FRAMEWORK — we deliberately don't load the whole file into the
+// child's process.env, because scripts/serve.js / vite do their own .env
+// loading and would otherwise refuse to override the dispatcher-set values,
+// including NEXT_PUBLIC_IS_PLATFORM which the e2e `.env.test` needs to flip to
+// `false`.
+//
+// Usage: node scripts/dispatch.js
+// target ∈ { dev, build, start }
+//
+// Resolves to `pnpm run :` where framework is `tanstack`
+// when STUDIO_FRAMEWORK=tanstack (set in the shell env, `.env`, or
+// `.env.local`), otherwise `next`.
+import { spawn } from 'node:child_process'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+import { readEnvFiles } from './lib/env.js'
+
+const target = process.argv[2]
+if (!target) {
+ console.error('dispatch.js: missing target (expected one of: dev, build, start)')
+ process.exit(2)
+}
+
+const studioRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
+
+// Shell env wins, then `.env.local`, then `.env` — the same precedence
+// scripts/serve.js and vite use, so STUDIO_FRAMEWORK set in either file is
+// picked up (not just `.env.local`).
+const fileEnv = readEnvFiles(studioRoot, ['.env', '.env.local'])
+const studioFramework = process.env.STUDIO_FRAMEWORK ?? fileEnv.STUDIO_FRAMEWORK
+const framework = studioFramework === 'tanstack' ? 'tanstack' : 'next'
+const script = `${target}:${framework}`
+
+// Use async `spawn` rather than `spawnSync` — long-running dev servers
+// (vite dev / next dev) wedge under `spawnSync` because Node holds the
+// event loop and stdin doesn't flow through cleanly. The dev server says
+// "ready" then exits ~1s later. `spawn` + manual forwarding keeps the
+// child interactive and lets the parent exit cleanly when the child does.
+const child = spawn('pnpm', ['run', script], {
+ stdio: 'inherit',
+ env: process.env,
+})
+
+const forwardSignal = (signal) => {
+ if (!child.killed) child.kill(signal)
+}
+for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT']) {
+ process.on(signal, () => forwardSignal(signal))
+}
+
+child.on('exit', (code, signal) => {
+ if (signal) process.kill(process.pid, signal)
+ else process.exit(code ?? 1)
+})
+
+child.on('error', (err) => {
+ console.error('dispatch.js: failed to spawn child:', err)
+ process.exit(1)
+})
diff --git a/apps/studio/scripts/lib/env.js b/apps/studio/scripts/lib/env.js
new file mode 100644
index 00000000000..0c8c2e95d30
--- /dev/null
+++ b/apps/studio/scripts/lib/env.js
@@ -0,0 +1,52 @@
+// Shared .env parsing for the studio build/serve scripts (dispatch.js,
+// serve.js). We parse the small subset of the dotenv format we actually rely
+// on — `KEY=value`, an optional `export` prefix, and surrounding single/double
+// quotes — rather than taking on the `dotenv` dependency for a couple of build
+// scripts. Crucially, nothing here touches `process.env`: callers decide what
+// to do with the parsed values, which is what lets dispatch.js read a single
+// key without leaking the whole file into the child process.
+//
+// dispatch.js and serve.js must agree on this format, so it lives here once.
+import { readFileSync } from 'node:fs'
+import path from 'node:path'
+
+const ENV_LINE = /^\s*(?:export\s+)?([\w.-]+)\s*=\s*(.*?)\s*$/
+
+// Parse the contents of a single env file into a plain key/value object.
+export function parseEnv(content) {
+ const parsed = {}
+ for (const raw of content.split('\n')) {
+ const m = ENV_LINE.exec(raw)
+ if (!m) continue
+ let value = m[2]
+ if (
+ (value.startsWith('"') && value.endsWith('"')) ||
+ (value.startsWith("'") && value.endsWith("'"))
+ ) {
+ value = value.slice(1, -1)
+ }
+ parsed[m[1]] = value
+ }
+ return parsed
+}
+
+// Read a cascade of env files from `dir`, with later files overriding earlier
+// ones — matching the precedence vite/Next use
+// (`.env` < `.env.local` < `.env.` < `.env..local`). A missing file
+// is skipped, but any other read error (permissions, IO) is surfaced rather
+// than silently dropping that file's values. Returns the merged key/value
+// object; never mutates process.env.
+export function readEnvFiles(dir, files) {
+ const parsed = {}
+ for (const file of files) {
+ let content
+ try {
+ content = readFileSync(path.join(dir, file), 'utf8')
+ } catch (err) {
+ if (err && typeof err === 'object' && err.code === 'ENOENT') continue
+ throw err
+ }
+ Object.assign(parsed, parseEnv(content))
+ }
+ return parsed
+}
diff --git a/apps/studio/scripts/ratchet-eslint-rules.ts b/apps/studio/scripts/ratchet-eslint-rules.ts
index a26de11e4af..e516aa446ad 100644
--- a/apps/studio/scripts/ratchet-eslint-rules.ts
+++ b/apps/studio/scripts/ratchet-eslint-rules.ts
@@ -128,7 +128,7 @@ function dangerouslyRunEsLint(eslintCmd: string, eslintArgs: string): ESLintExec
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
env: process.env,
- maxBuffer: 32 * 1024 * 1024, // allow large ESLint JSON payloads
+ maxBuffer: 128 * 1024 * 1024, // allow large ESLint JSON payloads (the studio repo regularly emits ~35MB+)
})
const stdout = typeof proc.stdout === 'string' ? proc.stdout : ''
diff --git a/apps/studio/scripts/serve.js b/apps/studio/scripts/serve.js
new file mode 100644
index 00000000000..18aff7f15bb
--- /dev/null
+++ b/apps/studio/scripts/serve.js
@@ -0,0 +1,183 @@
+#!/usr/bin/env node
+// Standalone Node HTTP server that hosts the production studio build.
+//
+// We export the fetch-handler shape from `dist/server/server.js` because
+// Vercel consumes it directly (see `apps/studio/api/server.js`). For
+// self-hosted / e2e, we need an HTTP listener of our own — this is that
+// listener.
+//
+// Responsibilities:
+// - Load env files in vite preview's order so non-NEXT_PUBLIC_* values
+// (POSTGRES_PASSWORD, PG_META_CRYPTO_KEY, etc.) are in process.env
+// at request time. NEXT_PUBLIC_* are already inlined into the bundle
+// at build time and don't need to be re-loaded.
+// - Serve static client assets from `dist/client/` directly with the
+// right MIME types and cache headers.
+// - Forward everything else to the TanStack Start handler exported
+// from `dist/server/server.js`.
+import { createReadStream } from 'node:fs'
+import { stat } from 'node:fs/promises'
+import { createServer } from 'node:http'
+import path from 'node:path'
+import { Readable } from 'node:stream'
+import { fileURLToPath } from 'node:url'
+
+import { readEnvFiles } from './lib/env.js'
+
+const studioRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
+const clientDir = path.join(studioRoot, 'dist/client')
+const mode = process.env.MODE || 'production'
+
+const envFiles = ['.env', '.env.local', `.env.${mode}`, `.env.${mode}.local`]
+const parsed = readEnvFiles(studioRoot, envFiles)
+// Don't clobber values the shell already provides — match `vite preview`.
+for (const [k, v] of Object.entries(parsed)) {
+ if (process.env[k] !== undefined) continue
+ process.env[k] = v.replace(
+ /\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?/g,
+ (_, name) => process.env[name] ?? parsed[name] ?? ''
+ )
+}
+
+const { default: handler } = await import(path.join(studioRoot, 'dist/server/server.js'))
+
+const mimeByExt = new Map([
+ ['.js', 'application/javascript; charset=utf-8'],
+ ['.mjs', 'application/javascript; charset=utf-8'],
+ ['.css', 'text/css; charset=utf-8'],
+ ['.html', 'text/html; charset=utf-8'],
+ ['.json', 'application/json; charset=utf-8'],
+ ['.map', 'application/json; charset=utf-8'],
+ ['.png', 'image/png'],
+ ['.jpg', 'image/jpeg'],
+ ['.jpeg', 'image/jpeg'],
+ ['.gif', 'image/gif'],
+ ['.svg', 'image/svg+xml'],
+ ['.ico', 'image/x-icon'],
+ ['.woff', 'font/woff'],
+ ['.woff2', 'font/woff2'],
+ ['.txt', 'text/plain; charset=utf-8'],
+ ['.webmanifest', 'application/manifest+json'],
+])
+
+// Vite emits hashed filenames (e.g. `index-DB4J79t9.js`) for everything
+// it bundles. Those are content-addressed so we serve them immutable.
+const HASHED_RE = /-[A-Za-z0-9_-]{6,}\.[a-z0-9]+$/
+
+async function serveStatic(req, res) {
+ let pathname
+ try {
+ pathname = new URL(req.url, 'http://localhost').pathname
+ } catch {
+ return false
+ }
+ if (pathname === '/' || pathname.endsWith('/')) return false
+ if (pathname.includes('..') || pathname.includes('\\')) return false
+ const filePath = path.join(clientDir, pathname)
+ if (!filePath.startsWith(clientDir + path.sep)) return false
+
+ let st
+ try {
+ st = await stat(filePath)
+ } catch {
+ return false
+ }
+ if (!st.isFile()) return false
+
+ res.statusCode = 200
+ res.setHeader(
+ 'content-type',
+ mimeByExt.get(path.extname(filePath).toLowerCase()) ?? 'application/octet-stream'
+ )
+ res.setHeader('content-length', String(st.size))
+ res.setHeader(
+ 'cache-control',
+ HASHED_RE.test(pathname) ? 'public, max-age=31536000, immutable' : 'no-cache'
+ )
+ await new Promise((resolve, reject) => {
+ const stream = createReadStream(filePath)
+ stream.on('error', reject)
+ stream.on('end', resolve)
+ stream.pipe(res)
+ })
+ return true
+}
+
+function toWebRequest(req) {
+ const protocol = req.socket.encrypted ? 'https' : 'http'
+ const url = `${protocol}://${req.headers.host ?? 'localhost'}${req.url}`
+ const headers = new Headers()
+ for (const [k, v] of Object.entries(req.headers)) {
+ if (k.startsWith(':')) continue
+ if (Array.isArray(v)) for (const vv of v) headers.append(k, vv)
+ else if (v !== undefined) headers.set(k, v)
+ }
+ const init = { method: req.method, headers }
+ // Only attach a body for methods that can carry one AND that actually
+ // have body bytes coming. Wrapping `req` in `Readable.toWeb(req)` for
+ // requests where Node has nothing to deliver leaves undici's
+ // `extractBody` looking at an already-consumed stream and throwing
+ // `TypeError: Response body object should not be disturbed or locked`
+ // at the `new Request(...)` call below.
+ const contentLength = Number(req.headers['content-length'] ?? '0')
+ const hasBody =
+ req.method !== 'GET' &&
+ req.method !== 'HEAD' &&
+ (contentLength > 0 || req.headers['transfer-encoding'] === 'chunked')
+ if (hasBody) {
+ init.body = Readable.toWeb(req)
+ init.duplex = 'half'
+ }
+ return new Request(url, init)
+}
+
+async function pipeWebResponse(response, res) {
+ res.statusCode = response.status
+ // The Headers iterator collapses duplicate keys, and for `set-cookie` it joins
+ // every cookie into one comma-separated value — which corrupts auth/session
+ // cookies. Pull the cookies out separately via getSetCookie() and set them as
+ // an array so each one becomes its own header.
+ const setCookies =
+ typeof response.headers.getSetCookie === 'function' ? response.headers.getSetCookie() : []
+ for (const [k, v] of response.headers) {
+ if (k.toLowerCase() === 'set-cookie') continue
+ res.setHeader(k, v)
+ }
+ if (setCookies.length > 0) res.setHeader('set-cookie', setCookies)
+ if (!response.body) {
+ res.end()
+ return
+ }
+ // Pipe via Readable.fromWeb so the underlying stream gets proper backpressure
+ // and gets released cleanly. `for await (chunk of response.body)` works in
+ // simple cases but can leave the body in a "disturbed / locked" state when
+ // the handler internally peeks at it — surfacing as
+ // `TypeError: Response body object should not be disturbed or locked` on a
+ // subsequent request.
+ await new Promise((resolve, reject) => {
+ const readable = Readable.fromWeb(response.body)
+ readable.on('error', reject)
+ res.on('error', reject)
+ res.on('close', resolve)
+ res.on('finish', resolve)
+ readable.pipe(res)
+ })
+}
+
+const port = Number(process.env.PORT || 8082)
+createServer(async (req, res) => {
+ try {
+ if (await serveStatic(req, res)) return
+ const response = await handler.fetch(toWebRequest(req))
+ await pipeWebResponse(response, res)
+ } catch (err) {
+ console.error('[serve] request failed:', err)
+ if (!res.headersSent) {
+ res.statusCode = 500
+ res.setHeader('content-type', 'text/plain; charset=utf-8')
+ }
+ res.end('Internal Server Error')
+ }
+}).listen(port, () => {
+ console.log(`Studio listening on http://localhost:${port} (mode=${mode})`)
+})
diff --git a/apps/studio/scripts/smoke-server.mjs b/apps/studio/scripts/smoke-server.mjs
new file mode 100644
index 00000000000..dda258119b3
--- /dev/null
+++ b/apps/studio/scripts/smoke-server.mjs
@@ -0,0 +1,73 @@
+#!/usr/bin/env node
+// Post-build smoke test: boot the built TanStack server handler and make a
+// real request with a platform-like environment, so module-scope boot
+// crashes fail the BUILD instead of the deployed function at runtime.
+//
+// Why this exists: the TanStack server entry eagerly imports the entire route
+// tree (`loadEntries`), so every route module is evaluated the first time the
+// single function handler serves a request. A bad top-level side effect in any
+// one route — e.g. `createClient(process.env.SUPABASE_URL!, ...)` at module
+// scope, where SUPABASE_URL is unset on platform — throws during that import
+// and 500s every route, including trivial ones like /api/get-utc-time. Those
+// failures only showed up at runtime on Vercel; this catches them at build.
+//
+// What this catches: anything that throws while the route tree is imported
+// (the most common class — missing env vars read at module scope).
+// What this does NOT catch: assets missing from the *Vercel function bundle*
+// (e.g. libpg-query.wasm). Local node_modules still has those, so booting the
+// plain `dist/server` build won't surface them — that needs booting the
+// `vercel build` output. Pass that function's entry as argv[1] to reuse this
+// script in a deploy-gating CI step.
+
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+const studioRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
+const serverEntry = process.argv[2]
+ ? path.resolve(process.argv[2])
+ : path.join(studioRoot, 'dist/server/server.js')
+
+// Simulate the platform function runtime: these are only set on self-hosted,
+// so removing them surfaces any route that needs them at module-load time.
+for (const key of ['SUPABASE_URL', 'SUPABASE_SERVICE_KEY', 'SUPABASE_SERVICE_ROLE_KEY']) {
+ delete process.env[key]
+}
+
+const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ''
+
+// A request to any route forces `loadEntries` to import the full route tree,
+// so a single cheap, dependency-free endpoint is enough to exercise the boot.
+const ROUTES = ['/api/get-utc-time']
+
+console.log(`[smoke] booting ${path.relative(studioRoot, serverEntry)}`)
+const { default: handler } = await import(serverEntry)
+
+let failed = false
+for (const route of ROUTES) {
+ const url = `http://localhost${basePath}${route}`
+ try {
+ const res = await handler.fetch(new Request(url))
+ if (res.status >= 500) {
+ failed = true
+ const body = await res.text().catch(() => '')
+ console.error(`[smoke] ✗ ${route} → ${res.status}\n${body.slice(0, 800)}`)
+ } else {
+ console.log(`[smoke] ✓ ${route} → ${res.status}`)
+ }
+ } catch (err) {
+ failed = true
+ console.error(`[smoke] ✗ ${route} threw while booting the server:\n`, err)
+ }
+}
+
+if (failed) {
+ console.error(
+ '\n[smoke] FAILED — the server bundle does not boot cleanly. This usually means a\n' +
+ 'route module has a top-level side effect (e.g. createClient at module scope with a\n' +
+ 'missing env var). Make it lazy so it only runs inside the handler.'
+ )
+ process.exit(1)
+}
+
+console.log('\n[smoke] passed — server boots and routes respond without a 5xx.')
+process.exit(0)
diff --git a/apps/studio/start.ts b/apps/studio/start.ts
new file mode 100644
index 00000000000..158248a618a
--- /dev/null
+++ b/apps/studio/start.ts
@@ -0,0 +1,35 @@
+import { createMiddleware, createStart } from '@tanstack/react-start'
+
+import { BASE_PATH, IS_PLATFORM } from '@/lib/constants'
+import { isHostedSupportedApiPath } from '@/lib/hosted-api-allowlist'
+
+// Self-hosted-only API routes must 404 in platform (hosted) mode. Under the
+// Next pages router this lives in middleware (proxy.ts), but TanStack Start
+// has no middleware runtime, so the guard is migrated here as a global
+// request middleware sharing the same allowlist (lib/hosted-api-allowlist.ts).
+// On Vercel our `/api/*` (and `/_serverFn/*`) requests are rewritten to the
+// api/server.js function which runs the Start handler, so createStartHandler
+// runs this server-side for every API request — even though pages are served
+// as a static SPA shell. The guard therefore covers all API routes from a
+// single place.
+
+const platformApiGuard = createMiddleware({ type: 'request' }).server(({ request, next }) => {
+ const { pathname } = new URL(request.url)
+ // Path relative to the configured basePath — mirrors Next's basePath-
+ // relative middleware matcher.
+ const relativePath =
+ BASE_PATH && pathname.startsWith(BASE_PATH) ? pathname.slice(BASE_PATH.length) : pathname
+
+ if (IS_PLATFORM && relativePath.startsWith('/api/') && !isHostedSupportedApiPath(relativePath)) {
+ return Response.json(
+ { success: false, message: 'Endpoint not supported on hosted' },
+ { status: 404 }
+ )
+ }
+
+ return next()
+})
+
+export const startInstance = createStart(() => ({
+ requestMiddleware: [platformApiGuard],
+}))
diff --git a/apps/studio/styles/fonts.css b/apps/studio/styles/fonts.css
new file mode 100644
index 00000000000..46905213121
--- /dev/null
+++ b/apps/studio/styles/fonts.css
@@ -0,0 +1,68 @@
+/*
+ * Font setup for the Vite/TanStack Start build.
+ * The Next.js build uses next/font via fonts/index.ts; keep the
+ * CSS var names (--font-custom, --font-source-code-pro) in sync so
+ * styles that reference them work in both pipelines.
+ * Source Code Pro is loaded via a in the root route's head().
+ */
+
+@font-face {
+ font-family: 'CustomFont';
+ src: url('../fonts/CustomFont-Book.woff2') format('woff2');
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'CustomFont';
+ src: url('../fonts/CustomFont-BookItalic.woff2') format('woff2');
+ font-weight: 400;
+ font-style: italic;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'CustomFont';
+ src: url('../fonts/CustomFont-Medium.woff2') format('woff2');
+ font-weight: 500;
+ font-style: normal;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'CustomFont';
+ src: url('../fonts/CustomFont-Bold.woff2') format('woff2');
+ font-weight: 700;
+ font-style: normal;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'CustomFont';
+ src: url('../fonts/CustomFont-BoldItalic.woff2') format('woff2');
+ font-weight: 700;
+ font-style: italic;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'CustomFont';
+ src: url('../fonts/CustomFont-Black.woff2') format('woff2');
+ font-weight: 800;
+ font-style: normal;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'CustomFont';
+ src: url('../fonts/CustomFont-BlackItalic.woff2') format('woff2');
+ font-weight: 800;
+ font-style: italic;
+ font-display: swap;
+}
+
+:root {
+ --font-custom: 'CustomFont', Circular, 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ --font-source-code-pro: 'Source Code Pro', 'Office Code Pro', Menlo, monospace;
+}
diff --git a/apps/studio/tsconfig.json b/apps/studio/tsconfig.json
index b1735434dee..5bf688391c2 100644
--- a/apps/studio/tsconfig.json
+++ b/apps/studio/tsconfig.json
@@ -10,7 +10,13 @@
"@/*": ["./*"],
"@ui/*": ["./../../packages/ui/src/*"] // handle ui package paths
},
- "plugins": [{ "name": "next" }]
+ "plugins": [{ "name": "next" }],
+ "jsx": "react-jsx",
+ "moduleResolution": "Bundler",
+ "module": "ESNext",
+ "target": "ES2022",
+ "skipLibCheck": true,
+ "strictNullChecks": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules", "public/deno/*.ts"]
diff --git a/apps/studio/turbo.jsonc b/apps/studio/turbo.jsonc
index 53b62b1c357..d47f1beea95 100644
--- a/apps/studio/turbo.jsonc
+++ b/apps/studio/turbo.jsonc
@@ -100,6 +100,22 @@
"STATUSPAGE_PAGE_ID",
"INCIDENT_IO_API_KEY",
"LIVE_SUPABASE_SECRET_KEY",
+ // Selects the build mode for the studio's `build`/`start` scripts
+ // (e.g. e2e sets `MODE=test`). Listed so turbo invalidates the
+ // cache when it changes — without this, switching between test and
+ // production builds reuses a stale cached output.
+ "MODE",
+ // Read by scripts/serve.js (the Node host for `pnpm start`).
+ // Declared here so the studio-package turbo env lint rule passes
+ // even though turbo doesn't directly drive `start`.
+ "PORT",
+ // Gates the TanStack vs Next path in api/server.js, vercel.ts,
+ // and scripts/dispatch.js (the dev/build/start dispatcher).
+ "STUDIO_FRAMEWORK",
+ // Vite's built-in `import.meta.env.SSR` flag (used in ConnectStepsSection
+ // to gate Vite-only `import.meta.glob`). Not a real process env var
+ // but turbo's `no-undeclared-env-vars` lint flags any `env.SSR` access.
+ "SSR",
],
"passThroughEnv": [
"CURRENT_CLI_VERSION",
@@ -110,7 +126,12 @@
"S3_PROTOCOL_ACCESS_KEY_ID",
"S3_PROTOCOL_ACCESS_KEY_SECRET",
],
- "outputs": [".next/**", "!.next/cache/**", "!.next/dev/**/*"],
+ "outputs": [
+ ".next/**",
+ "!.next/cache/**",
+ "!.next/dev/**/*",
+ "dist/**",
+ ],
},
},
}
diff --git a/apps/studio/vercel.ts b/apps/studio/vercel.ts
new file mode 100644
index 00000000000..2c0275a63a0
--- /dev/null
+++ b/apps/studio/vercel.ts
@@ -0,0 +1,155 @@
+import { routes, type Redirect, type VercelConfig } from '@vercel/config/v1'
+
+import {
+ getMaintenanceRedirects,
+ PLATFORM_REDIRECTS,
+ SELF_HOSTED_REDIRECTS,
+ SHARED_REDIRECTS,
+ type StudioRedirect,
+} from './redirects.shared'
+
+// STUDIO_FRAMEWORK gates the TanStack Start deploy. When the env var is
+// unset (the default — used by the Next.js prod deploy) this file returns
+// an empty `VercelConfig` so Vercel honours the dashboard-configured
+// Next.js preset untouched. Vercel reads `vercel.ts` regardless of the
+// framework preset (per vercel.com/docs/project-configuration —
+// `vercel.ts`'s `framework` field overrides the dashboard preset), so a
+// no-op early return is the only way to keep the TanStack rewrites,
+// `framework: null`, and `outputDirectory: 'dist/client'` below from
+// clobbering the Next build. Set `STUDIO_FRAMEWORK=tanstack` on the
+// TanStack Vercel project to opt in.
+const isTanstack = process.env.STUDIO_FRAMEWORK === 'tanstack'
+
+// Vite's `base` bakes the prefix into asset URLs but leaves the filesystem
+// layout at `dist/client/...`. On Vercel we strip the prefix for file lookups
+// and fall through to the SPA shell. When NEXT_PUBLIC_BASE_PATH is empty
+// the prefixed rule set is skipped and only the root-level rules fire.
+const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ''
+
+// Build the rewrites + headers for a given prefix ('' for root, or a base
+// path like '/dashboard'). We run this once for each prefix and concatenate
+// the results so we don't hand-duplicate every rule.
+//
+// Rewrite ordering: API + server-function passthrough first so extensioned
+// API paths (/api/foo.json) don't get caught by the asset rule. Asset rule
+// next — strips the basePath prefix so `/dashboard/assets/x.js` maps onto the
+// `dist/client/assets/x.js` filesystem layout (a no-op identity when
+// prefix=''). Shell rule LAST, and it deliberately matches only extensionless
+// paths via a negative lookahead.
+//
+// Why the lookahead matters: a request WITH a file extension that doesn't
+// resolve to a real file — e.g. a hashed chunk from an older deployment after
+// a redeploy — must fall through to a 404, NOT the HTML shell. A catch-all
+// `(.*)` shell swallows those misses and returns `text/html`, so the browser
+// gets HTML for a `.js` request and throws "Failed to load module script …
+// MIME type text/html" (and, because /assets/* is cached immutable, that HTML
+// poisons the edge cache under the asset URL). A clean 404 instead lets skew
+// protection's `__vdpl` routing serve the chunk from the deployment that still
+// has it, or the client's `vite:preloadError` backstop recover.
+function routesFor(prefix: string) {
+ return {
+ rewrites: [
+ routes.rewrite(`${prefix}/api/(.*)`, '/api/server'),
+ routes.rewrite(`${prefix}/_serverFn/(.*)`, '/api/server'),
+ routes.rewrite(`${prefix}/(.*\\.\\w+)`, '/$1'),
+ routes.rewrite(`${prefix}/((?!.*\\.\\w+$).*)`, '/_shell'),
+ ],
+ headers: [
+ // Dynamic function responses must not be cached by any shared cache —
+ // handlers can still opt in with their own Cache-Control on the
+ // Response when a response IS safe to cache.
+ routes.cacheControl(`${prefix}/api/(.*)`, { private: true, noStore: true }),
+ routes.cacheControl(`${prefix}/_serverFn/(.*)`, { private: true, noStore: true }),
+ // Hashed bundles under /assets/* are content-addressed — safe to
+ // cache forever.
+ routes.cacheControl(`${prefix}/assets/(.*)`, {
+ public: true,
+ maxAge: '1year',
+ immutable: true,
+ }),
+ ],
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Redirects — entries live in `redirects.shared.ts`, consumed by both
+// `next.config.ts` and this file. Next auto-prepends `basePath` to its
+// redirects; Vercel doesn't, so we apply it here.
+// ---------------------------------------------------------------------------
+
+function applyBasePath(r: StudioRedirect): Redirect {
+ if (!basePath) return r
+ const prefix = (path: string) =>
+ path.startsWith('/') ? (path === '/' ? basePath : `${basePath}${path}`) : path
+ return { ...r, source: prefix(r.source), destination: prefix(r.destination) }
+}
+
+function buildRedirects(): Redirect[] {
+ const isPlatform = process.env.NEXT_PUBLIC_IS_PLATFORM === 'true'
+ const maintenance = process.env.MAINTENANCE_MODE === 'true'
+ const conditional = isPlatform ? PLATFORM_REDIRECTS : SELF_HOSTED_REDIRECTS
+
+ // Bare-domain bounce to the basePath when one is configured. Source
+ // stays literally `/` (NOT prefixed) so the entry-point redirect fires.
+ const basePathBounce: Redirect[] = basePath
+ ? [{ source: '/', destination: basePath, permanent: false }]
+ : []
+
+ return [
+ ...conditional.map(applyBasePath),
+ ...SHARED_REDIRECTS.map(applyBasePath),
+ ...basePathBounce,
+ ...getMaintenanceRedirects(maintenance).map(applyBasePath),
+ ]
+}
+
+function buildTanstackConfig(): VercelConfig {
+ // When a base path is configured, emit both the prefixed and root rule
+ // sets (prefixed first so it wins for explicit /dashboard/* hits, root as
+ // a fallback for bare-domain traffic).
+ const ruleSets = (basePath ? [basePath, ''] : ['']).map(routesFor)
+
+ return {
+ framework: null,
+ outputDirectory: 'dist/client',
+ cleanUrls: true,
+ redirects: buildRedirects(),
+ rewrites: ruleSets.flatMap((r) => r.rewrites),
+ headers: ruleSets.flatMap((r) => r.headers),
+ // `api/server.js` imports the TanStack SSR bundle via a computed
+ // path so Vercel's function bundler doesn't try to statically
+ // resolve `dist/server/server.js` during the Next.js prod build
+ // (where `dist/` doesn't exist). `includeFiles` ships the SSR
+ // output into the function bundle for the TanStack build so the
+ // runtime import resolves.
+ functions: {
+ 'api/server.js': {
+ // Ship the SSR output, plus libpg-query's wasm. libpg-query is
+ // externalized for SSR and loads its `.wasm` relative to its own
+ // dir (`__dirname`) at import time; Vercel's function bundler
+ // doesn't trace that node_modules asset, so co-ship it explicitly
+ // or the server crashes at boot with ENOENT on libpg-query.wasm.
+ //
+ // Target the real pnpm store path (repo-root `.pnpm`, two levels up
+ // from this Root Directory) rather than `node_modules/libpg-query`,
+ // which is a pnpm symlink — Vercel rejects packaging files reached
+ // through symlinked dirs ("invalid deployment package"). The real
+ // path also matches where Node resolves __dirname at runtime
+ // (/var/task/node_modules/.pnpm/libpg-query@.../wasm/...).
+ includeFiles:
+ '{dist/server/**,../../node_modules/.pnpm/libpg-query@*/node_modules/libpg-query/wasm/libpg-query.wasm}',
+ },
+ },
+ }
+}
+
+// Empty config = no overrides; Vercel falls back to the dashboard preset.
+const passthrough: VercelConfig = {}
+
+export const config: VercelConfig = isTanstack ? buildTanstackConfig() : passthrough
+
+// Belt-and-braces: local @vercel/config CLI reads module.default, but the
+// docs claim Vercel's platform looks for a named `config` export. Export
+// both so whichever path runs wins.
+// eslint-disable-next-line no-restricted-exports
+export default config
diff --git a/apps/studio/vite.config.ts b/apps/studio/vite.config.ts
new file mode 100644
index 00000000000..5abdf9392fd
--- /dev/null
+++ b/apps/studio/vite.config.ts
@@ -0,0 +1,466 @@
+/* eslint-disable no-restricted-exports */
+
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import tailwindcss from '@tailwindcss/vite'
+import { devtools } from '@tanstack/devtools-vite'
+import { tanstackStart } from '@tanstack/react-start/plugin/vite'
+import viteReact from '@vitejs/plugin-react'
+import { defineConfig, loadEnv, type Plugin } from 'vite'
+
+const rootDir = path.dirname(fileURLToPath(import.meta.url))
+const compatRoot = path.resolve(rootDir, 'compat/next')
+
+// Map of Next imports we've shimmed to their TanStack-backed replacement.
+// Add an entry here + a file under compat/next/ when a new Next surface is
+// needed by app source.
+const nextShims: Record = {
+ 'next/compat/router': path.join(compatRoot, 'compat/router.ts'),
+ 'next/dynamic': path.join(compatRoot, 'dynamic.tsx'),
+ 'next/head': path.join(compatRoot, 'head.tsx'),
+ 'next/image': path.join(compatRoot, 'image.tsx'),
+ 'next/legacy/image': path.join(compatRoot, 'legacy/image.tsx'),
+ 'next/link': path.join(compatRoot, 'link.tsx'),
+ 'next/navigation': path.join(compatRoot, 'navigation.ts'),
+ 'next/router': path.join(compatRoot, 'router.ts'),
+ 'next/script': path.join(compatRoot, 'script.tsx'),
+ 'next/server': path.join(compatRoot, 'server.ts'),
+}
+
+// Combined compat + migration guard:
+// - If app source imports a shimmed `next/*` id, resolve it to the local
+// shim (acts like resolve.alias).
+// - Otherwise, if app source imports from `next` or `next/*`, fail the
+// build so we catch unshimmed usage at build time during the migration.
+// - node_modules imports (e.g. @sentry/nextjs reaching into next) pass
+// through untouched.
+function nextCompat(): Plugin {
+ return {
+ name: 'studio-next-compat',
+ enforce: 'pre',
+ resolveId(id, importer) {
+ if (!importer || importer.includes('/node_modules/')) return
+ if (nextShims[id]) return nextShims[id]
+ if (id === 'next' || id.startsWith('next/')) {
+ throw new Error(
+ `[next-compat] "${id}" imported from ${importer}.\n` +
+ `Add a shim under apps/studio/compat/next/ and register it in vite.config.ts, ` +
+ `or use a framework-agnostic equivalent.`
+ )
+ }
+ },
+ }
+}
+
+// Short-circuit UMD wrappers' AMD branch by string-replacing the
+// `define.amd` check. Vite's `config.define` doesn't reach pre-bundled
+// deps (Vite 8's Rolldown-based optimizer doesn't honour member-
+// expression define keys at the prebundle stage), and adding the same
+// substitution to `optimizeDeps.rolldownOptions.define` had no effect
+// on the emitted `node_modules/.vite/deps/*.js`. This transform fires
+// when Vite *serves* the prebundled file, rewriting the runtime AMD
+// check before it reaches the browser.
+//
+// Applied broadly to any module containing the AMD check (not just
+// papaparse) — UMD wrappers all share the same shape, and we never
+// want to take the AMD branch when Monaco's loader is around.
+//
+// Surfaces concretely on /functions/[slug]/invocations: papaparse
+// pre-bundled into `.vite/deps/papaparse.js` retained the literal
+// `"function" == typeof define && define.amd` check; Monaco's CDN
+// loader installs `window.define` first, so papaparse's UMD takes the
+// AMD branch and calls an anonymous `define([], t)` that Monaco
+// rejects with "Can only have one anonymous define call per script
+// file".
+function umdAmdShortCircuit(): Plugin {
+ // Matches both unminified (`typeof define === 'function' && define.amd`)
+ // and minified (`"function" == typeof define && define.amd`) forms of
+ // the UMD AMD-detection check. Replaces the whole expression with
+ // `false` so dead-code elimination drops the AMD branch.
+ const AMD_CHECK_PATTERNS = [
+ /typeof\s+define\s*===?\s*['"]function['"]\s*&&\s*define\.amd/g,
+ /['"]function['"]\s*===?\s*typeof\s+define\s*&&\s*define\.amd/g,
+ ]
+
+ return {
+ name: 'studio-umd-amd-short-circuit',
+ enforce: 'pre',
+ transform(code, id) {
+ if (!code.includes('define.amd')) return
+ // Skip Monaco's loader.js if it ever ends up in our graph — it
+ // legitimately needs `define.amd` to register itself as AMD.
+ if (id.includes('monaco-editor/min/vs/loader')) return
+ let next = code
+ for (const pattern of AMD_CHECK_PATTERNS) {
+ next = next.replace(pattern, 'false')
+ }
+ if (next === code) return
+ return { code: next, map: null }
+ },
+ }
+}
+
+// Replace our `components/interfaces/GraphQL/GraphiQL` module with a no-op
+// React component in SSR builds only.
+//
+// `@graphiql/react` transitively loads a codemirror addon that touches
+// `document` at module-evaluation time. During the SPA shell prerender,
+// that hard-crashes with "document is not defined" as soon as the graphiql
+// chunk gets loaded.
+//
+// Stubbing `@graphiql/react` directly would require enumerating its 30+ named
+// exports so Rolldown's static analysis is satisfied. Easier to stub the one
+// internal consumer — `GraphiQL.tsx` only exposes a default-export component,
+// and no SSR-reachable route renders it (the GraphiQL tab is client-only).
+function ssrStubGraphiql(): Plugin {
+ return {
+ name: 'studio-ssr-stub-graphiql',
+ enforce: 'pre',
+ transform(_code, id, options) {
+ if (!options?.ssr) return
+ if (id.endsWith('/components/interfaces/GraphQL/GraphiQL.tsx')) {
+ return { code: 'export default function GraphiQLStub() { return null }', map: null }
+ }
+ },
+ }
+}
+
+// Build-time guard: scan the emitted client chunks for cross-chunk
+// circular imports and fail the build if any are found. Catches the
+// class of bug that produces runtime errors like
+// "TypeError: is not a function" at module load — when chunk
+// A imports a binding from chunk B and B (transitively) imports A
+// back, ES module live-bindings can be undefined at the point the
+// chunk that evaluates first tries to use them.
+//
+// Cycles are matched by chunk basename prefix (stripping the
+// `assets/` directory and the `-.js` suffix), so the allowlist
+// stays stable across builds even as Rolldown reassigns hashes.
+const KNOWN_CHUNK_CYCLES: ReadonlyArray> = [
+ // `ui` ↔ `TreeView` chunk cycle. `cva` lives in the `ui` chunk
+ // (Rolldown pools it there because many ui files use it), TreeView
+ // imports `cva` back from `ui` while `ui`'s barrel re-exports
+ // TreeView — runtime crash is "cva is not a function" at SSR.
+ // Worked around via the `class-variance-authority` manualChunks
+ // pin below; the chunk graph still surfaces the SCC even though
+ // the top-level `cva(...)` call inside TreeView no longer crashes.
+ // The variants below are the same SCC in different shapes — they
+ // shuffle as Rolldown re-chunks across merges.
+ ['LoadingLine', 'TreeView', 'ui'],
+ ['FormLayout', 'LoadingLine', 'TreeView', 'ui', 'index'],
+ ['LoadingLine', 'TreeView', 'ui', 'index'],
+]
+
+function chunkPrefix(name: string): string {
+ return name
+ .replace(/^assets\//, '')
+ .replace(/-[A-Za-z0-9_-]{6,10}\.js$/, '')
+ .replace(/\.js$/, '')
+}
+
+function isKnownCycle(scc: string[]): boolean {
+ const prefixes = new Set(scc.map(chunkPrefix))
+ return KNOWN_CHUNK_CYCLES.some(
+ (known) => known.length === prefixes.size && known.every((p) => prefixes.has(p))
+ )
+}
+
+function assertNoChunkCycles(): Plugin {
+ return {
+ name: 'studio-assert-no-chunk-cycles',
+ apply: 'build',
+ generateBundle(_options, bundle) {
+ const graph: Record> = {}
+ for (const [name, asset] of Object.entries(bundle)) {
+ if (asset.type !== 'chunk') continue
+ graph[name] = new Set(asset.imports.filter((i) => i in bundle))
+ }
+
+ // Tarjan's strongly-connected-components algorithm. Any SCC with
+ // more than one node is a cycle in the output chunk graph.
+ const indices: Record = {}
+ const lowlinks: Record = {}
+ const onStack: Record = {}
+ const stack: string[] = []
+ const sccs: string[][] = []
+ let nextIndex = 0
+
+ const strongconnect = (v: string) => {
+ indices[v] = nextIndex
+ lowlinks[v] = nextIndex
+ nextIndex++
+ stack.push(v)
+ onStack[v] = true
+ for (const w of graph[v] || []) {
+ if (indices[w] === undefined) {
+ strongconnect(w)
+ lowlinks[v] = Math.min(lowlinks[v], lowlinks[w])
+ } else if (onStack[w]) {
+ lowlinks[v] = Math.min(lowlinks[v], indices[w])
+ }
+ }
+ if (lowlinks[v] === indices[v]) {
+ const scc: string[] = []
+ let w: string | undefined
+ do {
+ w = stack.pop()
+ if (w === undefined) break
+ onStack[w] = false
+ scc.push(w)
+ } while (w !== v)
+ if (scc.length > 1) sccs.push(scc)
+ }
+ }
+
+ for (const v of Object.keys(graph)) {
+ if (indices[v] === undefined) strongconnect(v)
+ }
+
+ const unexpected = sccs.filter((scc) => !isKnownCycle(scc))
+ if (unexpected.length === 0) return
+
+ const summary = unexpected
+ .map((scc, i) => ` Cycle ${i + 1}:\n` + scc.map((c) => ` ${c}`).join('\n'))
+ .join('\n\n')
+ const msg =
+ `studio-assert-no-chunk-cycles: detected ${unexpected.length} new chunk-level cycle(s) in the client bundle.\n` +
+ `These cause "X is not a function" runtime errors at module-load time. ` +
+ `Either restructure the modules involved or add the cycle to KNOWN_CHUNK_CYCLES ` +
+ `in apps/studio/vite.config.ts.\n\n` +
+ summary
+ this.error(msg)
+ },
+ }
+}
+
+export default defineConfig(({ command, mode }) => {
+ // Match Next's "always production-NODE_ENV during build" behaviour.
+ // `pnpm run e2e:setup:selfhosted` invokes the build with a shell
+ // `NODE_ENV=test` so Next can pick up `.env.test` for env loading;
+ // Next overrides NODE_ENV back to 'production' internally before
+ // emitting code, so the bundle never sees 'test'. Vite respects the
+ // user's NODE_ENV by default and would bake `process.env.NODE_ENV ===
+ // 'test'` into the client bundle, which trips vitest-only code paths
+ // (notably `API_URL` in `lib/constants/index.ts` pointing the browser
+ // at the vitest MSW host on port 3000, breaking every API fetch in
+ // e2e). Override here so `--mode test` still loads `.env.test` (via
+ // Vite's mode-based env resolution) while the bundle stays at
+ // `NODE_ENV='production'`, mirroring Next.
+ if (command === 'build') {
+ // Next's types declare NODE_ENV as read-only, so cast to assign it.
+ ;(process.env as Record).NODE_ENV = 'production'
+ }
+
+ // Inline NEXT_PUBLIC_* env vars at build time so `process.env.NEXT_PUBLIC_*`
+ // works in the browser bundle (mirrors Next.js behaviour).
+ const env = loadEnv(mode, rootDir, '')
+ const publicEnvDefines = Object.fromEntries(
+ Object.entries(env)
+ .filter(([key]) => key.startsWith('NEXT_PUBLIC_'))
+ .map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)])
+ )
+
+ // Vercel auto-populates `NEXT_PUBLIC_VERCEL_*` for Next.js projects but not
+ // for other frameworks. Mirror that behaviour by re-exposing the unprefixed
+ // system vars under their `NEXT_PUBLIC_VERCEL_*` names so call sites that
+ // predate the TanStack migration keep working.
+ const vercelPublicVars = [
+ 'VERCEL_ENV',
+ 'VERCEL_BRANCH_URL',
+ // Skew protection: the client pins its session to this deployment (see
+ // router.tsx). Both are build-time system env vars on Vercel.
+ 'VERCEL_DEPLOYMENT_ID',
+ 'VERCEL_SKEW_PROTECTION_ENABLED',
+ ] as const
+ for (const key of vercelPublicVars) {
+ const value = env[key]
+ if (value !== undefined) {
+ publicEnvDefines[`process.env.NEXT_PUBLIC_${key}`] = JSON.stringify(value)
+ }
+ }
+
+ // Mirror Next's `basePath` via NEXT_PUBLIC_BASE_PATH. Unlike Next, TanStack
+ // Start has no single knob — the prefix has to be declared in three places
+ // (see BASE_PATH_REDIRECT_GUIDE.md):
+ // - Vite `base` — bakes the prefix into asset URLs in the
+ // built bundle.
+ // - tanstackStart router.basepath — must be passed explicitly. If
+ // omitted, the plugin's internal
+ // `deriveRouterBasepath` derives a value
+ // from `publicBase` and strips both
+ // leading and trailing slashes
+ // (`/dashboard` → `dashboard`), which then
+ // surfaces in `useRouter().basePath`
+ // consumers as relative URLs (e.g.
+ // `${BASE_PATH}/img/...` becomes
+ // `dashboard/img/...` and the browser
+ // resolves it against the current path).
+ // See planning.js:14 in
+ // @tanstack/start-plugin-core.
+ // - createRouter({ basepath }) — runtime navigation prefix; configured
+ // in router.tsx off the same env var
+ // (inlined via `define` above).
+ // Leaving the var empty keeps the app at `/` as today.
+ const basePath = env.NEXT_PUBLIC_BASE_PATH || undefined
+
+ // Substitutions that have to apply to *both* our app source (via Vite's
+ // `define`) and any pre-bundled dependencies (via esbuild's optimizeDeps).
+ // The two pipelines don't share config — Vite's `define` only touches
+ // files going through Vite's transform, while optimizeDeps runs esbuild
+ // on `node_modules` deps with its own separate `define`.
+ // - `global` → `globalThis`: makes Node-style libs (`randombytes` via
+ // `generate-password-browser`, etc.) work in the browser. Surfaces
+ // on /auth/hooks via `randombytes/browser.js:16`.
+ // - `define.amd` → `false`: short-circuits the AMD branch in UMD
+ // wrappers (papaparse, others). Monaco's CDN loader installs an
+ // AMD-style `window.define` at runtime; without this substitution,
+ // UMD libs evaluate after Monaco has loaded and call an anonymous
+ // `define([], t)` that Monaco's queue rejects with "Can only have
+ // one anonymous define call per script file". Surfaces concretely
+ // on /functions/[slug] (papaparse pulled in by invocations).
+ // Monaco's loader.js itself runs from a CDN script tag (not in our
+ // bundle / not pre-bundled), so its own `define.amd = true` write
+ // isn't affected by the substitution.
+ const sharedDefines = {
+ global: 'globalThis',
+ 'define.amd': 'false',
+ }
+
+ return {
+ server: {
+ port: 3000,
+ },
+ resolve: {
+ tsconfigPaths: true,
+ },
+ ...(basePath && { base: basePath }),
+ define: {
+ ...publicEnvDefines,
+ ...sharedDefines,
+ },
+ // Circular-dep workaround: pin shared library code into dedicated
+ // chunks so per-component chunks don't import from a chunk that
+ // (transitively) imports them back.
+ //
+ // `class-variance-authority` — TreeView gets split into its own
+ // chunk that imports `cva` from the `ui` chunk while `ui` imports
+ // TreeView back. Leaves `cva` undefined at TreeView's top-level
+ // `cva(...)` call during SSR prerender.
+ //
+ // `lucide-react` — each icon (e.g. `FolderOpen`) gets a per-icon
+ // chunk that imports `createLucideIcon` from the `ui` chunk; the
+ // `ui` chunk in turn re-exports icons from `lucide-react`. The
+ // circular leaves `createLucideIcon` undefined when the icon
+ // chunk's top-level `createLucideIcon('FolderOpen', …)` runs —
+ // surfaces in the browser as "TypeError: e is not a function" at
+ // `folder-open-.js`.
+ //
+ // `react` / `react-dom` — pinning lucide-react alone caused
+ // Rolldown to suck React into the lucide-react chunk (lucide
+ // depends on React, no explicit pin further up the graph). That
+ // shifted live-bindings across the rest of the chunk graph and
+ // broke unrelated chunks (e.g. `Alert-.js` started crashing
+ // with `c is not a function` because its `styleHandler` import
+ // came in through the now-too-large `lucide-react` chunk). Pin
+ // React explicitly so it stays a leaf vendor chunk.
+ build: {
+ rollupOptions: {
+ output: {
+ manualChunks: (id) => {
+ if (id.includes('node_modules/class-variance-authority/')) {
+ return 'class-variance-authority'
+ }
+ // Pin React / React-DOM (and their JSX runtimes + scheduler)
+ // before lucide-react, so downstream chunks consume React
+ // from one place. Rolldown can still inline React into
+ // adjacent chunks for CJS interop, but the explicit pin
+ // anchors the canonical copy here.
+ if (
+ /node_modules\/(react|react-dom|scheduler)(\/|$)/.test(id) ||
+ /node_modules\/react\/jsx-(runtime|dev-runtime)/.test(id)
+ ) {
+ return 'react-vendor'
+ }
+ if (id.includes('node_modules/lucide-react/')) {
+ return 'lucide-react'
+ }
+ return undefined
+ },
+ },
+ },
+ },
+ css: {
+ // Disable PostCSS auto-discovery. Studio's postcss.config.cjs is kept
+ // for the Next build (`build:next`) and uses `@tailwindcss/postcss`,
+ // but under Vite we let `@tailwindcss/vite` (added below) handle
+ // Tailwind v4 directives directly. Running both plugins on the same
+ // CSS would double-process Tailwind output.
+ postcss: { plugins: [] },
+ },
+ ssr: {
+ // `lodash` is CJS; its named-export interop fails in Node ESM unless bundled.
+ // `next/*` must be bundled so our nextCompat shim wins — otherwise Vite's
+ // SSR externalizer leaves `next/router` as a runtime package import and
+ // Node resolves it to Next's real module.
+ // `tslib`'s Node ESM entry (`modules/index.js`) destructures from a
+ // default-imported CJS wrapper (`tslib.js`). When consumers like
+ // `@ai-sdk/amazon-bedrock` / `configcat-common` `import … from "tslib"`
+ // and that ESM-wrapper gets picked, Rolldown botches the flattened UMD
+ // body — "__extends is not a function" at SSR module evaluation time.
+ // Inlining `tslib` lets the bundler reach the pure ESM entry directly.
+ // `react-use` ships a CJS entry that Vite's SSR externalizer emits as
+ // `import pkg from 'react-use'` + destructure. Works locally but
+ // Vercel's Node resolves it differently and fails at module instantiate
+ // (`ModuleJob._instantiate`). Inlining sidesteps the interop entirely.
+ // `awesome-debounce-promise`'s CJS entry only emits
+ // `exports.default = fn` (no `module.exports = fn`, no `__esModule`
+ // flag). Node's CJS→ESM bridge therefore makes the default import the
+ // entire exports object `{ default: fn }`, and call sites like
+ // `AwesomeDebouncePromise(fn, 500)` crash with "is not a function" at
+ // SSR module evaluation. Surfaces on routes that load the table grid.
+ // `@sentry/nextjs`'s CJS entry doesn't surface `startSpan` (and other
+ // v8 APIs) onto the namespace shape Vite's SSR externalizer produces,
+ // so `import * as Sentry from '@sentry/nextjs'` + `Sentry.startSpan`
+ // crashes with "is not a function" inside the pg-meta proxy on the
+ // first table-editor request.
+ noExternal: [
+ 'lodash',
+ /^next(\/|$)/,
+ 'tslib',
+ 'react-use',
+ 'awesome-debounce-promise',
+ '@sentry/nextjs',
+ ],
+ // Vite 8.0.13's SSR module runner evaluates `@sentry/nextjs`'s
+ // CJS file via `runInlinedModule` without the CJS-compat wrapper
+ // older vite applied, crashing with "exports is not defined" at
+ // SSR. Forcing pre-bundling via esbuild rewrites it to ESM
+ // before the SSR runner sees it. Only `@sentry/nextjs` needs
+ // this — the other CJS deps in `noExternal` work via vite's SSR
+ // transform; pre-bundling React-using deps (e.g. `react-use`)
+ // inlines a duplicate React into the bundle and breaks hook
+ // dedupe at SSR (useRef → null).
+ optimizeDeps: {
+ include: ['@sentry/nextjs'],
+ },
+ },
+ plugins: [
+ nextCompat(),
+ ssrStubGraphiql(),
+ umdAmdShortCircuit(),
+ assertNoChunkCycles(),
+ devtools(),
+ tailwindcss(),
+ tanstackStart({
+ srcDirectory: './',
+ spa: {
+ enabled: true,
+ },
+ // Set `configuredBasepath` so `deriveRouterBasepath` short-circuits
+ // its slash-stripping branch. See the basePath comment above.
+ ...(basePath && { router: { basepath: basePath } }),
+ }),
+ viteReact(),
+ ],
+ }
+})
diff --git a/apps/ui-library/package.json b/apps/ui-library/package.json
index 99465e659cf..7f85d766cab 100644
--- a/apps/ui-library/package.json
+++ b/apps/ui-library/package.json
@@ -25,7 +25,7 @@
"@supabase/postgrest-js": "catalog:",
"@supabase/supa-mdx-lint": "0.2.6-alpha",
"@supabase/vue-blocks": "workspace:*",
- "@tanstack/react-query": "^5.83.0",
+ "@tanstack/react-query": "~5.83.0",
"@xyflow/react": "^12.10.1",
"api-types": "workspace:*",
"axios": "^1.12.0",
@@ -74,8 +74,8 @@
"@shikijs/compat": "^1.1.7",
"@supabase/ssr": "catalog:",
"@supabase/supabase-js": "catalog:",
- "@tanstack/react-router": "^1.168.0",
- "@tanstack/react-start": "^1.168.0",
+ "@tanstack/react-router": "catalog:",
+ "@tanstack/react-start": "catalog:",
"@types/common-tags": "^1.8.4",
"@types/lodash": "^4.17.16",
"@types/react": "catalog:",
diff --git a/package.json b/package.json
index 63813bec1dc..e1d95dfb8c4 100644
--- a/package.json
+++ b/package.json
@@ -29,8 +29,8 @@
"test:studio": "turbo run test --filter=studio",
"test:studio:watch": "turbo run test --filter=studio -- watch",
"e2e:setup:cli": "supabase stop --all --no-backup ; supabase start --exclude studio,mailpit && if [ -z \"${CI}\" ]; then supabase db reset; fi && supabase status --output json > keys.json && node scripts/generateLocalEnv.js",
- "e2e:setup:selfhosted": "SKIP_ASSET_UPLOAD=1 pnpm e2e:setup:cli && NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" pnpm run build:studio && NODE_ENV=test pnpm --prefix ./apps/studio start",
- "e2e:setup:selfhosted:start-studio": "SKIP_ASSET_UPLOAD=1 NODE_ENV=test pnpm --prefix ./apps/studio start",
+ "e2e:setup:selfhosted": "SKIP_ASSET_UPLOAD=1 pnpm e2e:setup:cli && NODE_ENV=test MODE=test NODE_OPTIONS=\"--max-old-space-size=4096\" pnpm run build:studio && NODE_ENV=test MODE=test pnpm --prefix ./apps/studio start",
+ "e2e:setup:selfhosted:start-studio": "SKIP_ASSET_UPLOAD=1 NODE_ENV=test MODE=test pnpm --prefix ./apps/studio start",
"e2e:setup:platform": "SKIP_ASSET_UPLOAD=1 NODE_OPTIONS=\"--max-old-space-size=4096\" pnpm run build:studio && pnpm --prefix ./apps/studio start",
"e2e": "pnpm --prefix e2e/studio run e2e",
"e2e:ui": "pnpm --prefix e2e/studio run e2e:ui",
@@ -67,13 +67,6 @@
"pnpm": "10.24",
"node": ">=22"
},
- "keywords": [
- "postgres",
- "firebase",
- "storage",
- "functions",
- "database",
- "auth"
- ],
+ "keywords": ["postgres", "firebase", "storage", "functions", "database", "auth"],
"packageManager": "pnpm@10.24.0"
}
diff --git a/packages/ui-patterns/package.json b/packages/ui-patterns/package.json
index 72b4cd0a574..78e1def0a02 100644
--- a/packages/ui-patterns/package.json
+++ b/packages/ui-patterns/package.json
@@ -805,7 +805,7 @@
"@std/toml": "jsr:^1.0.11",
"@supabase/sql-to-rest": "^0.1.6",
"@supabase/supabase-js": "catalog:",
- "@tanstack/react-table": "^8.21.3",
+ "@tanstack/react-table": "catalog:",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
diff --git a/packages/ui/src/components/AnimatedCounter/AnimatedCounter.tsx b/packages/ui/src/components/AnimatedCounter/AnimatedCounter.tsx
index 59103eab284..b50199fbcb2 100644
--- a/packages/ui/src/components/AnimatedCounter/AnimatedCounter.tsx
+++ b/packages/ui/src/components/AnimatedCounter/AnimatedCounter.tsx
@@ -1,7 +1,7 @@
'use client'
import { animate, motion, useInView, useMotionValue, useTransform } from 'framer-motion'
-import React, { FC, useEffect, useRef } from 'react'
+import { FC, useEffect, useRef } from 'react'
import { cn } from '../../lib/utils/cn'
diff --git a/packages/ui/src/components/Typography/Typography.tsx b/packages/ui/src/components/Typography/Typography.tsx
index adf311e2e1b..059f84acf14 100644
--- a/packages/ui/src/components/Typography/Typography.tsx
+++ b/packages/ui/src/components/Typography/Typography.tsx
@@ -1,5 +1,3 @@
-import React from 'react'
-
import Link from './Link'
import Text from './Text'
import Title from './Title'
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 673021e2845..1fce526b355 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -24,6 +24,15 @@ catalogs:
'@supabase/supabase-js':
specifier: 2.108.2
version: 2.108.2
+ '@tanstack/react-router':
+ specifier: ^1.169.2
+ version: 1.170.10
+ '@tanstack/react-start':
+ specifier: ^1.167.65
+ version: 1.168.18
+ '@tanstack/react-table':
+ specifier: ^8.21.3
+ version: 8.21.3
'@types/node':
specifier: ^22.0.0
version: 22.13.14
@@ -96,6 +105,7 @@ overrides:
cacache>tar: ^7.5.11
dompurify: ^3.3.2
express-rate-limit>ip-address: ^10.1.1
+ h3@1: 1.15.11
lodash: ^4.18.1
lodash-es: ^4.18.1
node-gyp>tar: ^7.5.11
@@ -169,7 +179,7 @@ importers:
specifier: ^3.1.1
version: 3.3.1(react-hook-form@7.72.1(react@19.2.6))
'@tanstack/react-table':
- specifier: ^8.21.3
+ specifier: 'catalog:'
version: 8.21.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
common:
specifier: workspace:*
@@ -356,7 +366,7 @@ importers:
specifier: 'catalog:'
version: 2.108.2
'@tanstack/react-query':
- specifier: ^5.13.4
+ specifier: ~5.83.0
version: 5.83.0(react@19.2.6)
acorn:
specifier: ^8.11.3
@@ -925,9 +935,21 @@ importers:
'@number-flow/react':
specifier: ^0.3.2
version: 0.3.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@radix-ui/react-slider':
+ specifier: ^1.1.2
+ version: 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@radix-ui/react-slot':
+ specifier: ^1.2.0
+ version: 1.2.4(@types/react@19.2.14)(react@19.2.6)
+ '@radix-ui/react-visually-hidden':
+ specifier: ^1.2.3
+ version: 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@sentry/nextjs':
specifier: 'catalog:'
version: 10.27.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.2.6(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6)(supports-color@8.1.1)(webpack@5.105.4(esbuild@0.28.1))
+ '@sentry/react':
+ specifier: ^10.27.0
+ version: 10.27.0(react@19.2.6)
'@std/path':
specifier: npm:@jsr/std__path@^1.0.8
version: '@jsr/std__path@1.0.8'
@@ -961,21 +983,36 @@ importers:
'@supabase/supabase-js':
specifier: 'catalog:'
version: 2.108.2
+ '@tanstack/react-devtools':
+ specifier: ^0.10.3
+ version: 0.10.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(solid-js@1.9.13)
'@tanstack/react-hotkeys':
- specifier: ^0.9.1
- version: 0.9.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ specifier: ^0.10.0
+ version: 0.10.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@tanstack/react-query':
- specifier: ^5.0.0
+ specifier: ~5.83.0
version: 5.83.0(react@19.2.6)
'@tanstack/react-query-devtools':
- specifier: ^5.0.0
+ specifier: ~5.90.2
version: 5.90.2(@tanstack/react-query@5.83.0(react@19.2.6))(react@19.2.6)
+ '@tanstack/react-router':
+ specifier: 'catalog:'
+ version: 1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@tanstack/react-router-devtools':
+ specifier: ^1.166.13
+ version: 1.167.0(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@tanstack/router-core@1.171.8)(csstype@3.2.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@tanstack/react-router-ssr-query':
+ specifier: ^1.166.12
+ version: 1.167.1(@tanstack/query-core@5.83.0)(@tanstack/react-query@5.83.0(react@19.2.6))(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@tanstack/router-core@1.171.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@tanstack/react-start':
+ specifier: 'catalog:'
+ version: 1.168.18(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1))
'@tanstack/react-table':
- specifier: ^8.21.3
+ specifier: 'catalog:'
version: 8.21.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@tanstack/react-virtual':
- specifier: ^3.13.12
- version: 3.13.12(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ specifier: ^3.13.24
+ version: 3.14.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@types/d3-geo':
specifier: ^3.1.0
version: 3.1.0
@@ -1220,6 +1257,15 @@ importers:
'@smithy/property-provider':
specifier: ^4.0.4
version: 4.0.4
+ '@tailwindcss/container-queries':
+ specifier: ^0.1.1
+ version: 0.1.1(tailwindcss@4.2.4)
+ '@tailwindcss/vite':
+ specifier: 4.2.4
+ version: 4.2.4(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))
+ '@tanstack/devtools-vite':
+ specifier: ^0.6.0
+ version: 0.6.1(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))
'@testing-library/dom':
specifier: ^10.0.0
version: 10.4.1
@@ -1289,6 +1335,9 @@ importers:
'@types/zxcvbn':
specifier: ^4.4.1
version: 4.4.2
+ '@vercel/config':
+ specifier: ^0.2.1
+ version: 0.2.1
'@vitejs/plugin-react':
specifier: 'catalog:'
version: 6.0.1(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))
@@ -1395,7 +1444,7 @@ importers:
specifier: workspace:*
version: link:../../blocks/vue
'@tanstack/react-query':
- specifier: ^5.83.0
+ specifier: ~5.83.0
version: 5.83.0(react@19.2.6)
'@xyflow/react':
specifier: ^12.10.1
@@ -1537,10 +1586,10 @@ importers:
specifier: 'catalog:'
version: 2.108.2
'@tanstack/react-router':
- specifier: ^1.168.0
- version: 1.168.18(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ specifier: 'catalog:'
+ version: 1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@tanstack/react-start':
- specifier: ^1.168.0
+ specifier: 'catalog:'
version: 1.168.18(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4)
'@types/common-tags':
specifier: ^1.8.4
@@ -1919,7 +1968,7 @@ importers:
specifier: ^2.1.1
version: 2.1.1
h3:
- specifier: ^1.15.10
+ specifier: 1.15.11
version: 1.15.11
lucide-vue-next:
specifier: ^0.562.0
@@ -2526,7 +2575,7 @@ importers:
specifier: 'catalog:'
version: 2.108.2
'@tanstack/react-table':
- specifier: ^8.21.3
+ specifier: 'catalog:'
version: 8.21.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
class-variance-authority:
specifier: ^0.7.1
@@ -7710,6 +7759,36 @@ packages:
resolution: {integrity: sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==}
engines: {node: '>=18.0.0'}
+ '@solid-primitives/event-listener@2.4.5':
+ resolution: {integrity: sha512-nwRV558mIabl4yVAhZKY8cb6G+O1F0M6Z75ttTu5hk+SxdOnKSGj+eetDIu7Oax1P138ZdUU01qnBPR8rnxaEA==}
+ peerDependencies:
+ solid-js: ^1.6.12
+
+ '@solid-primitives/keyboard@1.3.5':
+ resolution: {integrity: sha512-sav+l+PL+74z3yaftVs7qd8c2SXkqzuxPOVibUe5wYMt+U5Hxp3V3XCPgBPN2I6cANjvoFtz0NiU8uHVLdi9FQ==}
+ peerDependencies:
+ solid-js: ^1.6.12
+
+ '@solid-primitives/resize-observer@2.1.5':
+ resolution: {integrity: sha512-AiyTknKcNBaKHbcSMuxtSNM8FjIuiSuFyFghdD0TcCMU9hKi9EmsC5pjfjDwxE+5EueB1a+T/34PLRI5vbBbKw==}
+ peerDependencies:
+ solid-js: ^1.6.12
+
+ '@solid-primitives/rootless@1.5.3':
+ resolution: {integrity: sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA==}
+ peerDependencies:
+ solid-js: ^1.6.12
+
+ '@solid-primitives/static-store@0.1.3':
+ resolution: {integrity: sha512-uxez7SXnr5GiRnzqO2IEDjOJRIXaG+0LZLBizmUA1FwSi+hrpuMzVBwyk70m4prcl8X6FDDXUl9O8hSq8wHbBQ==}
+ peerDependencies:
+ solid-js: ^1.6.12
+
+ '@solid-primitives/utils@6.4.0':
+ resolution: {integrity: sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A==}
+ peerDependencies:
+ solid-js: ^1.6.12
+
'@speed-highlight/core@1.2.14':
resolution: {integrity: sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==}
@@ -7929,6 +8008,11 @@ packages:
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
+ '@tailwindcss/container-queries@0.1.1':
+ resolution: {integrity: sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==}
+ peerDependencies:
+ tailwindcss: '>=3.2.0'
+
'@tailwindcss/forms@0.5.11':
resolution: {integrity: sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==}
peerDependencies:
@@ -8036,21 +8120,58 @@ packages:
peerDependencies:
vite: ^5.2.0 || ^6 || ^7 || ^8
+ '@tanstack/devtools-client@0.0.6':
+ resolution: {integrity: sha512-f85ZJXJnDIFOoykG/BFIixuAevJovCvJF391LPs6YjBAPhGYC50NWlx1y4iF/UmK5/cCMx+/JqI5SBOz7FanQQ==}
+ engines: {node: '>=18'}
+
+ '@tanstack/devtools-client@0.0.7':
+ resolution: {integrity: sha512-bAqBnXQlg/1PqmIC3XhqzG8jV3YmUQ41fD9VlOVzSilFBD4Kp6WJFJa+7N6TvlYXqMAy8xoeF9gg0d2Lt74OEQ==}
+ engines: {node: '>=18'}
+
+ '@tanstack/devtools-event-bus@0.4.1':
+ resolution: {integrity: sha512-cNnJ89Q021Zf883rlbBTfsaxTfi2r73/qejGtyTa7ksErF3hyDyAq1aTbo5crK9dAL7zSHh9viKY1BtMls1QOA==}
+ engines: {node: '>=18'}
+
+ '@tanstack/devtools-event-bus@0.4.2':
+ resolution: {integrity: sha512-2LHzhwBFlKHCcklsQrGe8TeyjHd4XAF8nuCO6wHmva5fePUkJUULbu6CsCNAlGlCi0KkEsMXZSvRdR4HgMq4yA==}
+ engines: {node: '>=18'}
+
+ '@tanstack/devtools-event-client@0.4.4':
+ resolution: {integrity: sha512-6T5Yop/793YI+H+5J8Hsyj4kCih9sl4t3ElLgKioW5hk3ocn+ZdSJ94tT7vL7uabxSugWYBZlOTMPzEw2puvQw==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ '@tanstack/devtools-ui@0.5.3':
+ resolution: {integrity: sha512-iJjwWtdXhUGpeHyyW9+3NhXhmlVFhh3v3UBNKCouykG9UFXEtneVVNXgSRpd70DeYJFmvKOY19LafKRI5/cM7A==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ solid-js: '>=1.9.7'
+
+ '@tanstack/devtools-vite@0.6.1':
+ resolution: {integrity: sha512-4NMdFwVDoJZlh4gnKl/LJYSoEKuZr2GiFgbCITfaKYWXk6XbXilclfLwAAdoX5gF2p0mEPPv5JCuAlbJgO9aWQ==}
+ engines: {node: '>=18'}
+ hasBin: true
+ peerDependencies:
+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ '@tanstack/devtools@0.12.4':
+ resolution: {integrity: sha512-fYZ0KTEpKq7JyjULDe4kGQBN77aw5jtULs1MaVraWvwtcoIwe4UOKV48UJQ9/mEQMFTytbgKxYk2QaSDgI/Znw==}
+ engines: {node: '>=18'}
+ hasBin: true
+ peerDependencies:
+ solid-js: '>=1.9.7'
+
'@tanstack/eslint-plugin-query@5.91.2':
resolution: {integrity: sha512-UPeWKl/Acu1IuuHJlsN+eITUHqAaa9/04geHHPedY8siVarSaWprY0SVMKrkpKfk5ehRT7+/MZ5QwWuEtkWrFw==}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
- '@tanstack/history@1.161.6':
- resolution: {integrity: sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg==}
- engines: {node: '>=20.19'}
-
'@tanstack/history@1.162.0':
resolution: {integrity: sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA==}
engines: {node: '>=20.19'}
- '@tanstack/hotkeys@0.7.1':
- resolution: {integrity: sha512-YHVO1z6wnvUCu7bg870Kv5k2D+FIuIOSIcbN0dAmTTsJ3mLMDLwcTVx0qVaq+SZp1B514JJTqGVstvUp85yIpQ==}
+ '@tanstack/hotkeys@0.8.0':
+ resolution: {integrity: sha512-vqH7X9nb0MTJ/O08++dB5bP9jgj4+BIPOUu/U+6myG86lDsirZSVSobpq5UQpE7nBuk62i8eIYeOhd+OMl/UrA==}
engines: {node: '>=18'}
'@tanstack/query-core@5.83.0':
@@ -8059,8 +8180,17 @@ packages:
'@tanstack/query-devtools@5.90.1':
resolution: {integrity: sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ==}
- '@tanstack/react-hotkeys@0.9.1':
- resolution: {integrity: sha512-/qdQUUVkYAHAWRGdFXqFgWpW/S+a6OzkvxWNWKLLDHQODJlO6EPBPa073CglaafBfzig58RK07T09ET+NnZhpg==}
+ '@tanstack/react-devtools@0.10.7':
+ resolution: {integrity: sha512-AYHQH06uuK07Asqq8eASgJjpILlaFBpjnTesxx1JVHGoBl4ijwbyIlKnj3Z8+M8sEOPn5mvtV5o6mielPXKHWg==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@types/react': '>=16.8'
+ '@types/react-dom': '>=16.8'
+ react: '>=16.8'
+ react-dom: '>=16.8'
+
+ '@tanstack/react-hotkeys@0.10.0':
+ resolution: {integrity: sha512-GwOSndI5j3qBVYTmgP1mYyRTnlxb2MS17cwGlsavSxMQPSnmDf+m3LzMIpRMs+3zzQMjg3cYhHsFYizYlFI2tw==}
engines: {node: '>=18'}
peerDependencies:
react: '>=16.8'
@@ -8077,10 +8207,25 @@ packages:
peerDependencies:
react: ^18 || ^19
- '@tanstack/react-router@1.168.18':
- resolution: {integrity: sha512-RmBptS3/qtkGhvG/u41JWOgxz1FIWybBz7iBTgLUIoFkqOj6NE4XlhUOsP2fabxACtbZdJnpvCWcJFWpWGIngw==}
+ '@tanstack/react-router-devtools@1.167.0':
+ resolution: {integrity: sha512-nGw095EG7IHx0h5NtlEmzf6vcCTaFNPWdTSuDKazajhN0ct/v/TkekJ9J6KYUCeV1a8/2ZmToc58M+0rrOyn7w==}
engines: {node: '>=20.19'}
peerDependencies:
+ '@tanstack/react-router': ^1.170.0
+ '@tanstack/router-core': ^1.170.0
+ react: '>=18.0.0 || >=19.0.0'
+ react-dom: '>=18.0.0 || >=19.0.0'
+ peerDependenciesMeta:
+ '@tanstack/router-core':
+ optional: true
+
+ '@tanstack/react-router-ssr-query@1.167.1':
+ resolution: {integrity: sha512-W9j5JPnBikyafvuUfykFfHIWod58OAbAAa5leNkXBcoDoocghMmu6w9uZOmUZvAWT7CSvgj5tBUtF7CM2OoHXQ==}
+ engines: {node: '>=20.19'}
+ peerDependencies:
+ '@tanstack/query-core': '>=5.90.0'
+ '@tanstack/react-query': '>=5.90.0'
+ '@tanstack/react-router': '>=1.127.0'
react: '>=18.0.0 || >=19.0.0'
react-dom: '>=18.0.0 || >=19.0.0'
@@ -8139,6 +8284,12 @@ packages:
vite:
optional: true
+ '@tanstack/react-store@0.11.0':
+ resolution: {integrity: sha512-tX4YXh3PDkmpvGQWkWqKpzs/MSqbtuwY9dWdWhtV9Q50PmO+jOkUKIWIX4G85dwt7lxdHLXsiaEKPdKmC8F41w==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
'@tanstack/react-store@0.9.3':
resolution: {integrity: sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==}
peerDependencies:
@@ -8152,21 +8303,26 @@ packages:
react: '>=16.8'
react-dom: '>=16.8'
- '@tanstack/react-virtual@3.13.12':
- resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==}
+ '@tanstack/react-virtual@3.14.3':
+ resolution: {integrity: sha512-k/cnHPVaOfn46hSbiY6n4Dzf4QjCGWSF40zR5QIIYUqPAjpA6TN7InfYmcMiDVQGP2iUn9xsRbAl8u1v3UmeVQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
- '@tanstack/router-core@1.168.14':
- resolution: {integrity: sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA==}
- engines: {node: '>=20.19'}
- hasBin: true
-
'@tanstack/router-core@1.171.8':
resolution: {integrity: sha512-PbrTBbofFcacrH3RLgHYILRqTFnAGq+gXrXoA/vo7qUSkJpSO4GWfLtrtCahD4VayzRm19IPwcjPPLEugag6pw==}
engines: {node: '>=20.19'}
+ '@tanstack/router-devtools-core@1.168.0':
+ resolution: {integrity: sha512-wQoQhlBK7nlZgqzaqdYXKWNTpdHdsaREdaPhFZVH0/Ador+F+eM3/NF2i3f2LPeS0GgKraZUQXe1Q/1+KHyEYg==}
+ engines: {node: '>=20.19'}
+ peerDependencies:
+ '@tanstack/router-core': ^1.170.0
+ csstype: ^3.0.10
+ peerDependenciesMeta:
+ csstype:
+ optional: true
+
'@tanstack/router-generator@1.167.12':
resolution: {integrity: sha512-FGr7nn6VhjL53TUCTyDgApSkAYRxhId+v0HVQdSu0ADkNuHY+sUnYEMqiF6aN82jYWuXzrSL1xazg6/rfEP82g==}
engines: {node: '>=20.19'}
@@ -8192,6 +8348,13 @@ packages:
webpack:
optional: true
+ '@tanstack/router-ssr-query-core@1.169.1':
+ resolution: {integrity: sha512-rngux8s/3mPQzcjLYDLkNU31coYVyCgrVTfpdwqUdY5jIEHqGTXrO73DTkPR1PppwYUeVhmNCgl8TctRcnupjg==}
+ engines: {node: '>=20.19'}
+ peerDependencies:
+ '@tanstack/query-core': '>=5.90.0'
+ '@tanstack/router-core': '>=1.127.0'
+
'@tanstack/router-utils@1.162.1':
resolution: {integrity: sha512-62layyTGmclHDQS/eidwKRfN1hhCKwViG7iEBcVmL0MXgcAB3OOucWCEcDDGd9Cu11H6b4QQ5oOo47MWIqwz0A==}
engines: {node: '>=20.19'}
@@ -8224,6 +8387,9 @@ packages:
resolution: {integrity: sha512-geCsFpgCt+S2gQjzXILdPZ9obIxtzuN8C0Esc1fcyWZhwYyqo4C8G2o/dIck8xGixCMSvOsxL5NkCXDdOm2KOQ==}
engines: {node: '>=22.12.0'}
+ '@tanstack/store@0.11.0':
+ resolution: {integrity: sha512-WlzzCt3xi0G6pCAJu1U+2jiECwabETDpQDi3hfkFZvJii9AuZqEKbOiVarX1/bWhTNjU486yQtJCCasi/0q+Cw==}
+
'@tanstack/store@0.9.3':
resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==}
@@ -8231,8 +8397,8 @@ packages:
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
engines: {node: '>=12'}
- '@tanstack/virtual-core@3.13.12':
- resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==}
+ '@tanstack/virtual-core@3.17.1':
+ resolution: {integrity: sha512-VZyW2Uiml5tmBZwPGrSD3Sz73OxzljQMCmzYHsUTPEuTsERf5xwa+uWb01xEzkz3ZSYTjj8NEb/mKHvgKxyZdA==}
'@tanstack/virtual-file-routes@1.162.0':
resolution: {integrity: sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA==}
@@ -8775,6 +8941,10 @@ packages:
'@usercentrics/cmp-browser-sdk@4.42.0':
resolution: {integrity: sha512-/cik01TdeiYUV1+EasK83Ip05TDqmowM5tmWGfRDdaOrneSic5BedBdJHKya4pP2vwiYAtzDYVJe4BwPt2M16g==}
+ '@vercel/config@0.2.1':
+ resolution: {integrity: sha512-MWLB1WjVqj2z7oMBzhQez+uMr1JjK3Gq50tZ1EgcJpOua1Dyt2ITLNjh+nNyO0Z8VXdurszFeXvaDnswJRgSgQ==}
+ hasBin: true
+
'@vercel/functions@1.6.0':
resolution: {integrity: sha512-R6FKQrYT5MZs5IE1SqeCJWxMuBdHawFcCZboKKw8p7s+6/mcd55Gx6tWmyKnQTyrSEA04NH73Tc9CbqpEle8RA==}
engines: {node: '>= 16'}
@@ -8810,6 +8980,9 @@ packages:
resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==}
engines: {node: '>= 20'}
+ '@vercel/routing-utils@6.1.1':
+ resolution: {integrity: sha512-kUJn6CRuLvua5xvDZTJu1rC8GLdniC8D5VIzoJKm0dUdcUxv3LEx+pi8dokxWS+YM7cIs1Z3Y8ms/BDW1iBzzA==}
+
'@vitejs/plugin-react@6.0.1':
resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -9653,6 +9826,10 @@ packages:
resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+ chalk@5.6.2:
+ resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
+ engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+
change-case-all@1.0.15:
resolution: {integrity: sha512-3+GIFhk3sNuvFAJKU46o26OdzudQlPNBCu1ZQi3cMeMHhty1bhDxu2WrEilVNYaGvqUtR1VSigFcJOiS13dRhQ==}
@@ -11721,6 +11898,11 @@ packages:
globrex@0.1.2:
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
+ goober@2.1.19:
+ resolution: {integrity: sha512-U7veizMqxyKlM58+Z5j2ngJBH/r9siDmxpvNxSw0PylF6WQvrASJEZrxh1hidRBJc2jqoBVSyOban5u8m+6Rxg==}
+ peerDependencies:
+ csstype: ^3.0.10
+
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
@@ -14610,6 +14792,9 @@ packages:
path-to-regexp@0.1.13:
resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==}
+ path-to-regexp@6.1.0:
+ resolution: {integrity: sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==}
+
path-to-regexp@6.3.0:
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
@@ -14991,6 +15176,10 @@ packages:
resolution: {integrity: sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==}
engines: {node: '>=20'}
+ pretty-cache-header@1.0.0:
+ resolution: {integrity: sha512-xtXazslu25CdnGnUkByU1RoOjK55TqwatJkjjJLg5ZAdz2Lngko/mmaUgeET36P2GMlNwh3fdM7FWBO717pNcw==}
+ engines: {node: '>=12.13'}
+
pretty-format@27.5.1:
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
@@ -15880,12 +16069,6 @@ packages:
resolution: {integrity: sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==}
engines: {node: '>=20.0.0'}
- seroval-plugins@1.5.0:
- resolution: {integrity: sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA==}
- engines: {node: '>=10'}
- peerDependencies:
- seroval: ^1.0
-
seroval-plugins@1.5.4:
resolution: {integrity: sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==}
engines: {node: '>=10'}
@@ -16114,6 +16297,9 @@ packages:
resolution: {integrity: sha512-NlGELfPrgX2f1TAAcz0WawlLn+0r3FyhhCRpFFK2CemXenPYvzMWWZINv3eDNo9ucdwme7oCHRY0Jnbs4aIkog==}
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
+ solid-js@1.9.13:
+ resolution: {integrity: sha512-6hJeJMOcEX8ktqjpDoJZEmld3ijvcvWBDtiXBm7f4332SiFN66QeAQI1REQshvyUoISsSeJ4PHDauKYbwao9JQ==}
+
sonner@1.5.0:
resolution: {integrity: sha512-FBjhG/gnnbN6FY0jaNnqZOMmB73R+5IiyYAw8yBj7L54ER7HB3fOSE5OFiQiE2iXWxeXKvg6fIP4LtVppHEdJA==}
peerDependencies:
@@ -16565,6 +16751,10 @@ packages:
resolution: {integrity: sha512-YBGpG4bWsHoPvofT6y/5iqulfXIiIErl5B0LdtHT1mGXDFTAhhRrbUpTvBgYbovr+3cKblya2WAOcpoy90XguA==}
engines: {node: '>=16'}
+ timestring@6.0.0:
+ resolution: {integrity: sha512-wMctrWD2HZZLuIlchlkE2dfXJh7J2KDI9Dwl+2abPYg0mswQHfOAyQW3jJg1pY5VfttSINZuKcXoB3FGypVklA==}
+ engines: {node: '>=8'}
+
tiny-inflate@1.0.3:
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
@@ -20057,7 +20247,7 @@ snapshots:
'@floating-ui/react': 0.26.28(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@react-aria/focus': 3.20.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@react-aria/interactions': 3.25.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
- '@tanstack/react-virtual': 3.13.12(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@tanstack/react-virtual': 3.14.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
use-sync-external-store: 1.6.0(react@19.2.6)
@@ -23969,6 +24159,40 @@ snapshots:
dependencies:
tslib: 2.8.1
+ '@solid-primitives/event-listener@2.4.5(solid-js@1.9.13)':
+ dependencies:
+ '@solid-primitives/utils': 6.4.0(solid-js@1.9.13)
+ solid-js: 1.9.13
+
+ '@solid-primitives/keyboard@1.3.5(solid-js@1.9.13)':
+ dependencies:
+ '@solid-primitives/event-listener': 2.4.5(solid-js@1.9.13)
+ '@solid-primitives/rootless': 1.5.3(solid-js@1.9.13)
+ '@solid-primitives/utils': 6.4.0(solid-js@1.9.13)
+ solid-js: 1.9.13
+
+ '@solid-primitives/resize-observer@2.1.5(solid-js@1.9.13)':
+ dependencies:
+ '@solid-primitives/event-listener': 2.4.5(solid-js@1.9.13)
+ '@solid-primitives/rootless': 1.5.3(solid-js@1.9.13)
+ '@solid-primitives/static-store': 0.1.3(solid-js@1.9.13)
+ '@solid-primitives/utils': 6.4.0(solid-js@1.9.13)
+ solid-js: 1.9.13
+
+ '@solid-primitives/rootless@1.5.3(solid-js@1.9.13)':
+ dependencies:
+ '@solid-primitives/utils': 6.4.0(solid-js@1.9.13)
+ solid-js: 1.9.13
+
+ '@solid-primitives/static-store@0.1.3(solid-js@1.9.13)':
+ dependencies:
+ '@solid-primitives/utils': 6.4.0(solid-js@1.9.13)
+ solid-js: 1.9.13
+
+ '@solid-primitives/utils@6.4.0(solid-js@1.9.13)':
+ dependencies:
+ solid-js: 1.9.13
+
'@speed-highlight/core@1.2.14': {}
'@standard-schema/spec@1.0.0': {}
@@ -24178,6 +24402,10 @@ snapshots:
dependencies:
tslib: 2.8.1
+ '@tailwindcss/container-queries@0.1.1(tailwindcss@4.2.4)':
+ dependencies:
+ tailwindcss: 4.2.4
+
'@tailwindcss/forms@0.5.11(tailwindcss@4.2.4)':
dependencies:
mini-svg-data-uri: 1.4.4
@@ -24264,6 +24492,80 @@ snapshots:
tailwindcss: 4.2.4
vite: 7.3.5(@types/node@22.13.14)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0)
+ '@tailwindcss/vite@4.2.4(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))':
+ dependencies:
+ '@tailwindcss/node': 4.2.4
+ '@tailwindcss/oxide': 4.2.4
+ tailwindcss: 4.2.4
+ vite: 8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0)
+
+ '@tanstack/devtools-client@0.0.6':
+ dependencies:
+ '@tanstack/devtools-event-client': 0.4.4
+
+ '@tanstack/devtools-client@0.0.7':
+ dependencies:
+ '@tanstack/devtools-event-client': 0.4.4
+
+ '@tanstack/devtools-event-bus@0.4.1':
+ dependencies:
+ ws: 8.21.0
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
+
+ '@tanstack/devtools-event-bus@0.4.2':
+ dependencies:
+ ws: 8.21.0
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
+
+ '@tanstack/devtools-event-client@0.4.4': {}
+
+ '@tanstack/devtools-ui@0.5.3(csstype@3.2.3)(solid-js@1.9.13)':
+ dependencies:
+ clsx: 2.1.1
+ dayjs: 1.11.20
+ goober: 2.1.19(csstype@3.2.3)
+ solid-js: 1.9.13
+ transitivePeerDependencies:
+ - csstype
+
+ '@tanstack/devtools-vite@0.6.1(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))':
+ dependencies:
+ '@babel/core': 7.29.7(supports-color@8.1.1)
+ '@babel/generator': 7.29.7
+ '@babel/parser': 7.29.7
+ '@babel/traverse': 7.29.7(supports-color@8.1.1)
+ '@babel/types': 7.29.7
+ '@tanstack/devtools-client': 0.0.6
+ '@tanstack/devtools-event-bus': 0.4.1
+ chalk: 5.6.2
+ launch-editor: 2.14.1
+ picomatch: 4.0.4
+ vite: 8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0)
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
+ '@tanstack/devtools@0.12.4(csstype@3.2.3)(solid-js@1.9.13)':
+ dependencies:
+ '@solid-primitives/event-listener': 2.4.5(solid-js@1.9.13)
+ '@solid-primitives/keyboard': 1.3.5(solid-js@1.9.13)
+ '@solid-primitives/resize-observer': 2.1.5(solid-js@1.9.13)
+ '@tanstack/devtools-client': 0.0.7
+ '@tanstack/devtools-event-bus': 0.4.2
+ '@tanstack/devtools-ui': 0.5.3(csstype@3.2.3)(solid-js@1.9.13)
+ clsx: 2.1.1
+ goober: 2.1.19(csstype@3.2.3)
+ solid-js: 1.9.13
+ transitivePeerDependencies:
+ - bufferutil
+ - csstype
+ - utf-8-validate
+
'@tanstack/eslint-plugin-query@5.91.2(eslint@9.37.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.2)':
dependencies:
'@typescript-eslint/utils': 8.48.0(eslint@9.37.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.2)
@@ -24272,22 +24574,33 @@ snapshots:
- supports-color
- typescript
- '@tanstack/history@1.161.6': {}
-
'@tanstack/history@1.162.0': {}
- '@tanstack/hotkeys@0.7.1':
+ '@tanstack/hotkeys@0.8.0':
dependencies:
- '@tanstack/store': 0.9.3
+ '@tanstack/store': 0.11.0
'@tanstack/query-core@5.83.0': {}
'@tanstack/query-devtools@5.90.1': {}
- '@tanstack/react-hotkeys@0.9.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+ '@tanstack/react-devtools@0.10.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(solid-js@1.9.13)':
dependencies:
- '@tanstack/hotkeys': 0.7.1
- '@tanstack/react-store': 0.9.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@tanstack/devtools': 0.12.4(csstype@3.2.3)(solid-js@1.9.13)
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+ react: 19.2.6
+ react-dom: 19.2.6(react@19.2.6)
+ transitivePeerDependencies:
+ - bufferutil
+ - csstype
+ - solid-js
+ - utf-8-validate
+
+ '@tanstack/react-hotkeys@0.10.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+ dependencies:
+ '@tanstack/hotkeys': 0.8.0
+ '@tanstack/react-store': 0.11.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
@@ -24302,14 +24615,27 @@ snapshots:
'@tanstack/query-core': 5.83.0
react: 19.2.6
- '@tanstack/react-router@1.168.18(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+ '@tanstack/react-router-devtools@1.167.0(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@tanstack/router-core@1.171.8)(csstype@3.2.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
- '@tanstack/history': 1.161.6
- '@tanstack/react-store': 0.9.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
- '@tanstack/router-core': 1.168.14
- isbot: 5.1.36
+ '@tanstack/react-router': 1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@tanstack/router-devtools-core': 1.168.0(@tanstack/router-core@1.171.8)(csstype@3.2.3)
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
+ optionalDependencies:
+ '@tanstack/router-core': 1.171.8
+ transitivePeerDependencies:
+ - csstype
+
+ '@tanstack/react-router-ssr-query@1.167.1(@tanstack/query-core@5.83.0)(@tanstack/react-query@5.83.0(react@19.2.6))(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@tanstack/router-core@1.171.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+ dependencies:
+ '@tanstack/query-core': 5.83.0
+ '@tanstack/react-query': 5.83.0(react@19.2.6)
+ '@tanstack/react-router': 1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@tanstack/router-ssr-query-core': 1.169.1(@tanstack/query-core@5.83.0)(@tanstack/router-core@1.171.8)
+ react: 19.2.6
+ react-dom: 19.2.6(react@19.2.6)
+ transitivePeerDependencies:
+ - '@tanstack/router-core'
'@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
@@ -24328,6 +24654,28 @@ snapshots:
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
+ '@tanstack/react-start-rsc@0.1.17(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1))':
+ dependencies:
+ '@tanstack/react-router': 1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@tanstack/react-start-server': 1.167.13(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@tanstack/router-core': 1.171.8
+ '@tanstack/router-utils': 1.162.1(supports-color@8.1.1)
+ '@tanstack/start-client-core': 1.170.6
+ '@tanstack/start-fn-stubs': 1.162.0
+ '@tanstack/start-plugin-core': 1.171.10(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1))
+ '@tanstack/start-server-core': 1.169.8
+ '@tanstack/start-storage-context': 1.167.10
+ pathe: 2.0.3
+ react: 19.2.6
+ react-dom: 19.2.6(react@19.2.6)
+ transitivePeerDependencies:
+ - '@rsbuild/core'
+ - crossws
+ - supports-color
+ - vite
+ - vite-plugin-solid
+ - webpack
+
'@tanstack/react-start-rsc@0.1.17(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4)':
dependencies:
'@tanstack/react-router': 1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
@@ -24362,6 +24710,29 @@ snapshots:
transitivePeerDependencies:
- crossws
+ '@tanstack/react-start@1.168.18(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1))':
+ dependencies:
+ '@tanstack/react-router': 1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@tanstack/react-start-client': 1.168.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@tanstack/react-start-rsc': 0.1.17(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1))
+ '@tanstack/react-start-server': 1.167.13(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@tanstack/router-utils': 1.162.1(supports-color@8.1.1)
+ '@tanstack/start-client-core': 1.170.6
+ '@tanstack/start-plugin-core': 1.171.10(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1))
+ '@tanstack/start-server-core': 1.169.8
+ pathe: 2.0.3
+ react: 19.2.6
+ react-dom: 19.2.6(react@19.2.6)
+ optionalDependencies:
+ vite: 8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0)
+ transitivePeerDependencies:
+ - '@rspack/core'
+ - crossws
+ - react-server-dom-rspack
+ - supports-color
+ - vite-plugin-solid
+ - webpack
+
'@tanstack/react-start@1.168.18(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4)':
dependencies:
'@tanstack/react-router': 1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
@@ -24385,6 +24756,13 @@ snapshots:
- vite-plugin-solid
- webpack
+ '@tanstack/react-store@0.11.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+ dependencies:
+ '@tanstack/store': 0.11.0
+ react: 19.2.6
+ react-dom: 19.2.6(react@19.2.6)
+ use-sync-external-store: 1.6.0(react@19.2.6)
+
'@tanstack/react-store@0.9.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
'@tanstack/store': 0.9.3
@@ -24398,19 +24776,12 @@ snapshots:
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
- '@tanstack/react-virtual@3.13.12(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+ '@tanstack/react-virtual@3.14.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
- '@tanstack/virtual-core': 3.13.12
+ '@tanstack/virtual-core': 3.17.1
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
- '@tanstack/router-core@1.168.14':
- dependencies:
- '@tanstack/history': 1.161.6
- cookie-es: 3.1.1
- seroval: 1.5.4
- seroval-plugins: 1.5.0(seroval@1.5.4)
-
'@tanstack/router-core@1.171.8':
dependencies:
'@tanstack/history': 1.162.0
@@ -24418,6 +24789,14 @@ snapshots:
seroval: 1.5.4
seroval-plugins: 1.5.4(seroval@1.5.4)
+ '@tanstack/router-devtools-core@1.168.0(@tanstack/router-core@1.171.8)(csstype@3.2.3)':
+ dependencies:
+ '@tanstack/router-core': 1.171.8
+ clsx: 2.1.1
+ goober: 2.1.19(csstype@3.2.3)
+ optionalDependencies:
+ csstype: 3.2.3
+
'@tanstack/router-generator@1.167.12(supports-color@8.1.1)':
dependencies:
'@babel/types': 7.29.7
@@ -24431,6 +24810,28 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@tanstack/router-plugin@1.168.13(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1))':
+ dependencies:
+ '@babel/core': 7.29.7(supports-color@8.1.1)
+ '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.7(supports-color@8.1.1))
+ '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.7(supports-color@8.1.1))
+ '@babel/template': 7.29.7
+ '@babel/traverse': 7.29.7(supports-color@8.1.1)
+ '@babel/types': 7.29.7
+ '@tanstack/router-core': 1.171.8
+ '@tanstack/router-generator': 1.167.12(supports-color@8.1.1)
+ '@tanstack/router-utils': 1.162.1(supports-color@8.1.1)
+ '@tanstack/virtual-file-routes': 1.162.0
+ chokidar: 5.0.0
+ unplugin: 3.0.0
+ zod: 4.4.3
+ optionalDependencies:
+ '@tanstack/react-router': 1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ vite: 8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0)
+ webpack: 5.105.4(esbuild@0.28.1)
+ transitivePeerDependencies:
+ - supports-color
+
'@tanstack/router-plugin@1.168.13(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4)':
dependencies:
'@babel/core': 7.29.7(supports-color@8.1.1)
@@ -24453,6 +24854,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@tanstack/router-ssr-query-core@1.169.1(@tanstack/query-core@5.83.0)(@tanstack/router-core@1.171.8)':
+ dependencies:
+ '@tanstack/query-core': 5.83.0
+ '@tanstack/router-core': 1.171.8
+
'@tanstack/router-utils@1.162.1(supports-color@8.1.1)':
dependencies:
'@babel/core': 7.29.7(supports-color@8.1.1)
@@ -24476,6 +24882,39 @@ snapshots:
'@tanstack/start-fn-stubs@1.162.0': {}
+ '@tanstack/start-plugin-core@1.171.10(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1))':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/core': 7.29.7(supports-color@8.1.1)
+ '@babel/types': 7.29.7
+ '@rolldown/pluginutils': 1.0.1
+ '@tanstack/router-core': 1.171.8
+ '@tanstack/router-generator': 1.167.12(supports-color@8.1.1)
+ '@tanstack/router-plugin': 1.168.13(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1))
+ '@tanstack/router-utils': 1.162.1(supports-color@8.1.1)
+ '@tanstack/start-client-core': 1.170.6
+ '@tanstack/start-server-core': 1.169.8
+ exsolve: 1.0.8
+ lightningcss: 1.32.0
+ pathe: 2.0.3
+ picomatch: 4.0.4
+ seroval: 1.5.4
+ source-map: 0.7.6
+ srvx: 0.11.16
+ tinyglobby: 0.2.17
+ ufo: 1.6.4
+ vitefu: 1.1.1(vite@8.0.16(@types/node@22.13.14)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))
+ xmlbuilder2: 4.0.3
+ zod: 4.4.3
+ optionalDependencies:
+ vite: 8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0)
+ transitivePeerDependencies:
+ - '@tanstack/react-router'
+ - crossws
+ - supports-color
+ - vite-plugin-solid
+ - webpack
+
'@tanstack/start-plugin-core@1.171.10(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4)':
dependencies:
'@babel/code-frame': 7.27.1
@@ -24525,11 +24964,13 @@ snapshots:
dependencies:
'@tanstack/router-core': 1.171.8
+ '@tanstack/store@0.11.0': {}
+
'@tanstack/store@0.9.3': {}
'@tanstack/table-core@8.21.3': {}
- '@tanstack/virtual-core@3.13.12': {}
+ '@tanstack/virtual-core@3.17.1': {}
'@tanstack/virtual-file-routes@1.162.0': {}
@@ -25157,6 +25598,12 @@ snapshots:
lz-string: 1.5.0
uuid: 11.1.1
+ '@vercel/config@0.2.1':
+ dependencies:
+ '@vercel/routing-utils': 6.1.1
+ pretty-cache-header: 1.0.0
+ zod: 3.25.76
+
'@vercel/functions@1.6.0(@aws-sdk/credential-provider-web-identity@3.972.38)':
optionalDependencies:
'@aws-sdk/credential-provider-web-identity': 3.972.38
@@ -25194,6 +25641,13 @@ snapshots:
'@vercel/oidc@3.2.0': {}
+ '@vercel/routing-utils@6.1.1':
+ dependencies:
+ path-to-regexp: 6.1.0
+ path-to-regexp-updated: path-to-regexp@6.3.0
+ optionalDependencies:
+ ajv: 6.14.0
+
'@vitejs/plugin-react@6.0.1(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))':
dependencies:
'@rolldown/pluginutils': 1.0.0-rc.7
@@ -26256,6 +26710,8 @@ snapshots:
chalk@5.4.1: {}
+ chalk@5.6.2: {}
+
change-case-all@1.0.15:
dependencies:
change-case: 4.1.2
@@ -28591,6 +29047,10 @@ snapshots:
globrex@0.1.2: {}
+ goober@2.1.19(csstype@3.2.3):
+ dependencies:
+ csstype: 3.2.3
+
gopd@1.2.0: {}
gqlmin@0.3.1:
@@ -32431,6 +32891,8 @@ snapshots:
path-to-regexp@0.1.13: {}
+ path-to-regexp@6.1.0: {}
+
path-to-regexp@6.3.0: {}
path-to-regexp@8.4.1: {}
@@ -32779,6 +33241,10 @@ snapshots:
pretty-bytes@7.1.0: {}
+ pretty-cache-header@1.0.0:
+ dependencies:
+ timestring: 6.0.0
+
pretty-format@27.5.1:
dependencies:
ansi-regex: 5.0.1
@@ -33986,10 +34452,6 @@ snapshots:
serialize-javascript@7.0.5: {}
- seroval-plugins@1.5.0(seroval@1.5.4):
- dependencies:
- seroval: 1.5.4
-
seroval-plugins@1.5.4(seroval@1.5.4):
dependencies:
seroval: 1.5.4
@@ -34334,6 +34796,12 @@ snapshots:
ip-address: 10.2.0
smart-buffer: 4.2.0
+ solid-js@1.9.13:
+ dependencies:
+ csstype: 3.2.3
+ seroval: 1.5.4
+ seroval-plugins: 1.5.4(seroval@1.5.4)
+
sonner@1.5.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
dependencies:
react: 19.2.6
@@ -34825,6 +35293,8 @@ snapshots:
timeout-signal@2.0.0: {}
+ timestring@6.0.0: {}
+
tiny-inflate@1.0.3: {}
tiny-invariant@1.3.3: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 3ba74bde8df..3ebbde767ea 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -13,6 +13,9 @@ catalog:
'@supabase/realtime-js': 2.108.2
'@supabase/ssr': 0.10.2
'@supabase/supabase-js': 2.108.2
+ '@tanstack/react-router': ^1.169.2
+ '@tanstack/react-start': ^1.167.65
+ '@tanstack/react-table': ^8.21.3
'@types/node': ^22.0.0
'@types/react': ^19.2.14
'@types/react-dom': ^19.2.3
@@ -73,6 +76,10 @@ overrides:
cacache>tar: ^7.5.11
dompurify: ^3.3.2
express-rate-limit>ip-address: ^10.1.1
+ # Pin h3 v1 to a single version so the Nuxt registry example (vue-blocks)
+ # doesn't end up with two copies (1.15.10 + 1.15.11) and hit nominal
+ # H3Event type mismatches. v2 (h3@2) is intentionally left untouched.
+ 'h3@1': 1.15.11
lodash: 'catalog:'
lodash-es: 'catalog:'
node-gyp>tar: ^7.5.11
diff --git a/scripts/generateLocalEnv.js b/scripts/generateLocalEnv.js
index 808f964d89d..8f8f645f858 100644
--- a/scripts/generateLocalEnv.js
+++ b/scripts/generateLocalEnv.js
@@ -11,7 +11,10 @@ const defaultEnv = {
// NEXT_ANALYTICS_BACKEND_PROVIDER: 'postgres',
// SUPABASE_REST_URL: 'http://127.0.0.1:54321/rest/v1/',
// NEXT_PUBLIC_ENABLE_LOGS: 'false',
- // NEXT_PUBLIC_IS_PLATFORM: 'false',
+ // E2E tests target self-hosted studio (no auth). Explicitly written so that
+ // .env.test overrides any IS_PLATFORM=true that a developer's .env.local
+ // might set for their own platform-mode work.
+ NEXT_PUBLIC_IS_PLATFORM: 'false',
PG_META_CRYPTO_KEY: 'SAMPLE_KEY',
POSTGRES_PASSWORD: 'postgres',
POSTGRES_HOST: 'db',
@@ -31,7 +34,7 @@ const defaultEnv = {
NEXT_PUBLIC_HCAPTCHA_SITE_KEY: '10000000-ffff-ffff-ffff-000000000001',
NEXT_PUBLIC_NODE_ENV: 'test',
SNIPPETS_MANAGEMENT_FOLDER: '../../supabase/snippets',
- EDGE_FUNCTIONS_MANAGEMENT_FOLDER: '../../supabase/functions' // path relative to studio project
+ EDGE_FUNCTIONS_MANAGEMENT_FOLDER: '../../supabase/functions', // path relative to studio project
}
const environment = { ...generatedEnv, ...defaultEnv }
diff --git a/turbo.jsonc b/turbo.jsonc
index 26a5147b454..cbc370c99da 100644
--- a/turbo.jsonc
+++ b/turbo.jsonc
@@ -16,6 +16,7 @@
},
"dev": {
"cache": false,
+ "persistent": true,
},
"test": {
"cache": false,