<?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: Kripa Sindhu</title>
    <description>The latest articles on DEV Community by Kripa Sindhu (@kripasindhu007).</description>
    <link>https://dev.to/kripasindhu007</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%2F1812584%2F0cb093e2-5aae-4993-8260-ad867b20109b.jpeg</url>
      <title>DEV Community: Kripa Sindhu</title>
      <link>https://dev.to/kripasindhu007</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kripasindhu007"/>
    <language>en</language>
    <item>
      <title>Your .env.example is lying to you: catching config drift in TypeScript</title>
      <dc:creator>Kripa Sindhu</dc:creator>
      <pubDate>Wed, 02 Sep 2026 11:19:36 +0000</pubDate>
      <link>https://dev.to/kripasindhu007/your-envexample-is-lying-to-you-catching-config-drift-in-typescript-2j7c</link>
      <guid>https://dev.to/kripasindhu007/your-envexample-is-lying-to-you-catching-config-drift-in-typescript-2j7c</guid>
      <description>&lt;p&gt;Your &lt;code&gt;.env.example&lt;/code&gt; is probably lying to you right now.&lt;/p&gt;

&lt;p&gt;Someone added a variable three weeks ago, forgot the template, and nothing broke. No error, no warning, no failing test. The file just quietly stopped describing reality. You find out when a new joiner clones the repo, follows the README exactly, and loses an afternoon to a "works on my machine" that was a missing key the whole time.&lt;/p&gt;

&lt;p&gt;Nothing fails. That is precisely the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boot validation is the easy half
&lt;/h2&gt;

&lt;p&gt;The usual advice is to validate your environment at startup, and it is good advice. Most of us have written some version of 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;url&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;span class="c1"&gt;// "!" meaning: trust me&lt;/span&gt;
&lt;span class="k"&gt;if &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;DEBUG&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&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="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;// "false" is a truthy string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every line here is a small bet. &lt;code&gt;PORT=abc&lt;/code&gt; becomes &lt;code&gt;NaN&lt;/code&gt;, then &lt;code&gt;3000&lt;/code&gt;, and you never hear about it. &lt;code&gt;DATABASE_URL&lt;/code&gt; is asserted non-null by a &lt;code&gt;!&lt;/code&gt; that the type system believes and the runtime does not. &lt;code&gt;DEBUG=false&lt;/code&gt; is the string &lt;code&gt;"false"&lt;/code&gt;, which is truthy, so a strict-equality check is the only thing standing between you and a debug build in production.&lt;/p&gt;

&lt;p&gt;A schema fixes all of 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="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;defineEnv&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;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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prahari&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;defineEnv&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="s2"&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="s2"&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="s2"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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;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="k"&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;DATABASE_URL&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="nf"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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;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="nf"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sk_&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="k"&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="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;// 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;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;DEBUG&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// boolean&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a bad environment stops the process before it can serve a request, with every problem reported at once rather than one per restart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;prahari: 2 environment variables failed validation

  ✗ DATABASE_URL  (string)  is required but was not set
  ✗ STRIPE_KEY    (string)  must start with "sk_"   received: ***
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;***&lt;/code&gt;. A rejected secret should never end up in your logs, and a validation error is one of the easiest places for one to leak.&lt;/p&gt;

&lt;p&gt;This much is table stakes. envalid, znv, t3-env and a hand-rolled Zod schema all get you here, and any of them is a real improvement over raw &lt;code&gt;process.env&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The half that actually bites
&lt;/h2&gt;

&lt;p&gt;Here is what none of that solves. Your schema is now the source of truth about your configuration, and you have a second file, &lt;code&gt;.env.example&lt;/code&gt;, that claims to describe the same thing. Two artifacts, one set of facts, no mechanical relationship between them.&lt;/p&gt;

&lt;p&gt;That is a drift generator. It will diverge, because the only thing keeping them in sync is somebody remembering.&lt;/p&gt;

&lt;p&gt;So the fix is to stop treating the example file as a document and start treating it as build output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;prahari example    &lt;span class="c"&gt;# generate .env.example from the schema&lt;/span&gt;
prahari &lt;span class="nb"&gt;sync&lt;/span&gt;       &lt;span class="c"&gt;# diff schema against the file, exit 1 on drift&lt;/span&gt;
prahari doctor     &lt;span class="c"&gt;# validate the environment you are actually running in&lt;/span&gt;
prahari docs       &lt;span class="c"&gt;# emit a Markdown table for your README&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generation gives you a template with the documentation already in it, because the descriptions live on the schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Postgres connection string
# (required, string)
DATABASE_URL=

