Docs/Server/Environment Variables
GitHub

Environment Variables

Environment variables work differently depending on where the code runs. The rule of thumb: code that runs on the server reads process.env freely; code that ships to the browser must be inlined at build time or fetched at runtime.

# Where code runs

Code Runs on Can read process.env
page.data.ts, layout.data.ts Build (SSG) or server (SSR)
route.ts (API routes) Server
page.action.ts (server actions) Server
src/middleware.ts Server
page.ts components Build + browser (hydration) ⚠️ only via explicit inlining
Islands Browser

# Loaders (server-side)

Loaders run at build time for SSG and on request for SSR, so secrets are safe there:

// src/app/page.data.ts
import type { PageDataLoad } from "@deijose/nix-js-kit";

export const load: PageDataLoad = async () => {
  const api = process.env.CONTENT_API_URL!;
  const key = process.env.CONTENT_API_KEY!;

  const res = await fetch(api, {
    headers: { Authorization: `Bearer ${key}` },
  });
  return { items: await res.json() };
};
ℹ Note

Loader data is serialized into the HTML (<script id="nix-js-data">). Only what you return from the loader is exposed to the client — never put secrets in the returned object.

# API routes and server actions

// src/app/api/route.ts
export function GET() {
  return Response.json({ region: process.env.VERCEL_REGION ?? "local" });
}
// src/app/page.action.ts
import { fail } from "@deijose/nix-js-kit";

export async function subscribe(email: string) {
  const key = process.env.NEWSLETTER_KEY;
  if (!key) return fail(500, { error: "Newsletter not configured" });
  // ...
}

# Middleware

Middleware runs before API routes and pages, making it the right place for feature flags or tenant configuration:

// src/middleware.ts
import type { Middleware } from "@deijose/nix-js-kit";

export default (async (request, ctx) => {
  ctx.next({
    headers: {
      "x-app-mode": process.env.APP_MODE ?? "standard",
    },
  });
}) satisfies Middleware;

# Configuring values per environment

# Local

# .env.local (loaded by Bun/Node automatically)
CONTENT_API_URL=https://api.example.com
CONTENT_API_KEY=secret

# Vercel / Netlify / Bun / Node

Set the same names in the platform dashboard. Adapters inherit the host's environment — no extra configuration in Nix.js Kit.

# Client-visible values

If a component or island needs a value in the browser, pick one of these:

1. Inline at build time (public constants):

// src/app/lib/config.ts
export const siteUrl = process.env.SITE_URL ?? "https://example.com";

2. Pass through loader data:

export const load = async () => ({
  siteUrl: process.env.SITE_URL ?? "https://example.com",
});
export default function Page({ data }: PageProps<typeof load>) {
  return html`<p>Built by ${data.siteUrl}</p>`;
}

3. Fetch at runtime from an API route (for values that change per request):

// src/app/api/config/route.ts
export function GET() {
  return Response.json({ region: process.env.REGION });
}

# .env files

The dev server and CLI load .env, .env.local, .env.development, and .env.production using Bun/Node's native dotenv support. Prefix variables that must be exposed to the client — none are exposed automatically; everything is opt-in through the patterns above.

# Availability

  • process.env — every server-side module (data.ts, route.ts, action.ts, middleware.ts, vite.config.ts, scripts)
  • Bun.env — equivalent under Bun
  • Loader data serialization — safe to include public config values
✎ Edit this page on GitHub