From f0452eb43be1d2739de503b9d308bc9c304a4270 Mon Sep 17 00:00:00 2001
From: Manuel Schiller <manuel.schiller@caligano.de>
Date: Wed, 4 Feb 2026 00:29:09 +0100
Subject: [PATCH 1/3] feat: add start context bridge

---
 e2e/react-start/context-bridge/.gitignore     | 17 ++++
 e2e/react-start/context-bridge/package.json   | 37 ++++++++
 .../context-bridge/playwright.config.ts       | 29 ++++++
 .../context-bridge/src/routeTree.gen.ts       | 87 ++++++++++++++++++
 e2e/react-start/context-bridge/src/router.tsx | 34 +++++++
 .../context-bridge/src/routes/__root.tsx      | 62 +++++++++++++
 .../context-bridge/src/routes/index.tsx       | 78 ++++++++++++++++
 .../context-bridge/src/routes/next.tsx        | 34 +++++++
 e2e/react-start/context-bridge/src/start.ts   | 31 +++++++
 .../context-bridge/src/styles/app.css         |  6 ++
 .../context-bridge/src/utils/sortJson.ts      | 18 ++++
 .../tests/context-bridge.spec.ts              | 56 ++++++++++++
 e2e/react-start/context-bridge/tsconfig.json  | 22 +++++
 e2e/react-start/context-bridge/vite.config.ts | 19 ++++
 packages/router-core/package.json             |  4 +-
 .../src/createStartContextBridge.ts           | 90 +++++++++++++++++++
 packages/start-client-core/src/index.tsx      |  1 +
 pnpm-lock.yaml                                | 61 +++++++++++++
 18 files changed, 684 insertions(+), 2 deletions(-)
 create mode 100644 e2e/react-start/context-bridge/.gitignore
 create mode 100644 e2e/react-start/context-bridge/package.json
 create mode 100644 e2e/react-start/context-bridge/playwright.config.ts
 create mode 100644 e2e/react-start/context-bridge/src/routeTree.gen.ts
 create mode 100644 e2e/react-start/context-bridge/src/router.tsx
 create mode 100644 e2e/react-start/context-bridge/src/routes/__root.tsx
 create mode 100644 e2e/react-start/context-bridge/src/routes/index.tsx
 create mode 100644 e2e/react-start/context-bridge/src/routes/next.tsx
 create mode 100644 e2e/react-start/context-bridge/src/start.ts
 create mode 100644 e2e/react-start/context-bridge/src/styles/app.css
 create mode 100644 e2e/react-start/context-bridge/src/utils/sortJson.ts
 create mode 100644 e2e/react-start/context-bridge/tests/context-bridge.spec.ts
 create mode 100644 e2e/react-start/context-bridge/tsconfig.json
 create mode 100644 e2e/react-start/context-bridge/vite.config.ts
 create mode 100644 packages/start-client-core/src/createStartContextBridge.ts

diff --git a/e2e/react-start/context-bridge/.gitignore b/e2e/react-start/context-bridge/.gitignore
new file mode 100644
index 0000000000..8ecacbfa62
--- /dev/null
+++ b/e2e/react-start/context-bridge/.gitignore
@@ -0,0 +1,17 @@
+node_modules
+package-lock.json
+yarn.lock
+
+.DS_Store
+.cache
+.env
+.vercel
+.output
+
+/dist/
+/build/
+
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/e2e/react-start/context-bridge/package.json b/e2e/react-start/context-bridge/package.json
new file mode 100644
index 0000000000..f318b9ad1a
--- /dev/null
+++ b/e2e/react-start/context-bridge/package.json
@@ -0,0 +1,37 @@
+{
+  "name": "tanstack-react-start-e2e-context-bridge",
+  "private": true,
+  "sideEffects": false,
+  "type": "module",
+  "scripts": {
+    "dev": "vite dev --port 3000",
+    "dev:e2e": "vite dev",
+    "build": "vite build && tsc --noEmit",
+    "preview": "vite preview",
+    "start": "pnpx srvx --prod -s ../client dist/server/server.js",
+    "test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
+  },
+  "dependencies": {
+    "@tanstack/react-router": "workspace:^",
+    "@tanstack/react-router-devtools": "workspace:^",
+    "@tanstack/react-start": "workspace:^",
+    "express": "^5.1.0",
+    "http-proxy-middleware": "^3.0.5",
+    "react": "^19.0.0",
+    "react-dom": "^19.0.0"
+  },
+  "devDependencies": {
+    "@playwright/test": "^1.50.1",
+    "@tailwindcss/vite": "^4.1.18",
+    "@tanstack/router-e2e-utils": "workspace:^",
+    "@types/node": "^22.10.2",
+    "@types/react": "^19.0.8",
+    "@types/react-dom": "^19.0.3",
+    "@vitejs/plugin-react": "^4.3.4",
+    "srvx": "^0.10.1",
+    "tailwindcss": "^4.1.18",
+    "typescript": "^5.7.2",
+    "vite": "^7.3.1",
+    "vite-tsconfig-paths": "^5.1.4"
+  }
+}
diff --git a/e2e/react-start/context-bridge/playwright.config.ts b/e2e/react-start/context-bridge/playwright.config.ts
new file mode 100644
index 0000000000..22817e305e
--- /dev/null
+++ b/e2e/react-start/context-bridge/playwright.config.ts
@@ -0,0 +1,29 @@
+import { defineConfig, devices } from '@playwright/test'
+import { getTestServerPort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+export const PORT = await getTestServerPort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+
+export default defineConfig({
+  testDir: './tests',
+  workers: 1,
+  reporter: [['line']],
+  use: {
+    baseURL,
+  },
+  webServer: {
+    command: `VITE_SERVER_PORT=${PORT} pnpm build && PORT=${PORT} VITE_SERVER_PORT=${PORT} pnpm start`,
+    url: baseURL,
+    reuseExistingServer: !process.env.CI,
+    stdout: 'pipe',
+  },
+  projects: [
+    {
+      name: 'chromium',
+      use: {
+        ...devices['Desktop Chrome'],
+      },
+    },
+  ],
+})
diff --git a/e2e/react-start/context-bridge/src/routeTree.gen.ts b/e2e/react-start/context-bridge/src/routeTree.gen.ts
new file mode 100644
index 0000000000..3f2221d212
--- /dev/null
+++ b/e2e/react-start/context-bridge/src/routeTree.gen.ts
@@ -0,0 +1,87 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as NextRouteImport } from './routes/next'
+import { Route as IndexRouteImport } from './routes/index'
+
+const NextRoute = NextRouteImport.update({
+  id: '/next',
+  path: '/next',
+  getParentRoute: () => rootRouteImport,
+} as any)
+const IndexRoute = IndexRouteImport.update({
+  id: '/',
+  path: '/',
+  getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+  '/': typeof IndexRoute
+  '/next': typeof NextRoute
+}
+export interface FileRoutesByTo {
+  '/': typeof IndexRoute
+  '/next': typeof NextRoute
+}
+export interface FileRoutesById {
+  __root__: typeof rootRouteImport
+  '/': typeof IndexRoute
+  '/next': typeof NextRoute
+}
+export interface FileRouteTypes {
+  fileRoutesByFullPath: FileRoutesByFullPath
+  fullPaths: '/' | '/next'
+  fileRoutesByTo: FileRoutesByTo
+  to: '/' | '/next'
+  id: '__root__' | '/' | '/next'
+  fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+  IndexRoute: typeof IndexRoute
+  NextRoute: typeof NextRoute
+}
+
+declare module '@tanstack/react-router' {
+  interface FileRoutesByPath {
+    '/next': {
+      id: '/next'
+      path: '/next'
+      fullPath: '/next'
+      preLoaderRoute: typeof NextRouteImport
+      parentRoute: typeof rootRouteImport
+    }
+    '/': {
+      id: '/'
+      path: '/'
+      fullPath: '/'
+      preLoaderRoute: typeof IndexRouteImport
+      parentRoute: typeof rootRouteImport
+    }
+  }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+  IndexRoute: IndexRoute,
+  NextRoute: NextRoute,
+}
+export const routeTree = rootRouteImport
+  ._addFileChildren(rootRouteChildren)
+  ._addFileTypes<FileRouteTypes>()
+
+import type { getRouter } from './router.tsx'
+import type { startInstance } from './start.ts'
+declare module '@tanstack/react-start' {
+  interface Register {
+    ssr: true
+    router: Awaited<ReturnType<typeof getRouter>>
+    config: Awaited<ReturnType<typeof startInstance.getOptions>>
+  }
+}
diff --git a/e2e/react-start/context-bridge/src/router.tsx b/e2e/react-start/context-bridge/src/router.tsx
new file mode 100644
index 0000000000..d8c76aa7aa
--- /dev/null
+++ b/e2e/react-start/context-bridge/src/router.tsx
@@ -0,0 +1,34 @@
+import { createRouter } from '@tanstack/react-router'
+import { createStartContextBridge } from '@tanstack/react-start'
+import { routeTree } from './routeTree.gen'
+
+export type RouterContext = {
+  a: string
+  c: string
+  shared: string
+  static: string
+}
+
+const startContextBridge = createStartContextBridge({
+  select: (ctx) => ({
+    a: ctx.a,
+    c: ctx.c,
+    shared: ctx.shared,
+  }),
+})
+
+export function getRouter() {
+  const router = createRouter({
+    routeTree,
+    scrollRestoration: true,
+    defaultPreload: 'intent',
+    context: {
+      ...startContextBridge.get(),
+      static: 'static-value',
+    },
+  })
+
+  startContextBridge.setup(router)
+
+  return router
+}
diff --git a/e2e/react-start/context-bridge/src/routes/__root.tsx b/e2e/react-start/context-bridge/src/routes/__root.tsx
new file mode 100644
index 0000000000..5a66ac7099
--- /dev/null
+++ b/e2e/react-start/context-bridge/src/routes/__root.tsx
@@ -0,0 +1,62 @@
+/// <reference types="vite/client" />
+import * as React from 'react'
+import {
+  HeadContent,
+  Link,
+  Scripts,
+  createRootRouteWithContext,
+} from '@tanstack/react-router'
+import type { RouterContext } from '~/router'
+import appCss from '~/styles/app.css?url'
+
+export const Route = createRootRouteWithContext<RouterContext>()({
+  head: () => ({
+    meta: [
+      { charSet: 'utf-8' },
+      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
+    ],
+    links: [{ rel: 'stylesheet', href: appCss }],
+  }),
+  shellComponent: RootDocument,
+})
+
+const RouterDevtools =
+  process.env.NODE_ENV === 'production'
+    ? () => null
+    : React.lazy(() =>
+        import('@tanstack/react-router-devtools').then((res) => ({
+          default: res.TanStackRouterDevtools,
+        })),
+      )
+
+function RootDocument({ children }: { children: React.ReactNode }) {
+  return (
+    <html>
+      <head>
+        <HeadContent />
+      </head>
+      <body>
+        <div className="p-2 flex gap-3 text-lg">
+          <Link
+            to="/"
+            activeOptions={{ exact: true }}
+            activeProps={{ className: 'font-bold' }}
+          >
+            Home
+          </Link>
+          <Link
+            to="/next"
+            activeOptions={{ exact: true }}
+            activeProps={{ className: 'font-bold' }}
+          >
+            Next
+          </Link>
+        </div>
+        <hr />
+        {children}
+        <RouterDevtools position="bottom-right" />
+        <Scripts />
+      </body>
+    </html>
+  )
+}
diff --git a/e2e/react-start/context-bridge/src/routes/index.tsx b/e2e/react-start/context-bridge/src/routes/index.tsx
new file mode 100644
index 0000000000..c30c849533
--- /dev/null
+++ b/e2e/react-start/context-bridge/src/routes/index.tsx
@@ -0,0 +1,78 @@
+import { Link, createFileRoute, useRouter } from '@tanstack/react-router'
+import { getGlobalStartContext } from '@tanstack/react-start'
+import { sortJson } from '~/utils/sortJson'
+
+export const Route = createFileRoute('/')({
+  beforeLoad: async ({ context }) => {
+    const globalStartContext = getGlobalStartContext()
+    return {
+      serverContext: context,
+      globalStartContext,
+    }
+  },
+  component: Home,
+})
+
+function Home() {
+  const router = useRouter()
+  const { serverContext, globalStartContext } = Route.useRouteContext()
+  const routerCtx = router.options.context
+
+  return (
+    <div className="p-8 space-y-6">
+      <h1 className="font-bold text-lg">Start context bridge</h1>
+
+      <div>
+        <div className="font-semibold">
+          Bridged context (router.options.context)
+        </div>
+        <pre
+          data-testid="bridged-context"
+          className="bg-gray-100 p-2 rounded text-black"
+        >
+          {JSON.stringify(
+            {
+              context: sortJson(routerCtx),
+            },
+            null,
+            2,
+          )}
+        </pre>
+      </div>
+
+      <div>
+        <div className="font-semibold">Router context (from beforeLoad)</div>
+        <pre
+          data-testid="server-context"
+          className="bg-gray-100 p-2 rounded text-black"
+        >
+          {JSON.stringify(sortJson(serverContext), null, 2)}
+        </pre>
+      </div>
+
+      <div>
+        <div className="font-semibold">Global start context</div>
+        <pre
+          data-testid="global-start-context"
+          className="bg-gray-100 p-2 rounded text-black"
+        >
+          {JSON.stringify(sortJson(globalStartContext), null, 2)}
+        </pre>
+      </div>
+
+      <div className="flex items-center gap-3">
+        <Link data-testid="to-next" to="/next" className="underline">
+          Go to /next
+        </Link>
+        <button
+          type="button"
+          className="px-2 py-1 border rounded"
+          data-testid="invalidate"
+          onClick={() => router.invalidate()}
+        >
+          Invalidate
+        </button>
+      </div>
+    </div>
+  )
+}
diff --git a/e2e/react-start/context-bridge/src/routes/next.tsx b/e2e/react-start/context-bridge/src/routes/next.tsx
new file mode 100644
index 0000000000..197580483c
--- /dev/null
+++ b/e2e/react-start/context-bridge/src/routes/next.tsx
@@ -0,0 +1,34 @@
+import { createFileRoute, useRouter } from '@tanstack/react-router'
+import { sortJson } from '~/utils/sortJson'
+
+export const Route = createFileRoute('/next')({
+  component: Next,
+})
+
+function Next() {
+  const router = useRouter()
+
+  return (
+    <div className="p-8 space-y-6">
+      <h1 className="font-bold text-lg">Next</h1>
+
+      <div>
+        <div className="font-semibold">
+          Bridged context (router.options.context)
+        </div>
+        <pre
+          data-testid="bridged-context-next"
+          className="bg-gray-100 p-2 rounded text-black"
+        >
+          {JSON.stringify(
+            {
+              context: sortJson(router.options.context),
+            },
+            null,
+            2,
+          )}
+        </pre>
+      </div>
+    </div>
+  )
+}
diff --git a/e2e/react-start/context-bridge/src/start.ts b/e2e/react-start/context-bridge/src/start.ts
new file mode 100644
index 0000000000..cc08249a0b
--- /dev/null
+++ b/e2e/react-start/context-bridge/src/start.ts
@@ -0,0 +1,31 @@
+import { createMiddleware, createStart } from '@tanstack/react-start'
+
+export const mwA = createMiddleware().server(async ({ next }) => {
+  return next({
+    context: {
+      a: 'a-from-mwA',
+      shared: 'shared-from-mwA',
+    },
+  })
+})
+
+export const mwB = createMiddleware().server(async ({ next }) => {
+  return next({
+    context: {
+      b: 'b-from-mwB',
+      shared: 'shared-from-mwB',
+    },
+  })
+})
+
+export const mwC = createMiddleware().server(async ({ next }) => {
+  return next({
+    context: {
+      c: 'c-from-mwC',
+    },
+  })
+})
+
+export const startInstance = createStart(() => ({
+  requestMiddleware: [mwA, mwB, mwC],
+}))
diff --git a/e2e/react-start/context-bridge/src/styles/app.css b/e2e/react-start/context-bridge/src/styles/app.css
new file mode 100644
index 0000000000..2fad3f4207
--- /dev/null
+++ b/e2e/react-start/context-bridge/src/styles/app.css
@@ -0,0 +1,6 @@
+html,
+body {
+  padding: 0;
+  margin: 0;
+  font-family: ui-sans-serif, system-ui;
+}
diff --git a/e2e/react-start/context-bridge/src/utils/sortJson.ts b/e2e/react-start/context-bridge/src/utils/sortJson.ts
new file mode 100644
index 0000000000..312d10f68b
--- /dev/null
+++ b/e2e/react-start/context-bridge/src/utils/sortJson.ts
@@ -0,0 +1,18 @@
+export function sortJson<T>(value: T): T {
+  if (Array.isArray(value)) {
+    return value.map(sortJson) as T
+  }
+
+  if (!value || typeof value !== 'object') {
+    return value
+  }
+
+  const obj = value as Record<string, unknown>
+  const out: Record<string, unknown> = {}
+
+  for (const key of Object.keys(obj).sort()) {
+    out[key] = sortJson(obj[key])
+  }
+
+  return out as T
+}
diff --git a/e2e/react-start/context-bridge/tests/context-bridge.spec.ts b/e2e/react-start/context-bridge/tests/context-bridge.spec.ts
new file mode 100644
index 0000000000..ed6b446439
--- /dev/null
+++ b/e2e/react-start/context-bridge/tests/context-bridge.spec.ts
@@ -0,0 +1,56 @@
+import { expect, test } from '@playwright/test'
+
+test('bridges selected request context into router context', async ({
+  page,
+}) => {
+  await page.goto('/')
+
+  const routerContextText = await page
+    .getByTestId('server-context')
+    .textContent()
+  const routerContext = JSON.parse(routerContextText || '{}')
+
+  // Router beforeLoad context is router context (selected + isomorphic), not full request ctx
+  expect(routerContext).toMatchObject({
+    a: 'a-from-mwA',
+    c: 'c-from-mwC',
+    shared: 'shared-from-mwB',
+    static: 'static-value',
+  })
+
+  const globalStartContextText = await page
+    .getByTestId('global-start-context')
+    .textContent()
+  const globalStartContext = JSON.parse(globalStartContextText || '{}')
+  expect(globalStartContext).toMatchObject({
+    a: 'a-from-mwA',
+    b: 'b-from-mwB',
+    c: 'c-from-mwC',
+    shared: 'shared-from-mwB',
+  })
+
+  const bridgedText = await page.getByTestId('bridged-context').textContent()
+  const bridged = JSON.parse(bridgedText || '{}')
+  expect(bridged.context).toMatchObject({
+    a: 'a-from-mwA',
+    c: 'c-from-mwC',
+    shared: 'shared-from-mwB',
+    static: 'static-value',
+  })
+  expect(bridged.context.b).toBeUndefined()
+
+  await page.getByTestId('to-next').click()
+  await page.waitForURL('/next')
+
+  const bridgedNextText = await page
+    .getByTestId('bridged-context-next')
+    .textContent()
+  const bridgedNext = JSON.parse(bridgedNextText || '{}')
+  expect(bridgedNext.context).toMatchObject({
+    a: 'a-from-mwA',
+    c: 'c-from-mwC',
+    shared: 'shared-from-mwB',
+    static: 'static-value',
+  })
+  expect(bridgedNext.context.b).toBeUndefined()
+})
diff --git a/e2e/react-start/context-bridge/tsconfig.json b/e2e/react-start/context-bridge/tsconfig.json
new file mode 100644
index 0000000000..beb8fdd8db
--- /dev/null
+++ b/e2e/react-start/context-bridge/tsconfig.json
@@ -0,0 +1,22 @@
+{
+  "include": ["**/*.ts", "**/*.tsx", "**/*.d.ts"],
+  "compilerOptions": {
+    "strict": true,
+    "esModuleInterop": true,
+    "jsx": "react-jsx",
+    "module": "ESNext",
+    "moduleResolution": "Bundler",
+    "lib": ["DOM", "DOM.Iterable", "ES2022"],
+    "isolatedModules": true,
+    "resolveJsonModule": true,
+    "skipLibCheck": true,
+    "target": "ES2022",
+    "allowJs": true,
+    "forceConsistentCasingInFileNames": true,
+    "baseUrl": ".",
+    "paths": {
+      "~/*": ["./src/*"]
+    },
+    "noEmit": true
+  }
+}
diff --git a/e2e/react-start/context-bridge/vite.config.ts b/e2e/react-start/context-bridge/vite.config.ts
new file mode 100644
index 0000000000..23cb856b33
--- /dev/null
+++ b/e2e/react-start/context-bridge/vite.config.ts
@@ -0,0 +1,19 @@
+import tsConfigPaths from 'vite-tsconfig-paths'
+import { tanstackStart } from '@tanstack/react-start/plugin/vite'
+import viteReact from '@vitejs/plugin-react'
+import tailwindcss from '@tailwindcss/vite'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+  server: {
+    port: 3000,
+  },
+  plugins: [
+    tailwindcss(),
+    tsConfigPaths({
+      projects: ['./tsconfig.json'],
+    }),
+    tanstackStart(),
+    viteReact(),
+  ],
+})
diff --git a/packages/router-core/package.json b/packages/router-core/package.json
index 57c87c74e5..47618f36fd 100644
--- a/packages/router-core/package.json
+++ b/packages/router-core/package.json
@@ -140,11 +140,11 @@
         }
       },
       "import": {
-        "types": "./dist/esm/isServer/client.d.ts",
+        "types": "./dist/esm/isServer/development.d.ts",
         "default": "./dist/esm/isServer/client.js"
       },
       "require": {
-        "types": "./dist/cjs/isServer/client.d.cts",
+        "types": "./dist/cjs/isServer/development.d.cts",
         "default": "./dist/cjs/isServer/client.cjs"
       }
     },
