Per the @vercel/config docs, vercel.ts must expose a named `config`
export for Vercel's build pipeline to pick it up. The default export
was compiling to a shape Vercel rejected with a misleading
"rewrites[0] missing required property source" error.
Vercel's TypeScript config lets us read NEXT_PUBLIC_BASE_PATH at build
time and prepend it to the SPA rewrites. Vite's base only changes asset
URLs, not on-disk paths — without this, `/basepath/assets/*` 404s and
falls through to the shell.
Wire Vite `base`, Nitro `baseURL`, and the TanStack router `basepath` off
the same `NEXT_PUBLIC_BASE_PATH` env var so the prefix stays in sync (all
three layers are required — there's no single knob in TanStack Start).
Defaults are unchanged when the var is empty.
Also replace the `/` placeholder with the redirect rules that used to live
in `next.config.ts`'s `redirects()`: platform → `/org` (or
`/new/new-project` when deep-linked with `?next=new-project`), self-hosted
→ `/project/default`. Kept as a route (not a Nitro `routeRule`) so it
works in `vite dev` and can branch on the query param.
Thirteen self-hosted-only API routes under `pages/api/platform/{auth,
storage}/**` constructed a `createClient(process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_KEY!)` at module scope. On Next.js each
route was its own serverless function, so this only ran when the route
was hit. Under TanStack Start + Nitro the whole API surface is packed
into a single handler, so every request — including page renders —
evaluates every API route's module. On the platform build these env
vars intentionally aren't set (the routes only run on self-hosted), so
module evaluation hit `validateSupabaseUrl` and crashed every request
with a 500.
Introduces `selfHostedSupabaseAdmin`, a `SupabaseClient`-shaped Proxy
that constructs the real client lazily on first property access. Call
sites (`supabase.storage.getBucket(…)`) stay byte-identical; the only
per-file diff is the import.
Vercel's dashboard framework preset is still set to "Next.js" from the
old pipeline, which triggers `@vercel/next`'s post-build validation and
fails because our build now emits `.vercel/output/` (Nitro's Vercel
preset, Build Output API v3) instead of `.next/routes-manifest.json`.
Setting `"framework": null` tells Vercel to skip framework-specific
handling and trust the Build Output API files Nitro writes directly.
Adds the `nitro/vite` plugin so `vite build` produces a `.output/`
directory Vercel can pick up via the Build Output API. Along the way,
two SSR-prerender blockers had to be resolved:
- `@graphiql/react` transitively loads a codemirror addon that touches
`document` at module-evaluation time, which crashes Nitro's prerender
(it boots a real Node server and fetches `/`). Added an SSR-only
Vite plugin that swaps `components/interfaces/GraphQL/GraphiQL.tsx`
for a no-op default export — stubbing the single internal consumer
is simpler than matching graphiql's 30+ named exports to satisfy
Rolldown's static analysis, and no SSR-reachable route renders it.
- `tslib`'s Node ESM entry is a CJS-interop wrapper (`modules/index.js`
re-exporting from the UMD `tslib.js`). Rolldown inlines the UMD body
but flattens its IIFE, so `__extends` ends up declared but never
assigned — crashing any configcat-common / AWS SDK code path that
uses it. Force Nitro to prefer the `module` export condition so it
picks up the pure-ESM `tslib.es6.mjs`, mark it as `noExternals` so
the external-trace copy pass can't reintroduce the CJS wrapper at
runtime, and inline it in Vite's SSR bundle for the same reason.
Also updates package scripts to use Vite as the primary build/dev/start
commands and preserves the Next.js equivalents as `*:next` so the old
pipeline keeps working during the migration. `.output/` is gitignored.
Re-exports pages/organizations.tsx under routes/_app/organizations.tsx.
Page default already applies withAuth; PageLayout wraps the body with
the "Your Organizations" heading. Header title and hideMobileMenu flow
via route staticData through _app.tsx.
Re-exports sign-up, sign-in-sso, sign-in-partner, sign-in-mfa,
forgot-password(-mfa), reset-password, cli/login, and the Stripe
partners login page under `routes/_auth/`. Each wrapper replicates the
page's `getLayout` treatment; pages that inline their own layout are
rendered directly. `withAuth`-wrapped default exports stay intact.
Also hardens `compat/next/link.tsx` to accept Next's `UrlObject`
(`{pathname, query, hash}`) href form — sign-in.tsx uses this to forward
`router.query` into SSO/sign-up links, and the old shim was coercing it
to "[object Object]". Anchor attrs and `replace` now spread through;
remaining Next-only props are dropped silently.
Re-exports the Next 404 page as the router's notFoundComponent via a
thin wrapper (Next's `NextPage` type doesn't match TanStack's
`NotFoundRouteProps` directly).
Two App Router routes (incident-banner, incident-status) and a few
type-only uses elsewhere import from 'next/server'. The Vite
nextCompat guard was 500ing on the build. Shim exposes
NextResponse.json (the only value surface used) and a type alias
NextRequest = Request.
These live under apps/studio/app/api/ rather than pages/api/ and were
missed by the initial API-routes audit. Both are already Web-native
(NextResponse extends Response), so they just need direct method
re-exports with no shim.
Adds `compat/next/api.ts` exposing `toWebHandler(nextHandler)`: an
adapter that builds a NextApiRequest-shaped object from a Web
`Request` (lowercased headers, parsed cookies, merged query + route
params, JSON/urlencoded body parsing) and a NextApiResponse proxy
capturing `status` / `setHeader` / `json` / `send` / `write` / `end`
/ `redirect`. Handler output is buffered then returned as a Web
`Response`. Handlers that already return a `Response` (edge-runtime
style) pass through unchanged.
Each ported route in `routes/api/**` is a 9-line re-export: import
the default export from `pages/api/...`, wrap with `toWebHandler`,
register under `server.handlers.{GET,POST,...}`. `apiWrapper` and
`apiAuthenticate` stay untouched — they run inside the shim seeing
the shaped `req`/`res`.
Coverage: 88 of the 90 `pages/api/**` handlers. The two exceptions
need bespoke rewrites and are tracked in TANSTACK_MIGRATION.md:
- `v1/projects/[ref]/functions/[slug]/body.ts` — pipes
`fs.createReadStream` into `res`; needs a Web-stream rewrite.
- `mcp/index.ts` — the MCP SDK transport expects Node req/res; need
to check for a Web-fetch transport option.
Also adds `query` (params + search) to the existing `next/router`
shim to unblock components like FeedbackDropdown that read
`router.query.ref`.
First two pages ported under the minimum-diff Path A strategy: the
TanStack route wraps the page default-exported from pages/ rather
than rewriting the body. Layout chains provided by pathless _app /
_auth pathless routes + sibling-file layouts, reading per-leaf props
(AccountLayout title, DefaultLayout headerTitle) from route
staticData via useMatches.
Supporting fixes:
- compat/next/router: add `query` (params + search) so components
like FeedbackDropdown that read `router.query.ref` keep working
under the shim.
- ConnectSheet content loader: switch to `import.meta.glob` for
Vite (the template path spans multiple directories so
dynamic-import-vars can't analyse it), guarded with
`import.meta.env.SSR` to keep the 37 content modules out of the
SSR graph — they reshuffle chunks enough to surface circular-dep
bugs in unrelated modules. Next/webpack falls through a try/catch
to the existing `import(\`./content/\${filePath}/content\`)`.
TANSTACK_MIGRATION.md tracks per-page status during the migration.
Extends next/router shims to expose router.pathname as the route
pattern (Next pages-router semantics) rather than the resolved URL,
by converting TanStack's matched route id ($param → [param]). Also
adds push/replace/asPath to the next/router shim so consumers like
RouteValidationWrapper can keep working unchanged.
Also adds next/legacy/image and useSearchParams shims that the build
uncovered, and bumps @radix-ui/react-visually-hidden to ^1.2.3 so
react-select's import of VISUALLY_HIDDEN_STYLES resolves at runtime.
- Wrap Outlet with StudioCommandProvider (imported as CommandProvider,
matching pages/_app.tsx) inside AiAssistantStateContextProvider so
the command menu context works in the Vite build.
- Add an events adapter to both next/router and next/compat/router
shims so workspace code that does router.events.on('routeChangeX')
(CommandProvider, telemetry, usePreventNavigationOnUnsavedChanges,
QueryInsightsTable, ...) doesn't crash on `.on` of undefined.
Subscriptions proxy to TanStack Router's router.subscribe
(routeChangeStart -> onBeforeLoad, routeChangeComplete -> onResolved)
and .off unsubscribes by handler identity. The proxy is cached per
router instance via a WeakMap so effect deps stay stable.
Wrap Outlet with AiAssistantStateContextProvider from
@/state/ai-assistant-state inside DevToolbarProvider, matching the
wrap order in pages/_app.tsx so AI assistant state is available in
the Vite build.
- Wrap Outlet with DevToolbarProvider from 'dev-tools' inside the
ThemeProvider, and render <DevToolbar extraTabs={...} /> +
<DevToolbarTrigger /> as siblings, mirroring pages/_app.tsx.
- Guard everything behind IS_DEV_TOOLBAR_ENABLED (NEXT_PUBLIC_ENVIRONMENT
in {local, staging}) so prod bundles tree-shake the dev-only code.
- Use React.lazy + Suspense directly for ResourceWarningsTab since
__root.tsx is new code we control — the compat shim is only for
existing workspace code that still imports next/dynamic.
- Add the next/dynamic -> compat/next/dynamic.tsx entry to nextShims
so any workspace file pulled transitively (e.g. common, ui-patterns)
keeps working via the compat layer. Shim proxies to React.lazy +
Suspense and honours the ssr: false option.
- Wrap Outlet with ThemeProvider from 'common' (defaultTheme="system",
themes: dark/light/classic-dark, enableSystem, disableTransitionOnChange)
and call useThemeSandbox() in RootComponent, mirroring pages/_app.tsx.
- next-themes is framework-agnostic so no compat shim is needed; it's
not a next/* import and the lib uses plain DOM + localStorage.
- Add suppressHydrationWarning on <html>. next-themes writes
data-theme + color-scheme onto <html> from localStorage before
hydration so the prerendered shell (which can't know the user's
theme) necessarily differs. The flag is React's designed escape
hatch for this exact case and is one-level-deep, so descendants
still diff-check normally.
Wrap Outlet with TooltipProvider (delayDuration={0}) from 'ui',
mirroring the wrapper in pages/_app.tsx so Radix tooltip primitives
get their shared context in the Vite build.
MetaFaviconsPagesRouter references favicon-128.png (without
dimensions) for the 128x128 size but every other variant follows the
favicon-<size>.png pattern. My initial port generated the wrong href.
Track filename + sizes separately so the link points at the file that
actually ships in public/favicon.
- Move every static meta/link from pages/_app.tsx's <Head> and
MetaFaviconsPagesRouter into the root route's head() so TanStack
Start emits them via HeadContent. Covers viewport, og:image,
googlebot, msapplication-*, theme-color, 8 apple-touch-icon-
precomposed, 7 PNG favicons, shortcut icon, manifest, and the
IS_PLATFORM API preconnect.
- Drop next/font from the compat path: styles/fonts.css declares the
local CustomFont via @font-face and sets --font-custom /
--font-source-code-pro. Source Code Pro loads from Google Fonts via
a <link> in head(). pages/_app.tsx keeps its next/font setup for
the Next build.
- Add a small DynamicTitle effect so useCustomContent('app:title')
can override the default "Supabase" title at runtime, mirroring
<title>{appTitle ?? 'Supabase'}</title> in _app.tsx.
Use vite's loadEnv to read the .env hierarchy + process.env, filter to
NEXT_PUBLIC_*, and pass them to define so process.env.NEXT_PUBLIC_X is
replaced with its value at build time (matching Next.js behaviour).
Without this, every process.env.NEXT_PUBLIC_* access in the browser
bundle would be undefined since process isn't defined in browsers.