KosmoJS loads env files the way Vite does: the same files, in the same order, with envDir and envPrefix working as they do in Vite.
| File | Loaded |
|---|---|
.env | always |
.env.local | always, meant to stay on your machine |
.env.[mode] | in the given mode |
.env.[mode].local | in the given mode, meant to stay on your machine |
Later files win: .env.[mode].local over .env.[mode], over .env.local, over .env.
Keep .local files out of git by adding *.local to your .gitignore.
$VAR, ${VAR} and ${VAR:-default} expand, even when the key lives in another file. $(...) is not run: an env file is data, not a script.
Root first, then the folder
Env files are read from two places, in this order:
my-app/
├── .env -> every folder
├── .env.local
└── src/
└── admin/
├── .env -> admin only, wins over the root
└── kosmo.config.tsA folder's files win over the root's, even over a root .env.[mode].
So the root holds what every folder shares, and a folder holds only what it overrides.
Frontend, backend and sidecar
Each side of a source folder gets its env files separately, and reads them its own way.
Frontend values arrive in import.meta.env. Only keys matching envPrefix reach it, VITE_ by default, because they end up in the browser bundle:
const api = import.meta.env.VITE_API_URL;Backend and sidecar values arrive in process.env, so routes and libraries read them as they would anywhere else. They get every key, unless you give that side an envPrefix of its own:
export default defineConfig({
backend: {
stack: "hono",
base: "/api",
viteConfig: {
// only APP_ keys reach the backend
envPrefix: "APP_",
},
},
});The dev server runs every source folder in one process, so they share one process.env. A backend key that two folders set differently ends up with one value. Keep such keys at the root, or give each folder its own name for them.
envDir
To read one side's files from somewhere else, set its envDir, relative to the source folder:
export default defineConfig({
frontend: {
stack: "react",
base: "/admin",
viteConfig: {
envDir: "env/client",
},
},
});That side then reads that directory only, neither the root nor the folder. The other side keeps the default lookup.
envDir: false turns env files off for that side.
The real environment wins
A key that is already in process.env when the server starts is never overwritten by a file.
A value set by your shell, a container or CI always wins over any env file.
Modes
The mode picks which .env.[mode] files are loaded:
| Command | Default mode |
|---|---|
kosmo serve (pnpm dev) | development |
kosmo preview | production |
kosmo build | production |
pnpm test | test |
Pass --mode to pick another:
pnpm dev --mode stage
pnpm build --mode stage
pnpm test --mode stageWith --mode stage, .env.stage and .env.stage.local are loaded on top of .env and .env.local.
Changes restart the server
Saving an env file of the current mode, at the root or in a source folder, restarts the dev server in a fresh process, and preview the same way. Every value is read again from scratch. Details ›
Production
A built backend loads no env files.
dist/run.js reads process.env as it finds it, the twelve-factor way. Set the values in your host or container, or hand Node a file:
node --env-file .env.production dist/run.js -p 4556kosmo preview loads the env files of its mode before it starts dist/run.js, so a preview runs with the same values the files hold.
The frontend works the other way round: kosmo build inlines its values into the bundle. What import.meta.env.VITE_API_URL returns in production is whatever the build saw.