diff --git a/packages/start-client-core/src/createStartContextBridge.ts b/packages/start-client-core/src/createStartContextBridge.ts
new file mode 100644
index 0000000000..13a01f89bc
--- /dev/null
+++ b/packages/start-client-core/src/createStartContextBridge.ts
@@ -0,0 +1,90 @@
+import { createIsomorphicFn } from '@tanstack/start-fn-stubs'
+import { isServer } from '@tanstack/router-core/isServer'
+import { getGlobalStartContext } from './getGlobalStartContext'
+import { safeObjectMerge } from './safeObjectMerge'
+import type { AssignAllServerRequestContext } from './createMiddleware.js'
+import type {
+  AnyRouter,
+  Expand,
+  Register,
+  ValidateSerializableInput,
+} from '@tanstack/router-core'
+
+export type StartContextBridgeOptions<
+  TSelected extends Record<string, unknown>,
+> = {
+  key?: string
+  select: (
+    ctx: Expand<AssignAllServerRequestContext<Register, []>>,
+  ) => ValidateSerializableInput<Register, TSelected>
+}
+
+export type StartContextBridge<TSelected extends Record<string, unknown>> = {
+  key: string
+  /** Install dehydrate/hydrate integration for a router instance */
+  setup: (router: AnyRouter) => void
+  /** Get selected context */
+  get: () => TSelected
+}
+
+export function createStartContextBridge<
+  TSelected extends Record<string, unknown>,
+>(opts: StartContextBridgeOptions<TSelected>): StartContextBridge<TSelected> {
+  const key = opts.key ?? '__tsrStartContextBridge'
+
+  const get = createIsomorphicFn()
+    .client((): TSelected => {
+      return {} as unknown as TSelected
+    })
+    .server((): TSelected => {
+      return opts.select(getGlobalStartContext()) as unknown as TSelected
+    })
+
+  const setup = (router: AnyRouter) => {
+    if (isServer ?? router.isServer) {
+      const ogDehydrate = router.options.dehydrate
+      router.options.dehydrate = () => {
+        const og = ogDehydrate?.()
+        const startCtx = getGlobalStartContext()
+        const selected = opts.select(startCtx) as unknown as Record<
+          string,
+          unknown
+        >
+
+        if (og === undefined) {
+          return { [key]: selected } as any
+        }
+
+        if (!og || typeof og !== 'object') {
+          throw new Error(
+            `createStartContextBridge: router.options.dehydrate must return an object (or undefined). Got '${typeof og}'.`,
+          )
+        }
+
+        return {
+          ...og,
+          [key]: selected,
+        }
+      }
+    } else {
+      const ogHydrate = router.options.hydrate
+      router.options.hydrate = async (dehydrated: any) => {
+        await ogHydrate?.(dehydrated)
+        const selected = dehydrated?.[key] as
+          | Record<string, unknown>
+          | undefined
+        if (!selected) return
+
+        router.update({
+          context: safeObjectMerge(router.options.context, selected),
+        })
+      }
+    }
+  }
+
+  return {
+    key,
+    setup,
+    get,
+  }
+}
diff --git a/packages/start-client-core/src/index.tsx b/packages/start-client-core/src/index.tsx
index 218c984d60..b81080ac85 100644
--- a/packages/start-client-core/src/index.tsx
+++ b/packages/start-client-core/src/index.tsx
@@ -117,3 +117,4 @@ export { getRouterInstance } from './getRouterInstance'
 export { getDefaultSerovalPlugins } from './getDefaultSerovalPlugins'
 export { getGlobalStartContext } from './getGlobalStartContext'
 export { safeObjectMerge, createNullProtoObject } from './safeObjectMerge'
+export { createStartContextBridge } from './createStartContextBridge'
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a4efb9ef37..0ee7b3b7d1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1516,6 +1516,67 @@ importers:
         specifier: ^5.1.4
         version: 5.1.4(typescript@5.9.2)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
 
