CodeKits.shopNew developer offer

Keep production configuration out of public code

Separate browser, build and runtime configuration, protect environment files, and diagnose missing settings without printing private values.

By Jonathan · 6 min read

Published

Public browser configuration separated from private build and server runtime settings
In this article

An environment variable is a delivery mechanism, not encryption. If an application sends its value to the browser, writes it into a log or builds it into a public JavaScript file, the original .env permissions no longer protect that copy.

My study notes' distinction between encoded and protected information applies directly here. Base64, a hidden settings page and a name containing SECRET do not make a value confidential. This guide turns that idea into a deployment checklist for Node and Next.js.

Classify each setting before storing it

Setting Intended audience Typical location
Public site name or analytics ID Browser users Explicit public build configuration
Database password API runtime Restricted server configuration or secret manager
Migration credentials Controlled migration job Separate privileged secret
Encryption key Authorised server component Key-management system or restricted runtime secret
Upload directory Server runtime Private configuration, outside public web roots

Not all configuration is secret, but every secret needs an owner, purpose, rotation process and recovery plan. Keep a manifest of required variable names and formats without real values.

A value available during a build can be copied into generated output. A value needed at runtime may be unavailable to static generation. Decide which phase consumes it instead of providing every production secret to every build.

Browser variables are public by design

Next.js exposes specially prefixed variables to browser code. The environment-variable guide explains NEXT_PUBLIC_ inlining and build-time behaviour. Never put a database password, signing key or private API token under that prefix.

Configuration classification — placeholders only
NEXT_PUBLIC_SITE_NAME=Demo Studio
DATABASE_HOST=127.0.0.1
DATABASE_NAME=demo_app
DATABASE_PASSWORD=SET_PRIVATELY_ON_THE_SERVER

This is a classification example, not a production-ready file. A browser bundle can be downloaded by anyone. Minification and an unadvertised URL are not access controls. Conversely, a variable without NEXT_PUBLIC_ can still leak if your own route returns it or passes it to a client component.

For same-origin browser requests, a relative /api path often avoids baking a server address into frontend code. The server still validates authentication and authorisation; hiding the API address is not a security boundary.

Keep replaceable releases separate from persistent state

Example deployment layout
srv/demo/
├── releases/
│   ├── release-a/
│   └── release-b/
├── current -> releases/release-b/
└── shared/
    ├── runtime.env       # private, outside the web root
    └── uploads/          # private application storage

Neither shared directory belongs in public, a downloadable ZIP or a source-code product. Release transfers should use an explicit allowlist or carefully reviewed exclusions. The rsync verification sheet covers dry runs and destination checks.

Use a deployment account to own release code and a restricted runtime account to execute it. A common model is a root-owned configuration file readable by the runtime group, with no world access. Directory traversal permissions matter too: see Linux permissions for deployed apps.

Server terminal: inspect permissions without displaying values
stat -c '%A %U:%G %n' /srv/demo/shared/runtime.env
namei -l /srv/demo/shared/runtime.env

Do not run cat on a production secret file just to prove it exists. Do not paste environment dumps, process arguments or complete systemd configurations into public support messages.

Load configuration explicitly

A systemd service does not run your interactive shell startup files. Its environment-file syntax is not a general shell script either. A minimal example is:

demo-api.service — relevant service settings
[Service]
User=demo
Group=demo
WorkingDirectory=/srv/demo/current
EnvironmentFile=/srv/demo/shared/runtime.env
ExecStart=/usr/bin/node server.mjs

This excerpt assumes those accounts, paths and the application already exist; it is not a complete service unit. Use the actual absolute Node path on your server. Read the systemd execution-environment reference and the service environment troubleshooting sheet.

Ordinary environment variables are not ideal for every high-value secret. Privileged users, the runtime itself, crash tooling or debugging access may reveal them. Where supported, consider systemd credentials or a dedicated secret manager, and make the application read secrets through that mechanism. File permissions do not protect against a fully compromised runtime that legitimately needs the value.

Fail early without logging the secret

Validate required settings at startup. Report the missing or malformed name, never its value. Check valid port ranges, allowed modes and required URLs. Do not substitute a production default password because a variable is missing.

config.mjs — minimal presence validation
export function requiredSetting(env, name) {
  const value = env[name];
  if (typeof value !== 'string' || value.length === 0) {
    throw new Error('Missing required setting: ' + name);
  }
  return value;
}

This helper only checks presence; it does not validate entropy, strength, scope or correctness. It deliberately does not trim passwords, because changing bytes can change the credential. Extend validation for each setting's actual contract. Tests should use synthetic values and confirm errors omit them.

The download contains the helper, two local test groups and a README. Run it without connecting to any server:

Local terminal: synthetic configuration checks
node --test config.test.mjs

Avoid logging complete request headers, cookie strings or database connection options. Use a redacted diagnostic such as databaseConfigured: true, plus a separate bounded readiness check that does not return connection details.

Verify the boundary before release

In a disposable staging build, use a distinctive synthetic marker as a fake secret. Search the browser-served output and network responses for it. Do not use a real production secret as a test marker. Check source maps, downloadable archives, error pages and logs too.

A marker search is useful but not proof that every leak is impossible: values can be transformed or encoded. Review code paths and permissions alongside it. Confirm a missing runtime secret prevents startup, that public responses remain generic, and that old releases cannot read secrets they no longer require.

When rotating encryption keys, first plan how old data remains readable. Follow key rotation without losing records. When a secret has leaked, removing it from the latest file is not enough: revoke or rotate it and account for copies in history, backups and logs.

The snippets here are configuration patterns and a small pure function, not a claim that a particular production secret store was audited. Return to the VPS learning path to place these checks in a complete release process.