惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V2EX - 技术
V2EX - 技术
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
S
Schneier on Security
S
Securelist
P
Privacy & Cybersecurity Law Blog
Scott Helme
Scott Helme
T
Threatpost
C
Cybersecurity and Infrastructure Security Agency CISA
L
LINUX DO - 热门话题
Cyberwarzone
Cyberwarzone
Cisco Talos Blog
Cisco Talos Blog
量子位
博客园 - Franky
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Latest news
Latest news
T
Troy Hunt's Blog
N
News | PayPal Newsroom
Google Online Security Blog
Google Online Security Blog
Apple Machine Learning Research
Apple Machine Learning Research
N
Netflix TechBlog - Medium
小众软件
小众软件
P
Palo Alto Networks Blog
Spread Privacy
Spread Privacy
C
Cyber Attacks, Cyber Crime and Cyber Security
C
Check Point Blog
aimingoo的专栏
aimingoo的专栏
WordPress大学
WordPress大学
L
Lohrmann on Cybersecurity
L
LINUX DO - 最新话题
D
Darknet – Hacking Tools, Hacker News & Cyber Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The Last Watchdog
The Last Watchdog
S
Security @ Cisco Blogs
P
Privacy International News Feed
Last Week in AI
Last Week in AI
Microsoft Security Blog
Microsoft Security Blog
T
Tailwind CSS Blog
博客园_首页
云风的 BLOG
云风的 BLOG
V
Vulnerabilities – Threatpost
D
DataBreaches.Net
Recent Announcements
Recent Announcements
酷 壳 – CoolShell
酷 壳 – CoolShell
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
罗磊的独立博客
Engineering at Meta
Engineering at Meta
Forbes - Security
Forbes - Security
T
Tenable Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Stop your app from booting with broken env vars: a type-safe, universal config library
Krishna Adhikari · 2026-05-31 · via DEV Community

TL;DRprocess.env.PORT is a string | undefined, your bundler silently inlines client env at build time, and every runtime exposes env differently. @teispace/env is a zero-dependency library that loads, validates, coerces, and types your environment once and works the same on Node, Bun, Deno, Cloudflare Workers, Next.js, NestJS, Vite, Nuxt, Astro, and SvelteKit. Bring Zod/Valibot/ArkType, or use the built-in coercers. npm i @teispace/env.

import { defineEnv, e } from '@teispace/env';

export const env = defineEnv({
  schema: {
    NODE_ENV: e.enum(['development', 'production', 'test']).default('development'),
    PORT: e.port().default(3000),
    DATABASE_URL: e.url(),
    ENABLE_CACHE: e.boolean().default(false),
  },
});

env.PORT;          // number  ← coerced AND typed (not the string "3000")
env.DATABASE_URL;  // string
env.ENABLE_CACHE;  // boolean

If a variable is missing or malformed, your app fails fast at boot with one clear error — not three layers deep at 2 a.m. with a cryptic undefined.


The problem nobody admits is a problem

Environment variables feel trivial. You add dotenv, read process.env.WHATEVER, ship it. Then reality arrives:

1. process.env lies to TypeScript

Every value on process.env is typed string | undefined. So this compiles and explodes at runtime:

const port = process.env.PORT;     // string | undefined
server.listen(port);               // expected a number… 🙃
const retries = process.env.RETRIES * 2; // NaN, silently

You can augment NodeJS.ProcessEnv to claim PORT: number, but that's a lie: declaration merging tells TypeScript what should exist, not what does, and process.env.PORT is still the string "3000" at runtime. The type and the value disagree — the worst kind of bug.

2. The "transform" trap

Libraries like t3-env validate and coerce into a returned object. Great — if you read that object. But the moment a teammate reads raw process.env.PORT after a coercion, the type says number and the value is still a string. The type lies again, just one level removed.

3. Your bundler rewrites env behind your back

This is the one that eats afternoons. In Vite, import.meta.env.VITE_X is statically replaced at build time — and only for literal, VITE_-prefixed keys. In Next.js, process.env.NEXT_PUBLIC_X is inlined into the browser bundle at build. So:

// ❌ On the client, this is `undefined` after bundling:
const key = 'VITE_API_URL';
const url = import.meta.env[key];   // dynamic key → not statically replaced

A naive getEnv(key) helper that reads a dynamic key works on the server and silently returns undefined in the browser. The bundler can only see literal access.

4. Every runtime exposes env differently

Runtime Where env lives Global?
Node / Bun process.env
Deno Deno.env.get() (or process.env in compat)
Cloudflare Workers the env binding passed into your handler no global at all
Vite client import.meta.env.VITE_* (build-time replaced) ⚠️ build-time only
Next client process.env.NEXT_PUBLIC_* (build-time inlined) ⚠️ build-time only

