@freshpointcz/fresh-env
v0.0.2
Published
Type-safe environment loader with schema-driven validation, encoding, and coercion
Keywords
Readme
@freshpointcz/fresh-env
Type-safe environment loader with schema-driven validation, encoding, and coercion for Node.js services.
Why
process.env gives you Record<string, string | undefined>. Every service ends up with ad-hoc parsing scattered across the codebase — Number(process.env.PORT), process.env.DEBUG === "true", unchecked optional keys silently producing undefined at runtime.
fresh-env solves this with a single declarative schema that handles validation, type coercion, encoding/decoding, and deprecation warnings — all at startup, before your service serves a single request.
Installation
npm install @freshpointcz/fresh-envRequires Node.js ≥ 20. No runtime dependencies — only uses built-in fs and crypto.
Quick start
1. Define your environment type
import type { ServiceEnv, EnvSchema } from "@freshpointcz/fresh-env";
type MyServiceEnv = ServiceEnv<{
PORT: number;
API_URL: string;
DEBUG: boolean;
}>;2. Create a schema
The schema is the single source of truth — it declares every key your service expects, how to validate it, and what type to coerce it into.
const schema: EnvSchema<MyServiceEnv> = {
PORT: {
rule: "required",
type: "number",
default: 3000,
},
API_URL: {
rule: "required",
type: "string",
description: "Base URL for the upstream API",
},
DEBUG: {
rule: "optional",
type: "boolean",
default: false,
},
};3. Create the environment
import { Environment } from "@freshpointcz/fresh-env";
const service = new Environment<MyServiceEnv>(schema);
export const env = service.env;
env.PORT; // number — typed, coerced, validated
env.API_URL; // string
env.DEBUG; // booleanIf API_URL is missing from process.env and has no default, the constructor throws immediately with a clear error message.
Schema reference
Each key in the schema accepts the following options:
| Field | Type | Required | Description |
| ------------ | ------------------------------------------ | -------- | -------------------------------------------------------- |
| rule | "required" | "optional" | "deprecated" | Yes | Validation behavior (see below) |
| type | "string" | "number" | "boolean" | Yes | Target type — raw string is coerced at load time |
| encoding | "plain" | "base64" | "aes-256" | No | Decoding applied before coercion. Defaults to "plain" |
| default | Matches the declared type | No | Fallback when the key is absent from process.env |
| description| string | No | Used in deprecation warnings and documentation |
Key rules
required— must be present inprocess.envor have adefault. Missing keys cause a startup error.optional— may be absent. The value will beundefinedif not set and no default is provided.deprecated— still functional, but logs aconsole.warnwhen set, nudging operators to migrate.
Coercion
All process.env values are strings. The type field controls how they're converted:
| Type | Accepted values | Result |
| ----------- | -------------------------------- | --------- |
| "string" | Any string | string |
| "number" | Anything Number() doesn't NaN | number |
| "boolean" | "true", "1", "false", "0" | boolean |
Invalid coercion throws an error at startup.
Encoded values
For values that shouldn't sit in plaintext in .env files, the schema supports two encoding modes.
Base64 (obfuscation)
Prevents casual reading but is not encryption — anyone can decode it.
API_URL: {
rule: "required",
type: "string",
encoding: "base64",
},# .env
API_URL=aHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20=Generate with:
import { encodeBase64 } from "@freshpointcz/fresh-env";
console.log(encodeBase64("https://api.example.com"));
// aHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20=AES-256-CBC (encryption)
Real encryption. Values are stored as iv:ciphertext in hex. Requires a 32-byte secret key.
DB_PASSWORD: {
rule: "required",
type: "string",
encoding: "aes-256",
},# .env
DB_PASSWORD=a1b2c3d4e5f6....:d4e5f6a1b2c3....Generate with:
import { encryptAES } from "@freshpointcz/fresh-env";
import { randomBytes } from "crypto";
// Generate a key once, store it safely
const key = randomBytes(32).toString("hex");
console.log(encryptAES("my-secret-password", key));
// a1b2c3d4...:e5f6a7b8...Providing the secret key
The AES-256 decoder resolves the secret key in this order:
ENV_SECRET_KEYenvironment variable (64-character hex string), but that should only be used at local testing- File at
ENV_SECRET_KEY_PATH(defaults to/run/secrets/env_secret_key), that is imported via docker compose secrets
The key file encoding can be configured via ENV_SECRET_KEY_ENCODING (defaults to utf8).
Redundant variable warnings
Keys in process.env that are not declared in the schema are logged as warnings at startup:
[Environment] "OLD_UNUSED_VAR" is set but not declared in schema — ignoredThis helps catch typos, leftover variables, and env drift between services.
Custom source (testing)
The Environment constructor accepts an optional second argument to replace process.env:
const testEnv = new Environment<MyServiceEnv>(schema, {
PORT: "9999",
API_URL: "http://localhost:4000",
});
testEnv.env.PORT; // 9999 (number)Exported types
| Type | Description |
| ---------------------- | -------------------------------------------------------------- |
| PrimitiveTypes | string \| number \| boolean |
| CoercibleType | "string" \| "number" \| "boolean" |
| BasicRecord<T> | Record<string, T \| undefined> — base constraint for env maps |
| KeyRule | "required" \| "optional" \| "deprecated" |
| Encoding | "plain" \| "base64" \| "aes-256" |
| AppEnvironment<T, S> | Intersection of specific keys S + string index signature |
| DefaultProcessEnvType| Type-compatible alias for process.env |
| ServiceEnv<K> | AppEnvironment pinned to PrimitiveTypes |
| EnvSchema<E> | Declarative schema mapping for a ServiceEnv |
Exported functions
| Function | Description |
| -------------- | -------------------------------------------------------- |
| coerce | Converts a raw string to string, number, or boolean |
| decode | Decodes an encoded env value (plain, base64, aes-256) |
| encodeBase64 | Encodes a value to Base64 for .env file storage |
| encryptAES | Encrypts a value with AES-256-CBC for .env file storage |
Project structure
src/
├── index.ts # Public barrel — re-exports everything consumers need
├── types.ts # All type definitions (no runtime code)
├── coerce.ts # String → primitive coercion
├── decode.ts # Encoded env value → plaintext (base64, aes-256)
├── encode.ts # Plaintext → encoded value (base64, aes-256)
├── secret-key.ts # ENV_SECRET_KEY resolution (env var or file)
└── environment.ts # Environment classLicense
ISC
