---
title: "Installation"
description: "From an empty directory to a streaming RSC app."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.rsc-kit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Installation

import CodeFromFile from "@/components/CodeFromFile.astro";

There is no scaffolding command. An app is a Vite project with one plugin and
one request handler, so this is the whole of it.

## Create an app

One command, and it asks the rest:

```sh
bun create rsc-kit@latest my-app
```

It asks where it will run (Bun, Node or Cloudflare Workers), whether you want the
React Compiler and which implementation, and whether to include Tailwind. Every
answer has a flag, so it also runs unattended:

```sh
bun create rsc-kit@latest my-app --host=bun --compiler=oxc --tailwind
```

What comes out builds, prerenders, typechecks and serves before you edit it.

The rest of this page is for adding rsc-kit to an app you already have.

## Install

```sh
npm install @rsc-kit/core react react-dom
pnpm add @rsc-kit/core react react-dom
yarn add @rsc-kit/core react react-dom
bun add @rsc-kit/core react react-dom
```

```sh
npm install --save-dev vite @vitejs/plugin-rsc @vitejs/plugin-react @types/react @types/react-dom
pnpm add -D vite @vitejs/plugin-rsc @vitejs/plugin-react @types/react @types/react-dom
yarn add -D vite @vitejs/plugin-rsc @vitejs/plugin-react @types/react @types/react-dom
bun add -d vite @vitejs/plugin-rsc @vitejs/plugin-react @types/react @types/react-dom
```

`@rsc-kit/core` brings the Vite plugin, the render engine and the client
runtime. React 19 and Vite 8 are peer dependencies — the versions the RSC build
needs, not ones this package pins for you.

> **What @vitejs/plugin-react is for**
>
> Fast Refresh: edit a client component and React keeps its state, instead of
> the page reloading and losing it. Put it *after* `rscKit()`.

## Configure Vite

`sourceDir` is where your route tree lives. The defaults are plain Vite ones —
`src/app` in, `dist/client` and `.rsc` out — so a project that adopts them can
call `rscKit()` with nothing at all.

> **Add your own plugins after it**
>
> `rscKit()` includes `@vitejs/plugin-rsc`, which has to see modules before
> any React layer transforms them. Tailwind, the React Compiler and anything
> else go after it in the array.

## Write a root layout

The root layout renders the **whole document** — there is no separate HTML
template, and the build injects the bootstrap script and stylesheet links into
what you return here.

## Write a page

```tsx title="src/app/page.tsx"
export default function HomePage() {
  return <h1>Hello</h1>;
}
```

`app/page.tsx` serves `GET /`, wrapped in `app/layout.tsx`. Nothing registers
it — the plugin reads the directory at build time. See
[Routing](/guides/routing) for the rest of the conventions.

## Serve it

There is no server file to write. Nitro builds one around the route tree, and
`npm run build` leaves it in `.output/`:

```sh
npm run build
npm run start
```

Where it runs is the preset in `vite.config.ts` — see [Where it
runs](/hosts/where-it-runs).

## Scripts

```json title="package.json"
{
  "type": "module",
  "scripts": {
"dev": "vite",
"build": "vite build",
"start": "bun .output/server/index.mjs",
"compile": "bun build --compile .output/server/index.mjs --outfile my-app"
  }
}
```

`build` freezes every page it can, so there is no separate prerender step and
no command for one — freezing runs the app, which needs the bundle the build
just wrote, and only the build knows where that is. Turn it off with
`rscKit({ prerender: false })`.

`start` runs what the build wrote. There is no server file in a scaffolded app —
Nitro builds the server around the route tree, and where it runs is a preset in
`vite.config.ts`. See [Where it runs](/hosts/where-it-runs).

`compile` produces a standalone binary containing the engine, the route tree,
the frozen pages **and** the assets. That last part is `serveStatic: 'inline'`
in the vite config; without it the binary serves pages and 404s every asset.

> **Do not set NODE_ENV**
>
> The build stamps its mode into the bundle, so `npm run start` is
> production because it was *built* that way. Setting `NODE_ENV` yourself adds a
> second source of truth that can disagree — and when it disagrees, every page
> renders and none of them become interactive.
>
> For a development build with React's real error messages, use
> `vite build --mode development`.

Then:

