Environment Variables and Secrets in Web Projects

Environment variables let you configure an application without putting every setting in source code. They are useful for values that differ between your computer, a preview deployment, and production. But an environment variable is not automatically private: a frontend build can copy its value into files that every visitor can download.

The safest approach is to decide where a value is used, store it in the right place for each environment, and expose it only to the code that needs it. This guide covers local development, browser-facing builds, CI workflows, and deployment settings using current official documentation. It is a documentation-based guide, not a hands-on comparison of hosting platforms.

Separate configuration from secrets

Configuration describes how an application should run: a port number, feature flag, or public API base URL. A secret grants access to something: a database password, signing key, payment-provider credential, or deployment token.

Environment variables are a way to provide both kinds of values to a process. They do not encrypt a value, control who can read it, or keep it out of a browser bundle. Treat a secret as private only when the system that stores and supplies it enforces access controls.

Start by listing the values your app needs and where they are consumed:

Value Where it belongs
Public site URL or browser API base URL Frontend configuration, if visitors may see it
Database connection string or signing key Server-side runtime only
Deployment credential The specific CI job or deployment environment that needs it

Use separate credentials and data for development, previews, and production. A preview build should not need unrestricted access to a production database.

Keep local environment files out of Git

For local development, many JavaScript projects use a .env file that a framework or runtime loads into the process environment. Node.js also documents built-in support for .env files. The exact loading behavior can vary by framework, so check the documentation for your runtime rather than assuming every tool reads the same file names or precedence rules.

Ignore local environment files before adding any values:

.env
.env.*
!.env.example

You can commit an .env.example file that documents required variable names without real credentials. Keep local values in ignored files or a local secret store, and share the names and setup steps with collaborators. Node.js treats values read from .env files as text; validate and convert values such as ports or booleans in your application instead of assuming they already have the right type.

Fail early when a required value is missing, and never include its contents in an error message:

const required = ["DATABASE_URL", "SESSION_SECRET"];
const missing = required.filter((name) => !process.env[name]);

if (missing.length > 0) {
  throw new Error(`Missing required environment variables: ${missing.join(", ")}`);
}

This gives you a useful setup error without printing credentials. Do not provide a working default for a secret; a missing key should stop the affected feature or application rather than silently use a known value.

Treat browser-exposed variables as public

Frontend build tools often use a prefix to select variables that are safe to include in client code. That prefix does not make the values secure; it means the opposite.

  • Vite: variables prefixed with VITE_ are exposed to client-side code through import.meta.env. Vite explicitly warns against putting secrets in them.
  • Next.js: variables prefixed with NEXT_PUBLIC_ are inlined into the browser bundle during the build. Their values are fixed for that build, so changing a deployment setting later does not change the already-built client bundle.

For either framework, assume that anyone can inspect a prefixed value in the downloaded JavaScript. Use these variables only for information that is safe to publish, such as a public service URL. Keep database credentials, private API keys, and signing secrets in server-only code. If the browser needs privileged data, have your server make the authenticated request and return only what the user is allowed to see.

See the official guides for Vite environment variables and modes and Next.js environment variables.

Configure CI and deployment environments separately

The value available on your laptop is not automatically available in a continuous-integration job or a deployed application. Configure each environment explicitly:

  1. Put non-sensitive defaults in application configuration or documented examples.
  2. Store CI credentials in the CI provider's secret store, not in workflow YAML or shell commands committed to the repository.
  3. Add deployment values to the hosting platform's environment settings, with separate values for development, preview, and production where supported.
  4. Give each job and environment only the credentials it needs.

For example, a GitHub Actions step can receive a deployment token through the secrets context:

- name: Deploy
  env:
    DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
  run: npm run deploy

Only add that step to a workflow that should have deployment access. Do not expose secrets to untrusted pull-request code. GitHub notes that automatic log redaction is not guaranteed for every transformed value, so avoid printing secrets, use least-privilege credentials, and review who can change workflows that access them. For cloud deployments that support it, consider short-lived identity federation instead of a long-lived cloud key.

Hosting platforms can also distinguish build-time values from runtime values. Vercel, for example, provides Development, Preview, and Production scopes, and makes configured values available during builds or function execution. Select the right scope and verify a new deployment after changing a value; frontend values used during a build may need a rebuild before the client sees the change. For more on infrastructure workflows, see our guide to using GitHub Actions with Terraform.

Troubleshoot missing or incorrect values

When a setting works locally but fails after deployment, check these common causes:

  • The variable is not defined in that environment. Confirm the name and scope in the CI or hosting dashboard; local .env files are not deployed automatically.
  • The framework does not expose it to the browser. Check the framework's required prefix, and expose only values that are safe to make public.
  • The value was set after the build. Client bundles may capture public configuration during compilation. Trigger a new build and deployment.
  • A preview is using production credentials. Separate preview settings and use test services or limited-access accounts.
  • A value was parsed as the wrong type. Environment variables arrive as text in Node.js. Parse and validate numbers, booleans, URLs, and other structured values in your code.
  • A secret appears in logs or source control. Stop using it, revoke or rotate it with the service that issued it, and remove the exposed copy. Adding a file to .gitignore does not invalidate a credential that was already committed.

A short security checklist

  • Keep secrets on the server or in a trusted CI or hosting secret store.
  • Never put a secret behind a frontend prefix such as VITE_ or NEXT_PUBLIC_.
  • Use different, least-privilege credentials for development, previews, CI, and production.
  • Keep local environment files ignored and commit only setup documentation without credentials.
  • Validate required settings at startup, but never log their values.
  • Rotate any credential that has been exposed; do not rely on deleting a file or masking a log line.

For broader secret-lifecycle guidance, read the OWASP Secrets Management Cheat Sheet. Check your framework and provider's current documentation when you configure a project: environment names, scopes, and build behavior are platform-specific.

Official references

Leave a Reply