<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Stijn</title>
    <description>The latest articles on DEV Community by Stijn (@stijnwtf).</description>
    <link>https://dev.to/stijnwtf</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4180356%2F73378dd4-95cf-4bc6-9d7c-569996b86188.png</url>
      <title>DEV Community: Stijn</title>
      <link>https://dev.to/stijnwtf</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/stijnwtf"/>
    <language>en</language>
    <item>
      <title>Stop fixing env vars one failed deploy at a time</title>
      <dc:creator>Stijn</dc:creator>
      <pubDate>Mon, 12 Oct 2026 20:39:10 +0000</pubDate>
      <link>https://dev.to/stijnwtf/stop-fixing-env-vars-one-failed-deploy-at-a-time-eio</link>
      <guid>https://dev.to/stijnwtf/stop-fixing-env-vars-one-failed-deploy-at-a-time-eio</guid>
      <description>&lt;p&gt;You know this loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deploy.&lt;/li&gt;
&lt;li&gt;App crashes: &lt;code&gt;DATABASE_URL is not defined&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add it, redeploy.&lt;/li&gt;
&lt;li&gt;App crashes: &lt;code&gt;STRIPE_KEY is not defined&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add it, redeploy.&lt;/li&gt;
&lt;li&gt;App boots, but &lt;code&gt;PORT&lt;/code&gt; is &lt;code&gt;"3000 "&lt;/code&gt; with a trailing space and something somewhere quietly turns it into &lt;code&gt;NaN&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every round trip costs a build, a deploy and a bit of your sanity. I hit this often enough that I wrote a small library to make it go away: &lt;strong&gt;&lt;a href="https://github.com/stijnwtf/envella" rel="noopener noreferrer"&gt;envella&lt;/a&gt;&lt;/strong&gt;. This post is about the problem more than the library, though. Even if you never install it, the ideas apply.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;process.env&lt;/code&gt; lies to you
&lt;/h2&gt;

&lt;p&gt;As far as TypeScript knows, every environment variable is &lt;code&gt;string | undefined&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// string | undefined&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So you write something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DEBUG&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every line has a problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Number("abc")&lt;/code&gt; is &lt;code&gt;NaN&lt;/code&gt;, and &lt;code&gt;NaN || 3000&lt;/code&gt; silently gives you &lt;code&gt;3000&lt;/code&gt;. A typo in your config becomes a default you never asked for.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEBUG=1&lt;/code&gt; or &lt;code&gt;DEBUG=yes&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;!&lt;/code&gt; doesn't check anything. It just tells the compiler to stop worrying, and then your app crashes the first time a request actually touches the database. That might be minutes after startup, or only on one code path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix is old advice: &lt;strong&gt;parse your config once, at startup, and fail loudly if it's wrong.&lt;/strong&gt; After that, the rest of your code gets real types and never touches &lt;code&gt;process.env&lt;/code&gt; again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate everything up front
&lt;/h2&gt;

&lt;p&gt;With envella you describe the variables once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// env.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createEnv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;oneOf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;envella&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createEnv&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Postgres connection string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;port&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;oneOf&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;development&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;development&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;DEBUG&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;STRIPE_KEY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;SENTRY_DSN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the types follow from the schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// number&lt;/span&gt;
&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DEBUG&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// boolean&lt;/span&gt;
&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// "development" | "production" | "test"&lt;/span&gt;
&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SENTRY_DSN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// string | undefined&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;default&lt;/code&gt; removes &lt;code&gt;undefined&lt;/code&gt; from the type, &lt;code&gt;optional&lt;/code&gt; keeps it. No casting, no &lt;code&gt;!&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The parsing is env-friendly too. &lt;code&gt;bool()&lt;/code&gt; accepts &lt;code&gt;true/false&lt;/code&gt;, &lt;code&gt;1/0&lt;/code&gt;, &lt;code&gt;yes/no&lt;/code&gt; and &lt;code&gt;on/off&lt;/code&gt;. &lt;code&gt;port()&lt;/code&gt; only accepts integers from 1 to 65535. And an empty string counts as missing, so a &lt;code&gt;PORT=&lt;/code&gt; line in your &lt;code&gt;.env&lt;/code&gt; falls back to the default instead of failing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Report every problem at once
&lt;/h2&gt;

&lt;p&gt;This is the part that kills the deploy loop. When something is wrong, you don't get the &lt;em&gt;first&lt;/em&gt; problem, you get &lt;em&gt;all&lt;/em&gt; of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Invalid environment variables:
  x DATABASE_URL  missing, expected url (Postgres connection string)
  x PORT          "abc" is invalid: expected a port (1-65535)
  x STRIPE_KEY    missing, expected string
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One failed boot, one list, one fix. The &lt;code&gt;description&lt;/code&gt; you wrote in the schema shows up right next to the error, so whoever is fixing it (future you, a teammate, the person on call at 2 AM) knows what the variable is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't leak secrets in your error logs
&lt;/h2&gt;

&lt;p&gt;Showing the bad value is really useful: &lt;code&gt;"abc" is invalid&lt;/code&gt; tells you much more than &lt;code&gt;PORT is invalid&lt;/code&gt;. But if you do that naively, the first time someone pastes the wrong thing into &lt;code&gt;STRIPE_KEY&lt;/code&gt;, your actual secret ends up in your logs, your error tracker and maybe a Slack alert.&lt;/p&gt;

