Skip to main content
Use one extension codebase across browsers and environments without hardcoding values. Manage API hosts, keys, and browser-specific values cleanly across environments. Extension.js uses two different environment loading paths. One serves compiled extension bundles (browser/mode aware). The other runs while loading extension.config.* in Node. Both paths matter depending on where you read your variables.

Template examples

new-env

new-env template screenshot See environment variables in action with a new-tab extension that reads EXTENSION_PUBLIC_* values.
npx extension@latest create my-extension --template=new-env
Repository: extension-js/examples/new-env

content-env

content-env template screenshot Use environment variables inside content scripts injected into web pages.
npx extension@latest create my-extension --template=content-env
Repository: extension-js/examples/content-env

How it works

Extension bundles (compile time)

When building your extension, the compiler picks one env file from your extension package folder by the first match in this order:
  1. .env.[browser].[mode] (for example, .env.chrome.development)
  2. .env.[browser]
  3. .env.[mode]
  4. .env.local
  5. .env
  6. .env.example
Extension.js always merges .env.defaults first when present, then the selected file’s variables. Finally, system process.env takes highest precedence for overlapping keys.
Selection is a single env file from the list above (plus .env.defaults), not a full cascade through every file.
If no matching file exists next to the project, Extension.js repeats the same search from the nearest workspace root. That root is the closest ancestor folder containing pnpm-workspace.yaml. Extensions inside monorepos can share root-level env files for bundle injection. Monorepo constraint: Workspace fallback only runs when a pnpm-workspace.yaml marker exists on an ancestor. If you use pure npm or Yarn workspaces that rely only on package.json "workspaces", this automatic root lookup does not apply. Keep env files beside the extension package, or add a pnpm-workspace.yaml at the repository root.

extension.config.* (Node, before your configuration runs)

extension.config.js / .mjs / .cjs runs in Node. Before Extension.js evaluates the file, it preloads a small set of files into process.env so the configuration can read them:
  1. .env.defaults (merged when present)
  2. Then the first file that exists among: .env.development, .env.local, .env
Extension.js does not use browser-scoped files such as .env.chrome or .env.chrome.development for this preload step. Instead, set values through plain process.env in your shell or continuous integration (CI) pipeline. You can also rely on bundle-time env (described above) to inject EXTENSION_PUBLIC_* values into extension code. Workspace fallback: If none of those files exist in the extension package folder, the same preload runs from the nearest folder containing pnpm-workspace.yaml (same constraint as above). This split exists because configuration loading is browser-agnostic at file-read time, while the bundler knows the active browser and mode.

Built-in environment variables

Extension.js injects built-in variables at compile time, so browser and mode are always available in your extension code.
Variable NameDescription
EXTENSION_PUBLIC_BROWSERThe current browser target for your extension (for example, chrome, firefox, edge).
EXTENSION_PUBLIC_MODEThe mode in which your extension is running, such as development or production.
EXTENSION_BROWSERBrowser target (non-legacy alias).
EXTENSION_MODEBuild mode (non-legacy alias).
BROWSERShort browser alias.
MODEShort mode alias.
NODE_ENVNode environment aligned to compiler mode (development / production).
All built-ins above are available through both process.env.* and import.meta.env.*.

Environment variable inventory

Public/runtime variables (user-defined)

Variable patternPurposeAvailable in JS runtimeNotes
EXTENSION_PUBLIC_*Expose user-defined values to extension codeYes (process.env + import.meta.env)Safe-to-ship values only

Static placeholder variables

Variable patternPurposeAvailable in JS runtimeNotes
$EXTENSION_* tokens in emitted .html / .jsonBuild-time placeholder replacement in static assetsNot as JS varsAvoid using secrets in static templates

Built-in/alias variables

VariableTypeNotes
EXTENSION_PUBLIC_BROWSERbuilt-inBrowser target
EXTENSION_PUBLIC_MODEbuilt-inBuild mode
EXTENSION_BROWSERbuilt-in aliasBrowser target
EXTENSION_MODEbuilt-in aliasBuild mode
BROWSERbuilt-in aliasShort browser name
MODEbuilt-in aliasShort mode name
NODE_ENVbuilt-inCompiler mode

CLI and dev-server operational variables

VariablePurposeTypical usage
EXTENSION_AUTO_EXIT_MSAuto-exit dev process after N msCI hard-stop control
EXTENSION_FORCE_KILL_MSForce-kill timeout fallbackCI cleanup resilience
EXTENSION_VERBOSEVerbose diagnostics in selected flowsDebugging CLI behavior
EXTENSION_AUTHOR_MODEMaintainer/author diagnostics modeInternal diagnostics and tooling
EXTENSION_CLI_NO_BROWSERDisable browser launch from CLI (1 to set)Equivalent to --no-browser flag
EXTENSION_DEV_NO_BROWSERDisable browser launch in dev serverMonorepo watch without browser spawns
EXTENSION_DEV_DRY_RUNSkip dev server startup (return early)Smoke-testing CLI wiring
EXT_BROWSERS_CACHE_DIROverride managed browser cache folderCustom CI cache paths

Telemetry control variables

