Environment Variables
Two tiers of env for an extension that has no server.
Extro reads .env files at build time. Because an extension has no server, the model is two tiers: values that ship into your surfaces, and values that stay on the build machine.
The two tiers
Public: prefixed EXTRO_PUBLIC_*, inlined into every surface (popup, options, sidepanel, background, content) through import.meta.env. Declaring a var public is declaring "this ships to users."
const api = import.meta.env.EXTRO_PUBLIC_API_URLAt build time this becomes the literal string, which is why it works in the service worker and content scripts even though they have no import.meta at runtime.
Build-time: any other name, read through process.env in extro.config.ts and by the framework. Never inlined into a surface.
export default defineConfig({
version: process.env.APP_VERSION,
})EXTRO_* is reserved for framework-recognized vars (see the CRX key below). Put your own shipping values under EXTRO_PUBLIC_* and everything else under any non-EXTRO_ name.
There is no safe place for a secret
Every surface ships to the user, and the packaged extension is a zip anyone can unpack. The public/build-time split is mechanical, not a security boundary: a build-time var is simply not auto-inlined, but anything you write into the manifest (or any surface) ships in plaintext. Do not put a value in any tier that you would not hand to a user.
Files
.env # always loaded
.env.development # extro dev
.env.production # extro build
.env.local # always loaded, git-ignored
.env.development.local # dev-only local overridesPrecedence, lowest to highest: .env < .env.local < .env.[mode] < .env.[mode].local. extro dev loads the development set, extro build loads production. Real process env (CI) always wins over .env files. Commit a .env.example as the template; everything else is git-ignored by default.
CRX key
Set EXTRO_CRX_KEY to a base64 public key and Extro writes it to manifest.key, pinning a stable extension ID across machines and reloads (useful for OAuth redirect URIs and externally_connectable). An explicit manifest.key in extro.config.ts still wins.
EXTRO_CRX_KEY=MIIBIjANBgkqhki...Types
import.meta.env is typed automatically. Any project that imports a React subpath such as extrojs/link or extrojs/navigation (every routable surface does) picks up the ambient typing, so import.meta.env.EXTRO_PUBLIC_* resolves as string | undefined with no setup.
For a script that imports nothing from the framework (a bare background or content script), add the reference once in an extro-env.d.ts:
/// <reference types="extrojs/client" />To name specific vars instead of the generic EXTRO_PUBLIC_* index, augment ImportMetaEnv yourself.
Changing env in dev
Env values are inlined at build time, so a .env edit is not hot. Restart extro dev to pick up a change.