Skip to content

Commit 75d8718

Browse files
committed
feat: NEW BLOG POST
1 parent e54c06e commit 75d8718

3 files changed

Lines changed: 398 additions & 0 deletions

File tree

website/blog/2026-07-26-i-built-a-typescript-native-config-system-because-env-files-drive-me-crazy.mdx renamed to website/blog/2025-07-26-i-built-a-typescript-native-config-system-because-env-files-drive-me-crazy.mdx

File renamed without changes.
Lines changed: 398 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,398 @@
1+
---
2+
slug: holy-shit-this-escalated-quickly
3+
title:
4+
Holy Shit, This Escalated Quickly - Axogen v0.5.0 Is Here And I Can't Even
5+
authors: [oliverseifert]
6+
---
7+
8+
Originally I wanted to write this article at the end of the week, but it just
9+
happened too much in the meantime.
10+
11+
Since my last article about 1.5 weeks went past. I was on holidays for 5 days so
12+
not much progress then, but in the other time we got this shit that made me
13+
realize: maybe I actually built something people want?
14+
15+
**TL;DR: My .env rant got featured in DEV's Top 7, I got so hyped I locked
16+
myself in my house for 4 days straight and shipped secret detection, 10+ file
17+
formats, nested commands, backup systems, and proper type safety. We're at
18+
v0.5.0 now and honestly I can't believe this is working.**
19+
20+
<!-- truncate -->
21+
22+
## Wait, Dev.to Featured Me in Their Top 7?!
23+
24+
So first things first - I GOT PROMOTED ON THE DEV.TO WEEKLY TOP 7. Like how
25+
freaking crazy is that!!!!!
26+
27+
[Top 7 Featured DEV Posts of the Week](https://dev.to/devteam/top-7-featured-dev-posts-of-the-week-1490)
28+
29+
This happened 2 days ago and I'm still processing it. My little config tool rant
30+
sitting next to articles with thousands of views. We're at about 400 reads total
31+
now - not massive, but people are reading about my .env file frustrations and
32+
thinking "yeah, this guy gets it."
33+
34+
While my article was doing its thing, I was so nervous that I couldn't stop
35+
myself from coding. I haven't left my house in 4 days... So I did what any
36+
reasonable developer would do: I went completely overboard with new features and
37+
improvements.
38+
39+
## The Type Safety Revolution (Finally!)
40+
41+
Remember how the old API was kinda... loose? Yeah, I fixed that.
42+
43+
The new recommended way uses dedicated functions for each target type, and
44+
TypeScript now actually knows what you're doing:
45+
46+
```typescript
47+
import {defineConfig, env, json} from "@axonotes/axogen";
48+
49+
export default defineConfig({
50+
targets: {
51+
someService: json({
52+
path: "output/someService.json",
53+
variables: {
54+
name: "MyService",
55+
version: "1.3.0",
56+
},
57+
}),
58+
myEnvironment: env({
59+
path: "output/.env",
60+
variables: {
61+
PRODUCTION: "true",
62+
API_KEY: "12345",
63+
DATABASE_URL: "postgres://user:pass@localhost:5432/mydb",
64+
},
65+
}),
66+
},
67+
});
68+
```
69+
70+
You still can use the old structure, but now you get proper IntelliSense for
71+
each target type.
72+
73+
## Secret Detection That Prevents Disasters
74+
75+
Here's something that kept me up at night: what if someone accidentally pushes
76+
their production API keys because Axogen generated them into a non-gitignored
77+
file?
78+
79+
So I built secret detection. Before generation of each target, all variables are
80+
scanned. If they look like secrets (API keys, passwords, tokens, etc.) and the
81+
target file is NOT gitignored, Axogen refuses to generate.
82+
83+
![Secret Detection Example](/blog/2025-07-30-1.png)
84+
85+
> Also, did you notice 😏, this time I used a screenshot to show off the colored
86+
> output? I know, I know, I'm a rebel.
87+
88+
But sometimes you actually want to generate "secrets" - like development
89+
database URLs or test API keys. Just wrap them with `unsafe()`:
90+
91+
```typescript
92+
import {defineConfig, env, unsafe} from "@axonotes/axogen";
93+
94+
export default defineConfig({
95+
targets: {
96+
myEnvironment: env({
97+
path: "output/.env",
98+
variables: {
99+
PRODUCTION: false,
100+
API_KEY: unsafe("your-dev-api-key-here", "Development mode"),
101+
},
102+
}),
103+
},
104+
});
105+
```
106+
107+
The second parameter is required - you have to explicitly say WHY this is safe.
108+
No more "oops, I pushed the prod keys" moments.
109+
110+
## Zod Schema Validation (Because Type Safety Everywhere)
111+
112+
Remember how I said this was powered by Zod but you couldn't really use Zod's
113+
full power? Fixed that too.
114+
115+
Now you can validate your target configurations before generating, and
116+
TypeScript will complain if your variables don't match your schema:
117+
118+
```typescript
119+
import {defineConfig, json} from "@axonotes/axogen";
120+
import * as z from "zod";
121+
122+
export default defineConfig({
123+
targets: {
124+
someService: json({
125+
path: "output/someService.json",
126+
schema: z.object({
127+
name: z.string().describe("The name of the service"),
128+
version: z.string().describe("The version of the service"),
129+
}),
130+
variables: {
131+
name: "MyService",
132+
version: 2, // TSC will error: Type 'number' is not assignable to type 'string'
133+
},
134+
}),
135+
},
136+
});
137+
```
138+
139+
Your editor lights up with red squiggles before you even try to generate. And if
140+
you somehow ignore TypeScript (why would you do that?), Zod catches it at
141+
runtime with beautiful error messages.
142+
143+
## Backup System (I have trust-issue okay?)
144+
145+
You know that moment when you run a generation command and accidentally
146+
overwrite something important? Yeah, me too.
147+
148+
Axogen can now create backups of your targets before overwriting them:
149+
150+
```typescript
151+
import {defineConfig, json} from "@axonotes/axogen";
152+
153+
export default defineConfig({
154+
targets: {
155+
someService: json({
156+
path: "output/someService.json",
157+
variables: {
158+
name: "MyService",
159+
version: "1.3.0",
160+
},
161+
backup: true,
162+
// backupPath: "your/own/backup/path.json", // Optional
163+
}),
164+
},
165+
});
166+
```
167+
168+
Default backup location is `.axogen/backup/{{path}}`. Currently keeps one
169+
backup - though I'm thinking about adding more sophisticated backup options with
170+
retention policies.
171+
172+
## Conditional Generation
173+
174+
Sometimes you only want to generate certain configs under certain conditions.
175+
Now you can:
176+
177+
```typescript
178+
export default defineConfig({
179+
targets: {
180+
productionSecrets: env({
181+
path: "secrets/.env.prod",
182+
variables: {
183+
REAL_API_KEY: "super-secret-key",
184+
PROD_DATABASE_URL: "postgres://prod-server/db",
185+
},
186+
condition: process.env.NODE_ENV === "production",
187+
}),
188+
},
189+
});
190+
```
191+
192+
The `condition` is just a boolean - use it for whatever logic you want.
193+
Environment checks, feature flags, time-based generation, whatever makes sense
194+
for your use case.
195+
196+
## Command System Overhaul (Nested Commands Are Here)
197+
198+
The old command system was... functional. The new one is beautiful.
199+
200+
You can now create as many nested command groups as you want, and they're all
201+
auto-registered with full type safety. There are actually 5 different ways to
202+
define commands:
203+
204+
```typescript
205+
import {cmd, defineConfig, group, liveExec} from "@axonotes/axogen";
206+
import * as z from "zod";
207+
208+
export default defineConfig({
209+
commands: {
210+
hello: "echo 'Hello, world!'", // Simple string
211+
build: async (context) => {
212+
// Direct function
213+
console.log("Building...");
214+
await liveExec("bun run build");
215+
},
216+
dev: cmd({
217+
help: "Give a string command some help text",
218+
command: "echo 'This is a dev command'",
219+
}),
220+
echo: cmd({
221+
help: "A command that echoes the input",
222+
args: {
223+
input: z.string().describe("The input to echo"),
224+
},
225+
exec: (context) => {
226+
console.log(context.args.input);
227+
},
228+
}),
229+
database: group({
230+
help: "Database management commands",
231+
commands: {
232+
migrate: cmd({
233+
help: "Run database migrations",
234+
exec: (context) => console.log("Running migrations..."),
235+
}),
236+
seed: cmd({
237+
help: "Seed the database with test data",
238+
exec: (context) => console.log("Seeding database..."),
239+
}),
240+
backup: group({
241+
help: "Database backup operations",
242+
commands: {
243+
create: cmd({
244+
help: "Create a database backup",
245+
options: {
246+
name: z.string().describe("Backup name"),
247+
},
248+
exec: (ctx) => {
249+
console.log(
250+
`Creating backup: ${ctx.options.name}`
251+
);
252+
},
253+
}),
254+
},
255+
}),
256+
},
257+
}),
258+
},
259+
});
260+
```
261+
262+
Now `axogen run database backup create --name "before-migration"` just works.
263+
The help system is automatically generated. IntelliSense knows about all your
264+
args and options. It's like having a professional CLI framework built into your
265+
config tool.
266+
267+
## Environment Loading Revolution
268+
269+
The `loadEnv` function got a complete makeover. You can now configure the
270+
loading process and select different files:
271+
272+
```typescript
273+
import {loadEnv} from "@axonotes/axogen";
274+
import * as z from "zod";
275+
276+
const env = loadEnv(
277+
z.object({
278+
NODE_ENV: z.enum(["development", "production"]).default("development"),
279+
PORT: z.coerce.number().default(3000),
280+
DATABASE_URL: z.url().describe("The URL of the database"),
281+
}),
282+
{
283+
path: ".env.custom",
284+
// Any dotenvx options work here
285+
verbose: true,
286+
override: true,
287+
}
288+
);
289+
```
290+
291+
Full dotenvx compatibility means you get all the advanced environment loading
292+
features, but with Zod validation on top.
293+
294+
## Universal File Loading
295+
296+
But wait, there's more! Ever wanted to load TOML, YAML, or other config formats
297+
with the same type safety?
298+
299+
```typescript
300+
import {loadFile} from "@axonotes/axogen";
301+
import * as z from "zod";
302+
303+
const config = loadFile(
304+
"myFile.toml",
305+
"toml",
306+
z.object({
307+
key1: z.string().describe("Description for key1"),
308+
key2: z.number().describe("Description for key2"),
309+
key3: z.boolean().describe("Description for key3"),
310+
})
311+
);
312+
```
313+
314+
Works with basically any config format you throw at it. Same Zod validation,
315+
same type safety, same beautiful error messages.
316+
317+
## File Format Explosion
318+
319+
Speaking of formats, I went a bit crazy with the supported file types:
320+
321+
**For loading:**
322+
323+
- json, json5, jsonc, hjson
324+
- yaml
325+
- toml
326+
- ini, properties
327+
- env
328+
- xml, csv, cson
329+
330+
**For generating:**
331+
332+
- All the loading formats, plus...
333+
- template (nunjucks, handlebars, mustache)
334+
335+
## Performance & Code Quality (The Boring But Important Stuff)
336+
337+
Version 0.5.0 isn't just about features. I spent a lot of time on:
338+
339+
- **Better error handling**: Clearer messages, better stack traces
340+
- **Performance improvements**: Mainly TSC cleanup - wasn't slow before, just a
341+
tiny bit faster now
342+
- **Code structure**: Much cleaner internals, easier to contribute to
343+
- **Safety improvements**: More validation, fewer edge cases
344+
345+
The kind of stuff you don't notice until you don't have to think about it.
346+
347+
## Join the Discord (Why Not?)
348+
349+
Oh, and I haven't mentioned this before, but we already had a Discord server for
350+
Axonotes:
351+
352+
[Join the Axonotes Discord](https://discord.gg/myBMaaDeQu)
353+
354+
Come ask questions, complain about bugs, or just chat about config management.
355+
People can join and ask Axogen questions there too if they want.
356+
357+
## What's Next?
358+
359+
We're at v0.5.0 now, and honestly? I'm just getting started. Some big things on
360+
the roadmap:
361+
362+
- **Project initialization**: `npx @axonotes/axogen init` command
363+
- **Secrets management**: Doppler, Vault, AWS Secrets Manager integration
364+
- **Runtime loading**: Import your generated configs directly into your app
365+
366+
But right now, I'm just excited that this thing I built in frustration is
367+
helping some people. The few GitHub stars (about 5 now), the Discord
368+
conversations - it's all reminding me why I love building tools.
369+
370+
With that in mind, Axogen is still not production ready. But I'm working on it!
371+
I still have 1.5 weeks of spare time to spend lol.
372+
373+
## Try The New Hotness
374+
375+
If you tried the old version and bounced off, give v0.5.0 a shot. If you haven't
376+
tried it yet, now's a great time:
377+
378+
```bash
379+
npm install @axonotes/axogen@latest
380+
381+
# Check out the updated docs for examples
382+
# (not updated yet XD, maybe just use this article as a guide):
383+
# https://axonotes.github.io/axogen/
384+
```
385+
386+
The API is much more stable now, the type safety is useful, and the features
387+
solve real problems instead of just being neat ideas.
388+
389+
Configuration management doesn't have to suck. Your environment variables can
390+
have types. Your scripts can be intelligent. Your deployments can be consistent.
391+
392+
Still feels surreal, honestly.
393+
394+
---
395+
396+
_Built with ❤️, excessive caffeine, and the validation of internet strangers by
397+
[Oliver Seifert](https://github.com/imgajeed76). Now at v0.5.0 - still proceed
398+
with reasonable caution, but maybe a little less caution than before._
36.3 KB
Loading

0 commit comments

Comments
 (0)