Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Environment variables

Environment variables are settings provided from outside the app: the session secret, the database path, keys for external services. They differ between your computer and the server, while the names in the code stay the same. This keeps secrets out of the repository.

The .env file

Create .env in the project root:

SESSION_SECRET=long-random-string-for-dev
DATABASE_PATH=blog.db
VITE_SITE_NAME=My blog

The dev server npm run dev reads this file on start. Add .env to .gitignore, and commit a .env.example with the same names and empty values — so other developers know what to fill in.

On the server: process.env

In loaders, actions and .server.ts files the variables are available through process.env:

// app/db.server.ts
import Database from "better-sqlite3";
 
export const db = new Database(process.env.DATABASE_PATH ?? "blog.db");

The value is a string, or undefined if the variable is not set. That is why a default is given or the value is checked.

A required variable

Secrets must not have a default. Check the variable as soon as the module loads:

// app/sessions.server.ts
const secret = process.env.SESSION_SECRET;
if (!secret) {
  throw new Error("SESSION_SECRET is not set");
}

If you forget the variable on the server, the app stops at startup with a clear error instead of running with a weak secret.

In the browser

Component code also runs in the browser, where there is no process.env. Vite exposes to browser code only variables prefixed with VITE_, through import.meta.env:

// app/routes/layout.tsx
<header>{import.meta.env.VITE_SITE_NAME}</header>

The value is baked into the JavaScript at build time, so any visitor can see it. Give the VITE_ prefix only to things that are not secret.

Through a loader

If a value is needed in the browser but should be read at run time rather than build time, pass it from a loader:

// app/root.tsx
export async function loader() {
  return { contactEmail: process.env.CONTACT_EMAIL ?? "hello@example.com" };
}

The component gets it in loaderData, like any other data.

Where you need itHow to read it
loader, action, .server.tsprocess.env.NAME
Component, not secret, known at build timeimport.meta.env.VITE_NAME
Component, value from the serverReturn it from a loader

Common mistake

Reading process.env.SESSION_SECRET directly in a component body. On the server it works, but in the browser during hydration you get a process is not defined error or an empty value. Read secrets only in server code and pass the finished result to the component.

Thanks for reading!

Donate to the site — we work hard for you.