```bash
npm run build   # bundles, and freezes every page it can
npm run start
```

Worth adding a `typecheck` script beside them (`tsc --noEmit`) and running it
in CI. Server components are ordinary functions and pages are ordinary
modules, so the typechecker sees the whole route tree — a page whose props do
not match its `[param]` segment is a compile error rather than a blank page.

## TypeScript

`vite/client` is doing real work in that list: without it a side-effect import
of a stylesheet (`import './styles.css'` in the root layout) is an error, and
`import.meta.env` is untyped.

### Environment variables

There is nothing to install and nothing this package adds — Vite already owns
this. Two rules and one declaration.

Anything named `VITE_*` is **inlined into the client bundle** and ships to the
browser, whether or not a browser file reads it. Everything else stays on the
server, read through `process.env`.

The prefix is the whole boundary, so never put a secret behind it.
`VITE_STRIPE_KEY` is a published key.

Declare the ones you use, and Vite types them:

```ts title="src/env.d.ts"
interface ViteTypeOptions {
  // Turns an unknown import.meta.env key into an error instead of `any`.
  strictImportMetaEnv: unknown;
}

interface ImportMetaEnv {
  readonly VITE_API_URL: string;
}
```

> **On Bun, a misspelled variable still compiles**
>
> `@types/bun` declares its own `ImportMetaEnv`, which cancels out the strictness
> Vite's option adds. You still get autocomplete and `string` instead of
> `string | undefined` — but a typo is `any`, not an error.

#### When they are read

`import.meta.env.VITE_*` is a literal in the bundle: changing one means a
rebuild. `process.env.*` is a live read, so the deploy's environment wins —
with one exception the build makes for you. A route that reads an environment
variable and nothing request-bound is frozen at build time, value included,
because nothing marked it as needing a request. That is the right answer for
`PUBLIC_SITE_NAME` and the wrong one for a feature flag flipped per deploy. For
the second kind, read it after `await connection()`, which keeps the route on
the server. See [Static generation](/guides/static-generation).

#### Validating them

Vite checks the prefix and nothing else: a missing `DATABASE_URL` is
`undefined` until the first query fails. There is nothing native for this
because nothing native is needed — [t3-env](https://env.t3.gg/docs/core) works
unchanged, with any Standard Schema validator:

```ts title="src/env.ts"
import { createEnv } from '@t3-oss/env-core';
import { z } from 'zod';

export const env = createEnv({
  server: { DATABASE_URL: z.string().url() },
  clientPrefix: 'VITE_',
  client: { VITE_API_URL: z.string().url() },
  runtimeEnv: typeof process === 'undefined' ? import.meta.env : { ...import.meta.env, ...process.env },
  emptyStringAsUndefined: true,
});
```

Read `env.DATABASE_URL` instead of `process.env.DATABASE_URL` and three things
follow. It is a `string`, not `string | undefined`. A server variable touched
from a client component throws by name rather than being silently `undefined`.
And because the module validates when it is first imported, a missing variable
fails the **build** — the prerender imports it — instead of the first request
in production.

The build writes its declaration files into `.rsc-kit/` at the project root,
not among your own source. Three of them:

- `rsc-routes.d.ts` — the routes it found, which is what makes `Link` and
  `redirect()` [typed](/guides/routing#typed-links)
- `rsc-env.d.ts` — the host global
- `rsc-engine.d.ts` — the generated bundle

Only what has to be generated is. Anything you can import, you import —
[`Metadata`](/guides/metadata/) comes from the package and works before you have
built anything.

These are ambient, which needs only that they are inside the project and that
the typechecker is told the directory exists:

```json title="tsconfig.json"
{ "include": ["src/**/*", ".rsc-kit/**/*"] }
```

A scaffolded app has that line, and `rsc-kit init` adds it. Leave it out and
the build says so — otherwise typed routes fall back to plain `string` and
nothing else would tell you.

Add `.rsc-kit/` to `.gitignore`.

## Where next

- **Routing** — [Pages, layouts, slots and navigation →](/guides/routing)
- **Server actions** — [Calling the server from a client component →](/guides/server-actions)
- **Static generation** — [Rendering ahead of time, and exporting →](/guides/static-generation)

Source: https://docs.rsc-kit.dev/installation/index.mdx