Write a helper that reads process.env and it crashes on Workers. Write one for Workers and it's awkward everywhere else.

5. Secrets leak into client bundles

Forget the NEXT_PUBLIC_/VITE_ prefix discipline once and your STRIPE_SECRET ships to every browser. There's usually nothing stopping you.

The existing tools each solve part of this:

dotenv @t3-oss/env envalid
Loads .env
Validates + coerces + types
Types never lie ⚠️ (via raw process.env)
Bring any validator ⚠️ (Zod/Std Schema)
Built-in coercers (zero dep)
Client/server leak guard
Universal runtime (incl. Workers) ⚠️
Dependencies 0 0 (needs a validator) 1

Nobody does all of it in one lean package. That gap is @teispace/env.


Install

npm i @teispace/env
# pnpm add @teispace/env · yarn add @teispace/env · bun add @teispace/env · deno add npm:@teispace/env

ESM-only. Requires Node ≥ 22.12 (or Bun / Deno / Workers). Zero runtime dependencies.


The core idea: one validated, coerced, frozen object

defineEnv reads your environment, validates each variable, coerces it to the right type, and returns a frozen object that is the single source of truth:

import { defineEnv, e } from '@teispace/env';

export const env = defineEnv({
  schema: {
    NODE_ENV: e.enum(['development', 'production', 'test']).default('development'),
    PORT: e.port().default(3000),
    DATABASE_URL: e.url(),
    ENABLE_CACHE: e.boolean().default(false),
  },
});

env.PORT;          // 3000 — a real `number`
typeof env.PORT;   // "number"
env.ENABLE_CACHE;  // false — a real `boolean`
Object.isFrozen(env); // true

Because the value was actually coerced (not type-asserted), the type and the runtime value can never disagree. Read env.* everywhere; never touch process.env again. This is the "types never lie" guarantee — and it's the headline difference from approaches that only fix the type.

The output type is fully inferred from your schema. env.PORT is number, env.NODE_ENV is 'development' | 'production' | 'test', env.DATABASE_URL is string — no z.infer, no manual interface, no as.


Built-in coercers (e.*)

Each coercer turns the raw string | undefined into a typed, validated value. Constraints are passed as options; behaviors are chained:

e.string({ min, max, regex, startsWith, endsWith });
e.number({ min, max, int });
e.int({ min, max });
e.port();                          // 1–65535
e.boolean();                       // true/1/yes/on  vs  false/0/no/off/""
e.url({ protocol });               // WHATWG URL; e.urlObject() returns a URL instance
e.email();
e.enum(['a', 'b', 'c']);           // narrows to 'a' | 'b' | 'c'
e.json<T>(innerSchema?);           // JSON.parse + optional shape validation
e.array({ separator, trim, of });  // "a,b,c" → string[] (or coerced items via `of`)
e.host();
e.hostname();

Chainable on every coercer — each one narrows the type precisely:

e.string().optional();             // string | undefined
e.port().default(3000);            // number  (and no longer optional)
e.string({ min: 1 }).secret();     // value redacted in any error output
e.number().refine((n) => n % 2 === 0, 'must be even');
e.string().transform((s) => s.toUpperCase());
e.url().describe('Public API base URL');

A small but real example mixing several:

const env = defineEnv({
  schema: {
    EVEN_WORKERS: e.number({ int: true }).refine((n) => n % 2 === 0, 'must be even'),
    ALLOWED_ORIGINS: e.array({ of: e.url() }),   // "https://a.com,https://b.com" → string[]
    LOG_LEVEL: e.enum(['debug', 'info', 'warn', 'error']).default('info'),
    SERVICE_NAME: e.string().transform((s) => s.toLowerCase()),
  },
});

env.ALLOWED_ORIGINS is string[], env.LOG_LEVEL is the union, and each origin is itself URL-validated.


Bring your own validator (Standard Schema)

Already invested in Zod, Valibot, or ArkType? They all implement Standard Schema — a tiny shared interface (the spec was designed by the authors of Zod, Valibot, and ArkType). @teispace/env accepts any Standard-Schema validator as a schema entry, and you can mix it with the built-in coercers in one schema:

import { z } from 'zod';
import * as v from 'valibot';
import { defineEnv, e } from '@teispace/env';

export const env = defineEnv({
  schema: {
    DATABASE_URL: z.string().url(),        // Zod
    REGION: v.picklist(['us', 'eu']),      // Valibot
    PORT: e.port().default(3000),          // built-in coercer
  },
});