# (has default, port)
PORT=3000

# (required, secret, string)
STRIPE_KEY=
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;sync&lt;/code&gt; turns the drift into a failing check instead of a lost afternoon:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;prahari &lt;span class="nb"&gt;sync&lt;/span&gt;
&lt;span class="go"&gt;✗ .env.example has drifted from your schema:

  + STRIPE_KEY — in schema, missing from file
  - LEGACY_FLAG — in file, not in schema

Run `prahari example` to regenerate.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire that into CI and the class of bug disappears. Not "becomes less likely." Disappears, because the only way to merge a drifted file is to ignore a red build.&lt;/p&gt;

&lt;p&gt;You do not need my library to get this. If you already use envalid or a Zod schema, you can write thirty lines that walk your schema, render a template, diff it against the file on disk, and &lt;code&gt;process.exit(1)&lt;/code&gt;. The specific tool matters much less than the idea that &lt;strong&gt;the example file should be generated, never edited&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I did the exact thing the library exists to prevent
&lt;/h2&gt;

&lt;p&gt;Here is the part I did not enjoy.&lt;/p&gt;

&lt;p&gt;prahari ships a CLI. The CLI needs to evaluate your schema in order to generate a template, report drift, or validate the current environment. At some point, for reasons that felt entirely sensible in the moment, the CLI grew its own copy of the validation logic.&lt;/p&gt;

&lt;p&gt;A second implementation of the same rules. Living about forty lines from the first one.&lt;/p&gt;

&lt;p&gt;You already know what happened. The two implementations drifted. &lt;code&gt;prahari doctor&lt;/code&gt; began crashing on a schema that the library itself handled perfectly well, because the CLI's copy had missed a case the real evaluator had learned to handle. A library whose entire pitch is "two artifacts describing one truth will diverge" had two artifacts describing one truth, and they diverged.&lt;/p&gt;

&lt;p&gt;A review on the pull request caught it before release, which is the only reason this is a blog post rather than a bug report.&lt;/p&gt;

&lt;p&gt;The fix was not to patch the copy. It was to delete the copy and make the CLI call the same evaluator as everything else. That is now a rule in the project: &lt;strong&gt;one evaluator, one place, no exceptions.&lt;/strong&gt; Every consumer, whether library, CLI, or test, goes through the same function.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual lesson
&lt;/h2&gt;

&lt;p&gt;Config drift is not really about config. It is a specific case of a general rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Two implementations of one rule will drift. Not might. Will. The only reliable fix is to have one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The reason it keeps catching us is that duplication is cheap at the moment you create it and expensive only later, at a distance, in a way that never points back at the decision that caused it. The second copy always looks like the pragmatic choice on the day you write it.&lt;/p&gt;

&lt;p&gt;And notice that being aware of the problem bought me nothing. I was writing a tool about drift, thinking about drift full time, and I still shipped a duplicate evaluator into it. Knowing the rule is not the mechanism. The mechanism is either "there is only one implementation" or "something automated fails when the copies disagree."&lt;/p&gt;

&lt;h2&gt;
  
  
  What this isn't
&lt;/h2&gt;

&lt;p&gt;Being honest about the boundaries, since that is the part most library posts skip:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It does not manage secrets. It validates that &lt;code&gt;STRIPE_KEY&lt;/code&gt; is present and well-formed, not that it is the right key. Use a real secret manager.&lt;/li&gt;
&lt;li&gt;It does not do runtime reloading. Environment is read and frozen at boot, deliberately, because a config that changes under a running process is a different and harder problem.&lt;/li&gt;
&lt;li&gt;The built-in validators are intentionally small. If you need real validation power, bring &lt;a href="https://standardschema.dev" rel="noopener noreferrer"&gt;Standard Schema&lt;/a&gt; and use Zod, Valibot, or ArkType instead. The built-ins exist so the zero-dependency path works, not to compete with them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If you want to try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i prahari
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Node 18+, MIT, zero runtime dependencies on the import path, ESM and CJS with correct types for both, and a public API frozen by contract tests that fail the build if an export is renamed or removed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/prahari" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/prahari&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repo and docs: &lt;a href="https://github.com/kripa-sindhu-007/prahari" rel="noopener noreferrer"&gt;https://github.com/kripa-sindhu-007/prahari&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But honestly, if you take one thing from this, take the rule and go looking for your own duplicate evaluator. I promise there is one.&lt;/p&gt;

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