Show HN: Envapt, typed env config for decoupled TS codebases (Node to edge)

materwelon1 pts0 comments

Hi HN. I m Dhruv and I ve been working on envapt for about a year now and I ve FINALLY completed the roadmap I scope creeped and QA d over the past few months. It reads environment config in TypeScript and returns the typed value instead of string | undefined, from whatever source you bind. It runs on Node, Bun, Deno, Cloudflare Workers, the browser, and well, anywhere.Most typed-env libraries have you declare every variable in one central schema and read the result from one object. That works well for a single application. Mostly. That didn t work for me because in a framework or a monorepo, decoupled packages each read their own config values, and sharing one config object across every package just doesn t make sense to me, plus some other nits.envapt does the opposite. You bind a source once at startup, and on Node/Deno/Bun it binds your .env files and process.env for you, with cascading profiles per environment. After that, ANY typed read in ANY file uses that source, and each value is validated at the place that reads it. All you gotta do is import the reader from envapt .Say different files each need their own config. Each one just reads what it needs. // db.ts import { Envapter, Converters } from envapt ; export const dbUrl = Envapter.getRequired( DATABASE_URL , Converters.Url); export const poolSize = Envapter.getNumber( POOL_SIZE , 10); // getRequired throws if the var is missing. fail fast! // http.ts (your api file with CORS) import { Envapter, Converters } from envapt ; export const allowedOrigins = Envapter.getUsing( ALLOWED_ORIGINS , Converters.array({ of: Converters.Url }), [] ); // a.com,b.com - URL[] No config object is passed between them. Each reads from the source you bound once.Built-in converters cover the usual primitives plus JSON, URL, RegExp, Date, duration, port, email, and typed arrays. A fallback removes undefined from the return type so you don t need to do stuff like value ?? defaultValue everywhere. You can also provide your own converter or validator as a custom function.Oh. It also has both TC39 and legacy decorators: @EnvTime( TTL , 1h ) static accessor ttl. Fully type checked at compile time.If you already validate with zod, valibot, or arktype, hand that schema to envapt and it runs the value through it (if it exists). import { z } from zod ; import { Envapter } from envapt ; const logLevel = Envapter.parse( LOG_LEVEL , z.enum([ debug , info , warn , error ]), info ); // debug | info | warn | error envapt depends on the Standard Schema interface, so any conformant validator works and none is added to your lockfile. Aaaand it has zero runtime and zero peer dependencies.Oh, also. env values can reference each other and resolve at read time, like DATABASE_URL=postgres://${DB_HOST}/${DB_NAME}. A reference cycle is caught and left as text so it doesn t crash your app.The last four majors, mainly v8, came out of me using envapt in my own projects and finding more to offload to it. seedcord, a Discord bot framework I maintain, builds bots that run on a gateway server and on Cloudflare Workers. On the edge there is no filesystem and no process.env, and before v8 that split needed a different import per runtime and some manual wiring. Now it is one import. The package exports resolve the file build on Node/Bun/Deno and the portable build on Workers, edge, and the browser. seedcord binds the Worker env as the source in its build step, so a user writes their config once and the same code reads .env files in dev and the injected Worker env in production, with no per-runtime branch.Anyhow, it is on npm https://www.npmjs.com/package/envapt and JSR https://jsr.io/@materwelon/envapt . Docs https://envapt.materwelon.dev (It can do a lot more).Pls try :)!!!

envapt from config import envapter converters

Related Articles