No adapters, no lock-in. (Env validation is synchronous, so use synchronous schemas — async schemas throw a clear error rather than silently producing a pending value.)


Fail fast, fail clearly

A misconfigured environment should stop the app at boot with a report that tells you everything wrong at once — not the first error, then another redeploy, then the next error. Here's a real failure:

defineEnv({
  schema: {
    DATABASE_URL: e.url(),
    API_KEY: e.string({ min: 1 }),
    PORT: e.port(),
  },
  runtimeEnv: { PORT: '99999' },
});

❌ Invalid environment variables:

  • DATABASE_URL: Missing required environment variable "DATABASE_URL"
  • API_KEY: Missing required environment variable "API_KEY", received nothing (missing)
  • PORT: Expected a valid port (1-65535), received "99999"

Fix these variables and restart.

All three problems, one error, with what was received. And secrets are redacted automatically — by name heuristic (KEY, SECRET, TOKEN, PASSWORD, PRIVATE) or by marking a coercer .secret():

defineEnv({
  schema: { API_SECRET: e.string({ min: 50 }) },
  runtimeEnv: { API_SECRET: 'short' },
});
// • API_SECRET: Expected a string of length >= 50, received length 5, received "***"
//                                                                              ^^^^^ not your secret

The raw value never reaches your logs or CI output — even when it's an element inside an array.


The client/server split + leak guard

This is the feature that prevents a 3 a.m. incident. Declare which variables are server-only and which are client-exposed, and @teispace/env:

  1. asserts at define time that every client variable carries the framework prefix, and
  2. throws if you read a server secret from client code — so a secret physically cannot be read in a browser bundle.
import { defineEnvSplit, e } from '@teispace/env';

export const env = defineEnvSplit({
  clientPrefix: 'PUBLIC_',
  server: { DB_PASSWORD: e.string({ min: 1 }), STRIPE_SECRET: e.string({ min: 1 }) },
  client: { PUBLIC_API_URL: e.url() },
  // Bundlers can't read dynamic env on the client, so list the values explicitly:
  runtimeEnv: {
    DB_PASSWORD: process.env.DB_PASSWORD,
    STRIPE_SECRET: process.env.STRIPE_SECRET,
    PUBLIC_API_URL: process.env.PUBLIC_API_URL,
  },
});

env.PUBLIC_API_URL;  // ✅ ok everywhere
env.DB_PASSWORD;     // ❌ on the client, throws:
// "Attempted to access server-only env var "DB_PASSWORD" on the client.
//  This variable is declared under `server` and is intentionally not bundled
//  into client code…"

The guard is implemented with a Proxy, and it holds against every leak path I could think of — direct access, computed keys, destructuring, Object.values, JSON.stringify, spread, Reflect.get. On the client, the server value isn't even present in the underlying object, so there's nothing to leak before the throw.

