Skip to main content

add-env-var

Step-by-step checklist for adding a new NEXT_PUBLIC_* environment variable. Use when introducing any new runtime env var.

Jump to install

Source facts

Repository
blockscout/frontend
Last source activity
September 9, 2026 at 18:22
Detected SKILL.md language
English
Stars
307
Forks
738

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
add-env-var
description
Step-by-step checklist for adding a new NEXT_PUBLIC_* environment variable. Use when introducing any new runtime env var.
# Add a new env variable Read `.agents/rules/env-vars.md` first for background: how runtime delivery works, how the config object is structured, the three value types (primitive / JSON-encoded / JSON config URL), and where validation lives. This skill is the checklist; the rule is the concept doc. ## Step 0 — Decide the variable's shape Three answers drive every step below. Settle them first. 1. **Value type** (see env-vars.md § "Value types"): - **Primitive** — string, boolean, number. - **JSON-encoded string** — small synchronous structured config. - **JSON config URL** — large async-loaded payload. Variable name ends in `_URL`. 2. **External URL?** Does the value point to a third-party host (image, JSON payload, HTTP endpoint)? If yes → CSP and download-script considerations apply (see Step 4). 3. **Mode** — Does the variable apply in the default mode, multichain mode, or both? This decides which validator schema(s) you touch. ## Step 1 — Document in `docs/ENVS.md` Add a row in the section that mirrors where the variable's config lives: App configuration / APIs configuration / App shell / Slices / Features / External services / Misc. The doc sections mirror `src/config/index.ts`. Fill in the name, type, description, required/optional status, default, and example. Description: one sentence maximum; avoid validation logic and implementation details; be extremely concise. Set the **Version** column to `upcoming`. The release process replaces this with the actual version number when the change ships. Why this is mandatory: `collect_envs.sh` scans `docs/ENVS.md` for `NEXT_PUBLIC_*` names to emit `.env.registry`, which gates what `make_envs_script.sh` writes into `window.__envs`. An undocumented variable never reaches the browser. ## Step 2 — Expose through the config object ### Existing config area Find the matching sub-config: - Slices / features / services / shell areas: `src/<area>/<name>/config.ts`. - Cross-cutting sections (`app`, `apis`, `chain`, `misc`, `metadata`): `src/config/<area>.ts`. Read the value with the helper that matches its type: - Primitive — `getEnvValue('NEXT_PUBLIC_…')`; cast at the call site (`=== 'true'`, `Number(…)`, …). - JSON-encoded — `parseEnvJson<T>(getEnvValue('NEXT_PUBLIC_…'))` with a fallback (returns `null` on parse failure). - JSON config URL — `getExternalAssetFilePath('NEXT_PUBLIC_…_URL')`. Never read `process.env.NEXT_PUBLIC_*` in client code. ### New feature If the variable belongs to a **brand-new feature**, also: 1. Create the folder `src/features/<feature-name>/`. 2. Add `src/features/<feature-name>/config.ts` following the template below. 3. Register the feature in the aggregator `src/config/features.ts`: ```ts export { default as <featureName> } from 'src/features/<feature-name>/config'; ``` Keep the export list alphabetised by export name. #### Feature `config.ts` template ```ts // SPDX-License-Identifier: LicenseRef-Blockscout import { getEnvValue } from 'src/config/utils/envs'; import type { Feature } from 'src/config/utils/features'; const title = 'Human-readable feature name'; const config: Feature<{ /* payload fields available when enabled */ }> = (() => { if (getEnvValue('NEXT_PUBLIC_<FEATURE>_ENABLED') === 'true') { return Object.freeze({ title, isEnabled: true, // payload fields here }); } return Object.freeze({ title, isEnabled: false, }); })(); export default config; ``` `Feature<Payload>` is a discriminated union on `isEnabled`. Consumers narrow to the enabled branch (`if (config.isEnabled) { … config.payloadField … }`) to get typed access to the payload. Real examples: `src/features/web3-wallet/config.ts`, `src/features/chain-stats/config.ts`. ### Private mode Anything that integrates with a 3rd-party able to collect user info (analytics, error tracking, A/B testing, captcha, …) must respect the `app.isPrivateMode` flag. The flag lives in `src/config/app`. - **Feature config** — gate the entire enabled branch on `!app.isPrivateMode` so consumers see `isEnabled: false` in private mode. Example: `src/features/web3-wallet/config.ts`. ```ts if (!app.isPrivateMode && /* other conditions */) { return Object.freeze({ title, isEnabled: true, /* payload */ }); } return Object.freeze({ title, isEnabled: false }); ``` - **Service config** — never store the service's client key (or any identifier the SDK would use to attach the user's session to a remote account) in the config when private mode is on. Set the field to `undefined` and let consumers no-op when they see it missing. Other non-identifying fields (config overrides, etc.) can remain populated. Examples: `src/services/mixpanel/config.ts`, `src/services/google-analytics/config.ts`. ```ts const projectToken = !app.isPrivateMode ? getEnvValue('NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN') : undefined; ``` If the new variable doesn't touch user data (purely cosmetic, chain config, URL of a same-origin asset, etc.), this section doesn't apply. ## Step 3 — Validator schema and tests Add the rule and a test preset entry. The full procedure (which schema file, where in the schema, JSON shape conventions, JSON-URL example assets, companion-variable rules, running the tests, verifying the negative path) lives in `deploy/tools/envs-validator/CONTEXT.md` — follow the "Adding a new variable" section there. ## Step 4 — Only if the variable holds an external non-asset URL Most URL variables need a CSP allowance under `src/server/csp/policies/`. Gate the addition on the relevant config option being enabled — don't widen the CSP unconditionally. **Exceptions** — these are already auto-included by `src/server/csp/policies/app.ts` and need no manual CSP work: - new API `endpoint` and `socketEndpoint` values that flow into `config.apis.*`. If the new variable lands inside one of those config paths, skip this step. For any other external host (analytics, third-party services, custom integrations, etc.), add the domain to the matching policy and gate it on the feature/option being enabled. ## Step 5 — Only if the variable holds an asset URL (image or JSON config) Append the variable name to the `ASSETS_ENVS` array in `deploy/scripts/download_assets.sh`. The container entrypoint downloads the asset into the image at startup so the browser serves it same-origin.
View on GitHub