# Where it runs

> A host is a Nitro preset, not a server you write.

There used to be a page here for each runtime, and a server file generated to
match. There is no server file now, and no choice about it either:
[Nitro](https://nitro.build) builds the server around the route tree, and
choosing where an app runs is choosing a preset.

```ts title="vite.config.ts"
import { defineConfig } from 'vite';
import { nitro } from 'nitro/vite';
import react from '@vitejs/plugin-react';
import { rscKit } from '@rsc-kit/core/vite';

export default defineConfig({
  plugins: [
    nitro({ preset: 'bun', serveStatic: 'inline' }),
    rscKit(),
    react(),
  ],
});
```

`bun create rsc-kit@latest my-app` writes that for you. Changing where it deploys is
changing the one string.

## The presets

`rsc-kit` asks about three, because those are the ones it can check for you.
Nitro carries [many more](https://nitro.build/deploy) and they need nothing from
this package — a preset is a string, not an integration.

| `--host` | preset | what you get |
| --- | --- | --- |
| `bun` | `bun` | `.output/server/index.mjs`, and `bun run compile` for a single binary |
| `node` | `node` | `.output/server/index.mjs`, run with `node` |
| `worker` | `cloudflare_module` | a Worker, plus the `wrangler.json` and `_headers` Nitro generates |

Everything else — Vercel, Netlify, Azure, Deno, AWS Amplify — is the same
change:

```ts
nitro({ preset: 'vercel', serveStatic: 'inline' })
```

## Streams on serverless platforms

`events()` routes and `refreshOn` hold a connection open. On Workers and
serverless functions each isolate or instance watches for its own tabs, a
platform's request limits cut streams (they reconnect and catch up), and
versions must live in a shared store. [On Workers and serverless](/guides/live-data#on-workers-and-serverless)
has the details.

## Running it

```bash
npm run dev      # vite is the renderer; nothing is prebuilt
npm run build    # writes .output/
npm run start    # runs .output/server/index.mjs
```

A Worker has no `start`, because it is deployed rather than started:

```bash
npm run preview  # wrangler dev, on workerd
npm run deploy   # nitro deploy --prebuilt
```

## Compiling to a single binary

Bun only, and the whole application ends up inside one file — engine, route
tree, frozen pages and assets, the precompressed variants included:

```bash
npm run compile   # builds, then bun build --compile .output/server/compile.mjs
./dist/app
```

`compile.mjs` is written by the build beside the server. It is what puts the
frozen pages inside the binary: the server reads them through a computed
import, which a compile cannot see, and this entry imports them by name and
hands them over before starting the server. Compile `index.mjs` instead and
the binary still works — it renders those pages live. Nothing else has to
travel with the binary: not `.output/public`, not `.output/server`. A
`Dockerfile` copies `dist/app` and runs it.

It builds first on purpose. Compiling whatever `.output` happens to hold means
a binary one version behind the source with nothing to say so — and on a
project that has never been built, an `ENOENT` naming a path the app did not
write.

Partially prerendered pages are resumed inside the binary exactly as they are
by the built server. Compiling merges every module into one scope and renames
whatever collides — `--keep-names` does not prevent that — so the build pins
each component's name as a `displayName` the shell can rely on. No flag.

`--compile` implies `--production`; `--target bun` is the default. `--bytecode`
compiles the JavaScript ahead of time for a faster cold start at the cost
of a larger binary — a production port measured 275 ms → 72 ms to the first
healthy response for 81 MB → 117 MB, and left it out, since on a pod the
pull costs more than the boot saves.

Bytecode is CommonJS, and in CommonJS `import.meta` does not exist: Bun
lowers `import.meta.url`, `.dirname`, `.dir` and `.main`, and a module
using any other form — `.env`, `.filename`, `.resolve`, the object itself —
fails the step. Bun then reports "Failed to generate bytecode", names no
file, and **exits 0**, so a CI ships a binary that dies at boot with
"import.meta is only valid inside modules". The engine's own output is
clean; a dependency's `import.meta.env` or `import.meta.resolve` is the
usual cause. `--bytecode --format=esm` applies regardless: the same
bytecode, as a module.

<Aside type="note" title="A Worker serves the frozen pages from a module">
  A Worker has no filesystem, so the directory the build writes is not there
  at request time. The build also writes the same pages as one module beside
  the bundle, `rsc-static-inline.mjs`, which wrangler uploads with the rest
  and the server imports when the directory is missing. Nothing to configure;
  it is why `○` on a Worker means the stored page and not a live render that
  happens to be the same. It counts toward the Worker's script size, which is
  worth knowing on a site with hundreds of frozen pages.
</Aside>

<Aside type="caution" title="serveStatic: 'inline' is what makes this work">
  Without it the binary compiles, starts, serves pages, and 404s every asset.
  Inside a compiled binary the static path resolves into Bun's virtual
  filesystem — where the files on disk are not — and the failure is
  `ENOENT: /$bunfs/public/assets/…` with the pages themselves looking fine.

  The generated config sets it. If you write your own, set it too.
</Aside>

## Native dependencies stay outside the bundle

A package with a native binary — `sharp`, `bcrypt`, `better-sqlite3`,
`@prisma/client` — cannot be rolled into a server bundle: the build succeeds
and the server cannot load its own binary. The usual ones are left external
by default, and Nitro traces each into `.output/server/node_modules` with its
binaries, so the deployment is still one directory. For one that is not on
the list:

```ts
rscKit({ serverExternalPackages: ['@acme/native-thing'] })
```

## A polyfill runs first

A dependency that checks for a polyfill at module evaluation — `tsyringe`
wants `Reflect.getMetadata`, under `@peculiar/x509`, under
`@simplewebauthn/server` — depends on `import 'reflect-metadata'` running
before it, and the source has it there. A bundler does not keep that place:
it emits a chunk's imports before its external ones, whatever the source
said, so the check ran first — survivable by luck as a directory, fatal
compiled into a binary. When the project's graph has `reflect-metadata`
anywhere in it, the build loads it in a Nitro plugin, which the server's
entry evaluates before any of the app is imported; a project without it
has nothing that checks. Nothing to add to `instrumentation.ts`.

## A dependency's `"use client"` is read

The other direction. A dependency is bundled into the server graphs — and
so has its `"use client"` directives read — when it declares `react` as a
peer dependency, which is what a React library does. One that imports React
and never says so (a generated component wrapper, a workspace package with
`react` under `dependencies`, a package whose author forgot) would be left
external, and its directive is then a string nobody reads: the server loads
it as a server module and the first `useState` in it fails at render, with an
error that names React and not the package.

So the build reads the direct dependencies once, and bundles any that carry
a `"use client"` file and plugin-rsc would not have. It says so:

```text
[rsc-kit] bundling @acme/chat-widget: it has "use client" files but does not
declare react as a peer dependency, so its components would otherwise run
on the server.
```

Nothing to configure; the line is there so the package's author hears about
the missing peer. The React-using dependencies under such a package — the
runtime a generated wrapper calls into — are bundled with it, so there is
one React for all of them: left external, that runtime would load React
through Node while everything else got it through Vite, and the hooks
dispatcher is null in one of the two. Only direct dependencies are read for
the directive itself — a directive two levels down is the concern of the
package between, which declared it.

## Offline

`rscKit({ offline: true })` writes a service worker into `.output/public`
alongside the assets, so a page someone has visited survives a reload with no
network. Off by default, and covered in [Offline](/guides/offline#surviving-without-one).

## Why `nitro` is pinned

The generated `package.json` pins an exact version rather than a range:

```json
"nitro": "3.0.260903-beta"
```

Nitro's own `latest` tag is a dated prerelease, and it sorts **above** the plain
`3.0.0` on npm. So `^3.0.0` resolves to the older release, which builds without
complaint and then answers 404 to every route. TanStack Start pins a dated beta
for the same reason.

## Assets are Nitro's

The build writes browser assets to `.output/public`, and Nitro serves them from
its own root. There is no `assetsDir` or `assetsUrl` to set: both were removed,
and `rscKit()` refuses a config that still passes them rather than reading a
prefix and ignoring it.

The alternative was the silent version — the markup asks for the app's prefix,
Nitro answers at its own, and the page arrives unstyled and never hydrates with
nothing logged anywhere.

Assets live in `.output/public` and are served by the same process that serves
your pages. If you want nginx or a CDN serving them instead, point it at
`.output/public` — that directory is the deployment.

## Nothing is sent raw

A bun or node server answering the internet by itself compresses what it
sends, because nothing in front of it will. The build writes a `.br` and a
`.gz` beside every public asset larger than a kilobyte, and Nitro serves
whichever the request accepts — a 147 kB stylesheet is 18 kB on the wire.
The host gzips what it answers itself — documents, streamed pages, RSC
payloads, stored pages, api routes — for a request that accepts it, flushing
every chunk so a streamed shell still reaches the browser before the holes
fill; a stored page is compressed once and kept.

A port measured what this is worth: with every byte raw, first paint on a
throttled phone was 3.8 s where 1.2 s was the baseline. That was the whole
regression.

On a Worker the platform compresses and the host does nothing. Behind a
CDN or nginx that compresses, the proxy sees an already-encoded answer and
passes it through. A deployment that would rather its proxy did all of it
turns the host's half off with `compress: false` on the handler, and Nitro's
with `compressPublicAssets: false` in its config; `Cache-Control:
no-transform` on an answer leaves that answer alone.

---

Next: [Deploying →](/hosts/deployment)

## Which entry point is whose

`@rsc-kit/core/host` is the engine's front door for a **host adapter** — the
handler, and beside it the few functions an adapter needs when it embeds the
engine. Several of those are re-exports of app-facing modules, so an editor's
auto-import may offer `redirect` or `revalidate` from `host`. They are the
same functions; an app imports them from their own entry points:

| in an app | not |
| --- | --- |
| `@rsc-kit/core/redirect` | `@rsc-kit/core/host` |
| `@rsc-kit/core/revalidate` | `@rsc-kit/core/host` |

Only an adapter imports `host`. The re-exports there are marked internal, so
they sort below the app entry in a completion list.
