Skip to content

Environments and schemas ​

Environments ​

Use top-level env and $env for values shared by all products. Put product-specific values under the product when different products assemble the same projects with different backends:

ts
import { defineMatrixConfig, defineMatrixEnv } from '@xova/matrix'

export default defineMatrixConfig({
  projects: {},
  products: {
    app: {
      appId: 'com.example.app',
      env: { VITE_API_BASE: 'http://localhost:3000' },
      $env: defineMatrixEnv({
        staging: { VITE_API_BASE: 'https://staging-api.example.com' },
        production: { VITE_API_BASE: 'https://api.example.com' },
      }),
      variants: {},
    },
  },
})

defineMatrixEnv() is syntax sugar for the c12-compatible $env.<environment>.env shape. The raw shape remains supported.

Ordinary application variables are merged from low to high precedence:

text
global env/$env < product env/$env < .env layers < process.env

Product identity overrides are resolved from the merged environment before suffixes are applied:

text
product identity < variant identity < merged identity override < environment suffix

MATRIX_PRODUCT_NAME, MATRIX_PRODUCT_SLUG, and MATRIX_PRODUCT_APP_ID may provide identity overrides through the environment. The final values, including suffixes, are written back to both task metadata and the corresponding MATRIX_PRODUCT_* variables. MATRIX_PRODUCT_ID, MATRIX_PRODUCT_KEY, execution-context variables, and NODE_ENV are generated by Matrix and cannot be overridden by the environment.

Matrix injects the following execution-context variables into target child processes. Project preparation has a narrower project-scoped context; see project preparation.

text
MATRIX_ENV_NAME
MATRIX_TARGET
MATRIX_PRODUCT_KEY
MATRIX_PRODUCT_ID
MATRIX_PRODUCT_NAME
MATRIX_PRODUCT_SLUG
MATRIX_PRODUCT_APP_ID
MATRIX_PRODUCT_VERSION
MATRIX_VARIANT
MATRIX_PROJECT
MATRIX_NODE_ENV
NODE_ENV

MATRIX_ENV_NAME is the selected Matrix configuration environment and may be a custom name such as staging or qa. NODE_ENV describes the target process mode: dev uses development, test uses test, while build, dist, and preview use production. MATRIX_NODE_ENV is the same generated value under the reserved MATRIX_ prefix so Vite's prefixed config.env can carry it into the build-time runtime module. Therefore a staging build normally receives MATRIX_ENV_NAME=staging, MATRIX_NODE_ENV=production, and NODE_ENV=production. MATRIX_PRODUCT_APP_ID is emitted only when the resolved product identity has an appId; environment suffixes are applied before it is exported.

Product-level environment values are resolved independently for each product. This lets two products reuse the same Desktop project while connecting it to different Web variants or services.

Custom environment names are supported. Define them with the same helper and pass the name explicitly to the CLI:

ts
import { defineMatrixConfig, defineMatrixEnv } from '@xova/matrix'

export default defineMatrixConfig({
  $env: defineMatrixEnv({
    qa: {
      VITE_API_BASE: 'https://qa-api.example.com',
    },
  }),
  projects: {},
  products: {},
})
bash
matrix build app --env qa
matrix plan app --target preview --env qa

Custom environments are available through --env. When running interactively, Matrix adds names found in the top-level and product $env configuration to the selector.

Dotenv files are loaded as .env, .env.local, .env.<environment>, and .env.<environment>.local. Matrix passes variables such as VITE_* and NUXT_* to child processes; application frameworks keep ownership of their own runtime configuration.

Configuration files can read the selected dotenv values through process.env during evaluation, with inherited Shell values taking precedence. Each configuration load or environment discovery runs in a short-lived Worker with its own environment and complete ESM/CommonJS module cache. Local configuration imports are evaluated together for that load; the host's environment and module caches are not modified. The Worker is terminated after returning the result, so configuration files should produce data rather than start persistent services. Execution plans retain a separate environment snapshot; returned layers are JSON diagnostic snapshots, not executable objects.

Environment schema ​

Declare types, defaults, and optionality once in root-level envSchema. Products still override values through env / $env; they do not declare separate schemas or infer types from overrides:

ts
import { defineMatrixConfig, defineMatrixEnv } from '@xova/matrix'

export default defineMatrixConfig({
  envSchema: {
    VITE_RENDERER_MODE: { type: 'enum', values: ['remote', 'bundled'], default: 'remote' },
    VITE_CLOUD_SUBMISSION_ENABLED: { type: 'boolean', default: false },
    VITE_RETRY_COUNT: { type: 'number', default: 3 },
    VITE_LABEL: { type: 'string', optional: true },
  },
  projects: { web: { targets: { build: 'vite build' } } },
  products: {
    app: {
      env: { VITE_RENDERER_MODE: 'bundled' },
      variants: { web: 'web' },
    },
  },
})

defineMatrixConfig() constrains global, product, and $env inputs from the root declaration, including enum defaults. Defaults use native types. Boolean overrides accept booleans or exactly 'true' / 'false'; numbers accept finite numbers or decimal/scientific-notation strings, not whitespace, empty strings, hexadecimal, NaN, or Infinity. Strings are not implicitly coerced; enums match strings exactly.

Precedence remains schema default < global env/$env < product env/$env < dotenv < process.env. Defaults only fill missing fields. Invalid final overrides fail without falling back; errors identify the field and expected type, not its value. Undeclared fields retain their existing behavior.

For builds launched through Matrix, matrix.config.cloudSubmissionEnabled is a boolean, retryCount is a number, and rendererMode has an enum literal-union type. Raw process.env and generated ImportMetaEnv fields remain strings. An absent optional: true field without a default is omitted from the runtime object and gets a ? property; defaulted fields are non-optional. Both matrix prepare and build adapters generate types from the declaration, without writing actual values or non-public fields into type files.

Required-field checks apply to the consuming adapter's public prefixes: a Web build using VITE_ does not require missing MAIN_VITE_* fields. Planned tasks and preparation commands validate present values in their own final environments and apply defaults, without globally requiring every field. Type generation itself does not require values. Vite validates fields within the final envPrefix when configuration is resolved; other adapters validate at build startup. Validation does not depend on importing the runtime module and still runs with type generation disabled. Required declarations apply to all projects consuming the same prefix; use distinct prefixes or optional declarations for project-specific fields. Present non-public values are validated too, but applications own their missing-value checks. scope still isolates module and declaration files; envPrefix still controls exposure, and schemas never expand it. Ambiguous public property mappings involving schema fields are rejected to avoid mismatched types and values.

Matrix passes the schema to adapters through internal child-process context, never through matrix.config. Standalone builds not launched by Matrix keep the existing string behavior. MATRIX_*, __MATRIX_*, and NODE_ENV are reserved and cannot be declared in a schema. Plan output may still contain actual declared environment values and should not be posted publicly.