[]previous and next page

Environment variables

Environment variables in the Next.js to ShadowJS guide.

For agentsUpdated

Dusk has two kinds, and the prefix is the rule.

Next.js Dusk
NEXT_PUBLIC_API_URL, read as process.env.NEXT_PUBLIC_API_URL DUSK_PUBLIC_API_URL, read as import.meta.env.DUSK_PUBLIC_API_URL (or API_URL in .env.public, with no prefix). Replaced at build time
process.env.DATABASE_URL on the server process.env.DATABASE_URL, validated at startup by defineEnv and read from event.env
process.env.NODE_ENV import.meta.env.MODE, import.meta.env.DEV, import.meta.env.PROD
.env, .env.local, .env.production Same files for server values. Values the browser may read go in .env.public (committed), which Next has no equivalent for. Dusk loads the server files into process.env in development and at build time (see below)
Reading a secret in a client component by mistake: undefined at runtime Build error [ENV_NOT_PUBLIC] naming the variable

Only the public names reach the browser: the DUSK_PUBLIC_ ones and the names declared in .env.public. A client file that reads import.meta.env.DATABASE_URL, or VITE_X (Dusk replaces Vite's VITE_ default with DUSK_PUBLIC_), fails the build. A public value that looks like a key or a password fails the build too ([ENV_PUBLIC_VALUE]), so a value in .env.public is a value you would put in the browser deliberately. Server variables come from the real process environment. In development, and while dusk build prerenders, dusk() loads .env.public, .env, .env.[mode], .env.local, .env.[mode].local and .env.public.local into process.env for server code, the later ones winning (a variable that is already set wins over all of them); in production there is no file loading, so use your host's environment settings, or node --env-file for a file next to the server. This order is Dusk's own: .env.local beats .env.[mode], which is the opposite of Vite's. Declare the server variables once with defineEnv; the handler validates them at startup and refuses to serve a misconfigured app, naming the variables (never their values).

src/env.tsts
import { defineEnv, EnvError } from "@shadow-js/dusk/server";

export const env = defineEnv({
  DATABASE_URL: { type: "url", protocols: ["postgres:"], secret: true },
  PORT: { type: "number", integer: true, default: 3000 },
  DUSK_PUBLIC_API_URL: { type: "url", required: false },
});

const ok = env.parse({ DATABASE_URL: "postgres://localhost/blog" });
console.log(ok.all.PORT, ok.all.DATABASE_URL);

try {
  env.parse({ PORT: "3.5" });
} catch (error) {
  if (error instanceof EnvError) console.log(error.message);
}
Expected output
3000 postgres://localhost/blog
Invalid environment: DATABASE_URL (required), PORT (expected an integer)

Pass the schema as env in src/entry-server.ts (env: env); event.env.DATABASE_URL and handler.env hold the validated values. A client file imports nothing from here; it reads import.meta.env.DUSK_PUBLIC_API_URL, which the build replaces. basePath becomes Vite's base; Dusk passes it to the handler and the router.