A new app
bun create rsc-kit@latest my-app
cd my-app
bun run devThat is it. You get a page, a layout, a client component and a Vite config, wired together and running. There is no server file — Nitro builds one from the preset in that config when you build.
It asks a few questions, each with a flag for a script: where it runs, the
React Compiler, Tailwind, oxlint, and which validation library you want —
Zod, Valibot or ArkType. That one is asked once because everything schema-shaped
hangs off it: a form’s schema, action.input(), a page’s searchParams, and
typed environment variables, which it offers next. Say yes and you get
src/env.ts:
export const env = createEnv({
server: {
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
// DATABASE_URL: z.url(),
},
clientPrefix: 'VITE_',
client: {},
// No bare process.env: import.meta.env carries the VITE_ values, and
// the file does not throw in the browser if a client component imports it.
runtimeEnv: { ...(typeof process === 'undefined' ? {} : process.env), ...import.meta.env },
emptyStringAsUndefined: true,
})A missing or malformed variable is refused at startup with its name, not as an
undefined three calls later; env.DATABASE_URL is typed everywhere it is
read; and a server variable can never reach the browser — a browser-readable
one has to start with VITE_. That is @t3-oss/env-core,
with the schema in whichever library you chose. .env.example is written
beside it and is the one that gets committed.
Import env from server code. A client component reads its VITE_ value
from import.meta.env.VITE_… directly: importing src/env.ts there brings
the validator along to read one string, about 20 kB gzipped for Zod. The
value is still checked, because the server imports env when it starts.
bun create rsc-kit@latest my-app --validation=valibot --env # scriptedWhen you are ready to ship:
bun run build
bun run startThe build prints what it did:
○ /
○ (Static) prerendered as static content
1 staticPages marked ○ are static: rendered once at build time rather than for
each visitor.
An app you already have
Already have a project? Add rsc-kit to it:
bunx rsc-kit@latest initIt reads your package.json, works out which server you use and where your
source lives, and writes only what is missing. A project with no
package.json — a Go or Rust service getting its frontend — is given its
first one. And in an empty directory, or a fresh clone of an empty repository,
there is nothing to add to, so init starts a new app right there, leaving the
README alone and adding what the app needs ignored to the .gitignore:
server hono
source src
react will be added
+ src/app/layout.tsx
+ src/app/page.tsx
+ vite.config.ts
~ package.json — added @rsc-kit/core, react, react-dom, vite, nitro, …
~ scripts — dev, build, start, compile, typecheckNo server file. Nitro builds one around the route tree, so the app owns a route tree and a vite config and nothing in between.
It never overwrites anything. If you already have a vite.config.ts, it
prints the edit to make instead of replacing work you have done. Running it twice is safe — the second run just tells you what is already
in place.
Then:
bun install
bun run devAdd a page
Routes are directories under src/app. Create a folder, put a page.tsx in
it, and it exists:
export default function AboutPage() {
return <h1>About</h1>
}bun run build # the route tree is read at build timeDoing it by hand
If you would rather wire it up yourself — or the CLI cannot reach your setup — Installation walks through the same result one file at a time.
Where next
- Routing — layouts, dynamic segments, parallel slots
- Server actions — mutations without an API route
- Where it runs — presets, and compiling to a binary