&lt;p&gt;So anything marked &lt;code&gt;secret: true&lt;/code&gt; gets masked. Say someone forgets the &lt;code&gt;https://&lt;/code&gt; on a webhook URL that carries a token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;SLACK_WEBHOOK_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Invalid environment variables:
  x SLACK_WEBHOOK_URL  **** is invalid: expected a URL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You still see &lt;em&gt;which&lt;/em&gt; variable is broken, just not its contents.&lt;/p&gt;

&lt;p&gt;If you'd rather exit cleanly than throw a stack trace, there's a hook for that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createEnv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;error.issues&lt;/code&gt; gives you &lt;code&gt;{ key, message }[]&lt;/code&gt; if you want to format things yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep &lt;code&gt;.env.example&lt;/code&gt; honest
&lt;/h2&gt;

&lt;p&gt;Every project has a &lt;code&gt;.env.example&lt;/code&gt;. Almost every project's &lt;code&gt;.env.example&lt;/code&gt; is out of date, because nobody remembers to update it when they add a variable.&lt;/p&gt;

&lt;p&gt;The schema already knows every variable, its type, whether it's required, whether it's secret, and what it's for. So it can write the file for you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Postgres connection string (url, required)&lt;/span&gt;
&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;

&lt;span class="c"&gt;# (port, optional)&lt;/span&gt;
&lt;span class="nv"&gt;PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3000

&lt;span class="c"&gt;# (string, required, secret)&lt;/span&gt;
&lt;span class="nv"&gt;STRIPE_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make that work from the command line, export the schema (not the result of &lt;code&gt;createEnv&lt;/code&gt;) from its own file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// env.schema.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;envella&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Postgres connection string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;port&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;STRIPE_KEY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx envella example env.schema.ts          &lt;span class="c"&gt;# writes .env.example&lt;/span&gt;
npx envella example env.schema.ts &lt;span class="nt"&gt;--check&lt;/span&gt;  &lt;span class="c"&gt;# fails if it's out of date&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Catch it in CI, not in production
&lt;/h2&gt;

&lt;p&gt;Startup validation is great, but it still means a deploy has to happen before you find out. The same CLI can check the environment before you ship:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx envella check env.schema.ts &lt;span class="nt"&gt;--env-file&lt;/span&gt; .env.production
&lt;span class="c"&gt;# ✓ 3 environment variables valid&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In GitHub Actions that's two extra steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx envella check env.schema.ts&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DATABASE_URL }}&lt;/span&gt;
    &lt;span class="na"&gt;STRIPE_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.STRIPE_KEY }}&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx envella example env.schema.ts --check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first one fails the pipeline when a secret is missing or malformed. The second fails a PR that adds a variable without updating &lt;code&gt;.env.example&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;TypeScript schema files need Node 22.18+ (native type stripping), Bun (&lt;code&gt;bunx envella&lt;/code&gt;) or Deno.&lt;/p&gt;

&lt;h2&gt;
  
  
  No &lt;code&gt;process.env&lt;/code&gt;? Pass the binding
&lt;/h2&gt;

&lt;p&gt;Not every runtime has &lt;code&gt;process.env&lt;/code&gt;. Envella reads &lt;code&gt;process.env&lt;/code&gt; on Node and Bun, &lt;code&gt;Deno.env&lt;/code&gt; on Deno, and on Cloudflare Workers you pass the &lt;code&gt;env&lt;/code&gt; binding yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;workerEnv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createEnv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;workerEnv&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same goes for anything else that hands you an object, like Vite's &lt;code&gt;import.meta.env&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not envalid, t3-env or Zod?
&lt;/h2&gt;

&lt;p&gt;They're all good, and envella borrows ideas from them. Quick honest version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;envalid&lt;/strong&gt; is the closest in spirit. Envella adds secret masking, the &lt;code&gt;.env.example&lt;/code&gt; generator and the CLI, and it's smaller (~2 kB vs ~7 kB gzipped).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;t3-env&lt;/strong&gt; is the better pick if you use Next.js or Vite and need to stop server variables from leaking into the client bundle. Envella doesn't do that split.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zod&lt;/strong&gt; on its own works fine if your whole app already speaks Zod. You'll just write the coercion (&lt;code&gt;"yes"&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;, port ranges, lists) yourself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want something tiny, dependency-free, that runs anywhere and documents itself, that's the gap envella fills.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;envella
&lt;span class="c"&gt;# or&lt;/span&gt;
deno add jsr:@stijnwtf/envella
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero dependencies, ~2 kB gzipped, MIT licensed. Code, docs and the full validator list are on GitHub: &lt;strong&gt;&lt;a href="https://github.com/stijnwtf/envella" rel="noopener noreferrer"&gt;github.com/stijnwtf/envella&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I'd love feedback on two things in particular: does the CLI convention (export the schema, not the parsed env) feel reasonable, and which validators would you expect to be built in? And if it saves you a broken deploy, a ⭐ on the repo helps others find it.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>node</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
