Bun is the default host, and most of an app never notices. What follows is what an app does notice, collected from real ports: the framework’s part first, then the things that will look like the framework’s fault and are not.
Vite runs on Bun too
vite is a bin with a Node shebang, so bun run dev alone starts Vite —
and with it the dev server, the build and the prerender — under Node. An
app that imports bun, bun:sqlite or a Bun-only driver then fails at the
first render with Cannot find package 'bun', in a project that just said
it was a Bun app.
The scaffold’s scripts run Vite on Bun’s runtime, and an existing app should too:
{
"scripts": {
"dev": "bun --bun vite",
"build": "bun --bun vite build",
"start": "bun .output/server/index.mjs"
}
}createTestApp() runs that build script, so a test builds on the same
runtime the server ships on.
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:
rscKit({ serverExternalPackages: ['@acme/native-thing'] })The same applies on Node; it is only more visible on Bun because the scaffold builds there.
A build machine without the secrets
The build prerenders pages, which runs the app, which runs instrumentation.ts,
which imports env.ts. A build machine without the production variables
sets SKIP_ENV_VALIDATION=1; the server that runs the build validates at
startup regardless. And never NODE_ENV in a .env — Vite honours it, and
NODE_ENV=development turns vite build into a build that cannot render;
the build refuses it and names the
line.
Not the framework’s, but you will meet them
bun testreads.env. Bun loads the package’s.envinto every test run. A test that must not see the app’s variables — or must see only the ones it sets — runs withbun test --env-file=/dev/null.- Stripe’s SDK is async-only on Bun.
stripe.webhooks.constructEvent()throws on every call because Bun has no synchronous WebCrypto;constructEventAsync()is the same check, awaited. pgputs SQLSTATE inerrno. Bun’s Postgres driver reports the five-character SQLSTATE (23505) where Node’spgreports it ascode. Match on both if the app runs on both.- Bun’s
setTimeout(0)is a real millisecond, as Node’s is. Nothing to do; worth knowing when a test counts ticks.
Everything else — presets, deployment, the single binary — is on Where it runs.