envil envil Docs

Core Concepts

Target-aware fragments, arbitrary Effect Schemas, runtime sources, and type inference.

Target-aware fragments

server, client, and shared create immutable definitions. They do not read configuration when declared:

const appEnv = createEnv(
  shared({ APP_NAME: "My App" }),
  server(serverValues),
  client(clientValues, {
    runtimeEnv: import.meta.env,
    prefix: "VITE_",
  }),
);

appEnv.client resolves client and shared fragments. appEnv.server resolves server, client, and shared fragments so the server has the complete application configuration.

Fragments are the unit of composition and compilation. Their contents remain ordinary TypeScript expressions, so values may come from object spreads, factories, imported schemas, and custom resolver adapters.

Effect Schema

Variable definitions may use Effect Schemas with transforms, refinements, asynchronous decoding, and context requirements. Schema outputs must not be functions. Schema errors and requirements are preserved in the resulting Effect type:

const serverConfig = server({
  DATABASE_URL: postgresUrl,
  PORT: port,
});

Static values are also accepted in server and client fragments. shared is reserved for public static values available in both outputs; schemas and redacted values are rejected there.

Runtime names and sources

By default, a schema reads its property name with the fragment prefix:

client(
  {
    API_URL: url, // reads VITE_API_URL
  },
  {
    runtimeEnv: import.meta.env,
    prefix: "VITE_",
  },
);

fromEnv maps one property to an exact external name:

ANALYTICS_KEY: requiredString.pipe(
  fromEnv("PUBLIC_ANALYTICS_KEY"),
)

Each fragment owns its runtimeEnv, prefix, and emptyStringAsUndefined options. Independently authored fragments therefore keep their original runtime source when composed.

Runtime values may come from an ordinary object, a ReadonlyMap, or parsed JSON. Dot-separated fromEnv names navigate nested objects:

server(
  {
    DATABASE_URL: postgresUrl.pipe(fromEnv("database.url")),
  },
  {
    runtimeEnv: {
      database: {
        url: "postgres://user:pass@localhost:5432/app",
      },
    },
  },
);

For objects, an exact key such as "database.url" takes precedence over the equivalent nested path. Map keys are always exact.

Resolver sources

fromResolver makes a configured resolver authoritative for one server variable. A resolver adapter is ordinary runtime code: Envil batches references that use the same configured resolver, validates the returned values with their original schemas, and redacts resolver-backed outputs automatically.

Composition and precedence

Compose an existing environment with pipe(extendEnv(...)):

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

Inputs are applied from left to right. If the same key appears more than once in one runtime context, the last complete definition wins, including its value, schema, source, runtime options, errors, and Effect requirements. A shadowed resolver is not executed.

Type inference

import type {
  InferClientEnv,
  InferEnv,
  InferServerEnv,
} from "@ayronforge/envil";

type ServerEnvironment = InferServerEnv<typeof appEnv>;
type ClientEnvironment = InferClientEnv<typeof appEnv>;
type ServerEffectValue = InferEnv<typeof appEnv.server>;

Runtime access guards

Materialized environments are immutable Proxies. An unknown property does not silently return undefined; Envil reports whether the property is unavailable in the current target.

appEnv.server also verifies the runtime when its Effect executes. Browser and plugin-marked client builds receive a ServerEnvironmentAccessError before server fragments or resolvers are evaluated.

Use the build plugins to make the runtime target exact and physically remove server fragment expressions from client bundles.