GioJSdocs
On this page

Environment Variables

Where configuration and secrets come from, which ones reach the browser, and how to keep the rest on the server.

Every variable is server-only unless its name starts with GIO_PUBLIC_. Server code - getServerSideProps, page actions, route.ts handlers, anything they import - reads process.env as usual. Code that also runs in the browser (pages, layouts, components) sees only the GIO_PUBLIC_* values, inlined when the client bundles are built. This page walks through the whole flow; the configuration reference lists every variable GioJS itself reads.

Where values come from

At startup the Rust server reads .env files from the project root (the folder holding app/ and gio.toml), before it parses gio.toml and before the Node worker starts, so both halves see the same values. For each variable the first source that defines it wins:

PrecedenceSourceCommit it?
1The real environment (shell, systemd, Docker, your host's dashboard)-
2.env.{mode}.localno - machine-specific values and secrets
3.env.localno - machine-specific values and secrets
4.env.{mode}yes - per-mode defaults
5.envyes - shared defaults
  • {mode} is development when the server starts with NODE_ENV=development (npm run dev) and production otherwise - an unset NODE_ENV included. NODE_ENV itself is never read from a file: set it in the real environment.
  • Real environment variables always win, so a value your deploy sets is never shadowed by a file left on the server.
  • Files load once. Restart the server after editing one (the dev watcher restarts the worker for source changes, not for .env edits).
  • The startup log names the files it loaded - never their values - and a file that cannot be parsed stops startup with its name and line number.
  • On a platform that injects the whole environment, stray files can be ignored: [env] files = false in gio.toml, or GIO_ENV_FILES=0 (which wins over gio.toml, as GIO_ENV_FILES=1 does the other way).

The syntax is the usual dotenv one, with quotes, multiline values, export prefixes and ${VAR} references - see .env files for the details.

The starter's .env.example

A new app ships an .env.example listing the variables it knows about, with comments, and a .gitignore that keeps .env*.local out of git. Copy it to start your own:

bash
cp .env.example .env.local     # local values and secrets, never committed

The starter's file:

bash
# Copy to .env.local (git-ignored) and fill in. Env files load at server
# start - restart after editing. Order: .env.{mode}.local, .env.local,
# .env.{mode}, .env; the first file that sets a variable wins, and real
# environment variables always win over files.

# Session secret for createSessionStorage() and [[guards]] require_session.
# Required in production (dev generates a temporary one). Generate with:
#   node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
# GIO_SESSION_SECRET=

# Only GIO_PUBLIC_* variables reach browser code (process.env.GIO_PUBLIC_X),
# so never put a secret in one.
# GIO_PUBLIC_SITE_NAME=My GioJS app

# The port the server listens on (overrides [server] port in gio.toml).
# Hosting platforms usually set PORT for you.
# PORT=3000

Keep .env.example current as you add variables: it is the list a teammate or a deploy pipeline works from. Real secrets go in .env.local on your machine and in the host's environment in production.

Reading variables on the server

Read secrets where only the server runs: getServerSideProps, actions, route handlers, and modules only they import. Collecting them in one server-only module gives you a single place that fails loudly when something is missing:

lib/env.server.ts
import '@gio.js/core/server-only';

function required(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`${name} is not set - see .env.example`);
  return value;
}

export const env = {
  databaseUrl: required('DATABASE_URL'),
  stripeSecretKey: required('STRIPE_SECRET_KEY'),
};
app/orders/page.tsx
import { env } from '../../lib/env.server';

export async function getServerSideProps() {
  const orders = await fetchOrders(env.databaseUrl);
  return { props: { orders } };          // props are sent to the browser - no secrets
}

export default function Orders({ orders }) { /* ... */ }

"Fails loudly" means: every URL whose module imports it answers 500 - a page renders its error page, and a route.ts answers { "error": "Internal Server Error", "digest": "..." } for every method (in development, the import error itself). The server log carries the file and the error under the same digest, and a route.ts that fails to import is also logged once at startup. The server still starts, so the rest of the app keeps serving; npx gio routes marks such a route (failed to load). The same applies to createSessionStorage() at module scope with no GIO_SESSION_SECRET in production.

getServerSideProps and everything only it imports are removed from the browser bundle, but its return value is not secret: props are serialized into the page so it can hydrate. Return the data the page needs, never a key, a token or a whole database row with fields the visitor should not see.

Variables in the browser: GIO_PUBLIC_

In client code, process.env.GIO_PUBLIC_* reads are replaced with the value at build time; every other process.env.X is undefined in the browser (NODE_ENV is always available). The server renders with the same values, so the HTML and the hydrated page agree:

tsx
export default function Footer() {
  return <footer>{process.env.GIO_PUBLIC_SITE_NAME}</footer>;   // works on both sides
}

When "build time" is depends on how you ship:

DeployGIO_PUBLIC_* values are readTo change one
npm start / gio (from source)when the server starts and builds the client bundlesrestart
gio build standaloneduring the build, from the build environment and the project's production .env filesrebuild
gio exportduring the exportre-export

Anything named GIO_PUBLIC_* is readable by every visitor in the page's JavaScript. Use the prefix for public configuration - an API base URL, a publishable payment key, an analytics site id - and never for a secret.

Keeping server code out of the browser

A component that imports a module holding secrets would pull that module into the browser bundle. Mark such modules server-only and the mistake becomes a build error instead of a leak: import @gio.js/core/server-only at the top, or name the file *.server.ts (.tsx, .js, .jsx). A route whose client bundle reaches one is rejected - it still server-renders but does not hydrate - and the error names the import chain, in the dev overlay and the server log. See Keeping server code out of the browser.

Secrets GioJS uses

VariableNeeded whenNotes
GIO_SESSION_SECRETYou use sessions or require_session guardsAt least 32 bytes; comma-separated to rotate (the first signs, all verify). In production, missing means createSessionStorage() throws - every page and route.ts importing the session module answers 500 - and guards deny everyone; in development an ephemeral secret is generated, and the @gio.js/core/testing kit sets a random one for tests.
GIO_REVALIDATE_TOKENA CMS or script purges pages through POST /_gio/revalidateAt least 32 bytes or the server refuses to start. Without it the endpoint does not exist.

Generate either with:

bash
node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"

gio.toml has no variable substitution, and it is usually committed. Keep secrets out of it: prefer GIO_REVALIDATE_TOKEN over [revalidate] token, and protect /_gio/metrics with ip_allowlist - its token can only be set in the file, so if you use one, keep that gio.toml out of public repositories.

In production

  • Set variables in the platform's environment (dashboard, fly secrets, a systemd EnvironmentFile, Kubernetes Secrets). They win over every file, and nothing secret has to live on disk next to the app.
  • If you do use a file on the server, use .env.production.local, readable only by the service's user (chmod 600).
  • A standalone build copies no .env file into its output: server variables are read at runtime from the environment or from .env files you place in the deploy folder. Container images built from it carry none either.
  • Changing a server variable needs a restart; changing a GIO_PUBLIC_* variable needs whatever the table above says.

Tests load the same files with the same rules - see Testing. For the rest of the production setup, work through the production checklist.