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.