VariablePurposeDefault
EXTENSION_TELEMETRY_DISABLEDDisable telemetry entirely (1 to set)unset
EXTENSION_TELEMETRYBack-compat disable (0 to disable)unset
EXTENSION_TELEMETRY_SAMPLE_RATESampling rate for command_executed (0.0–1.0)0.2
EXTENSION_TELEMETRY_MAX_EVENTSMaximum events emitted per CLI process3
EXTENSION_TELEMETRY_DEBOUNCE_MSDedup window for identical event tuples (ms)60000
EXTENSION_TELEMETRY_TIMEOUT_MSHTTP timeout for telemetry requests (ms)300
EXTENSION_TELEMETRY_DEBUGPrint telemetry payloads to stderr (1 to set)unset
See Telemetry and privacy for the full opt-out contract.

Browser transport tuning variables

These variables override internal Chrome DevTools Protocol (CDP) and Remote Debugging Protocol (RDP) timeouts. They are useful for slow continuous integration (CI) environments, Docker containers, or debugging flaky browser connections.
VariablePurposeDefault
EXTENSION_CDP_COMMAND_TIMEOUT_MSCDP sendCommand timeout (ms)12000
EXTENSION_CDP_HTTP_TIMEOUT_MSCDP HTTP /json discovery timeout (ms)1200
EXTENSION_CDP_HEARTBEAT_INTERVAL_MSCDP WebSocket heartbeat interval (ms)30000
EXTENSION_RDP_EVAL_TIMEOUT_MSFirefox RDP evaluation timeout (ms)8000
EXTENSION_RDP_MAX_RETRIESFirefox RDP connect retry count150
EXTENSION_RDP_RETRY_INTERVAL_MSFirefox RDP connect retry interval (ms)1000

Browser-specific environment files

The rules below apply to compile-time / bundle env selection (see Extension bundles above). They do not apply to the narrow extension.config.* preload in Node. Need different values per browser? Extension.js supports browser-scoped env files such as .env.chrome and .env.firefox. You can also combine browser and mode for a single build variant:
  • .env.chrome.development: Extension.js applies this only when running the extension in Chrome during development mode.
  • .env.firefox.production: Extension.js applies this only when building the extension for Firefox in production mode.
Priority order is:
  • .env.[browser].[mode]
  • .env.[browser]
  • .env.[mode]
  • .env.local
  • .env
  • .env.example

Example files

# .env.chrome.development
EXTENSION_PUBLIC_API_URL=https://api-dev.chrome.com
# .env.firefox.production
EXTENSION_PUBLIC_API_URL=https://api.firefox.com

Custom environment variables

You can define custom variables in env files at project root.
Extension.js only injects variables prefixed with EXTENSION_PUBLIC_ into JavaScript bundles (process.env / import.meta.env).
# .env
EXTENSION_PUBLIC_API_KEY=your_api_key_here
EXTENSION_PUBLIC_SITE_URL=https://example.com
PRIVATE_KEY=abc123  # Not injected into JS bundles
Important: Extension.js does not inject variables without EXTENSION_PUBLIC_ into JS bundles.
However, placeholders in emitted .json/.html files can resolve $EXTENSION_* tokens, so avoid referencing secrets in static asset templates.

Using environment variables

You can use environment variables in manifest.json, locales, HTML, and JavaScript/TypeScript files.

1. In manifest.json

manifest.json does not natively support environment variables, but Extension.js replaces supported placeholders during build. For example:
{
  "name": "My Extension",
  "version": "1.0",
  "description": "This extension is connected to $EXTENSION_PUBLIC_API_KEY",
  "background": {
    "service_worker": "service_worker.js"
  }
}
During compilation, Extension.js replaces $EXTENSION_PUBLIC_API_KEY with the resolved env value.

2. In locale files

You can also use placeholders in locale files when values should change by environment. For example:
{
  "appName": {
    "message": "My Extension - $EXTENSION_PUBLIC_SITE_URL"
  },
  "appDescription": {
    "message": "Connected to API at $EXTENSION_PUBLIC_API_KEY"
  }
}
When Extension.js emits assets, it replaces placeholders such as $EXTENSION_PUBLIC_SITE_URL with resolved values.

3. In HTML files

You can also use placeholders in static HTML files (for example, under pages/):
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My Extension</title>
  </head>
  <body>
    <h1>Welcome to My Extension</h1>
    <p>API Key: $EXTENSION_PUBLIC_API_KEY</p>
  </body>
</html>
During compilation, Extension.js replaces $EXTENSION_PUBLIC_API_KEY in the output HTML.

4. In JSX components

In React/JSX/TS files, read env values with process.env:
const ApiInfo = () => {
  const apiUrl = process.env.EXTENSION_PUBLIC_SITE_URL
  const apiKey = process.env.EXTENSION_PUBLIC_API_KEY

  return (
    <div>
      <h1>API Information</h1>
      <p>URL: {apiUrl}</p>
      <p>Key: {apiKey}</p>
    </div>
  )
}

export default ApiInfo
Extension.js inlines these values at compile time, and they can vary by browser/mode.

import.meta support

For ECMAScript Module (ESM) workflows, Extension.js also supports import.meta.env:
service_worker.mjs
const apiUrl = import.meta.env.EXTENSION_PUBLIC_API_URL
console.log(`API URL for the current environment: ${apiUrl}`)
import.meta.env and process.env have parity for injected env keys.

Best practices

  • Expose only what must ship: Prefix only client-safe keys with EXTENSION_PUBLIC_.
  • Use .env.defaults for shared defaults: Keep predictable team defaults while allowing local/system overrides.
  • Keep secrets out of static placeholders: Avoid putting secret $EXTENSION_* tokens in HTML/JSON templates.
  • Version control hygiene: Commit .env.example and ignore real env files (.env, .env.local, browser/mode variants).

Next steps