Why runtimeEnv? Because bundlers statically replace env access at build time and only for literal keys (see problem #3). On the server, @teispace/env auto-reads process.env, so runtimeEnv is optional. On the client, you must list the keys so the bundler can inline them — there's no way around the bundler, so the library makes the requirement explicit and type-checked rather than letting it fail silently.


Universal: the same library on every runtime

Node / NestJS / Express / Fastify / Hono

On the server, env is auto-sourced from process.env / Deno.env / Bun.env (detected defensively — it never crashes on import, even in a browser bundle). The /node preset adds optional .env loading:

import { defineEnv, e } from '@teispace/env/node';

export const env = defineEnv({
  load: true, // load the .env cascade before validating
  schema: {
    NODE_ENV: e.enum(['development', 'production', 'test']).default('development'),
    PORT: e.port().default(3000),
    DATABASE_URL: e.url(),
  },
});

// NestJS: feed the validated object straight into ConfigModule
// ConfigModule.forRoot({ validate: () => env, isGlobal: true });

Cloudflare Workers (no global process.env)

Workers pass bindings into the request handler, so there's nothing to read at import time. Use createEnv to build a parser and call it per request — it's memoized per binding, so you pay validation once:

import { createEnv, e } from '@teispace/env';

const parseEnv = createEnv({
  schema: { API_KEY: e.string({ min: 1 }), UPSTREAM: e.url() },
});

export default {
  fetch(req: Request, env: unknown) {
    const config = parseEnv(env); // fully typed: config.API_KEY, config.UPSTREAM
    return new Response(config.UPSTREAM);
  },
};

Framework presets

Each preset bakes in the correct client prefix and re-exports e:

import { defineEnv, e } from '@teispace/env/next';       // NEXT_PUBLIC_
import { defineEnv, e } from '@teispace/env/vite';       // VITE_   (reads import.meta.env)
import { defineEnv, e } from '@teispace/env/nuxt';       // NUXT_PUBLIC_
import { defineEnv, e } from '@teispace/env/astro';      // PUBLIC_ (reads import.meta.env)
import { defineEnv, e } from '@teispace/env/sveltekit';  // PUBLIC_
import { defineEnv, e } from '@teispace/env/node';       // flat + optional .env loading

A full Next.js example:

// env.ts
import { defineEnv, e } from '@teispace/env/next';

export const env = defineEnv({
  server: {
    DATABASE_URL: e.url(),
    STRIPE_SECRET: e.string({ min: 1 }),
  },
  client: {
    NEXT_PUBLIC_API_URL: e.url(),
    NEXT_PUBLIC_GA_ID: e.string().optional(),
  },
  runtimeEnv: {
    DATABASE_URL: process.env.DATABASE_URL,
    STRIPE_SECRET: process.env.STRIPE_SECRET,
    NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
    NEXT_PUBLIC_GA_ID: process.env.NEXT_PUBLIC_GA_ID,
  },
});


Loading .env files

@teispace/env/load loads the standard cascade with ${VAR} expansion — a drop-in superset of dotenv:

import { loadEnv } from '@teispace/env/load';

loadEnv(); // populates process.env, returns the merged values

Precedence (highest wins): .env.env.local.env.[mode].env.[mode].local (and .env.local is skipped in test mode, matching Vite). It supports ${VAR}, ${VAR:-default}, \$ escapes, quoted/multiline values, and export KEY=…. Or, the dotenv/config muscle-memory version:

import '@teispace/env/config'; // side-effect import; loads the cascade

It's also safe off-Node: if there's no filesystem (browser, Workers), it degrades to a no-op instead of crashing.


Robustness & security details

A config library sits at your app's front door, so the small things matter:

  • Aggregated errors — every problem reported at once, never first-only.
  • Secret redaction — by name heuristic and .secret(), including values nested inside array elements. (This was a real bug an adversarial review caught and we fixed before release: array-element values weren't being redacted. They are now.)
  • skipValidation keeps defaults — for CI/Docker build steps you can skip throwing while still applying coercion and defaults, so the typed shape holds. (A subtle footgun in some tools is that skipping validation also drops defaults — this one doesn't.)
  • Parse once — validation runs at module evaluation; reads are plain property access.
  • Frozen output — the result is Object.freezed.
  • Never crashes on import — runtime detection is fully defensive across Node/Bun/Deno/Workers/browser.
  • Secure-by-default URLse.url() rejects javascript:, data:, vbscript:, and file: schemes by default (they have no business in an env var and are dangerous if they reach the DOM), while still accepting normal network schemes so postgres://, redis://, amqp://, mongodb:// connection strings keep working. Opt out per-field if you really need to.
  • ReDoS-safe — the built-in email/URL/host regexes are linear-time (verified against pathological inputs).
  • Zero dependencies, ESM, fully tree-shakeable, ships types.
// skipValidation example: defaults still apply, no throw on missing required
const env = defineEnv({
  schema: { PORT: e.port().default(3000), DATABASE_URL: e.url() },
  runtimeEnv: {},
  skipValidation: true,
});
env.PORT; // 3000 — default applied even though validation was skipped


Migrating from dotenv or t3-env

From dotenv: replace import 'dotenv/config' with import '@teispace/env/config' to get the same loading plus the cascade and expansion. Then wrap your reads in a defineEnv schema to add validation and types incrementally.

From t3-env: the shape is intentionally familiar — server, client, clientPrefix, runtimeEnv. The differences you gain: built-in coercers (no required validator), a true coerced single-source-of-truth object, universal runtimes (Workers/Deno), and the same API across backend and frontend instead of framework-specific entry points.


Why we built it

We maintain a set of open-source packages at Teispace (a Next.js starter CLI, a theme manager, a Lexical editor), and every one of them re-solved environment configuration slightly differently — and slightly wrong. We wanted one small, correct, dependency-free primitive that we could use in a Next app, a NestJS API, a Worker, and a build script without thinking about it. So we researched the whole space, wrote down every footgun, and built the package that fixes all of them at once — then had it adversarially reviewed before shipping.

It's MIT, open source, and the full readable source lives on GitHub (the published bundle is minified for size). If it saves you one 2 a.m. "why is this undefined" debugging session, it did its job.

  • npm: @teispace/env
  • Install: npm i @teispace/env

If you try it, I'd love to hear what runtime/framework combo you're on and whether anything's missing. Drop a comment. 👇

Built by Teispace.