Astilba Env is a local-first configuration contract compiler for TypeScript. You declare which application artifacts need configuration, when each value is resolved, and where it may be exposed; Env generates typed, physically separated interfaces and value-free compatibility evidence.
Env does not replace a secret manager or .env files. It does not receive, store, or transmit configuration values; compilation, checking, and planning run in your project.
pnpm add @astilba/envThe declaration builders, generator, and CLI require Node.js 22.14.0 or later within the published major-version ranges. Generated deployment-lifecycle server targets using Astilba's built-in codecs can also use @astilba/env/runtime in Cloudflare Workers without the nodejs_compat compatibility flag. Workers refuse build-lifecycle, request-lifecycle, and opaque-schema target execution. Env adds no compatibility-date floor; use the latest date supported by your installed Wrangler. The archive admission lane pins Wrangler 4.115.0 at its supported 2026-07-29 date and observes its bundled workerd 1.20260722.1; this is singleton admission evidence, not a wider compatibility claim. Vite support is optional and supports Vite versions from 8.1.5 up to, but not including, 9.0.0.
Create astilba.env.ts in an ESM package:
import { defineEnvironment, env } from "@astilba/env";
export default defineEnvironment({
id: "com.example.application",
entries: {
apiOrigin: env.public.deployment.origin(),
databaseUrl: env.private.deployment.secret(),
},
consumers: {
browser: env.browser(["apiOrigin"]),
server: env.server(["databaseUrl"]),
},
targets: {
browserDeployment: env.process("browser", {
apiOrigin: "PUBLIC_API_ORIGIN",
}),
serverDeployment: env.process("server", {
databaseUrl: "DATABASE_URL",
}),
},
});Generate the project-owned interfaces, then make drift checking a required CI step:
astilba-env generate
astilba-env generate --checkGenerated server modules expose typed check and load operations. Generated browser modules expose only the selected public projection; private names, codecs, bindings, values, and full-contract metadata do not enter the browser graph.
The CLI can compile a deterministic, value-free name inventory for one process target:
astilba-env inventory export --target serverDeploymentThe document contains logical entry IDs, bound names, lifecycle, visibility, and required presence. It contains no configuration values and makes no provider-kind claim. Without --json, the command writes the canonical astilba.env.contract-inventory/v1 document to stdout; --json wraps it in the versioned CLI response.
Provider tooling stays outside Env. Convert a provider's name-only list into the strict observed form before checking it:
{
"entries": [{ "name": "DATABASE_URL" }],
"format": "astilba.env.observed-name-inventory/v1"
}astilba-env inventory check \
--target serverDeployment \
--observed ./observed-names.json \
--ownership closedopen ownership is the default; unexpected names are reported without failing. closed ownership is always explicit and makes unexpected names fail. A missing required name fails in either mode; a missing optional name is reported without failing. Exit 0 means the name inventory is acceptable, exit 1 means drift or invalid evidence, and exit 2 is reserved for command-line misuse.
Inventory checks prove name presence only. They do not inspect values, verify that a provider stored a value as a secret, move configuration, infer namespace ownership, or turn required: false into warning severity. Keep provider conversion, routing, sync, prune, warning policy, and all values application-owned. Bound names remain sensitive operational metadata; do not upload inventory files as CI artefacts by default.
@astilba/envprovides declaration builders;@astilba/env/runtimesupports generated Node target modules and generated Cloudflare Workers deployment targets using built-in codecs;@astilba/env/browservalidates public browser bootstrap data;@astilba/env/viterejects private Env modules from browser graphs;astilba-envis the Node command-line interface.
Applications own their configuration endpoint and response headers. Responses that vary by request must use Cache-Control: private, no-store; the browser runtime also fetches with cache: "no-store".
There is no hosted control plane and no framework-specific semantic layer. Next.js integration is application-owned wiring around generated modules and the browser protocol; there is no @astilba/env/next export.
Read the public documentation for the supported release boundary and the migration guide before replacing next-dynamic-env. The guide names intentional compatibility changes, including validation and browser-delivery differences.
The examples workspace contains small, independent Node, Cloudflare Worker, Next.js, and Vite applications. Each installs the released npm package rather than this checkout. From examples, run pnpm install --frozen-lockfile, then pnpm verify:all to regenerate-check and exercise them.
Please report vulnerabilities through the security policy. Do not include secrets or configuration values in a public issue.
MIT