+  e2e/react-start/context-bridge:
+    dependencies:
+      '@tanstack/react-router':
+        specifier: workspace:*
+        version: link:../../../packages/react-router
+      '@tanstack/react-router-devtools':
+        specifier: workspace:^
+        version: link:../../../packages/react-router-devtools
+      '@tanstack/react-start':
+        specifier: workspace:*
+        version: link:../../../packages/react-start
+      express:
+        specifier: ^5.1.0
+        version: 5.1.0
+      http-proxy-middleware:
+        specifier: ^3.0.5
+        version: 3.0.5
+      react:
+        specifier: ^19.2.3
+        version: 19.2.3
+      react-dom:
+        specifier: ^19.2.3
+        version: 19.2.3(react@19.2.3)
+    devDependencies:
+      '@playwright/test':
+        specifier: ^1.57.0
+        version: 1.57.0
+      '@tailwindcss/vite':
+        specifier: ^4.1.18
+        version: 4.1.18(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+      '@tanstack/router-e2e-utils':
+        specifier: workspace:^
+        version: link:../../e2e-utils
+      '@types/node':
+        specifier: 25.0.9
+        version: 25.0.9
+      '@types/react':
+        specifier: ^19.2.8
+        version: 19.2.8
+      '@types/react-dom':
+        specifier: ^19.2.3
+        version: 19.2.3(@types/react@19.2.8)
+      '@vitejs/plugin-react':
+        specifier: ^4.3.4
+        version: 4.7.0(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+      srvx:
+        specifier: ^0.10.1
+        version: 0.10.1
+      tailwindcss:
+        specifier: ^4.1.18
+        version: 4.1.18
+      typescript:
+        specifier: ^5.7.2
+        version: 5.9.3
+      vite:
+        specifier: ^7.3.1
+        version: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+      vite-tsconfig-paths:
+        specifier: ^5.1.4
+        version: 5.1.4(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+
   e2e/react-start/csp:
     dependencies:
       '@tanstack/react-router':

From 43b1e547eaa13553b854420f7a7cce23fe12526d Mon Sep 17 00:00:00 2001
From: Manuel Schiller <manuel.schiller@caligano.de>
Date: Fri, 6 Feb 2026 19:35:37 +0100
Subject: [PATCH 2/3] feat: add plugins

---
 docs/router/integrations/query.md             |  52 +++--
 docs/start/config.json                        |   8 +
 .../framework/react/guide/context-bridge.md   | 179 +++++++++++++++
 .../framework/solid/guide/context-bridge.md   |  11 +
 .../basic-react-query/src/routeTree.gen.ts    |   4 +-
 .../basic-react-query/src/router.tsx          |  11 +-
 e2e/react-start/context-bridge/src/router.tsx |  24 +-
 e2e/react-start/multi-plugin/.gitignore       |  17 ++
 e2e/react-start/multi-plugin/package.json     |  38 ++++
 .../multi-plugin/playwright.config.ts         |  29 +++
 .../multi-plugin/src/routeTree.gen.ts         |  87 +++++++
 e2e/react-start/multi-plugin/src/router.tsx   |  36 +++
 .../multi-plugin/src/routes/__root.tsx        |  65 ++++++
 .../multi-plugin/src/routes/index.tsx         |  84 +++++++
 .../multi-plugin/src/routes/query-test.tsx    |  70 ++++++
 e2e/react-start/multi-plugin/src/start.ts     |  31 +++
 .../multi-plugin/src/styles/app.css           |   6 +
 .../multi-plugin/src/utils/sortJson.ts        |  18 ++
 .../multi-plugin/tests/multi-plugin.spec.ts   | 112 +++++++++
 e2e/react-start/multi-plugin/tsconfig.json    |  22 ++
 e2e/react-start/multi-plugin/vite.config.ts   |  19 ++
 .../query-integration/src/router.tsx          |  11 +-
 .../server-functions/src/router.tsx           |   8 +-
 e2e/react-start/server-routes/src/router.tsx  |   8 +-
 e2e/react-start/streaming-ssr/src/router.tsx  |  11 +-
 .../basic-solid-query/src/routeTree.gen.ts    |   4 +-
 .../basic-solid-query/src/router.tsx          |  11 +-
 .../query-integration/src/router.tsx          |  11 +-
 .../server-functions/src/router.tsx           |   9 +-
 e2e/solid-start/server-routes/src/router.tsx  |   8 +-
 .../basic-vue-query/src/routeTree.gen.ts      |   4 +-
 e2e/vue-start/basic-vue-query/src/router.tsx  |  11 +-
 .../query-integration/src/router.tsx          |  11 +-
 e2e/vue-start/server-functions/src/router.tsx |  14 +-
 e2e/vue-start/server-routes/src/router.tsx    |   8 +-
 packages/react-router-ssr-query/src/index.tsx |  86 +++++--
 packages/react-router/src/index.tsx           |   3 +
 packages/react-router/src/router.ts           |  10 +-
 packages/react-router/tests/router.test-d.tsx | 161 ++++++++++++-
 packages/router-core/src/index.ts             |   7 +
 packages/router-core/src/plugin.ts            |  51 +++++
 packages/router-core/src/router.ts            | 106 +++++++--
 packages/router-core/tests/plugin.test.ts     | 212 ++++++++++++++++++
 packages/router-ssr-query-core/src/index.ts   |  65 +++++-
 packages/solid-router-ssr-query/src/index.tsx |  89 ++++++--
 packages/solid-router/src/index.tsx           |   3 +
 packages/solid-router/src/router.ts           |  10 +-
 packages/solid-router/tests/router.test-d.tsx | 159 ++++++++++++-
 .../src/createStartContextBridge.ts           | 129 ++++++-----
 packages/vue-router-ssr-query/src/index.tsx   |  97 ++++++--
 packages/vue-router/src/index.tsx             |   3 +
 packages/vue-router/src/router.ts             |  10 +-
 packages/vue-router/tests/router.test-d.tsx   | 159 ++++++++++++-
 pnpm-lock.yaml                                |  94 ++++++--
 54 files changed, 2224 insertions(+), 282 deletions(-)
 create mode 100644 docs/start/framework/react/guide/context-bridge.md
 create mode 100644 docs/start/framework/solid/guide/context-bridge.md
 create mode 100644 e2e/react-start/multi-plugin/.gitignore
 create mode 100644 e2e/react-start/multi-plugin/package.json
 create mode 100644 e2e/react-start/multi-plugin/playwright.config.ts
 create mode 100644 e2e/react-start/multi-plugin/src/routeTree.gen.ts
 create mode 100644 e2e/react-start/multi-plugin/src/router.tsx
 create mode 100644 e2e/react-start/multi-plugin/src/routes/__root.tsx
 create mode 100644 e2e/react-start/multi-plugin/src/routes/index.tsx
 create mode 100644 e2e/react-start/multi-plugin/src/routes/query-test.tsx
 create mode 100644 e2e/react-start/multi-plugin/src/start.ts
 create mode 100644 e2e/react-start/multi-plugin/src/styles/app.css
 create mode 100644 e2e/react-start/multi-plugin/src/utils/sortJson.ts
 create mode 100644 e2e/react-start/multi-plugin/tests/multi-plugin.spec.ts
 create mode 100644 e2e/react-start/multi-plugin/tsconfig.json
 create mode 100644 e2e/react-start/multi-plugin/vite.config.ts
 create mode 100644 packages/router-core/src/plugin.ts
 create mode 100644 packages/router-core/tests/plugin.test.ts

diff --git a/docs/router/integrations/query.md b/docs/router/integrations/query.md
index ab6687a870..631a7a7e26 100644
--- a/docs/router/integrations/query.md
+++ b/docs/router/integrations/query.md
@@ -29,38 +29,62 @@ bun add @tanstack/react-router-ssr-query
 
 ## Setup
 
-Create your router and wire up the integration. Ensure a fresh `QueryClient` is created per request in SSR environments.
+Create your router and wire up the integration using the `createSsrQueryPlugin` function. The plugin automatically adds `queryClient` to your router context, so you don't need to pass it manually. Ensure a fresh `QueryClient` is created per request in SSR environments.
 
 ```tsx
 // src/router.tsx
 import { QueryClient } from '@tanstack/react-query'
 import { createRouter } from '@tanstack/react-router'
-import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
+import { createSsrQueryPlugin } from '@tanstack/react-router-ssr-query'
 import { routeTree } from './routeTree.gen'
 
 export function getRouter() {
   const queryClient = new QueryClient()
-  const router = createRouter({
+
+  return createRouter({
     routeTree,
-    // optionally expose the QueryClient via router context
-    context: { queryClient },
     scrollRestoration: true,
     defaultPreload: 'intent',
+    plugins: [
+      createSsrQueryPlugin({
+        queryClient,
+        // optional:
+        // handleRedirects: true,
+        // wrapQueryClient: true,
+      }),
+    ],
   })
+}
+```
+
+The plugin provides the `queryClient` to your router context automatically via the `plugins` system. This means `context.queryClient` is available in route loaders and components without needing to pass `context: { queryClient }` when creating the router.
+
+By default, the integration wraps your router with a `QueryClientProvider`. If you already provide your own provider, pass `wrapQueryClient: false` and keep your custom wrapper.
+
+
+### Migrating from `setupRouterSsrQueryIntegration`
 
-  setupRouterSsrQueryIntegration({
-    router,
-    queryClient,
-    // optional:
-    // handleRedirects: true,
-    // wrapQueryClient: true,
+> [!NOTE]
+> `setupRouterSsrQueryIntegration` is deprecated. Use `createSsrQueryPlugin` instead.
+
+If you are migrating from the imperative setup function:
+
+```diff
+- import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
++ import { createSsrQueryPlugin } from '@tanstack/react-router-ssr-query'
+
+  const queryClient = new QueryClient()
+
+  const router = createRouter({
+    routeTree,
+-   context: { queryClient },
++   plugins: [createSsrQueryPlugin({ queryClient })],
   })
 
-  return router
-}
+- setupRouterSsrQueryIntegration({ router, queryClient })
 ```
 
-By default, the integration wraps your router with a `QueryClientProvider`. If you already provide your own provider, pass `wrapQueryClient: false` and keep your custom wrapper.
+The plugin handles both adding `queryClient` to the router context and wiring up SSR dehydration/hydration, so you can remove the manual `context: { queryClient }` and the `setupRouterSsrQueryIntegration` call.
 
 ## SSR behavior and streaming
 
diff --git a/docs/start/config.json b/docs/start/config.json
index 396b1be4af..e933ffb799 100644
--- a/docs/start/config.json
+++ b/docs/start/config.json
@@ -105,6 +105,10 @@
               "label": "Middleware",
               "to": "framework/react/guide/middleware"
             },
+            {
+              "label": "Context Bridge",
+              "to": "framework/react/guide/context-bridge"
+            },
             {
               "label": "Error Boundaries",
               "to": "framework/react/guide/error-boundaries"
@@ -218,6 +222,10 @@
               "label": "Middleware",
               "to": "framework/solid/guide/middleware"
             },
+            {
+              "label": "Context Bridge",
+              "to": "framework/solid/guide/context-bridge"
+            },
             {
               "label": "Error Boundaries",
               "to": "framework/solid/guide/error-boundaries"
diff --git a/docs/start/framework/react/guide/context-bridge.md b/docs/start/framework/react/guide/context-bridge.md
new file mode 100644
index 0000000000..5b11cdccee
--- /dev/null
+++ b/docs/start/framework/react/guide/context-bridge.md
@@ -0,0 +1,179 @@
+---
+id: context-bridge
+title: Context Bridge
+---
+
+## What is a Context Bridge?
+
+A context bridge transports server-side context values — produced by [middleware](./middleware.md) — to the client via the router's dehydrate/hydrate mechanism. This allows your route loaders and components to access server middleware context on both the server and the client without any manual serialization.
+
+### Why do I need it?
+
+When global request middleware provides context (e.g. user info, feature flags, tenant data), that context is only available on the server during SSR. The context bridge selects specific values from your middleware context, serializes them during dehydration, and restores them on the client during hydration — making them available in the router context everywhere.
+
+## Setup
+
+### 1. Define middleware that provides context
+
+In your `src/start.ts`, define global request middleware that provides context values:
+
+```tsx
+// src/start.ts
+import { createMiddleware, createStart } from '@tanstack/react-start'
+
+const authMiddleware = createMiddleware().server(async ({ next }) => {
+  return next({
+    context: {
+      userId: 'user-123',
+      role: 'admin',
+    },
+  })
+})
+
+export const startInstance = createStart(() => ({
+  requestMiddleware: [authMiddleware],
+}))
+```
+
+### 2. Create the context bridge
+
+Use `createStartContextBridge` to select which middleware context values to bridge to the client. The `select` function receives the accumulated context from all global request middleware.
+
+```tsx
+// src/router.tsx
+import { createRouter } from '@tanstack/react-router'
+import { createStartContextBridge } from '@tanstack/react-start'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+  return createRouter({
+    routeTree,
+    plugins: [
+      createStartContextBridge({
+        select: (ctx) => ({
+          userId: ctx.userId,
+          role: ctx.role,
+        }),
+      }),
+    ],
+  })
+}
+```
+
+That's it. The bridged values (`userId`, `role`) are now available in `context` throughout your route tree — on both server and client.
+
+### 3. Use the bridged context in routes
+
+```tsx
+// src/routes/index.tsx
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/')({
+  loader: ({ context }) => {
+    // context.userId and context.role are available here
+    console.log('User:', context.userId, 'Role:', context.role)
+  },
+  component: IndexPage,
+})
+
+function IndexPage() {
+  const { userId, role } = Route.useRouteContext()
+  return (
+    <div>
+      <p>User: {userId}</p>
+      <p>Role: {role}</p>
+    </div>
+  )
+}
+```
+
+## How it works
+
+The context bridge is a router plugin that hooks into the router's dehydrate/hydrate lifecycle:
+
+1. **On the server**: The plugin reads the global start context (populated by your middleware), runs the `select` function, and merges the selected values into `router.options.context`. It also hooks into `router.options.dehydrate` to serialize those values into the dehydrated payload sent to the client.
+
+2. **On the client**: The plugin hooks into `router.options.hydrate` to read the serialized values from the dehydrated payload and merge them back into `router.options.context`.
+
+This means the bridged values are available synchronously in route loaders and components on both server and client.
+
+## The `select` function
+
+The `select` function controls which middleware context values are bridged to the client. It receives the full accumulated context from all global request middleware.
+
+```tsx
+createStartContextBridge({
+  select: (ctx) => ({
+    // Only bridge what you need on the client
+    userId: ctx.userId,
+    role: ctx.role,
+    // Don't bridge secrets or large objects
+  }),
+})
+```
+
+> [!IMPORTANT]
+> The return value of `select` must be serializable. Do not include functions, class instances, or other non-serializable values. The type system will warn you if you try to return non-serializable data.
+
+> [!WARNING]
+> Be careful not to bridge sensitive values (API keys, secrets, tokens) since they will be serialized and sent to the client in the HTML payload.
+
+## Combining with other plugins
+
+The context bridge works alongside other router plugins. Each plugin contributes its own keys to the router context, and the type system tracks which keys are provided by plugins so you only need to manually supply the remaining ones.
+
+```tsx
+import { QueryClient } from '@tanstack/react-query'
+import { createRouter } from '@tanstack/react-router'
+import { createSsrQueryPlugin } from '@tanstack/react-router-ssr-query'
+import { createStartContextBridge } from '@tanstack/react-start'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+  const queryClient = new QueryClient()
+
+  return createRouter({
+    routeTree,
+    // Only need to provide keys NOT covered by plugins
+    context: {
+      appName: 'My App',
+    },
+    plugins: [
+      createStartContextBridge({
+        select: (ctx) => ({
+          userId: ctx.userId,
+          role: ctx.role,
+        }),
+      }),
+      createSsrQueryPlugin({ queryClient }),
+    ],
+  })
+}
+```
+
+In this example:
+
+- `startContextBridge` provides `userId` and `role`
+- `ssrQueryPlugin` provides `queryClient`
+- You only need to manually supply `appName` in `context`
+
+The type system enforces this — if a plugin provides a key, you don't need to include it in `context`, and if you're missing a required key that no plugin provides, you'll get a type error.
+
+## Custom bridge key
+
+If you need multiple context bridges (uncommon), you can provide a custom `key` to avoid collisions in the dehydrated payload:
+
+```tsx
+createStartContextBridge({
+  key: '__myCustomBridgeKey',
+  select: (ctx) => ({ userId: ctx.userId }),
+})
+```
+
+## Type safety
+
+The context bridge integrates with the router plugin type system:
+
+- The `select` function is typed based on your registered middleware 
+- The `context` option in `createRouter` becomes optional for keys that plugins provide
+- Route loaders and components see the bridged values in their `context` parameter with full type inference
diff --git a/docs/start/framework/solid/guide/context-bridge.md b/docs/start/framework/solid/guide/context-bridge.md
new file mode 100644
index 0000000000..c3876cd9a9
--- /dev/null
+++ b/docs/start/framework/solid/guide/context-bridge.md
@@ -0,0 +1,11 @@
+---
+ref: docs/start/framework/react/guide/context-bridge.md
+replace:
+  {
+    '@tanstack/react-start': '@tanstack/solid-start',
+    '@tanstack/react-router': '@tanstack/solid-router',
+    '@tanstack/react-query': '@tanstack/solid-query',
+    '@tanstack/react-router-ssr-query': '@tanstack/solid-router-ssr-query',
+    'React': 'Solid',
+  }
+---
diff --git a/e2e/react-start/basic-react-query/src/routeTree.gen.ts b/e2e/react-start/basic-react-query/src/routeTree.gen.ts
index c140825485..a1bed86dfa 100644
--- a/e2e/react-start/basic-react-query/src/routeTree.gen.ts
+++ b/e2e/react-start/basic-react-query/src/routeTree.gen.ts
@@ -282,7 +282,7 @@ declare module '@tanstack/react-router' {
     '/_layout': {
       id: '/_layout'
       path: ''
-      fullPath: ''
+      fullPath: '/'
       preLoaderRoute: typeof LayoutRouteImport
       parentRoute: typeof rootRouteImport
     }
@@ -331,7 +331,7 @@ declare module '@tanstack/react-router' {
     '/_layout/_layout-2': {
       id: '/_layout/_layout-2'
       path: ''
-      fullPath: ''
+      fullPath: '/'
       preLoaderRoute: typeof LayoutLayout2RouteImport
       parentRoute: typeof LayoutRoute
     }
diff --git a/e2e/react-start/basic-react-query/src/router.tsx b/e2e/react-start/basic-react-query/src/router.tsx
index 18b9e194ec..f3c4fa181d 100644
--- a/e2e/react-start/basic-react-query/src/router.tsx
+++ b/e2e/react-start/basic-react-query/src/router.tsx
@@ -1,23 +1,18 @@
 import { QueryClient } from '@tanstack/react-query'
 import { createRouter } from '@tanstack/react-router'
-import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
+import { createSsrQueryPlugin } from '@tanstack/react-router-ssr-query'
 import { routeTree } from './routeTree.gen'
 import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
 import { NotFound } from './components/NotFound'
 
 export function getRouter() {
   const queryClient = new QueryClient()
-  const router = createRouter({
+  return createRouter({
     routeTree,
-    context: { queryClient },
     scrollRestoration: true,
     defaultPreload: 'intent',
     defaultErrorComponent: DefaultCatchBoundary,
     defaultNotFoundComponent: () => <NotFound />,
+    plugins: [createSsrQueryPlugin({ queryClient })],
   })
-  setupRouterSsrQueryIntegration({
-    router,
-    queryClient,
-  })
-  return router
 }
diff --git a/e2e/react-start/context-bridge/src/router.tsx b/e2e/react-start/context-bridge/src/router.tsx
index d8c76aa7aa..71bd77c2f9 100644
--- a/e2e/react-start/context-bridge/src/router.tsx
+++ b/e2e/react-start/context-bridge/src/router.tsx
@@ -9,26 +9,22 @@ export type RouterContext = {
   static: string
 }
 
-const startContextBridge = createStartContextBridge({
-  select: (ctx) => ({
-    a: ctx.a,
-    c: ctx.c,
-    shared: ctx.shared,
-  }),
-})
-
 export function getRouter() {
-  const router = createRouter({
+  return createRouter({
     routeTree,
     scrollRestoration: true,
     defaultPreload: 'intent',
     context: {
-      ...startContextBridge.get(),
       static: 'static-value',
     },
+    plugins: [
+      createStartContextBridge({
+        select: (ctx) => ({
+          a: ctx.a,
+          c: ctx.c,
+          shared: ctx.shared,
+        }),
+      }),
+    ],
   })
-
-  startContextBridge.setup(router)
-
-  return router
 }
diff --git a/e2e/react-start/multi-plugin/.gitignore b/e2e/react-start/multi-plugin/.gitignore
new file mode 100644
index 0000000000..8ecacbfa62
--- /dev/null
+++ b/e2e/react-start/multi-plugin/.gitignore
@@ -0,0 +1,17 @@
+node_modules
+package-lock.json
+yarn.lock
+
+.DS_Store
+.cache
+.env
+.vercel
+.output
+
+/dist/
+/build/
+
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/e2e/react-start/multi-plugin/package.json b/e2e/react-start/multi-plugin/package.json
new file mode 100644
index 0000000000..4865e5691b
--- /dev/null
+++ b/e2e/react-start/multi-plugin/package.json
@@ -0,0 +1,38 @@
+{
+  "name": "tanstack-react-start-e2e-multi-plugin",
+  "private": true,
+  "sideEffects": false,
+  "type": "module",
+  "scripts": {
+    "dev": "vite dev --port 3000",
+    "dev:e2e": "vite dev",
+    "build": "vite build && tsc --noEmit",
+    "preview": "vite preview",
+    "start": "pnpx srvx --prod -s ../client dist/server/server.js",
+    "test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
+  },
+  "dependencies": {
+    "@tanstack/react-query": "^5.90.0",
+    "@tanstack/react-query-devtools": "^5.90.0",
+    "@tanstack/react-router": "workspace:^",
+    "@tanstack/react-router-devtools": "workspace:^",
+    "@tanstack/react-router-ssr-query": "workspace:^",
+    "@tanstack/react-start": "workspace:^",
+    "react": "^19.0.0",
+    "react-dom": "^19.0.0"
+  },
+  "devDependencies": {
+    "@playwright/test": "^1.50.1",
+    "@tailwindcss/vite": "^4.1.18",
+    "@tanstack/router-e2e-utils": "workspace:^",
+    "@types/node": "^22.10.2",
+    "@types/react": "^19.0.8",
+    "@types/react-dom": "^19.0.3",
+    "@vitejs/plugin-react": "^4.3.4",
+    "srvx": "^0.10.1",
+    "tailwindcss": "^4.1.18",
+    "typescript": "^5.7.2",
+    "vite": "^7.3.1",
+    "vite-tsconfig-paths": "^5.1.4"
+  }
+}
diff --git a/e2e/react-start/multi-plugin/playwright.config.ts b/e2e/react-start/multi-plugin/playwright.config.ts
new file mode 100644
index 0000000000..22817e305e
--- /dev/null
+++ b/e2e/react-start/multi-plugin/playwright.config.ts
@@ -0,0 +1,29 @@
+import { defineConfig, devices } from '@playwright/test'
+import { getTestServerPort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+export const PORT = await getTestServerPort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+
+export default defineConfig({
+  testDir: './tests',
+  workers: 1,
+  reporter: [['line']],
+  use: {
+    baseURL,
+  },
+  webServer: {
+    command: `VITE_SERVER_PORT=${PORT} pnpm build && PORT=${PORT} VITE_SERVER_PORT=${PORT} pnpm start`,
+    url: baseURL,
+    reuseExistingServer: !process.env.CI,
+    stdout: 'pipe',
+  },
+  projects: [
+    {
+      name: 'chromium',
+      use: {
+        ...devices['Desktop Chrome'],
+      },
+    },
+  ],
+})
diff --git a/e2e/react-start/multi-plugin/src/routeTree.gen.ts b/e2e/react-start/multi-plugin/src/routeTree.gen.ts
new file mode 100644
index 0000000000..2022f1e351
--- /dev/null
+++ b/e2e/react-start/multi-plugin/src/routeTree.gen.ts
@@ -0,0 +1,87 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as QueryTestRouteImport } from './routes/query-test'
+import { Route as IndexRouteImport } from './routes/index'
+
+const QueryTestRoute = QueryTestRouteImport.update({
+  id: '/query-test',
+  path: '/query-test',
+  getParentRoute: () => rootRouteImport,
+} as any)
+const IndexRoute = IndexRouteImport.update({
+  id: '/',
+  path: '/',
+  getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+  '/': typeof IndexRoute
+  '/query-test': typeof QueryTestRoute
+}
+export interface FileRoutesByTo {
+  '/': typeof IndexRoute
+  '/query-test': typeof QueryTestRoute
+}
+export interface FileRoutesById {
+  __root__: typeof rootRouteImport
+  '/': typeof IndexRoute
+  '/query-test': typeof QueryTestRoute
+}
+export interface FileRouteTypes {
+  fileRoutesByFullPath: FileRoutesByFullPath
+  fullPaths: '/' | '/query-test'
+  fileRoutesByTo: FileRoutesByTo
+  to: '/' | '/query-test'
+  id: '__root__' | '/' | '/query-test'
+  fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+  IndexRoute: typeof IndexRoute
+  QueryTestRoute: typeof QueryTestRoute
+}
+
+declare module '@tanstack/react-router' {
+  interface FileRoutesByPath {
+    '/query-test': {
+      id: '/query-test'
+      path: '/query-test'
+      fullPath: '/query-test'
+      preLoaderRoute: typeof QueryTestRouteImport
+      parentRoute: typeof rootRouteImport
+    }
+    '/': {
+      id: '/'
+      path: '/'
+      fullPath: '/'
+      preLoaderRoute: typeof IndexRouteImport
+      parentRoute: typeof rootRouteImport
+    }
+  }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+  IndexRoute: IndexRoute,
+  QueryTestRoute: QueryTestRoute,
+}
+export const routeTree = rootRouteImport
+  ._addFileChildren(rootRouteChildren)
+  ._addFileTypes<FileRouteTypes>()
+
+import type { getRouter } from './router.tsx'
+import type { startInstance } from './start.ts'
+declare module '@tanstack/react-start' {
+  interface Register {
+    ssr: true
+    router: Awaited<ReturnType<typeof getRouter>>
+    config: Awaited<ReturnType<typeof startInstance.getOptions>>
+  }
+}
diff --git a/e2e/react-start/multi-plugin/src/router.tsx b/e2e/react-start/multi-plugin/src/router.tsx
new file mode 100644
index 0000000000..0d0044d2b9
--- /dev/null
+++ b/e2e/react-start/multi-plugin/src/router.tsx
@@ -0,0 +1,36 @@
+import { QueryClient } from '@tanstack/react-query'
+import { createRouter } from '@tanstack/react-router'
+import { createSsrQueryPlugin } from '@tanstack/react-router-ssr-query'
+import { createStartContextBridge } from '@tanstack/react-start'
+import { routeTree } from './routeTree.gen'
+
+export type RouterContext = {
+  a: string
+  c: string
+  shared: string
+  static: string
+  queryClient: QueryClient
+}
+
+export function getRouter() {
+  const queryClient = new QueryClient()
+
+  return createRouter({
+    routeTree,
+    scrollRestoration: true,
+    defaultPreload: 'intent',
+    context: {
+      static: 'static-value',
+    },
+    plugins: [
+      createSsrQueryPlugin({ queryClient }),
+      createStartContextBridge({
+        select: (ctx) => ({
+          a: ctx.a,
+          c: ctx.c,
+          shared: ctx.shared,
+        }),
+      }),
+    ],
+  })
+}
diff --git a/e2e/react-start/multi-plugin/src/routes/__root.tsx b/e2e/react-start/multi-plugin/src/routes/__root.tsx
new file mode 100644
index 0000000000..b88235edd0
--- /dev/null
+++ b/e2e/react-start/multi-plugin/src/routes/__root.tsx
@@ -0,0 +1,65 @@
+/// <reference types="vite/client" />
+import * as React from 'react'
+import {
+  HeadContent,
+  Link,
+  Outlet,
+  Scripts,
+  createRootRouteWithContext,
+} from '@tanstack/react-router'
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
+import type { RouterContext } from '~/router'
+import appCss from '~/styles/app.css?url'
+
+export const Route = createRootRouteWithContext<RouterContext>()({
+  head: () => ({
+    meta: [
+      { charSet: 'utf-8' },
+      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
+    ],
+    links: [{ rel: 'stylesheet', href: appCss }],
+  }),
+  shellComponent: RootDocument,
+})
+
+const RouterDevtools =
+  process.env.NODE_ENV === 'production'
+    ? () => null
+    : React.lazy(() =>
+        import('@tanstack/react-router-devtools').then((res) => ({
+          default: res.TanStackRouterDevtools,
+        })),
+      )
+
+function RootDocument({ children }: { children: React.ReactNode }) {
+  return (
+    <html>
+      <head>
+        <HeadContent />
+      </head>
+      <body>
+        <div className="p-2 flex gap-3 text-lg">
+          <Link
+            to="/"
+            activeOptions={{ exact: true }}
+            activeProps={{ className: 'font-bold' }}
+          >
+            Home
+          </Link>
+          <Link
+            to="/query-test"
+            activeOptions={{ exact: true }}
+            activeProps={{ className: 'font-bold' }}
+          >
+            Query Test
+          </Link>
+        </div>
+        <hr />
+        {children}
+        <RouterDevtools position="bottom-right" />
+        <ReactQueryDevtools buttonPosition="bottom-left" />
+        <Scripts />
+      </body>
+    </html>
+  )
+}
diff --git a/e2e/react-start/multi-plugin/src/routes/index.tsx b/e2e/react-start/multi-plugin/src/routes/index.tsx
new file mode 100644
index 0000000000..506e2e5e6b
--- /dev/null
+++ b/e2e/react-start/multi-plugin/src/routes/index.tsx
@@ -0,0 +1,84 @@
+import { Link, createFileRoute, useRouter } from '@tanstack/react-router'
+import { getGlobalStartContext } from '@tanstack/react-start'
+import { sortJson } from '~/utils/sortJson'
+
+export const Route = createFileRoute('/')({
+  beforeLoad: async ({ context }) => {
+    const globalStartContext = getGlobalStartContext()
+    // Extract only serializable keys from context (exclude queryClient)
+    const { queryClient: _qc, ...serializableContext } = context
+    return {
+      serverContext: serializableContext,
+      globalStartContext,
+    }
+  },
+  component: Home,
+})
+
+function Home() {
+  const router = useRouter()
+  const { serverContext, globalStartContext } = Route.useRouteContext()
+  const routerCtx = router.options.context
+
+  return (
+    <div className="p-8 space-y-6">
+      <h1 className="font-bold text-lg">Multi-Plugin Test</h1>
+
+      <div>
+        <div className="font-semibold">
+          Router context (router.options.context)
+        </div>
+        <pre
+          data-testid="router-context"
+          className="bg-gray-100 p-2 rounded text-black"
+        >
+          {JSON.stringify(
+            {
+              context: sortJson(routerCtx),
+            },
+            null,
+            2,
+          )}
+        </pre>
+      </div>
+
+      <div>
+        <div className="font-semibold">Server context (from beforeLoad)</div>
+        <pre
+          data-testid="server-context"
+          className="bg-gray-100 p-2 rounded text-black"
+        >
+          {JSON.stringify(sortJson(serverContext), null, 2)}
+        </pre>
+      </div>
+
+      <div>
+        <div className="font-semibold">Global start context</div>
+        <pre
+          data-testid="global-start-context"
+          className="bg-gray-100 p-2 rounded text-black"
+        >
+          {JSON.stringify(sortJson(globalStartContext), null, 2)}
+        </pre>
+      </div>
+
+      <div className="flex items-center gap-3">
+        <Link
+          data-testid="to-query-test"
+          to="/query-test"
+          className="underline"
+        >
+          Go to /query-test
+        </Link>
+        <button
+          type="button"
+          className="px-2 py-1 border rounded"
+          data-testid="invalidate"
+          onClick={() => router.invalidate()}
+        >
+          Invalidate
+        </button>
+      </div>
+    </div>
+  )
+}
diff --git a/e2e/react-start/multi-plugin/src/routes/query-test.tsx b/e2e/react-start/multi-plugin/src/routes/query-test.tsx
new file mode 100644
index 0000000000..88a239d757
--- /dev/null
+++ b/e2e/react-start/multi-plugin/src/routes/query-test.tsx
@@ -0,0 +1,70 @@
+import { queryOptions, useSuspenseQuery } from '@tanstack/react-query'
+import { createFileRoute, useRouter } from '@tanstack/react-router'
+import { createServerFn } from '@tanstack/react-start'
+import { sortJson } from '~/utils/sortJson'
+
+const fetchTimestamp = createServerFn({ method: 'GET' }).handler(async () => {
+  return {
+    message: 'Hello from server!',
+    timestamp: new Date().toISOString(),
+  }
+})
+
+const timestampQueryOptions = () =>
+  queryOptions({
+    queryKey: ['timestamp'],
+    queryFn: () => fetchTimestamp(),
+  })
+
+export const Route = createFileRoute('/query-test')({
+  loader: async ({ context }) => {
+    await context.queryClient.ensureQueryData(timestampQueryOptions())
+  },
+  component: QueryTestComponent,
+})
+
+function QueryTestComponent() {
+  const router = useRouter()
+  const timestampQuery = useSuspenseQuery(timestampQueryOptions())
+
+  return (
+    <div className="p-8 space-y-6">
+      <h1 className="font-bold text-lg">Query Test</h1>
+
+      <div>
+        <div className="font-semibold">
+          Router context (router.options.context)
+        </div>
+        <pre
+          data-testid="router-context-query"
+          className="bg-gray-100 p-2 rounded text-black"
+        >
+          {JSON.stringify(
+            {
+              context: sortJson(router.options.context),
+            },
+            null,
+            2,
+          )}
+        </pre>
+      </div>
+
+      <div>
+        <div className="font-semibold">Query data (from TanStack Query)</div>
+        <pre
+          data-testid="query-data"
+          className="bg-gray-100 p-2 rounded text-black"
+        >
+          {JSON.stringify(timestampQuery.data, null, 2)}
+        </pre>
+      </div>
+
+      <div>
+        <div className="font-semibold">Query client available</div>
+        <span data-testid="query-client-available">
+          {router.options.context.queryClient ? 'true' : 'false'}
+        </span>
+      </div>
+    </div>
+  )
+}
diff --git a/e2e/react-start/multi-plugin/src/start.ts b/e2e/react-start/multi-plugin/src/start.ts
new file mode 100644
index 0000000000..cc08249a0b
--- /dev/null
+++ b/e2e/react-start/multi-plugin/src/start.ts
@@ -0,0 +1,31 @@
+import { createMiddleware, createStart } from '@tanstack/react-start'
+
+export const mwA = createMiddleware().server(async ({ next }) => {
+  return next({
+    context: {
+      a: 'a-from-mwA',
+      shared: 'shared-from-mwA',
+    },
+  })
+})
+
+export const mwB = createMiddleware().server(async ({ next }) => {
+  return next({
+    context: {
+      b: 'b-from-mwB',
+      shared: 'shared-from-mwB',
+    },
+  })
+})
+
+export const mwC = createMiddleware().server(async ({ next }) => {
+  return next({
+    context: {
+      c: 'c-from-mwC',
+    },
+  })
+})
+
+export const startInstance = createStart(() => ({
+  requestMiddleware: [mwA, mwB, mwC],
+}))
diff --git a/e2e/react-start/multi-plugin/src/styles/app.css b/e2e/react-start/multi-plugin/src/styles/app.css
new file mode 100644
index 0000000000..2fad3f4207
--- /dev/null
+++ b/e2e/react-start/multi-plugin/src/styles/app.css
@@ -0,0 +1,6 @@
+html,
+body {
+  padding: 0;
+  margin: 0;
+  font-family: ui-sans-serif, system-ui;
+}
diff --git a/e2e/react-start/multi-plugin/src/utils/sortJson.ts b/e2e/react-start/multi-plugin/src/utils/sortJson.ts
new file mode 100644
index 0000000000..312d10f68b
--- /dev/null
+++ b/e2e/react-start/multi-plugin/src/utils/sortJson.ts
@@ -0,0 +1,18 @@
+export function sortJson<T>(value: T): T {
+  if (Array.isArray(value)) {
+    return value.map(sortJson) as T
+  }
+
+  if (!value || typeof value !== 'object') {
+    return value
+  }
+
+  const obj = value as Record<string, unknown>
+  const out: Record<string, unknown> = {}
+
+  for (const key of Object.keys(obj).sort()) {
+    out[key] = sortJson(obj[key])
+  }
+
+  return out as T
+}
diff --git a/e2e/react-start/multi-plugin/tests/multi-plugin.spec.ts b/e2e/react-start/multi-plugin/tests/multi-plugin.spec.ts
new file mode 100644
index 0000000000..eb885aa46c
--- /dev/null
+++ b/e2e/react-start/multi-plugin/tests/multi-plugin.spec.ts
@@ -0,0 +1,112 @@
+import { expect, test } from '@playwright/test'
+
+test('both plugins contribute context on initial SSR load', async ({
+  page,
+}) => {
+  await page.goto('/')
+
+  // Verify context-bridge plugin contributed its values
+  const serverContextText = await page
+    .getByTestId('server-context')
+    .textContent()
+  const serverContext = JSON.parse(serverContextText || '{}')
+
+  expect(serverContext).toMatchObject({
+    a: 'a-from-mwA',
+    c: 'c-from-mwC',
+    shared: 'shared-from-mwB',
+    static: 'static-value',
+  })
+
+  // queryClient should be present in context (it's a class instance so won't
+  // appear in sortJson output, but the key should exist in the router context)
+  const routerContextText = await page
+    .getByTestId('router-context')
+    .textContent()
+  const routerContext = JSON.parse(routerContextText || '{}')
+  expect(routerContext.context).toMatchObject({
+    a: 'a-from-mwA',
+    c: 'c-from-mwC',
+    shared: 'shared-from-mwB',
+    static: 'static-value',
+  })
+  // 'b' should NOT be present (not selected in the bridge)
+  expect(routerContext.context.b).toBeUndefined()
+
+  // Verify global start context has all middleware values
+  const globalStartContextText = await page
+    .getByTestId('global-start-context')
+    .textContent()
+  const globalStartContext = JSON.parse(globalStartContextText || '{}')
+  expect(globalStartContext).toMatchObject({
+    a: 'a-from-mwA',
+    b: 'b-from-mwB',
+    c: 'c-from-mwC',
+    shared: 'shared-from-mwB',
+  })
+})
+
+test('query plugin provides queryClient in context for data fetching', async ({
+  page,
+}) => {
+  await page.goto('/query-test')
+
+  // The query-test route uses ensureQueryData via context.queryClient,
+  // meaning the SSR query plugin successfully contributed queryClient
+  const queryDataText = await page.getByTestId('query-data').textContent()
+  const queryData = JSON.parse(queryDataText || '{}')
+
+  expect(queryData).toHaveProperty('message', 'Hello from server!')
+  expect(queryData).toHaveProperty('timestamp')
+
+  // queryClient is available in router context
+  const queryClientAvailable = await page
+    .getByTestId('query-client-available')
+    .textContent()
+  expect(queryClientAvailable?.trim()).toBe('true')
+
+  // Bridged context values should also be present on this page
+  const routerContextText = await page
+    .getByTestId('router-context-query')
+    .textContent()
+  const routerContext = JSON.parse(routerContextText || '{}')
+  expect(routerContext.context).toMatchObject({
+    a: 'a-from-mwA',
+    c: 'c-from-mwC',
+    shared: 'shared-from-mwB',
+    static: 'static-value',
+  })
+})
+
+test('navigating between pages preserves both plugins context', async ({
+  page,
+}) => {
+  await page.goto('/')
+
+  // Navigate to query-test via client-side navigation
+  await page.getByTestId('to-query-test').click()
+  await page.waitForURL('/query-test')
+
+  // Query data should load
+  const queryDataText = await page.getByTestId('query-data').textContent()
+  const queryData = JSON.parse(queryDataText || '{}')
+  expect(queryData).toHaveProperty('message', 'Hello from server!')
+
+  // Bridged context still present
+  const routerContextText = await page
+    .getByTestId('router-context-query')
+    .textContent()
+  const routerContext = JSON.parse(routerContextText || '{}')
+  expect(routerContext.context).toMatchObject({
+    a: 'a-from-mwA',
+    c: 'c-from-mwC',
+    shared: 'shared-from-mwB',
+    static: 'static-value',
+  })
+
+  // queryClient available
+  const queryClientAvailable = await page
+    .getByTestId('query-client-available')
+    .textContent()
+  expect(queryClientAvailable?.trim()).toBe('true')
+})
diff --git a/e2e/react-start/multi-plugin/tsconfig.json b/e2e/react-start/multi-plugin/tsconfig.json
new file mode 100644
index 0000000000..beb8fdd8db
--- /dev/null
+++ b/e2e/react-start/multi-plugin/tsconfig.json
@@ -0,0 +1,22 @@
+{
+  "include": ["**/*.ts", "**/*.tsx", "**/*.d.ts"],
+  "compilerOptions": {
+    "strict": true,
+    "esModuleInterop": true,
+    "jsx": "react-jsx",
+    "module": "ESNext",
+    "moduleResolution": "Bundler",
+    "lib": ["DOM", "DOM.Iterable", "ES2022"],
+    "isolatedModules": true,
+    "resolveJsonModule": true,
+    "skipLibCheck": true,
+    "target": "ES2022",
+    "allowJs": true,
+    "forceConsistentCasingInFileNames": true,
+    "baseUrl": ".",
+    "paths": {
+      "~/*": ["./src/*"]
+    },
+    "noEmit": true
+  }
+}
diff --git a/e2e/react-start/multi-plugin/vite.config.ts b/e2e/react-start/multi-plugin/vite.config.ts
new file mode 100644
index 0000000000..23cb856b33
--- /dev/null
+++ b/e2e/react-start/multi-plugin/vite.config.ts
@@ -0,0 +1,19 @@
+import tsConfigPaths from 'vite-tsconfig-paths'
+import { tanstackStart } from '@tanstack/react-start/plugin/vite'
+import viteReact from '@vitejs/plugin-react'
+import tailwindcss from '@tailwindcss/vite'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+  server: {
+    port: 3000,
+  },
+  plugins: [
+    tailwindcss(),
+    tsConfigPaths({
+      projects: ['./tsconfig.json'],
+    }),
+    tanstackStart(),
+    viteReact(),
+  ],
+})
diff --git a/e2e/react-start/query-integration/src/router.tsx b/e2e/react-start/query-integration/src/router.tsx
index ca830a530b..92e8373a97 100644
--- a/e2e/react-start/query-integration/src/router.tsx
+++ b/e2e/react-start/query-integration/src/router.tsx
@@ -1,19 +1,14 @@
 import { QueryClient } from '@tanstack/react-query'
 import { createRouter } from '@tanstack/react-router'
-import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
+import { createSsrQueryPlugin } from '@tanstack/react-router-ssr-query'
 import { routeTree } from './routeTree.gen'
 
 export function getRouter() {
   const queryClient = new QueryClient()
-  const router = createRouter({
+  return createRouter({
     routeTree,
-    context: { queryClient },
     scrollRestoration: true,
     defaultPreload: 'intent',
+    plugins: [createSsrQueryPlugin({ queryClient })],
   })
-  setupRouterSsrQueryIntegration({
-    router,
-    queryClient,
-  })
-  return router
 }
diff --git a/e2e/react-start/server-functions/src/router.tsx b/e2e/react-start/server-functions/src/router.tsx
index b773cd79c9..9b9fcf35a0 100644
--- a/e2e/react-start/server-functions/src/router.tsx
+++ b/e2e/react-start/server-functions/src/router.tsx
@@ -1,13 +1,13 @@
 import { createRouter } from '@tanstack/react-router'
 import { QueryClient } from '@tanstack/react-query'
-import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
+import { createSsrQueryPlugin } from '@tanstack/react-router-ssr-query'
 import { routeTree } from './routeTree.gen'
 import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
 import { NotFound } from './components/NotFound'
 
 export function getRouter() {
   const queryClient = new QueryClient()
-  const router = createRouter({
+  return createRouter({
     routeTree,
     defaultPreload: 'intent',
     defaultErrorComponent: DefaultCatchBoundary,
@@ -18,8 +18,6 @@ export function getRouter() {
         bar: 'baz',
       },
     },
+    plugins: [createSsrQueryPlugin({ queryClient })],
   })
-  setupRouterSsrQueryIntegration({ router, queryClient })
-
-  return router
 }
diff --git a/e2e/react-start/server-routes/src/router.tsx b/e2e/react-start/server-routes/src/router.tsx
index d19308cc30..3ae73a4357 100644
--- a/e2e/react-start/server-routes/src/router.tsx
+++ b/e2e/react-start/server-routes/src/router.tsx
@@ -1,5 +1,5 @@
 import { createRouter } from '@tanstack/react-router'
-import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
+import { createSsrQueryPlugin } from '@tanstack/react-router-ssr-query'
 import { QueryClient } from '@tanstack/react-query'
 import { routeTree } from './routeTree.gen'
 import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
@@ -7,14 +7,12 @@ import { NotFound } from './components/NotFound'
 
 export function getRouter() {
   const queryClient = new QueryClient()
-  const router = createRouter({
+  return createRouter({
     routeTree,
     defaultPreload: 'intent',
     defaultErrorComponent: DefaultCatchBoundary,
     defaultNotFoundComponent: () => <NotFound />,
     scrollRestoration: true,
+    plugins: [createSsrQueryPlugin({ queryClient })],
   })
-  setupRouterSsrQueryIntegration({ router, queryClient })
-
-  return router
 }
diff --git a/e2e/react-start/streaming-ssr/src/router.tsx b/e2e/react-start/streaming-ssr/src/router.tsx
index 238fc5ef25..44ddf15309 100644
--- a/e2e/react-start/streaming-ssr/src/router.tsx
+++ b/e2e/react-start/streaming-ssr/src/router.tsx
@@ -1,18 +1,13 @@
 import { QueryClient } from '@tanstack/react-query'
 import { createRouter } from '@tanstack/react-router'
-import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
+import { createSsrQueryPlugin } from '@tanstack/react-router-ssr-query'
 import { routeTree } from './routeTree.gen'
 
 export function getRouter() {
   const queryClient = new QueryClient()
-  const router = createRouter({
+  return createRouter({
     routeTree,
-    context: { queryClient },
     scrollRestoration: true,
+    plugins: [createSsrQueryPlugin({ queryClient })],
   })
-  setupRouterSsrQueryIntegration({
-    router,
-    queryClient,
-  })
-  return router
 }
diff --git a/e2e/solid-start/basic-solid-query/src/routeTree.gen.ts b/e2e/solid-start/basic-solid-query/src/routeTree.gen.ts
index c100f876d5..ee02d13d55 100644
--- a/e2e/solid-start/basic-solid-query/src/routeTree.gen.ts
+++ b/e2e/solid-start/basic-solid-query/src/routeTree.gen.ts
@@ -262,7 +262,7 @@ declare module '@tanstack/solid-router' {
     '/_layout': {
       id: '/_layout'
       path: ''
-      fullPath: ''
+      fullPath: '/'
       preLoaderRoute: typeof LayoutRouteImport
       parentRoute: typeof rootRouteImport
     }
@@ -311,7 +311,7 @@ declare module '@tanstack/solid-router' {
     '/_layout/_layout-2': {
       id: '/_layout/_layout-2'
       path: ''
-      fullPath: ''
+      fullPath: '/'
       preLoaderRoute: typeof LayoutLayout2RouteImport
       parentRoute: typeof LayoutRoute
     }
diff --git a/e2e/solid-start/basic-solid-query/src/router.tsx b/e2e/solid-start/basic-solid-query/src/router.tsx
index fb853d3c22..98d5e7f8b9 100644
--- a/e2e/solid-start/basic-solid-query/src/router.tsx
+++ b/e2e/solid-start/basic-solid-query/src/router.tsx
@@ -1,23 +1,18 @@
 import { QueryClient } from '@tanstack/solid-query'
 import { createRouter } from '@tanstack/solid-router'
-import { setupRouterSsrQueryIntegration } from '@tanstack/solid-router-ssr-query'
+import { createSsrQueryPlugin } from '@tanstack/solid-router-ssr-query'
 import { routeTree } from './routeTree.gen'
 import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
 import { NotFound } from './components/NotFound'
 
 export function getRouter() {
   const queryClient = new QueryClient()
-  const router = createRouter({
+  return createRouter({
     routeTree,
-    context: { queryClient },
     scrollRestoration: true,
     defaultPreload: 'intent',
     defaultErrorComponent: DefaultCatchBoundary,
     defaultNotFoundComponent: () => <NotFound />,
+    plugins: [createSsrQueryPlugin({ queryClient })],
   })
-  setupRouterSsrQueryIntegration({
-    router,
-    queryClient,
-  })
-  return router
 }
diff --git a/e2e/solid-start/query-integration/src/router.tsx b/e2e/solid-start/query-integration/src/router.tsx
index 59ce312b7e..7fcfc8760b 100644
--- a/e2e/solid-start/query-integration/src/router.tsx
+++ b/e2e/solid-start/query-integration/src/router.tsx
@@ -1,19 +1,14 @@
 import { QueryClient } from '@tanstack/solid-query'
 import { createRouter } from '@tanstack/solid-router'
-import { setupRouterSsrQueryIntegration } from '@tanstack/solid-router-ssr-query'
+import { createSsrQueryPlugin } from '@tanstack/solid-router-ssr-query'
 import { routeTree } from './routeTree.gen'
 
 export function getRouter() {
   const queryClient = new QueryClient()
-  const router = createRouter({
+  return createRouter({
     routeTree,
-    context: { queryClient },
     scrollRestoration: true,
     defaultPreload: 'intent',
+    plugins: [createSsrQueryPlugin({ queryClient })],
   })
-  setupRouterSsrQueryIntegration({
-    router,
-    queryClient,
-  })
-  return router
 }
diff --git a/e2e/solid-start/server-functions/src/router.tsx b/e2e/solid-start/server-functions/src/router.tsx
index e5566c4617..6a9248017c 100644
--- a/e2e/solid-start/server-functions/src/router.tsx
+++ b/e2e/solid-start/server-functions/src/router.tsx
@@ -1,5 +1,5 @@
 import { createRouter } from '@tanstack/solid-router'
-import { setupRouterSsrQueryIntegration } from '@tanstack/solid-router-ssr-query'
+import { createSsrQueryPlugin } from '@tanstack/solid-router-ssr-query'
 import { QueryClient } from '@tanstack/solid-query'
 import { routeTree } from './routeTree.gen'
 import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
@@ -7,7 +7,7 @@ import { NotFound } from './components/NotFound'
 
 export function getRouter() {
   const queryClient = new QueryClient()
-  const router = createRouter({
+  return createRouter({
     routeTree,
     defaultPreload: 'intent',
     defaultErrorComponent: DefaultCatchBoundary,
@@ -18,11 +18,8 @@ export function getRouter() {
         bar: 'baz',
       },
     },
+    plugins: [createSsrQueryPlugin({ queryClient })],
   })
-
-  setupRouterSsrQueryIntegration({ router, queryClient })
-
-  return router
 }
 
 declare module '@tanstack/solid-router' {
diff --git a/e2e/solid-start/server-routes/src/router.tsx b/e2e/solid-start/server-routes/src/router.tsx
index fd79f34af0..cf23fc4640 100644
--- a/e2e/solid-start/server-routes/src/router.tsx
+++ b/e2e/solid-start/server-routes/src/router.tsx
@@ -1,5 +1,5 @@
 import { createRouter } from '@tanstack/solid-router'
-import { setupRouterSsrQueryIntegration } from '@tanstack/solid-router-ssr-query'
+import { createSsrQueryPlugin } from '@tanstack/solid-router-ssr-query'
 import { QueryClient } from '@tanstack/solid-query'
 import { routeTree } from './routeTree.gen'
 import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
@@ -7,14 +7,12 @@ import { NotFound } from './components/NotFound'
 
 export function getRouter() {
   const queryClient = new QueryClient()
-  const router = createRouter({
+  return createRouter({
     routeTree,
     defaultPreload: 'intent',
     defaultErrorComponent: DefaultCatchBoundary,
     defaultNotFoundComponent: () => <NotFound />,
     scrollRestoration: true,
+    plugins: [createSsrQueryPlugin({ queryClient })],
   })
-  setupRouterSsrQueryIntegration({ router, queryClient })
-
-  return router
 }
diff --git a/e2e/vue-start/basic-vue-query/src/routeTree.gen.ts b/e2e/vue-start/basic-vue-query/src/routeTree.gen.ts
index 588ab8b52d..2c514d54ea 100644
--- a/e2e/vue-start/basic-vue-query/src/routeTree.gen.ts
+++ b/e2e/vue-start/basic-vue-query/src/routeTree.gen.ts
@@ -262,7 +262,7 @@ declare module '@tanstack/vue-router' {
     '/_layout': {
       id: '/_layout'
       path: ''
-      fullPath: ''
+      fullPath: '/'
       preLoaderRoute: typeof LayoutRouteImport
       parentRoute: typeof rootRouteImport
     }
@@ -311,7 +311,7 @@ declare module '@tanstack/vue-router' {
     '/_layout/_layout-2': {
       id: '/_layout/_layout-2'
       path: ''
-      fullPath: ''
+      fullPath: '/'
       preLoaderRoute: typeof LayoutLayout2RouteImport
       parentRoute: typeof LayoutRoute
     }
diff --git a/e2e/vue-start/basic-vue-query/src/router.tsx b/e2e/vue-start/basic-vue-query/src/router.tsx
index af6d6afff7..f81e2ed54b 100644
--- a/e2e/vue-start/basic-vue-query/src/router.tsx
+++ b/e2e/vue-start/basic-vue-query/src/router.tsx
@@ -1,25 +1,20 @@
 import { QueryClient } from '@tanstack/vue-query'
 import { createRouter } from '@tanstack/vue-router'
-import { setupRouterSsrQueryIntegration } from '@tanstack/vue-router-ssr-query'
+import { createSsrQueryPlugin } from '@tanstack/vue-router-ssr-query'
 import { routeTree } from './routeTree.gen'
 import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
 import { NotFound } from './components/NotFound'
 
 export function getRouter() {
   const queryClient = new QueryClient()
-  const router = createRouter({
+  return createRouter({
     routeTree,
-    context: { queryClient },
     scrollRestoration: true,
     defaultPreload: 'intent',
     defaultErrorComponent: DefaultCatchBoundary,
     defaultNotFoundComponent: () => <NotFound />,
+    plugins: [createSsrQueryPlugin({ queryClient })],
   })
-  setupRouterSsrQueryIntegration({
-    router,
-    queryClient,
-  })
-  return router
 }
 
 declare module '@tanstack/vue-router' {
diff --git a/e2e/vue-start/query-integration/src/router.tsx b/e2e/vue-start/query-integration/src/router.tsx
index a1d985b426..01dd54007a 100644
--- a/e2e/vue-start/query-integration/src/router.tsx
+++ b/e2e/vue-start/query-integration/src/router.tsx
@@ -1,19 +1,14 @@
 import { QueryClient } from '@tanstack/vue-query'
 import { createRouter } from '@tanstack/vue-router'
-import { setupRouterSsrQueryIntegration } from '@tanstack/vue-router-ssr-query'
+import { createSsrQueryPlugin } from '@tanstack/vue-router-ssr-query'
 import { routeTree } from './routeTree.gen'
 
 export function getRouter() {
   const queryClient = new QueryClient()
-  const router = createRouter({
+  return createRouter({
     routeTree,
-    context: { queryClient },
     scrollRestoration: true,
     defaultPreload: 'intent',
+    plugins: [createSsrQueryPlugin({ queryClient })],
   })
-  setupRouterSsrQueryIntegration({
-    router,
-    queryClient,
-  })
-  return router
 }
diff --git a/e2e/vue-start/server-functions/src/router.tsx b/e2e/vue-start/server-functions/src/router.tsx
index 11122f6b8a..5fe7bcafa2 100644
--- a/e2e/vue-start/server-functions/src/router.tsx
+++ b/e2e/vue-start/server-functions/src/router.tsx
@@ -1,5 +1,5 @@
 import { createRouter } from '@tanstack/vue-router'
-import { setupRouterSsrQueryIntegration } from '@tanstack/vue-router-ssr-query'
+import { createSsrQueryPlugin } from '@tanstack/vue-router-ssr-query'
 import { QueryClient } from '@tanstack/vue-query'
 import { routeTree } from './routeTree.gen'
 import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
@@ -7,7 +7,7 @@ import { NotFound } from './components/NotFound'
 
 export function getRouter() {
   const queryClient = new QueryClient()
-  const router = createRouter({
+  return createRouter({
     routeTree,
     defaultPreload: 'intent',
     defaultErrorComponent: DefaultCatchBoundary,
@@ -18,15 +18,7 @@ export function getRouter() {
         bar: 'baz',
       },
     },
+    plugins: [createSsrQueryPlugin({ queryClient })],
   })
-
-  setupRouterSsrQueryIntegration({ router, queryClient })
-
-  return router
 }
 
-declare module '@tanstack/vue-router' {
-  interface Register {
-    router: ReturnType<typeof getRouter>
-  }
-}
diff --git a/e2e/vue-start/server-routes/src/router.tsx b/e2e/vue-start/server-routes/src/router.tsx
index 683d7776dd..4e25ff4e40 100644
--- a/e2e/vue-start/server-routes/src/router.tsx
+++ b/e2e/vue-start/server-routes/src/router.tsx
@@ -1,5 +1,5 @@
 import { createRouter } from '@tanstack/vue-router'
-import { setupRouterSsrQueryIntegration } from '@tanstack/vue-router-ssr-query'
+import { createSsrQueryPlugin } from '@tanstack/vue-router-ssr-query'
 import { QueryClient } from '@tanstack/vue-query'
 import { routeTree } from './routeTree.gen'
 import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
@@ -7,14 +7,12 @@ import { NotFound } from './components/NotFound'
 
 export function getRouter() {
   const queryClient = new QueryClient()
-  const router = createRouter({
+  return createRouter({
     routeTree,
     defaultPreload: 'intent',
     defaultErrorComponent: DefaultCatchBoundary,
     defaultNotFoundComponent: () => <NotFound />,
     scrollRestoration: true,
+    plugins: [createSsrQueryPlugin({ queryClient })],
   })
-  setupRouterSsrQueryIntegration({ router, queryClient })
-
-  return router
 }
diff --git a/packages/react-router-ssr-query/src/index.tsx b/packages/react-router-ssr-query/src/index.tsx
index c060cc98dc..aeebd0886d 100644
--- a/packages/react-router-ssr-query/src/index.tsx
+++ b/packages/react-router-ssr-query/src/index.tsx
@@ -1,29 +1,81 @@
 import { Fragment } from 'react'
 import { QueryClientProvider } from '@tanstack/react-query'
-import { setupCoreRouterSsrQueryIntegration } from '@tanstack/router-ssr-query-core'
-import type { RouterSsrQueryOptions } from '@tanstack/router-ssr-query-core'
-import type { AnyRouter } from '@tanstack/react-router'
+import { createCoreSsrQueryPlugin } from '@tanstack/router-ssr-query-core'
+import type {
+  RouterSsrQueryOptions,
+  SsrQueryPluginOptions,
+} from '@tanstack/router-ssr-query-core'
+import type { AnyRouter, RouterPlugin } from '@tanstack/react-router'
+import type { QueryClient } from '@tanstack/react-query'
 
 export type Options<TRouter extends AnyRouter> =
   RouterSsrQueryOptions<TRouter> & {
     wrapQueryClient?: boolean
   }
 
+export type SsrQueryPluginReactOptions = SsrQueryPluginOptions & {
+  /**
+   * If `false`, the plugin will not wrap the router with `QueryClientProvider`.
+   * Useful if you are already providing the QueryClient via a parent provider.
+   *
+   * @default true
+   */
+  wrapQueryClient?: boolean
+}
+
+/**
+ * Create a router plugin that integrates TanStack Query with SSR for React.
+ *
+ * The plugin contributes `{ queryClient: QueryClient }` to the router context,
+ * sets up dehydrate/hydrate for SSR, and wraps the app with `QueryClientProvider`.
+ *
+ * @example
+ * ```tsx
+ * const queryClient = new QueryClient()
+ * const ssrQueryPlugin = createSsrQueryPlugin({ queryClient })
+ *
+ * const router = createRouter({
+ *   routeTree,
+ *   plugins: [ssrQueryPlugin],
+ * })
+ * ```
+ */
+export function createSsrQueryPlugin(
+  opts: SsrQueryPluginReactOptions,
+): RouterPlugin<{ queryClient: QueryClient }> {
+  const corePlugin = createCoreSsrQueryPlugin(opts)
+
+  return {
+    '~types': null as any,
+    setup: (router: AnyRouter) => {
+      corePlugin.setup(router)
+
+      if (opts.wrapQueryClient === false) {
+        return
+      }
+
+      const OGWrap = router.options.Wrap || Fragment
+
+      router.options.Wrap = ({ children }) => {
+        return (
+          <QueryClientProvider client={opts.queryClient}>
+            <OGWrap>{children}</OGWrap>
+          </QueryClientProvider>
+        )
+      }
+    },
+  }
+}
+
+/**
+ * @deprecated Use `createSsrQueryPlugin` instead. This function will be removed in a future version.
+ */
 export function setupRouterSsrQueryIntegration<TRouter extends AnyRouter>(
   opts: Options<TRouter>,
 ) {
-  setupCoreRouterSsrQueryIntegration(opts)
-
-  if (opts.wrapQueryClient === false) {
-    return
-  }
-  const OGWrap = opts.router.options.Wrap || Fragment
-
-  opts.router.options.Wrap = ({ children }) => {
-    return (
-      <QueryClientProvider client={opts.queryClient}>
-        <OGWrap>{children}</OGWrap>
-      </QueryClientProvider>
-    )
-  }
+  createSsrQueryPlugin({
+    queryClient: opts.queryClient,
+    handleRedirects: opts.handleRedirects,
+    wrapQueryClient: opts.wrapQueryClient,
+  }).setup(opts.router)
 }
diff --git a/packages/react-router/src/index.tsx b/packages/react-router/src/index.tsx
index 3ed13c09c9..265d598f3f 100644
--- a/packages/react-router/src/index.tsx
+++ b/packages/react-router/src/index.tsx
@@ -215,6 +215,9 @@ export type {
   RouteMask,
   MatchRouteOptions,
   CreateLazyFileRoute,
+  RouterPlugin,
+  RouterPluginTypes,
+  PluginProvidedContext,
 } from '@tanstack/router-core'
 export type {
   UseLinkPropsOptions,
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index b3a20910f8..eae62ce6c3 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -3,6 +3,7 @@ import { createFileRoute, createLazyFileRoute } from './fileRoute'
 import type { RouterHistory } from '@tanstack/history'
 import type {
   AnyRoute,
+  RouterPlugin,
   CreateRouterFn,
   RouterConstructorOptions,
   TrailingSlashOption,
@@ -89,7 +90,11 @@ declare module '@tanstack/router-core' {
  * @link https://tanstack.com/router/latest/docs/framework/react/api/router/createRouterFunction
  */
 export const createRouter: CreateRouterFn = (options) => {
-  return new Router(options)
+  // `as any` is safe: CreateRouterFn enforces full type safety at the call
+  // site (including plugin-aware context). The Router constructor widens
+  // TPlugins to ReadonlyArray<RouterPlugin<any>> since the class only
+  // iterates plugins at runtime and doesn't need the exact tuple type.
+  return new Router(options as any)
 }
 
 export class Router<
@@ -111,7 +116,8 @@ export class Router<
       TTrailingSlashOption,
       TDefaultStructuralSharingOption,
       TRouterHistory,
-      TDehydrated
+      TDehydrated,
+      ReadonlyArray<RouterPlugin<any>>
     >,
   ) {
     super(options)
diff --git a/packages/react-router/tests/router.test-d.tsx b/packages/react-router/tests/router.test-d.tsx
index 82ce702222..7e32c942cf 100644
--- a/packages/react-router/tests/router.test-d.tsx
+++ b/packages/react-router/tests/router.test-d.tsx
@@ -6,7 +6,7 @@ import {
   createRoute,
   createRouter,
 } from '../src'
-import type { RouterHistory } from '../src'
+import type { RouterHistory, RouterPlugin } from '../src'
 
 test('when creating a router without context', () => {
   // eslint-disable-next-line unused-imports/no-unused-vars
@@ -253,3 +253,162 @@ test('when creating a router with custom router history', () => {
   expectTypeOf(router.history).toMatchTypeOf<RouterHistory>()
   expectTypeOf(router.history).toEqualTypeOf<typeof customRouterHistory>()
 })
+
+test('when a plugin provides all required context keys, context becomes optional', () => {
+  const rootRoute = createRootRouteWithContext<{ queryClient: string }>()()
+
+  type RouteTree = typeof rootRoute
+  type Plugins = readonly [RouterPlugin<{ queryClient: string }>]
+
+  // Plugin provides queryClient, so context becomes optional
+  expectTypeOf(
+    createRouter<
+      RouteTree,
+      'never',
+      boolean,
+      RouterHistory,
+      Record<string, any>,
+      Plugins
+    >,
+  )
+    .parameter(0)
+    .not.toMatchTypeOf<{ context: { queryClient: string } }>()
+
+  expectTypeOf(
+    createRouter<
+      RouteTree,
+      'never',
+      boolean,
+      RouterHistory,
+      Record<string, any>,
+      Plugins
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('context')
+    .toMatchTypeOf<{ queryClient?: string } | undefined>()
+})
+
+test('when a plugin provides some required context keys, remaining keys are still required', () => {
+  const rootRoute = createRootRouteWithContext<{
+    queryClient: string
+    userToken: string
+  }>()()
+
+  type RouteTree = typeof rootRoute
+  type Plugins = readonly [RouterPlugin<{ queryClient: string }>]
+
+  // userToken is required, queryClient is optional (provided by plugin)
+  expectTypeOf(
+    createRouter<
+      RouteTree,
+      'never',
+      boolean,
+      RouterHistory,
+      Record<string, any>,
+      Plugins
+    >,
+  )
+    .parameter(0)
+    .toMatchTypeOf<{
+      context: { userToken: string }
+    }>()
+
+  expectTypeOf(
+    createRouter<
+      RouteTree,
+      'never',
+      boolean,
+      RouterHistory,
+      Record<string, any>,
+      Plugins
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('context')
+    .toEqualTypeOf<{ userToken: string } & { queryClient?: string }>()
+})
+
+test('when multiple plugins provide different context keys, all plugin keys are omitted', () => {
+  const rootRoute = createRootRouteWithContext<{
+    queryClient: string
+    authToken: string
+    appName: string
+  }>()()
+
+  type RouteTree = typeof rootRoute
+  type Plugins = readonly [
+    RouterPlugin<{ queryClient: string }>,
+    RouterPlugin<{ authToken: string }>,
+  ]
+
+  // appName required, queryClient + authToken optional (provided by plugins)
+  expectTypeOf(
+    createRouter<
+      RouteTree,
+      'never',
+      boolean,
+      RouterHistory,
+      Record<string, any>,
+      Plugins
+    >,
+  )
+    .parameter(0)
+    .toMatchTypeOf<{
+      context: { appName: string }
+    }>()
+
+  expectTypeOf(
+    createRouter<
+      RouteTree,
+      'never',
+      boolean,
+      RouterHistory,
+      Record<string, any>,
+      Plugins
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('context')
+    .toEqualTypeOf<
+      { appName: string } & { queryClient?: string; authToken?: string }
+    >()
+})
+
+test('when no plugins are provided, context is still required if route tree demands it', () => {
+  const rootRoute = createRootRouteWithContext<{ userId: string }>()()
+
+  type RouteTree = typeof rootRoute
+
+  expectTypeOf(createRouter<RouteTree, 'never', boolean>)
+    .parameter(0)
+    .toHaveProperty('context')
+    .toEqualTypeOf<{ userId: string }>()
+
+  expectTypeOf(createRouter<RouteTree, 'never', boolean>)
+    .parameter(0)
+    .toMatchTypeOf<{
+      context: { userId: string }
+    }>()
+})
+
+test('when a plugin provides context keys not defined in the route tree, context remains optional', () => {
+  const rootRoute = createRootRoute()
+
+  type RouteTree = typeof rootRoute
+  type Plugins = readonly [RouterPlugin<{ analytics: object }>]
+
+  expectTypeOf(
+    createRouter<
+      RouteTree,
+      'never',
+      boolean,
+      RouterHistory,
+      Record<string, any>,
+      Plugins
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('context')
+    .toEqualTypeOf<{} | undefined>()
+})
diff --git a/packages/router-core/src/index.ts b/packages/router-core/src/index.ts
index 1897f07880..10d4120528 100644
--- a/packages/router-core/src/index.ts
+++ b/packages/router-core/src/index.ts
@@ -455,3 +455,10 @@ export {
   executeRewriteOutput,
 } from './rewrite'
 export type { LocationRewrite, LocationRewriteFunction } from './router'
+
+/** @private */
+export type {
+  RouterPlugin,
+  RouterPluginTypes,
+  PluginProvidedContext,
+} from './plugin'
diff --git a/packages/router-core/src/plugin.ts b/packages/router-core/src/plugin.ts
new file mode 100644
index 0000000000..efd09b9ee8
--- /dev/null
+++ b/packages/router-core/src/plugin.ts
@@ -0,0 +1,51 @@
+import type { AnyRouter } from './router'
+
+/**
+ * Phantom type container for a router plugin.
+ *
+ * @private
+ */
+export interface RouterPluginTypes<
+  TContext extends Record<string, unknown> = Record<string, unknown>,
+> {
+  context: TContext
+}
+
+/**
+ * Interface that router plugins must satisfy.
+ * Defined in router-core so the router can accept plugins
+ * without depending on specific plugin implementations.
+ *
+ * The `TContext` phantom type carries the shape of the context values
+ * that this plugin provides, enabling the router to compute which
+ * context keys the user must supply manually.
+ *
+ * @private
+ */
+export interface RouterPlugin<
+  TContext extends Record<string, unknown> = Record<string, unknown>,
+> {
+  /**
+   * Phantom type container carrying the provided context type.
+   * @private
+   */
+  '~types': RouterPluginTypes<TContext>
+  /**
+   * Called by the router constructor to wire up the plugin.
+   * @private
+   */
+  setup: (router: AnyRouter) => void
+}
+
+/**
+ * Merge the context types from a tuple of router plugins into
+ * a single intersection.  Returns `{}` for an empty tuple.
+ *
+ * @private
+ */
+export type PluginProvidedContext<TPlugins> = TPlugins extends readonly [
+  RouterPlugin<infer TContext>,
+  ...infer TRest,
+]
+  ? TContext & PluginProvidedContext<TRest>
+  : {}
diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts
index 6520f30799..27e1b09412 100644
--- a/packages/router-core/src/router.ts
+++ b/packages/router-core/src/router.ts
@@ -1,6 +1,7 @@
 import { Store } from '@tanstack/store'
 import { createBrowserHistory, parseHref } from '@tanstack/history'
 import { isServer } from '@tanstack/router-core/isServer'
+import type { PluginProvidedContext, RouterPlugin } from './plugin'
 import { batch } from './utils/batch'
 import {
   createControlledPromise,
@@ -59,6 +60,7 @@ import type {
   Awaitable,
   Constrain,
   ControlledPromise,
+  MakeDifferenceOptional,
   NoInfer,
   NonNullableUpdater,
   PickAsRequired,
@@ -317,6 +319,17 @@ export interface RouterOptions<
    */
   context?: InferRouterContext<TRouteTree>
 
+  /**
+   * An array of router plugins that can provide context values,
+   * hook into dehydrate/hydrate, or perform other setup tasks.
+   *
+   * When plugins provide context values, the router's `context` option
+   * only needs to supply the remaining (non-plugin-provided) keys.
+   *
+   * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/router-context)
+   */
+  plugins?: Array<RouterPlugin<any>>
+
   additionalContext?: any
 
   /**
@@ -602,21 +615,68 @@ export interface MatchRoutesOpts {
 export type InferRouterContext<TRouteTree extends AnyRoute> =
   TRouteTree['types']['routerContext']
 
-export type RouterContextOptions<TRouteTree extends AnyRoute> =
-  AnyContext extends InferRouterContext<TRouteTree>
+/**
+ * Conditional type that makes `context` optional when all required keys
+ * are provided by plugins.
+ *
+ * Uses `MakeDifferenceOptional` so that plugin-provided keys become
+ * optional (the user may still override them), while keys NOT provided
+ * by any plugin stay required.
+ *
+ * @private
+ */
+export type RouterContextOptions<
+  TRouteTree extends AnyRoute,
+  TPlugins extends ReadonlyArray<RouterPlugin<any>> = [],
+> =
+  AnyContext extends MakeDifferenceOptional<
+    PluginProvidedContext<TPlugins>,
+    InferRouterContext<TRouteTree>
+  >
     ? {
-        context?: InferRouterContext<TRouteTree>
+        context?: MakeDifferenceOptional<
+          PluginProvidedContext<TPlugins>,
+          InferRouterContext<TRouteTree>
+        >
+        plugins?: ValidatePluginContext<TRouteTree, TPlugins>
       }
     : {
-        context: InferRouterContext<TRouteTree>
+        context: MakeDifferenceOptional<
+          PluginProvidedContext<TPlugins>,
+          InferRouterContext<TRouteTree>
+        >
+        plugins?: ValidatePluginContext<TRouteTree, TPlugins>
       }
 
+/**
+ * Validates that the merged context provided by `TPlugins` is
+ * assignable to the route tree's expected context.
+ *
+ * Resolves to `TPlugins` when valid. When a mismatch is detected, resolves
+ * to a type that the offending plugin cannot satisfy, causing a type error
+ * at the call-site.
+ *
+ * @private
+ */
+type ValidatePluginContext<
+  TRouteTree extends AnyRoute,
+  TPlugins extends ReadonlyArray<RouterPlugin<any>>,
+> =
+  PluginProvidedContext<TPlugins> extends Partial<
+    InferRouterContext<TRouteTree>
+  >
+    ? TPlugins
+    : ReadonlyArray<
+        RouterPlugin<InferRouterContext<TRouteTree> & Record<string, unknown>>
+      >
+
 export type RouterConstructorOptions<
   TRouteTree extends AnyRoute,
   TTrailingSlashOption extends TrailingSlashOption,
   TDefaultStructuralSharingOption extends boolean,
   TRouterHistory extends RouterHistory,
   TDehydrated extends Record<string, any>,
+  TPlugins extends ReadonlyArray<RouterPlugin<any>> = [],
 > = Omit<
   RouterOptions<
     TRouteTree,
@@ -625,9 +685,9 @@ export type RouterConstructorOptions<
     TRouterHistory,
     TDehydrated
   >,
-  'context' | 'serializationAdapters' | 'defaultSsr'
+  'context' | 'plugins' | 'serializationAdapters' | 'defaultSsr'
 > &
-  RouterContextOptions<TRouteTree>
+  RouterContextOptions<TRouteTree, TPlugins>
 
 export type PreloadRouteFn<
   TRouteTree extends AnyRoute,
@@ -684,12 +744,15 @@ export type UpdateFn<
   TRouterHistory extends RouterHistory,
   TDehydrated extends Record<string, any>,
 > = (
-  newOptions: RouterConstructorOptions<
-    TRouteTree,
-    TTrailingSlashOption,
-    TDefaultStructuralSharingOption,
-    TRouterHistory,
-    TDehydrated
+  newOptions: Omit<
+    RouterConstructorOptions<
+      TRouteTree,
+      TTrailingSlashOption,
+      TDefaultStructuralSharingOption,
+      TRouterHistory,
+      TDehydrated
+    >,
+    'plugins'
   >,
 ) => void
 
@@ -855,12 +918,16 @@ export function getLocationChangeInfo(routerState: {
   return { fromLocation, toLocation, pathChanged, hrefChanged, hashChanged }
 }
 
+/**
+ * @private
+ */
 export type CreateRouterFn = <
   TRouteTree extends AnyRoute,
   TTrailingSlashOption extends TrailingSlashOption = 'never',
   TDefaultStructuralSharingOption extends boolean = false,
   TRouterHistory extends RouterHistory = RouterHistory,
   TDehydrated extends Record<string, any> = Record<string, any>,
+  const TPlugins extends ReadonlyArray<RouterPlugin<any>> = [],
 >(
   options: undefined extends number
     ? 'strictNullChecks must be enabled in tsconfig.json'
@@ -869,7 +936,8 @@ export type CreateRouterFn = <
         TTrailingSlashOption,
         TDefaultStructuralSharingOption,
         TRouterHistory,
-        TDehydrated
+        TDehydrated,
+        TPlugins
       >,
 ) => RouterCore<
   TRouteTree,
@@ -971,7 +1039,8 @@ export class RouterCore<
       TTrailingSlashOption,
       TDefaultStructuralSharingOption,
       TRouterHistory,
-      TDehydrated
+      TDehydrated,
+      ReadonlyArray<RouterPlugin<any>>
     >,
   ) {
     this.update({
@@ -984,7 +1053,14 @@ export class RouterCore<
       notFoundMode: options.notFoundMode ?? 'fuzzy',
       stringifySearch: options.stringifySearch ?? defaultStringifySearch,
       parseSearch: options.parseSearch ?? defaultParseSearch,
-    })
+    } as any)
+
+    // Wire up plugins if provided
+    if (this.options.plugins) {
+      for (const plugin of this.options.plugins) {
+        plugin.setup(this)
+      }
+    }
 
     if (typeof document !== 'undefined') {
       self.__TSR_ROUTER__ = this
diff --git a/packages/router-core/tests/plugin.test.ts b/packages/router-core/tests/plugin.test.ts
new file mode 100644
index 0000000000..83af6e9cce
--- /dev/null
+++ b/packages/router-core/tests/plugin.test.ts
@@ -0,0 +1,212 @@
+import { describe, expect, it, vi } from 'vitest'
+import { createMemoryHistory } from '@tanstack/history'
+import { BaseRootRoute, BaseRoute, RouterCore } from '../src'
+import type { RouterPlugin } from '../src'
+
+describe('plugin system', () => {
+  function createRouteTree() {
+    const rootRoute = new BaseRootRoute({})
+    const indexRoute = new BaseRoute({
+      getParentRoute: () => rootRoute,
+      path: '/',
+    })
+    return rootRoute.addChildren([indexRoute])
+  }
+
+  describe('setup()', () => {
+    it('calls plugin setup with the router instance', () => {
+      const setupFn = vi.fn()
+      const plugin: RouterPlugin<{ foo: string }> = {
+        '~types': null as any,
+        setup: setupFn,
+      }
+
+      const routeTree = createRouteTree()
+      const router = new RouterCore({
+        routeTree,
+        history: createMemoryHistory(),
+        plugins: [plugin],
+      } as any)
+
+      expect(setupFn).toHaveBeenCalledTimes(1)
+      expect(setupFn).toHaveBeenCalledWith(router)
+    })
+
+    it('calls multiple plugins in order', () => {
+      const callOrder: Array<string> = []
+
+      const pluginA: RouterPlugin<{ a: string }> = {
+        '~types': null as any,
+        setup: () => callOrder.push('a'),
+      }
+      const pluginB: RouterPlugin<{ b: number }> = {
+        '~types': null as any,
+        setup: () => callOrder.push('b'),
+      }
+      const pluginC: RouterPlugin<{ c: boolean }> = {
+        '~types': null as any,
+        setup: () => callOrder.push('c'),
+      }
+
+      const routeTree = createRouteTree()
+      new RouterCore({
+        routeTree,
+        history: createMemoryHistory(),
+        plugins: [pluginA, pluginB, pluginC],
+      } as any)
+
+      expect(callOrder).toEqual(['a', 'b', 'c'])
+    })
+
+    it('does not error when no plugins are provided', () => {
+      const routeTree = createRouteTree()
+      expect(
+        () =>
+          new RouterCore({
+            routeTree,
+            history: createMemoryHistory(),
+          }),
+      ).not.toThrow()
+    })
+
+    it('does not error when plugins array is empty', () => {
+      const routeTree = createRouteTree()
+      expect(
+        () =>
+          new RouterCore({
+            routeTree,
+            history: createMemoryHistory(),
+            plugins: [],
+          } as any),
+      ).not.toThrow()
+    })
+  })
+
+  describe('context merging', () => {
+    it('plugin can merge values into router.options.context', () => {
+      const plugin: RouterPlugin<{ queryClient: string }> = {
+        '~types': null as any,
+        setup: (router) => {
+          router.options.context = {
+            ...router.options.context,
+            queryClient: 'test-client',
+          }
+        },
+      }
+
+      const routeTree = createRouteTree()
+      const router = new RouterCore({
+        routeTree,
+        history: createMemoryHistory(),
+        plugins: [plugin],
+      } as any)
+
+      expect((router.options.context as any).queryClient).toBe('test-client')
+    })
+
+    it('multiple plugins can each merge different context values', () => {
+      const pluginA: RouterPlugin<{ serviceA: string }> = {
+        '~types': null as any,
+        setup: (router) => {
+          router.options.context = {
+            ...router.options.context,
+            serviceA: 'value-a',
+          }
+        },
+      }
+      const pluginB: RouterPlugin<{ serviceB: number }> = {
+        '~types': null as any,
+        setup: (router) => {
+          router.options.context = {
+            ...router.options.context,
+            serviceB: 42,
+          }
+        },
+      }
+
+      const routeTree = createRouteTree()
+      const router = new RouterCore({
+        routeTree,
+        history: createMemoryHistory(),
+        plugins: [pluginA, pluginB],
+      } as any)
+
+      const ctx = router.options.context as any
+      expect(ctx.serviceA).toBe('value-a')
+      expect(ctx.serviceB).toBe(42)
+    })
+
+    it('user-provided context is preserved alongside plugin context', () => {
+      const plugin: RouterPlugin<{ fromPlugin: string }> = {
+        '~types': null as any,
+        setup: (router) => {
+          router.options.context = {
+            ...router.options.context,
+            fromPlugin: 'plugin-value',
+          }
+        },
+      }
+
+      const routeTree = createRouteTree()
+      const router = new RouterCore({
+        routeTree,
+        history: createMemoryHistory(),
+        context: { fromUser: 'user-value' },
+        plugins: [plugin],
+      } as any)
+
+      const ctx = router.options.context as any
+      expect(ctx.fromUser).toBe('user-value')
+      expect(ctx.fromPlugin).toBe('plugin-value')
+    })
+  })
+
+  describe('dehydrate/hydrate hooking', () => {
+    it('plugin can hook into dehydrate by chaining', () => {
+      const plugin: RouterPlugin<Record<string, unknown>> = {
+        '~types': null as any,
+        setup: (router) => {
+          const originalDehydrate = router.options.dehydrate
+          router.options.dehydrate = () => {
+            const base = originalDehydrate?.() ?? {}
+            return { ...base, pluginData: 'dehydrated' }
+          }
+        },
+      }
+
+      const routeTree = createRouteTree()
+      const router = new RouterCore({
+        routeTree,
+        history: createMemoryHistory(),
+        plugins: [plugin],
+      } as any)
+
+      const result = router.options.dehydrate?.()
+      expect(result).toEqual({ pluginData: 'dehydrated' })
+    })
+
+    it('plugin can hook into hydrate by chaining', () => {
+      const hydrateSpy = vi.fn()
+      const plugin: RouterPlugin<Record<string, unknown>> = {
+        '~types': null as any,
+        setup: (router) => {
+          const originalHydrate = router.options.hydrate
+          router.options.hydrate = (data: any) => {
+            originalHydrate?.(data)
+            hydrateSpy(data)
+          }
+        },
+      }
+
+      const routeTree = createRouteTree()
+      const router = new RouterCore({
+        routeTree,
+        history: createMemoryHistory(),
+        plugins: [plugin],
+      } as any)
+
+      router.options.hydrate?.({ test: true } as any)
+      expect(hydrateSpy).toHaveBeenCalledWith({ test: true })
+    })
+  })
+})
diff --git a/packages/router-ssr-query-core/src/index.ts b/packages/router-ssr-query-core/src/index.ts
index 1ef00b1a7a..4423f5c62e 100644
--- a/packages/router-ssr-query-core/src/index.ts
+++ b/packages/router-ssr-query-core/src/index.ts
@@ -4,7 +4,7 @@ import {
 } from '@tanstack/query-core'
 import { isRedirect } from '@tanstack/router-core'
 import { isServer } from '@tanstack/router-core/isServer'
-import type { AnyRouter } from '@tanstack/router-core'
+import type { AnyRouter, RouterPlugin } from '@tanstack/router-core'
 import type {
   QueryClient,
   DehydratedState as QueryDehydratedState,
@@ -23,16 +23,79 @@ export type RouterSsrQueryOptions<TRouter extends AnyRouter> = {
   handleRedirects?: boolean
 }
 
+export type SsrQueryPluginOptions = {
+  queryClient: QueryClient
+
+  /**
+   * If `true`, the QueryClient will handle errors thrown by `redirect()` inside of mutations and queries.
+   *
+   * @default true
+   * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/api/router/redirectFunction)
+   */
+  handleRedirects?: boolean
+}
+
 type DehydratedRouterQueryState = {
   dehydratedQueryClient?: QueryDehydratedState
   queryStream: ReadableStream<QueryDehydratedState>
 }
 
+/**
+ * Create a router plugin that integrates TanStack Query with SSR.
+ *
+ * The plugin contributes `{ queryClient: QueryClient }` to the router context,
+ * so you do not need to pass `queryClient` in `context` when creating the router.
+ *
+ * @example
+ * ```ts
+ * const queryClient = new QueryClient()
+ * const ssrQueryPlugin = createSsrQueryPlugin({ queryClient })
+ *
+ * createRouter({
+ *   routeTree,
+ *   plugins: [ssrQueryPlugin],
+ * })
+ * ```
+ */
+export function createCoreSsrQueryPlugin(
+  opts: SsrQueryPluginOptions,
+): RouterPlugin<{ queryClient: QueryClient }> {
+  const { queryClient, handleRedirects = true } = opts
+
+  return {
+    '~types': null as any,
+    setup: (router: AnyRouter) => {
+      // Merge queryClient into router context
+      router.options.context = {
+        ...router.options.context,
+        queryClient,
+      }
+
+      _setupCoreSsrQueryIntegration({ router, queryClient, handleRedirects })
+    },
+  }
+}
+
+/**
+ * @deprecated Use `createSsrQueryPlugin` instead. This function will be removed in a future version.
+ */
 export function setupCoreRouterSsrQueryIntegration<TRouter extends AnyRouter>({
   router,
   queryClient,
   handleRedirects = true,
 }: RouterSsrQueryOptions<TRouter>) {
+  createCoreSsrQueryPlugin({ queryClient, handleRedirects }).setup(router)
+}
+
+function _setupCoreSsrQueryIntegration({
+  router,
+  queryClient,
+  handleRedirects,
+}: {
+  router: AnyRouter
+  queryClient: QueryClient
+  handleRedirects: boolean
+}) {
   const ogHydrate = router.options.hydrate
   const ogDehydrate = router.options.dehydrate
 
diff --git a/packages/solid-router-ssr-query/src/index.tsx b/packages/solid-router-ssr-query/src/index.tsx
index 91f764f15f..2ef6fe7a65 100644
--- a/packages/solid-router-ssr-query/src/index.tsx
+++ b/packages/solid-router-ssr-query/src/index.tsx
@@ -1,7 +1,11 @@
 import { QueryClientProvider } from '@tanstack/solid-query'
-import { setupCoreRouterSsrQueryIntegration } from '@tanstack/router-ssr-query-core'
-import type { RouterSsrQueryOptions } from '@tanstack/router-ssr-query-core'
-import type { AnyRouter } from '@tanstack/solid-router'
+import { createCoreSsrQueryPlugin } from '@tanstack/router-ssr-query-core'
+import type {
+  RouterSsrQueryOptions,
+  SsrQueryPluginOptions,
+} from '@tanstack/router-ssr-query-core'
+import type { AnyRouter, RouterPlugin } from '@tanstack/solid-router'
+import type { QueryClient } from '@tanstack/solid-query'
 import type { JSX } from 'solid-js'
 
 export type Options<TRouter extends AnyRouter> =
@@ -9,24 +13,71 @@ export type Options<TRouter extends AnyRouter> =
     wrapQueryClient?: boolean
   }
 
-export function setupRouterSsrQueryIntegration<TRouter extends AnyRouter>(
-  opts: Options<TRouter>,
-) {
-  setupCoreRouterSsrQueryIntegration(opts)
+export type SsrQueryPluginSolidOptions = SsrQueryPluginOptions & {
+  /**
+   * If `false`, the plugin will not wrap the router with `QueryClientProvider`.
+   * Useful if you are already providing the QueryClient via a parent provider.
+   *
+   * @default true
+   */
+  wrapQueryClient?: boolean
+}
 
-  if (opts.wrapQueryClient === false) {
-    return
-  }
+/**
+ * Create a router plugin that integrates TanStack Query with SSR for Solid.
+ *
+ * The plugin contributes `{ queryClient: QueryClient }` to the router context,
+ * sets up dehydrate/hydrate for SSR, and wraps the app with `QueryClientProvider`.
+ *
+ * @example
+ * ```tsx
+ * const queryClient = new QueryClient()
+ * const ssrQueryPlugin = createSsrQueryPlugin({ queryClient })
+ *
+ * const router = createRouter({
+ *   routeTree,
+ *   plugins: [ssrQueryPlugin],
+ * })
+ * ```
+ */
+export function createSsrQueryPlugin(
+  opts: SsrQueryPluginSolidOptions,
+): RouterPlugin<{ queryClient: QueryClient }> {
+  const corePlugin = createCoreSsrQueryPlugin(opts)
+
+  return {
+    '~types': null as any,
+    setup: (router: AnyRouter) => {
+      corePlugin.setup(router)
 
-  const OGWrap =
-    opts.router.options.Wrap ||
-    ((props: { children: JSX.Element }) => props.children)
+      if (opts.wrapQueryClient === false) {
+        return
+      }
 
-  opts.router.options.Wrap = (props) => {
-    return (
-      <QueryClientProvider client={opts.queryClient}>
-        <OGWrap>{props.children}</OGWrap>
-      </QueryClientProvider>
-    )
+      const OGWrap =
+        router.options.Wrap ||
+        ((props: { children: JSX.Element }) => props.children)
+
+      router.options.Wrap = (props) => {
+        return (
+          <QueryClientProvider client={opts.queryClient}>
+            <OGWrap>{props.children}</OGWrap>
+          </QueryClientProvider>
+        )
+      }
+    },
   }
 }
+
+/**
+ * @deprecated Use `createSsrQueryPlugin` instead. This function will be removed in a future version.
+ */
+export function setupRouterSsrQueryIntegration<TRouter extends AnyRouter>(
+  opts: Options<TRouter>,
+) {
+  createSsrQueryPlugin({
+    queryClient: opts.queryClient,
+    handleRedirects: opts.handleRedirects,
+    wrapQueryClient: opts.wrapQueryClient,
+  }).setup(opts.router)
+}
diff --git a/packages/solid-router/src/index.tsx b/packages/solid-router/src/index.tsx
index b370c360a1..0060c385e0 100644
--- a/packages/solid-router/src/index.tsx
+++ b/packages/solid-router/src/index.tsx
@@ -187,6 +187,9 @@ export type {
   AnySerializationAdapter,
   SerializationAdapter,
   SerializableExtensions,
+  RouterPlugin,
+  RouterPluginTypes,
+  PluginProvidedContext,
 } from '@tanstack/router-core'
 
 export {
diff --git a/packages/solid-router/src/router.ts b/packages/solid-router/src/router.ts
index fd79cc6cf6..422e2affe4 100644
--- a/packages/solid-router/src/router.ts
+++ b/packages/solid-router/src/router.ts
@@ -3,6 +3,7 @@ import { createFileRoute, createLazyFileRoute } from './fileRoute'
 import type { RouterHistory } from '@tanstack/history'
 import type {
   AnyRoute,
+  RouterPlugin,
   CreateRouterFn,
   RouterConstructorOptions,
   TrailingSlashOption,
@@ -74,7 +75,11 @@ declare module '@tanstack/router-core' {
 }
 
 export const createRouter: CreateRouterFn = (options) => {
-  return new Router(options)
+  // `as any` is safe: CreateRouterFn enforces full type safety at the call
+  // site (including plugin-aware context). The Router constructor widens
+  // TPlugins to ReadonlyArray<RouterPlugin<any>> since the class only
+  // iterates plugins at runtime and doesn't need the exact tuple type.
+  return new Router(options as any)
 }
 
 export class Router<
@@ -96,7 +101,8 @@ export class Router<
       TTrailingSlashOption,
       TDefaultStructuralSharingOption,
       TRouterHistory,
-      TDehydrated
+      TDehydrated,
+      ReadonlyArray<RouterPlugin<any>>
     >,
   ) {
     super(options)
diff --git a/packages/solid-router/tests/router.test-d.tsx b/packages/solid-router/tests/router.test-d.tsx
index 78efa0b318..1ea3575591 100644
--- a/packages/solid-router/tests/router.test-d.tsx
+++ b/packages/solid-router/tests/router.test-d.tsx
@@ -6,7 +6,7 @@ import {
   createRoute,
   createRouter,
 } from '../src'
-import type { RouterHistory } from '../src'
+import type { RouterHistory, RouterPlugin } from '../src'
 
 test('when creating a router without context', () => {
   const rootRoute = createRootRoute()
@@ -250,3 +250,160 @@ test('when creating a router with custom router history', () => {
   expectTypeOf(router.history).toMatchTypeOf<RouterHistory>()
   expectTypeOf(router.history).toEqualTypeOf<typeof customRouterHistory>()
 })
+
+test('when a plugin provides all required context keys, context becomes optional', () => {
+  const rootRoute = createRootRouteWithContext<{ queryClient: string }>()()
+
+  type RouteTree = typeof rootRoute
+  type Plugins = readonly [RouterPlugin<{ queryClient: string }>]
+
+  // Plugin provides queryClient, so context becomes optional
+  expectTypeOf(
+    createRouter<
+      RouteTree,
+      'never',
+      false,
+      RouterHistory,
+      Record<string, any>,
+      Plugins
+    >,
+  )
+    .parameter(0)
+    .not.toMatchTypeOf<{ context: { queryClient: string } }>()
+
+  expectTypeOf(
+    createRouter<
+      RouteTree,
+      'never',
+      false,
+      RouterHistory,
+      Record<string, any>,
+      Plugins
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('context')
+    .toMatchTypeOf<{ queryClient?: string } | undefined>()
+})
+
+test('when a plugin provides some required context keys, remaining keys are still required', () => {
+  const rootRoute = createRootRouteWithContext<{
+    queryClient: string
+    userToken: string
+  }>()()
+
+  type RouteTree = typeof rootRoute
+  type Plugins = readonly [RouterPlugin<{ queryClient: string }>]
+
+  expectTypeOf(
+    createRouter<
+      RouteTree,
+      'never',
+      false,
+      RouterHistory,
+      Record<string, any>,
+      Plugins
+    >,
+  )
+    .parameter(0)
+    .toMatchTypeOf<{
+      context: { userToken: string }
+    }>()
+
+  expectTypeOf(
+    createRouter<
+      RouteTree,
+      'never',
+      false,
+      RouterHistory,
+      Record<string, any>,
+      Plugins
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('context')
+    .toEqualTypeOf<{ userToken: string } & { queryClient?: string }>()
+})
+
+test('when multiple plugins provide different context keys, all plugin keys are omitted', () => {
+  const rootRoute = createRootRouteWithContext<{
+    queryClient: string
+    authToken: string
+    appName: string
+  }>()()
+
+  type RouteTree = typeof rootRoute
+  type Plugins = readonly [
+    RouterPlugin<{ queryClient: string }>,
+    RouterPlugin<{ authToken: string }>,
+  ]
+
+  expectTypeOf(
+    createRouter<
+      RouteTree,
+      'never',
+      false,
+      RouterHistory,
+      Record<string, any>,
+      Plugins
+    >,
+  )
+    .parameter(0)
+    .toMatchTypeOf<{
+      context: { appName: string }
+    }>()
+
+  expectTypeOf(
+    createRouter<
+      RouteTree,
+      'never',
+      false,
+      RouterHistory,
+      Record<string, any>,
+      Plugins
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('context')
+    .toEqualTypeOf<
+      { appName: string } & { queryClient?: string; authToken?: string }
+    >()
+})
+
+test('when no plugins are provided, context is still required if route tree demands it', () => {
+  const rootRoute = createRootRouteWithContext<{ userId: string }>()()
+
+  type RouteTree = typeof rootRoute
+
+  expectTypeOf(createRouter<RouteTree, 'never'>)
+    .parameter(0)
+    .toHaveProperty('context')
+    .toEqualTypeOf<{ userId: string }>()
+
+  expectTypeOf(createRouter<RouteTree, 'never'>)
+    .parameter(0)
+    .toMatchTypeOf<{
+      context: { userId: string }
+    }>()
+})
+
+test('when a plugin provides context keys not defined in the route tree, context remains optional', () => {
+  const rootRoute = createRootRoute()
+
+  type RouteTree = typeof rootRoute
+  type Plugins = readonly [RouterPlugin<{ analytics: object }>]
+
+  expectTypeOf(
+    createRouter<
+      RouteTree,
+      'never',
+      false,
+      RouterHistory,
+      Record<string, any>,
+      Plugins
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('context')
+    .toEqualTypeOf<{} | undefined>()
+})
diff --git a/packages/start-client-core/src/createStartContextBridge.ts b/packages/start-client-core/src/createStartContextBridge.ts
index 13a01f89bc..ee15d878f7 100644
--- a/packages/start-client-core/src/createStartContextBridge.ts
+++ b/packages/start-client-core/src/createStartContextBridge.ts
@@ -1,4 +1,3 @@
-import { createIsomorphicFn } from '@tanstack/start-fn-stubs'
 import { isServer } from '@tanstack/router-core/isServer'
 import { getGlobalStartContext } from './getGlobalStartContext'
 import { safeObjectMerge } from './safeObjectMerge'
@@ -7,6 +6,7 @@ import type {
   AnyRouter,
   Expand,
   Register,
+  RouterPlugin,
   ValidateSerializableInput,
 } from '@tanstack/router-core'
 
@@ -19,72 +19,89 @@ export type StartContextBridgeOptions<
   ) => ValidateSerializableInput<Register, TSelected>
 }
 
-export type StartContextBridge<TSelected extends Record<string, unknown>> = {
-  key: string
-  /** Install dehydrate/hydrate integration for a router instance */
-  setup: (router: AnyRouter) => void
-  /** Get selected context */
-  get: () => TSelected
-}
-
+/**
+ * Create a context bridge that transports server-side context values
+ * to the client via dehydrate/hydrate.
+ *
+ * Pass the returned plugin in `plugins` when creating a router:
+ *
+ * @example
+ * ```ts
+ * createRouter({
+ *   routeTree,
+ *   context: { static: 'static-value' },
+ *   plugins: [
+ *     createStartContextBridge({
+ *       select: (ctx) => ({ a: ctx.a, c: ctx.c }),
+ *     }),
+ *   ],
+ * })
+ * ```
+ */
 export function createStartContextBridge<
   TSelected extends Record<string, unknown>,
->(opts: StartContextBridgeOptions<TSelected>): StartContextBridge<TSelected> {
+>(opts: StartContextBridgeOptions<TSelected>): RouterPlugin<TSelected> {
   const key = opts.key ?? '__tsrStartContextBridge'
 
-  const get = createIsomorphicFn()
-    .client((): TSelected => {
-      return {} as unknown as TSelected
-    })
-    .server((): TSelected => {
-      return opts.select(getGlobalStartContext()) as unknown as TSelected
-    })
-
-  const setup = (router: AnyRouter) => {
-    if (isServer ?? router.isServer) {
-      const ogDehydrate = router.options.dehydrate
-      router.options.dehydrate = () => {
-        const og = ogDehydrate?.()
+  const plugin: RouterPlugin<TSelected> = {
+    '~types': null as any,
+    setup: (router: AnyRouter) => {
+      if (isServer ?? router.isServer) {
+        // On the server: merge selected context into the router context eagerly
         const startCtx = getGlobalStartContext()
-        const selected = opts.select(startCtx) as unknown as Record<
-          string,
-          unknown
-        >
-
-        if (og === undefined) {
-          return { [key]: selected } as any
-        }
-
-        if (!og || typeof og !== 'object') {
-          throw new Error(
-            `createStartContextBridge: router.options.dehydrate must return an object (or undefined). Got '${typeof og}'.`,
+        if (startCtx) {
+          const selected = opts.select(startCtx) as unknown as Record<
+            string,
+            unknown
+          >
+          router.options.context = safeObjectMerge(
+            router.options.context,
+            selected,
           )
         }
 
-        return {
-          ...og,
-          [key]: selected,
+        // Hook into dehydrate to serialize the selected values for the client
+        const ogDehydrate = router.options.dehydrate
+        router.options.dehydrate = async () => {
+          const og = await ogDehydrate?.()
+          const startCtx = getGlobalStartContext()
+          const selected = opts.select(startCtx) as unknown as Record<
+            string,
+            unknown
+          >
+
+          if (og === undefined) {
+            return { [key]: selected } as any
+          }
+
+          if (!og || typeof og !== 'object') {
+            throw new Error(
+              `createStartContextBridge: router.options.dehydrate must return an object (or undefined). Got '${typeof og}'.`,
+            )
+          }
+
+          return {
+            ...og,
+            [key]: selected,
+          }
         }
-      }
-    } else {
-      const ogHydrate = router.options.hydrate
-      router.options.hydrate = async (dehydrated: any) => {
-        await ogHydrate?.(dehydrated)
-        const selected = dehydrated?.[key] as
-          | Record<string, unknown>
-          | undefined
-        if (!selected) return
+      } else {
+        // On the client: hook into hydrate to deserialize bridged values
+        const ogHydrate = router.options.hydrate
+        router.options.hydrate = async (dehydrated: any) => {
+          await ogHydrate?.(dehydrated)
+          const selected = dehydrated?.[key] as
+            | Record<string, unknown>
+            | undefined
+          if (!selected) return
 
-        router.update({
-          context: safeObjectMerge(router.options.context, selected),
-        })
+          router.update({
+            context: safeObjectMerge(router.options.context, selected),
+          })
+        }
       }
-    }
+    },
   }
 
-  return {
-    key,
-    setup,
-    get,
-  }
+  return plugin
 }
diff --git a/packages/vue-router-ssr-query/src/index.tsx b/packages/vue-router-ssr-query/src/index.tsx
index c9b666acad..c492de6449 100644
--- a/packages/vue-router-ssr-query/src/index.tsx
+++ b/packages/vue-router-ssr-query/src/index.tsx
@@ -1,7 +1,11 @@
 import * as Vue from 'vue'
-import { setupCoreRouterSsrQueryIntegration } from '@tanstack/router-ssr-query-core'
-import type { RouterSsrQueryOptions } from '@tanstack/router-ssr-query-core'
-import type { AnyRouter } from '@tanstack/vue-router'
+import { createCoreSsrQueryPlugin } from '@tanstack/router-ssr-query-core'
+import type {
+  RouterSsrQueryOptions,
+  SsrQueryPluginOptions,
+} from '@tanstack/router-ssr-query-core'
+import type { AnyRouter, RouterPlugin } from '@tanstack/vue-router'
+import type { QueryClient } from '@tanstack/vue-query'
 
 // Vue Query uses this string as the injection key
 const VUE_QUERY_CLIENT = 'VUE_QUERY_CLIENT'
@@ -11,28 +15,75 @@ export type Options<TRouter extends AnyRouter> =
     wrapQueryClient?: boolean
   }
 
-export function setupRouterSsrQueryIntegration<TRouter extends AnyRouter>(
-  opts: Options<TRouter>,
-) {
-  setupCoreRouterSsrQueryIntegration(opts)
+export type SsrQueryPluginVueOptions = SsrQueryPluginOptions & {
+  /**
+   * If `false`, the plugin will not wrap the router with the Vue Query provider.
+   * Useful if you are already providing the QueryClient via a parent provider.
+   *
+   * @default true
+   */
+  wrapQueryClient?: boolean
+}
 
-  if (opts.wrapQueryClient === false) {
-    return
-  }
+/**
+ * Create a router plugin that integrates TanStack Query with SSR for Vue.
+ *
+ * The plugin contributes `{ queryClient: QueryClient }` to the router context,
+ * sets up dehydrate/hydrate for SSR, and provides the QueryClient via Vue's injection system.
+ *
+ * @example
+ * ```ts
+ * const queryClient = new QueryClient()
+ * const ssrQueryPlugin = createSsrQueryPlugin({ queryClient })
+ *
+ * const router = createRouter({
+ *   routeTree,
+ *   plugins: [ssrQueryPlugin],
+ * })
+ * ```
+ */
+export function createSsrQueryPlugin(
+  opts: SsrQueryPluginVueOptions,
+): RouterPlugin<{ queryClient: QueryClient }> {
+  const corePlugin = createCoreSsrQueryPlugin(opts)
+
+  return {
+    '~types': null as any,
+    setup: (router: AnyRouter) => {
+      corePlugin.setup(router)
 
-  const OGWrap = opts.router.options.Wrap
-
-  opts.router.options.Wrap = Vue.defineComponent({
-    name: 'QueryClientWrapper',
-    setup(_, { slots }) {
-      Vue.provide(VUE_QUERY_CLIENT, opts.queryClient)
-      return () => {
-        const children = slots.default?.()
-        if (OGWrap) {
-          return Vue.h(OGWrap, null, () => children)
-        }
-        return children
+      if (opts.wrapQueryClient === false) {
+        return
       }
+
+      const OGWrap = router.options.Wrap
+
+      router.options.Wrap = Vue.defineComponent({
+        name: 'QueryClientWrapper',
+        setup(_, { slots }) {
+          Vue.provide(VUE_QUERY_CLIENT, opts.queryClient)
+          return () => {
+            const children = slots.default?.()
+            if (OGWrap) {
+              return Vue.h(OGWrap, null, () => children)
+            }
+            return children
+          }
+        },
+      }) as any
     },
-  }) as any
+  }
+}
+
+/**
+ * @deprecated Use `createSsrQueryPlugin` instead. This function will be removed in a future version.
+ */
+export function setupRouterSsrQueryIntegration<TRouter extends AnyRouter>(
+  opts: Options<TRouter>,
+) {
+  createSsrQueryPlugin({
+    queryClient: opts.queryClient,
+    handleRedirects: opts.handleRedirects,
+    wrapQueryClient: opts.wrapQueryClient,
+  }).setup(opts.router)
 }
diff --git a/packages/vue-router/src/index.tsx b/packages/vue-router/src/index.tsx
index 61c15c53f6..3d2e124bee 100644
--- a/packages/vue-router/src/index.tsx
+++ b/packages/vue-router/src/index.tsx
@@ -214,6 +214,9 @@ export type {
   RouteMask,
   MatchRouteOptions,
   CreateLazyFileRoute,
+  RouterPlugin,
+  RouterPluginTypes,
+  PluginProvidedContext,
 } from '@tanstack/router-core'
 export type {
   UseLinkPropsOptions,
diff --git a/packages/vue-router/src/router.ts b/packages/vue-router/src/router.ts
index 8c9679c08d..cab9d6b4b6 100644
--- a/packages/vue-router/src/router.ts
+++ b/packages/vue-router/src/router.ts
@@ -2,6 +2,7 @@ import { RouterCore } from '@tanstack/router-core'
 import type { RouterHistory } from '@tanstack/history'
 import type {
   AnyRoute,
+  RouterPlugin,
   CreateRouterFn,
   RouterConstructorOptions,
   TrailingSlashOption,
@@ -73,7 +74,11 @@ declare module '@tanstack/router-core' {
 }
 
 export const createRouter: CreateRouterFn = (options) => {
-  return new Router(options)
+  // `as any` is safe: CreateRouterFn enforces full type safety at the call
+  // site (including plugin-aware context). The Router constructor widens
+  // TPlugins to ReadonlyArray<RouterPlugin<any>> since the class only
+  // iterates plugins at runtime and doesn't need the exact tuple type.
+  return new Router(options as any)
 }
 
 export class Router<
@@ -95,7 +100,8 @@ export class Router<
       TTrailingSlashOption,
       TDefaultStructuralSharingOption,
       TRouterHistory,
-      TDehydrated
+      TDehydrated,
+      ReadonlyArray<RouterPlugin<any>>
     >,
   ) {
     super(options)
diff --git a/packages/vue-router/tests/router.test-d.tsx b/packages/vue-router/tests/router.test-d.tsx
index 78efa0b318..179250de3e 100644
--- a/packages/vue-router/tests/router.test-d.tsx
+++ b/packages/vue-router/tests/router.test-d.tsx
@@ -6,7 +6,7 @@ import {
   createRoute,
   createRouter,
 } from '../src'
-import type { RouterHistory } from '../src'
+import type { RouterHistory, RouterPlugin } from '../src'
 
 test('when creating a router without context', () => {
   const rootRoute = createRootRoute()
@@ -250,3 +250,160 @@ test('when creating a router with custom router history', () => {
   expectTypeOf(router.history).toMatchTypeOf<RouterHistory>()
   expectTypeOf(router.history).toEqualTypeOf<typeof customRouterHistory>()
 })
+
+test('when a plugin provides all required context keys, context becomes optional', () => {
+  const rootRoute = createRootRouteWithContext<{ queryClient: string }>()()
+
+  type RouteTree = typeof rootRoute
+  type Plugins = readonly [RouterPlugin<{ queryClient: string }>]
+
+  // context is optional (plugin provides all keys)
+  expectTypeOf(
+    createRouter<
+      RouteTree,
+      'never',
+      false,
+      RouterHistory,
+      Record<string, any>,
+      Plugins
+    >,
+  )
+    .parameter(0)
+    .not.toMatchTypeOf<{ context: { queryClient: string } }>()
+
+  expectTypeOf(
+    createRouter<
+      RouteTree,
+      'never',
+      false,
+      RouterHistory,
+      Record<string, any>,
+      Plugins
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('context')
+    .toMatchTypeOf<{ queryClient?: string } | undefined>()
+})
+
+test('when a plugin provides some required context keys, remaining keys are still required', () => {
+  const rootRoute = createRootRouteWithContext<{
+    queryClient: string
+    userToken: string
+  }>()()
+
+  type RouteTree = typeof rootRoute
+  type Plugins = readonly [RouterPlugin<{ queryClient: string }>]
+
+  expectTypeOf(
+    createRouter<
+      RouteTree,
+      'never',
+      false,
+      RouterHistory,
+      Record<string, any>,
+      Plugins
+    >,
+  )
+    .parameter(0)
+    .toMatchTypeOf<{
+      context: { userToken: string }
+    }>()
+
+  expectTypeOf(
+    createRouter<
+      RouteTree,
+      'never',
+      false,
+      RouterHistory,
+      Record<string, any>,
+      Plugins
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('context')
+    .toEqualTypeOf<{ userToken: string } & { queryClient?: string }>()
+})
+
+test('when multiple plugins provide different context keys, all plugin keys are omitted', () => {
+  const rootRoute = createRootRouteWithContext<{
+    queryClient: string
+    authToken: string
+    appName: string
+  }>()()
+
+  type RouteTree = typeof rootRoute
+  type Plugins = readonly [
+    RouterPlugin<{ queryClient: string }>,
+    RouterPlugin<{ authToken: string }>,
+  ]
+
+  expectTypeOf(
+    createRouter<
+      RouteTree,
+      'never',
+      false,
+      RouterHistory,
+      Record<string, any>,
+      Plugins
+    >,
+  )
+    .parameter(0)
+    .toMatchTypeOf<{
+      context: { appName: string }
+    }>()
+
+  expectTypeOf(
+    createRouter<
+      RouteTree,
+      'never',
+      false,
+      RouterHistory,
+      Record<string, any>,
+      Plugins
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('context')
+    .toEqualTypeOf<
+      { appName: string } & { queryClient?: string; authToken?: string }
+    >()
+})
+
+test('when no plugins are provided, context is still required if route tree demands it', () => {
+  const rootRoute = createRootRouteWithContext<{ userId: string }>()()
+
+  type RouteTree = typeof rootRoute
+
+  expectTypeOf(createRouter<RouteTree, 'never'>)
+    .parameter(0)
+    .toHaveProperty('context')
+    .toEqualTypeOf<{ userId: string }>()
+
+  expectTypeOf(createRouter<RouteTree, 'never'>)
+    .parameter(0)
+    .toMatchTypeOf<{
+      context: { userId: string }
+    }>()
+})
+
+test('when a plugin provides context keys not defined in the route tree, context remains optional', () => {
+  const rootRoute = createRootRoute()
+
+  type RouteTree = typeof rootRoute
+  type Plugins = readonly [RouterPlugin<{ analytics: object }>]
+
+  expectTypeOf(
+    createRouter<
+      RouteTree,
+      'never',
+      false,
+      RouterHistory,
+      Record<string, any>,
+      Plugins
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('context')
+    .toEqualTypeOf<{} | undefined>()
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0ee7b3b7d1..ad836bc11f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1840,6 +1840,70 @@ importers:
         specifier: ^5.1.4
         version: 5.1.4(typescript@5.9.2)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
 
+  e2e/react-start/multi-plugin:
+    dependencies:
+      '@tanstack/react-query':
+        specifier: ^5.90.19
+        version: 5.90.19(react@19.2.3)
+      '@tanstack/react-query-devtools':
+        specifier: ^5.90.0
+        version: 5.90.2(@tanstack/react-query@5.90.19(react@19.2.3))(react@19.2.3)
+      '@tanstack/react-router':
+        specifier: workspace:*
+        version: link:../../../packages/react-router
+      '@tanstack/react-router-devtools':
+        specifier: workspace:^
+        version: link:../../../packages/react-router-devtools
+      '@tanstack/react-router-ssr-query':
+        specifier: workspace:*
+        version: link:../../../packages/react-router-ssr-query
+      '@tanstack/react-start':
+        specifier: workspace:*
+        version: link:../../../packages/react-start
+      react:
+        specifier: ^19.2.3
+        version: 19.2.3
+      react-dom:
+        specifier: ^19.2.3
+        version: 19.2.3(react@19.2.3)
+    devDependencies:
+      '@playwright/test':
+        specifier: ^1.57.0
+        version: 1.57.0
+      '@tailwindcss/vite':
+        specifier: ^4.1.18
+        version: 4.1.18(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+      '@tanstack/router-e2e-utils':
+        specifier: workspace:^
+        version: link:../../e2e-utils
+      '@types/node':
+        specifier: 25.0.9
+        version: 25.0.9
+      '@types/react':
+        specifier: ^19.2.8
+        version: 19.2.8
+      '@types/react-dom':
+        specifier: ^19.2.3
+        version: 19.2.3(@types/react@19.2.8)
+      '@vitejs/plugin-react':
+        specifier: ^4.3.4
+        version: 4.7.0(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+      srvx:
+        specifier: ^0.10.1
+        version: 0.10.1
+      tailwindcss:
+        specifier: ^4.1.18
+        version: 4.1.18
+      typescript:
+        specifier: ^5.7.2
+        version: 5.9.3
+      vite:
+        specifier: ^7.3.1
+        version: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+      vite-tsconfig-paths:
+        specifier: ^5.1.4
+        version: 5.1.4(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+
   e2e/react-start/query-integration:
     dependencies:
       '@tanstack/react-query':
@@ -8397,7 +8461,7 @@ importers:
         version: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
       vitest:
         specifier: ^3.2.4
-        version: 3.2.4(@types/node@25.0.9)(@vitest/browser@4.0.17(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.17))(@vitest/ui@4.0.17(vitest@4.0.17))(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+        version: 3.2.4(@types/node@25.0.9)(@vitest/browser@4.0.17(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.17))(@vitest/ui@4.0.17)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
       web-vitals:
         specifier: ^5.1.0
         version: 5.1.0
@@ -10687,7 +10751,7 @@ importers:
     devDependencies:
       '@netlify/vite-plugin-tanstack-start':
         specifier: ^1.1.4
-        version: 1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.9.2)(rollup@4.55.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+        version: 1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(rollup@4.55.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
       '@tailwindcss/vite':
         specifier: ^4.1.18
         version: 4.1.18(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
@@ -10913,7 +10977,7 @@ importers:
         version: 2.11.10(@testing-library/jest-dom@6.6.3)(solid-js@1.9.10)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
       vitest:
         specifier: ^3.2.4
-        version: 3.2.4(@types/node@25.0.9)(@vitest/browser@4.0.17(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.17))(@vitest/ui@4.0.17(vitest@4.0.17))(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+        version: 3.2.4(@types/node@25.0.9)(@vitest/browser@4.0.17(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.17))(@vitest/ui@4.0.17)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
       web-vitals:
         specifier: ^5.1.0
         version: 5.1.0
@@ -27354,13 +27418,13 @@ snapshots:
       uuid: 11.1.0
       write-file-atomic: 5.0.1
 
-  '@netlify/dev@4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.9.2)(rollup@4.55.3)':
+  '@netlify/dev@4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(rollup@4.55.3)':
     dependencies:
       '@netlify/blobs': 10.1.0
       '@netlify/config': 23.2.0
       '@netlify/dev-utils': 4.3.0
       '@netlify/edge-functions-dev': 1.0.0
-      '@netlify/functions-dev': 1.0.0(encoding@0.1.13)(rollup@4.55.3)
+      '@netlify/functions-dev': 1.0.0(rollup@4.55.3)
       '@netlify/headers': 2.1.0
       '@netlify/images': 1.3.0(@netlify/blobs@10.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)
       '@netlify/redirects': 3.1.0
@@ -27428,12 +27492,12 @@ snapshots:
     dependencies:
       '@netlify/types': 2.1.0
 
-  '@netlify/functions-dev@1.0.0(encoding@0.1.13)(rollup@4.55.3)':
+  '@netlify/functions-dev@1.0.0(rollup@4.55.3)':
     dependencies:
       '@netlify/blobs': 10.1.0
       '@netlify/dev-utils': 4.3.0
       '@netlify/functions': 5.0.0
-      '@netlify/zip-it-and-ship-it': 14.1.11(encoding@0.1.13)(rollup@4.55.3)
+      '@netlify/zip-it-and-ship-it': 14.1.11(rollup@4.55.3)
       cron-parser: 4.9.0
       decache: 4.6.2
       extract-zip: 2.0.1
@@ -27523,9 +27587,9 @@ snapshots:
 
   '@netlify/types@2.1.0': {}
 
-  '@netlify/vite-plugin-tanstack-start@1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.9.2)(rollup@4.55.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))':
+  '@netlify/vite-plugin-tanstack-start@1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(rollup@4.55.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))':
     dependencies:
-      '@netlify/vite-plugin': 2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.9.2)(rollup@4.55.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+      '@netlify/vite-plugin': 2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(rollup@4.55.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
       vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
     optionalDependencies:
       '@tanstack/solid-start': link:packages/solid-start
@@ -27553,9 +27617,9 @@ snapshots:
       - supports-color
       - uploadthing
 
-  '@netlify/vite-plugin@2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.9.2)(rollup@4.55.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))':
+  '@netlify/vite-plugin@2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(rollup@4.55.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))':
     dependencies:
-      '@netlify/dev': 4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.9.2)(rollup@4.55.3)
+      '@netlify/dev': 4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(rollup@4.55.3)
       '@netlify/dev-utils': 4.3.0
       dedent: 1.7.0(babel-plugin-macros@3.1.0)
       vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
@@ -27583,13 +27647,13 @@ snapshots:
       - supports-color
       - uploadthing
 
-  '@netlify/zip-it-and-ship-it@14.1.11(encoding@0.1.13)(rollup@4.55.3)':
+  '@netlify/zip-it-and-ship-it@14.1.11(rollup@4.55.3)':
     dependencies:
       '@babel/parser': 7.28.5
       '@babel/types': 7.28.4
       '@netlify/binary-info': 1.0.0
       '@netlify/serverless-functions-api': 2.7.1
-      '@vercel/nft': 0.29.4(encoding@0.1.13)(rollup@4.55.3)
+      '@vercel/nft': 0.29.4(rollup@4.55.3)
       archiver: 7.0.1
       common-path-prefix: 3.0.0
       copy-file: 11.1.0
@@ -30794,7 +30858,7 @@ snapshots:
   '@unrs/resolver-binding-win32-x64-msvc@1.11.1':
     optional: true
 
-  '@vercel/nft@0.29.4(encoding@0.1.13)(rollup@4.55.3)':
+  '@vercel/nft@0.29.4(rollup@4.55.3)':
     dependencies:
       '@mapbox/node-pre-gyp': 2.0.0(encoding@0.1.13)
       '@rollup/pluginutils': 5.1.4(rollup@4.55.3)
@@ -38261,7 +38325,7 @@ snapshots:
     optionalDependencies:
       vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
 
-  vitest@3.2.4(@types/node@25.0.9)(@vitest/browser@4.0.17(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.17))(@vitest/ui@4.0.17(vitest@4.0.17))(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1):
+  vitest@3.2.4(@types/node@25.0.9)(@vitest/browser@4.0.17(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.17))(@vitest/ui@4.0.17)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1):
     dependencies:
       '@types/chai': 5.2.2
       '@vitest/expect': 3.2.4

From 3d09332664e7bea7d7f469350de81d77bc536506 Mon Sep 17 00:00:00 2001
From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com>
Date: Fri, 6 Feb 2026 18:38:00 +0000
Subject: [PATCH 3/3] ci: apply automated fixes

---
 docs/router/integrations/query.md                  | 1 -
 docs/start/framework/react/guide/context-bridge.md | 2 +-
 e2e/vue-start/server-functions/src/router.tsx      | 1 -
 3 files changed, 1 insertion(+), 3 deletions(-)

diff --git a/docs/router/integrations/query.md b/docs/router/integrations/query.md
index 631a7a7e26..5df16ebad7 100644
--- a/docs/router/integrations/query.md
+++ b/docs/router/integrations/query.md
@@ -61,7 +61,6 @@ The plugin provides the `queryClient` to your router context automatically via t
 
 By default, the integration wraps your router with a `QueryClientProvider`. If you already provide your own provider, pass `wrapQueryClient: false` and keep your custom wrapper.
 
-
 ### Migrating from `setupRouterSsrQueryIntegration`
 
 > [!NOTE]
diff --git a/docs/start/framework/react/guide/context-bridge.md b/docs/start/framework/react/guide/context-bridge.md
index 5b11cdccee..ac67a44fb3 100644
--- a/docs/start/framework/react/guide/context-bridge.md
+++ b/docs/start/framework/react/guide/context-bridge.md
@@ -174,6 +174,6 @@ createStartContextBridge({
 
 The context bridge integrates with the router plugin type system:
 
-- The `select` function is typed based on your registered middleware 
+- The `select` function is typed based on your registered middleware
 - The `context` option in `createRouter` becomes optional for keys that plugins provide
 - Route loaders and components see the bridged values in their `context` parameter with full type inference
diff --git a/e2e/vue-start/server-functions/src/router.tsx b/e2e/vue-start/server-functions/src/router.tsx
index 5fe7bcafa2..ef05e70515 100644
--- a/e2e/vue-start/server-functions/src/router.tsx
+++ b/e2e/vue-start/server-functions/src/router.tsx
@@ -21,4 +21,3 @@ export function getRouter() {
     plugins: [createSsrQueryPlugin({ queryClient })],
   })
 }
-
