Guides: Environment variables

Guides

Environment variables

Configuration that changes between machines, like database paths, API keys and the site's public URL, belongs in environment variables. Grimoire gives you four modules for reading them, split two ways: public or private, and read at build time or at run time.

Setting variables

Bun reads a .env file in the project root on its own, so for development:

.env
DATABASE_PATH=books.sqlite
SESSION_SECRET=change-me
PUBLIC_SITE_NAME=Reading list

Keep .env out of version control. In production, set real environment variables however your host does it (a systemd unit, docker run -e, a compose file).

Public and private

A variable whose name starts with PUBLIC_ is public: it may be sent to the browser. Everything else is private and stays on the server.

The private modules can only be imported by server code (+page.server.ts, +layout.server.ts, +server.ts, hooks.server.ts, and modules only they import). Importing one from a component fails the build, rather than shipping a secret in a bundle.

The four modules

ModuleHoldsRead
$env/static/publicPUBLIC_*when the app is built
$env/static/privateeverything elseonce, when the server first loads it
$env/dynamic/publicPUBLIC_*on each access
$env/dynamic/privateevery variableon each access

The static modules export each variable by name, and also as an env object:

src/lib/server/db.ts (first lines)
import { DATABASE_PATH } from "$env/static/private";
import { Database } from "bun:sqlite";

export const db = new Database(DATABASE_PATH, { create: true });

The dynamic modules export only env, an object you read like process.env:

src/lib/Footer.tsx
import { env } from "$env/dynamic/public";

export const Footer = () => <footer>{env.PUBLIC_SITE_NAME}</footer>;

A variable that isn't set reads as an empty string from env. A named import is stricter: importing a variable that isn't set is an error, which fails the build for browser code and the module's first load on the server. The generated types list the variables in .env, so your editor flags a misspelled name.

Static or dynamic

In the browser, $env/static/public values are written into the JavaScript bundles by sigil build. They stay what they were at build time, which suits values that belong to the code.

$env/dynamic/public in the browser holds the values the server rendered into the page when it was requested, so changing a variable and restarting the server is enough; nothing needs rebuilding. That's the one to use when the same build runs in several places, such as a Docker image deployed to staging and production.

On the server, process.env works as well. The $env modules add the public/private split, the build-time check, and types.

Edit this page