> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tuturuuu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Start bridge: generic pages and the conversion codemod

> Render App Router pages generically in Start's RSC environment and convert a Next satellite onto @tuturuuu/start-bridge with one command.

This page extends the [Start bridge](https://docs.tuturuuu.com/build/devops/start-bridge.md) v0 contract
with two additive pieces: an RSC-based **generic page adapter**, so a converted
app needs no hand-written page models, and a **codemod** that converts a Next
satellite with no hand edits. Production is unchanged; conversion produces a
staging candidate only.

## Generic page adapter (RSC mode)

Next pages are async server components with `'use client'` islands. Start's RSC
environment (`@tanstack/react-start` RSC mode with `@vitejs/plugin-rsc`) runs
that code with the same boundaries Next enforces, so retained page source
renders unchanged.

Per request, a Start server function in the app (generated
`src/start-bridge/page-route.tsx`) calls `resolveBridgePage` from
`start/page-render`:

1. Resolve the location the app sees (`start/page-location`): on SSR the
   request the handler forwarded after `proxy.ts`, so rewrites apply while the
   browser keeps its URL; on client navigation the app proxy runs for the
   target, so the same URL renders the same page, a proxy redirect becomes a
   Start redirect and any other terminal response reloads the document.
   Match that app-relative path (mount stripped; a leading locale segment
   addresses a manifest with a stripped `[locale]`) against the generated
   page manifest. No match answers 404.
2. Load the page and its layout/template chain from the generated
   `page-modules.bridge.gen.ts` (lazy imports keyed by the manifest `file`).
3. Pass Next's props: `params` and `searchParams` as Promises; repeated query
   keys become arrays. A stripped `[locale]` segment resolves from the
   middleware `x-next-intl-locale` header, then `NEXT_LOCALE`, then the default.
4. Merge `metadata`/`generateMetadata` and `viewport` like Next (shallow merge,
   nearest `title.template`) and convert them to `head()` data.
5. Lift the root layout's `<html>`/`<body>` attributes into the Start shell
   (`createBridgeRootRoute`). Server-component wrappers and the RSC CSS
   fragment around the root layout are expanded first; client references are
   never called.
6. Await async pages and layouts before streaming, so `notFound()` and
   `redirect()` in their bodies answer 404 and redirect. Sync server
   components stay elements because they may call hooks.
7. Return the tree through `renderServerComponent`; the catch-all and index
   routes render it, on SSR and on client navigation alike.

| Piece | Behaviour |
| - | - |
| `startBridgePreset({ rsc: true })` | Enables Start RSC mode on top of the bridge's `next-intl/*` and `next/root-params` shims; the adapter feeds the resolved `[locale]` to `next/root-params` |
| `startBridgeRscPlugin()` (`vite/rsc`) | Resolves `next/navigation`, `next-intl` and `next-intl/navigation` per environment (server variants in `rsc`), makes `next/font/*` inert, names the rsc entries `<name>.js` because plugin-rsc imports that name from SSR while Void emits `.mjs`, and adds `rscEntries` |
| `start/server-actions` | React `'use server'` calls go through a Start server function (TanStack disables plugin-rsc's action handler); arguments use React's reply encoding, so form actions receive their `FormData` (sent as JSON entries with base64 files, because the function proxy accepts JSON POSTs only; uploads over 10 MB belong in storage); Start's CSRF middleware applies |
| `request-context` | The store is keyed on a global symbol: the rsc and SSR environments bundle separate module copies in one isolate |

### Route handlers run in the rsc environment

plugin-rsc rejects `server-only` in every environment except `rsc`, and the
Worker entry is bundled in `ssr`. The generated `vite.config.mts` registers the
API registry as the `bridge-api` rsc entry (`rscEntries`), and
`cloudflare/worker.ts` loads it with
`import.meta.viteRsc.loadModule('rsc', 'bridge-api')` before building the
handler. Route modules (and the `server-only` packages they import) are
compiled once, in `rsc`; the request store is global, so `next/headers`,
cookies and the mount see the same request. Never alias or disable plugin-rsc's
import validation to make a build pass.

`redirect()` from `next/navigation` keeps an absolute URL absolute, as in
Next. Start's router reduces a same-origin absolute href to its path, and on
the apex Web's `https://tuturuuu.com/login` is same-origin with
`tuturuuu.com/<app>`, so the bridge marks the target for its response
finalization (which otherwise adds the mount) and sets `reloadDocument` for
client navigation. Without it, a mounted app's login redirect loops into its
own `/<app>/login`.

Fonts from `next/font` resolve to inert objects; load font CSS through the app
stylesheet. A page that needs Next behaviour the adapter lacks (parallel or
intercepting routes) stays a hand-written page via `owned`/`ignored` in
`start-bridge.routes.json`.

## Converting an app

```bash theme={null}
node scripts/start-bridge-codemod/cli.mjs <app>            # dry run: unified diff + coverage
node scripts/start-bridge-codemod/cli.mjs <app> --apply    # write files, run bun add
node scripts/start-bridge-codemod/cli.mjs <app> --report   # coverage only
node scripts/start-bridge-codemod/cli.mjs <app> --apply --hold  # cutover prep: add the held record
```

The dry run reads only. `--apply` writes the same change set and installs the
pinned toolchain through `bun add --exact` in the app workspace; dependencies
are never edited into `package.json` by hand. Re-running an applied app is a
no-op. Every owned file carries a "Generated by the start-bridge codemod"
marker; an unmarked hand-written file is a conflict that blocks `--apply`
unless `--adopt` is passed. Codegen errors (an unregistered `route.*`/`page.*`),
a missing `src/proxy.ts` and, with `--hold`, a missing Vercel publisher also
block. Owned
files and the app's JSON edits are formatted with the repository's Biome.

The codemod writes or edits:

* `start-bridge.routes.json` (hand-maintained `owned`/`ignored` entries win on
  re-runs) and the bridge codegen registries, plus `page-modules.bridge.gen.ts`
  and `public-assets.bridge.gen.ts`.
* `vite.config.mts` (preset in RSC mode), `void.config.ts` (the held
  `tuturuuu-<app>-vite-staging` Worker with `TUTURUUU_NOINDEX=1`),
  `cloudflare/worker.ts` (`mounts`: `<app>.tuturuuu.com` root and the
  `tuturuuu.com/<app>` mount), `tsconfig.cloudflare.json`.
* `src/start.ts` (`bridgeServerFnFetch`), `src/router.tsx`
  (`routerBasepath()`, `ssr: { nonce: requestNonce() }`), the
  root/index/catch-all routes and `routeTree.gen.ts`. The Worker does not
  enforce `csp` by default: each app adds `createBridgeHandler({ csp })` with
  a policy that fits its scripts before it mounts on the apex.
* Package scripts `build:cloudflare`, `dev:cloudflare`, `preview:cloudflare`
  (`--configLoader runner`), `routes:check` and `type-check:cloudflare`. `build` stays `next build`, so
  the current Vercel deployment is unchanged.
* `.env.example` moves to `env.example`: Void accepts only `.env`.
* The `biome.json` override that keeps every generated `routeTree.gen.ts`
  out of Biome (formatter, linter and assist off), once for the repository.
  TanStack's generator rewrites the file in its own format on each build, and
  `bun check:changed` passes changed files to Biome explicitly.
* With `--hold` only, the held record: `tuturuuu.ci.ts`
  `productionPublicationPolicy`, the protected surface in
  `scripts/ci/production-publication-policy.ts` and its regression's app list,
  and the `production-policy` gate in `vercel-production-<app>.yaml` (as for
  Shortener).
* `tsconfig.json` excludes the Start-only sources, so the existing Next
  `type-check` is unchanged; `type-check:cloudflare` checks them against
  `tsconfig.cloudflare.json`.

> [!WARNING]
>
> A held record also holds that app's `vercel-production-<app>.yaml`, so the
> current Vercel production deployment freezes until the cutover admits the app.
> Conversion PRs therefore run without `--hold`; the app's cutover-prep PR adds
> the hold and merges just before the flip.

### Worker options

`start-bridge.config.ts` at the app root is app-owned: the codemod creates
it once (`defineBridgeAppConfig({})`) and never updates or replaces it. The
generated `cloudflare/worker.ts` passes its options on, so an app changes
Worker behaviour without editing generated files:

| Option | Effect |
| - | - |
| `csp(nonce)` | `createBridgeHandler({ csp })`: the response's Content-Security-Policy; the generated router already stamps the nonce |
| `mounts` | Overrides the generated hosts, `basePath` or `unknownHost` (`'reject'` answers 404 for unlisted hosts) |
| `apiOrigin(request, { env, path })` | Origin for one unowned `/api/*` request (e.g. Storefront's `/api/v1/inventory/**` to Inventory); `undefined` keeps the central Web API. Forwarding keeps the central forward's base stripping, loop guard and header removal |
| `wrapResponse(response, { request, env })` | Decorates every response, e.g. a staging noindex header |

API paths the app owns but forwards (Storefront's Inventory APIs) are listed
client-safely in `start-bridge.routes.json` as
`"api": { "forwarded": ["/api/v1/inventory/[...path]"] }`. The codegen merges
them into `appApiPatterns`, so the shared app-base resolver mounts them like
local routes, and the Worker, finding no local handler, forwards them through
`apiOrigin`. `start-bridge.config.ts` holds server code and is never imported
by the browser.

### Route handlers load lazily

The generated API registry (`api.lazy`) imports each route on its first
request (`lazyRouteModule`), with the route's HTTP methods read statically
from its source so `OPTIONS` and `405` need no import. One route's
dependencies (for example a Node-only SDK chain) therefore cannot fail the
Worker at startup; a route that re-exports with `export *` stays eager. In
the rsc build, CommonJS `require()` of Node built-ins becomes an ESM import
served by `nodejs_compat`, and `.wasm`/`.bin` imports become Wrangler
CompiledWasm/Data modules (as `next/og`'s Workers port needs). An app whose
Worker code runs in native SSR (`rsc: { enabled: false }`, as Calendar does)
opts its `ssr` environment into the same conversion by adding
`startBridgeNodeBuiltinsPlugin()` from `@tuturuuu/start-bridge/vite/rsc` to its
Vite plugins; rsc apps need nothing. A route that
needs a Node API workerd does not provide still fails when it is hit; serve it
from Web (`centralFirst`) or port it.

### Native CI

When the repository has the reusable `.github/workflows/native-start-app.yaml`
(#6753), the codemod also writes the app's qualification CI, modelled on
Shortener:

* `.github/workflows/<app>-cloudflare.yaml`: a thin `workflow_call` caller with
  path filters for the app, its transitive workspace dependencies, the template
  and the shared native CI scripts, and Shortener's inputs. An app without a
  `test` script runs its Vitest suite with `--passWithNoTests`. Registered in
  `tuturuuu.ci.ts`.
* `scripts/ci/<app>-native-artifact.py`: the app's Worker identity and public
  files over `scripts/ci/apps-native-artifact.py`.
* `cloudflare/native-built-worker.runtime.mjs` (`test:runtime`, run with
  `node --test` after the build; not a `*.test.*` file, so source test runs
  never pick it up): a generic
  emitted-Worker smoke run on the sealed artifact, with all egress denied:
  Worker identity, public-file parity, build identity when the app has
  `/api/build-info`, and no server error on the root, the `tuturuuu.com/<app>`
  mount or an unknown path. App-specific acceptance goes in further
  `cloudflare/*.runtime.mjs` files, which `test:runtime`
  (`node --test cloudflare/*.runtime.mjs`) runs with it.

There is no publisher: production stays with the held record and the
cutover PR.

### Generator inputs

The codemod reads app-specific behaviour from the app's existing
configuration, so apps regenerate instead of forking generated files:

| Input | Read from | Generated result |
| - | - | - |
| Proxy matcher | `src/proxy.ts` `export const config = { matcher }` | `matches: createProxyMatcher(proxyConfig.matcher, defaultProxyMatches)` in `cloudflare/worker.ts`; Next's path-to-regexp evaluates each source on the app-relative path (`has`/`missing` entries always run the proxy) |
| `centralFirst` | `next.config.*` calling `createTuturuuuWebWorkspaceApiRewrites` (`beforeFiles`) | `src/start-bridge/central-first.ts` re-exports `TUTURUUU_WEB_WORKSPACE_API_RESERVED_PATHS`; the Worker and the app-base resolver share it |
| `next/og` | any `next/og` import under `src/` | preset `og: '@cloudflare/pages-plugin-vercel-og/api'` (as Meet) and the pinned dependency |
| Assets binding | `@tuturuuu/start-bridge` exports `./assets` (#6753) | the Worker env types `ASSETS: AssetsBinding` (`import type` from `@tuturuuu/start-bridge/assets`), which the Workers `Fetcher` satisfies; the handler serves it through `fetchAsset` |
| Internal API app base | `@tuturuuu/internal-api` exports `registerDefaultAppBaseResolver` (#6764) and the app depends on it | `src/start-bridge/app-base.ts` registers `createAppBaseResolver(appApiPatterns, centralFirst)` from the codegen's client-safe `api-patterns.bridge.gen.ts`: app-owned routes are mounted in the browser and absolute on the request host on the server; Web routes are untouched |

The preset renders `@tuturuuu/vercel`'s Analytics and Speed Insights as
nothing, because Workers serve no `/_vercel/*` scripts; an app with an explicit
`shims` list opts in with `inertVercel: true`.

Each environment bundle (browser, SSR, rsc) carries its own internal-api
module, so the registration is imported by `src/router.tsx`, the page route
and the rsc API entry (`src/start-bridge/api-entry.ts`).

### Coverage report

The report classifies each page through its layouts and local import graph. A
page is generic unless it reaches an unshimmed framework specifier or a
parallel/intercepting segment. The shim list follows the bridge preset;
`'use server'` modules and `next/og` are features, not blockers.

### Verification

Codemod regressions run with `node scripts/start-bridge-codemod/<file>.test.mjs`
on disposable fixture repositories; edits to repository files are tested
against the real anchors. Each converted app still needs exact-commit CI:
unit, types, lint, the real `vp build`, the sealed artifact and the
emitted-runtime and browser acceptance.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.