envil envil Docs

Environment Composition

Compose independent server contexts through one canonical extension path.

Fragments let each module own its values and runtime options:

import {
  createEnv,
  extendEnv,
  port,
  postgresUrl,
  server,
  shared,
} from "@ayronforge/envil";

export const baseEnv = createEnv(
  shared({ APP_NAME: "My App" }),
  server({ DATABASE_URL: postgresUrl }),
);

export const serviceEnv = createEnv(
  server({ SERVICE_NAME: "payments" }),
);

export const appEnv = baseEnv.pipe(
  extendEnv(serviceEnv),
  extendEnv(server({ PORT: port })),
);

extendEnv accepts AppEnv values and individual fragments. It is the canonical way to compose an existing environment; createEnv accepts only fragments. This keeps ordering explicit and gives pipe one predictable extension model.

Last definition wins

Composition is applied from left to right:

const appEnv = baseEnv.pipe(
  extendEnv(server({ PORT: port })),
  extendEnv(
    server(
      { PORT: positiveInt },
      { runtimeEnv: deploymentEnv, prefix: "APP_" },
    ),
  ),
);

The second PORT is the entire winning definition. Its schema, source, runtimeEnv, prefix, redaction, errors, and Effect requirements replace the earlier definition. Envil does not merge pieces of duplicate definitions, and a resolver owned only by a shadowed definition is never initialized.

Server and client may use the same property name. A server output includes both targets, so the later fragment among server(...), client(...), and shared(...) wins for that key there. A client output considers only client and shared fragments.

No file-layout convention

Fragments may be declared inline, exported from feature modules, returned by ordinary factories, or assembled with spreads. Envil imposes no filename or directory convention. The compiler only uses the explicit server(...) boundary; it does not interpret the expressions inside it.