Skip to content
Env Files

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.

FileLoaded
.envalways
.env.localalways, meant to stay on your machine
.env.[mode]in the given mode
.env.[mode].localin 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:

txt
my-app/
├── .env              -> every folder
├── .env.local
└── src/
    └── admin/
        ├── .env      -> admin only, wins over the root
        └── kosmo.config.ts

A 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:

ts
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:

kosmo.config.ts
ts
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:

src/admin/kosmo.config.ts
ts
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:

CommandDefault mode
kosmo serve (pnpm dev)development
kosmo previewproduction
kosmo buildproduction
pnpm testtest

Pass --mode to pick another:

sh
pnpm dev --mode stage
pnpm build --mode stage
pnpm test --mode stage

With --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:

sh
node --env-file .env.production dist/run.js -p 4556

kosmo 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.

Released under the MIT License.