My MeDo Hackathon Experience

I came across the “Build with MeDo Hackathon” on DevPost. As someone who does a fair amount of vibe coding, I’m always looking for AI-assisted tools that can handle full-stack deployment without falling apart halfway through.

The hackathon had five tracks:

  • Work & Productivity
  • Business & E‑commerce
  • Learning & Education
  • Lifestyle & Game
  • Surprise Us!

I picked the first three.

What I Built: Kit

I built a multi-tool productivity app called Kit. It includes:

Images & Assets

Compress JPEG, Compress PNG, Image Converter, Image Resizer, Background Remover, Favicon Generator

Text & OCR

Word/Character Counter, Markdown Editor, Extract Text from Image

AI Tools

Text to Speech, Image to Video, Text to Video

Templates

Invoice, Menu, Email Signature, Resume, Cover Letter

Others

Barcode Generator, QR Generator

The Vibe Coding Experience

What I liked:

MeDo generates a preliminary requirements text file before building anything. That’s useful. As a vibe coder, you can review the product requirements and make corrections before MeDo builds the first version. It saves time.

I also appreciated that MeDo includes backend plugins. You don’t have to write or copy-paste integration code for things like Supabase authentication. It handles that part.

What I’d like to see:

An argument section. The AI should be able to look at what you’re trying to build and offer a better approach if yours has problems.

Right now, MeDo mostly does what you ask. That works if you already know what you’re doing. For beginners still learning the ropes, this feature would reduce the barrier to bringing ideas to life. Seasoned software engineers won’t need it. They already know how to prompt for specific integrations.

The Browser Was the Engine

How a Wolfenstein 3D HTML5 port worked in April-May 2012

In April-May 2012, I ported Wolfenstein 3D to HTML5. The port is still available at wolf3d.wadcmd.com.

Not as a small raycasting demo. Not as a framework exercise. Not as a canvas toy that borrowed the feeling of the original. It was a browser port built from the platform that existed at the time: JavaScript, HTML, CSS, Canvas, XHR, audio elements, a little localStorage, and the patience to make those parts behave like a game.

There was no engine underneath it.

That sentence sounds simple now, but it is the most important architectural fact about the project. No game engine meant there was no inherited world model. No package manager meant there was no dependency graph quietly deciding what the program wanted to become. No framework meant there was no lifecycle to adapt to, no plugin interface to satisfy, no build-time abstraction standing between the game and the browser.

The browser was the engine.

That did not make the implementation easier. A complete game does not become simple because you avoid dependencies. You still need a renderer, resource loading, maps, collision, enemies, animation, input, sound, save games, menus, HUD, transitions, and timing. The complexity does not disappear. It moves closer.

In this port, that was the point. Every piece of the implementation could be shaped around one problem: make Wolfenstein 3D run in the browser that existed in 2012.

Internet Explorer 9 mattered. Chrome and Firefox mattered. Current browsers still run it today, but the architecture was formed by the constraints of that earlier browser landscape. Canvas 2D was the stable rendering target. Audio was fragmented. JavaScript engines were fast, but not generous enough to waste work casually. WebGL could not be the foundation if the goal was broad compatibility. The project had to be static, direct, and careful.

So it became a game made of ordinary browser parts.

Before Browser Ports Felt Normal

It is easy to forget how unusual this felt at the time.

Today, seeing an old game run in a browser is not surprising. We have WebAssembly, mature WebGL, better audio APIs, faster JavaScript engines, and a decade of examples proving that the browser can host serious ports. In 2012, that expectation was not really there yet.

Classic game ports to the browser were not a common thing in my memory. I remember a great Lemmings port from that period, but not a long list of comparable projects. The browser was becoming capable, but the idea that you could take the structure of an old game and make it feel at home in HTML5 still had a little bit of improbability around it.

That was part of the attraction.

The question was not only “can I draw a raycaster?” The question was whether the browser could carry the whole feeling of a game: the menus, the timing, the input, the sound, the little transitions, the data loading, the save slots, the sense that you were not looking at a web demo but playing something complete.

That question stayed with me after this port. Since then I have also ported DOOM and Another World, partially ported other smaller games, and I have been occasionally working for years on a Morrowind game port. The projects are different, but the underlying fascination is the same: what does it take to move an existing game into the browser without losing the thing that made it feel like itself?

Wolfenstein 3D was the project where I first answered that question seriously.

Static Files as an Architecture

The entry point is just index.html.

It loads a stylesheet and then a list of JavaScript files with script tags. The order of those tags is the dependency graph. There is no module loader to explain, no bundler contract, no runtime manifest. The files appear in the page in the order the program needs them.

That may look old-fashioned now, but it was also liberating. The deployment model was almost impossible to simplify further: serve the folder and open the page. The application did not need a server-side process to define itself. It did not need a compilation step before it could be understood. A browser could load it because the browser already knew the primitives it was made from.

The code is organized around global subsystem objects: game, raycast, maps, resources, player, ai, audio, controller, and a few smaller systems around animation, doors, secrets, scripts, fonts, and menus. Each file adds a part to the whole.

The pattern is plain:

game.extend({
    newGame: function(map, difficulty, speed, callback){
        ...
    }
});

Today, we would usually reach for imports. In 2012, for a static browser game targeting IE9, this was the smallest reliable module system available: objects, files, and load order. It was not elegant in the modern packaging sense, but it was concrete. You could read the HTML and know what the program was made of.

That concreteness shows up everywhere else.

Screens are not generated by a UI framework. They are small HTML fragments under html/. When the game needs the menu, the difficulty screen, the game view, the high score table, the sound settings, or a message dialog, it loads the fragment with XMLHttpRequest, puts it into the document, converts its text into the game’s bitmap-style text, and fades it in.

The game shell is therefore static, but not monolithic. It is a set of browser-native pieces, loaded only when the game asks for them.

That is the kind of architecture that comes from trusting the platform directly. There is no router. There is no component tree. There is no templating runtime. There is an HTML file, a request, a DOM node, and a screen.

The View Is Layered, Not Abstracted

The game screen is a stack of canvases and DOM elements.

At the back there is a canvas for the background. Above it there is the main view, where the raycaster draws walls and sprites. Above that is the weapon hand. Another canvas handles the fizzle-fade mask. A transparent control layer catches touch and mouse input. The HUD sits at the front with small canvases for score, floor, lives, face, health, ammo, keys, and weapon state.

This is not a general renderer. It is a layout for this game.

The separation matters because not every part of the screen changes for the same reason. The 3D view changes every frame. The HUD changes when a value changes. The weapon canvas changes when the weapon animation advances. The mask changes during transitions. By giving those concerns their own surfaces, the code avoids pretending that the whole screen is one thing.

That is a recurring theme in the port. There are abstractions, but they are local. They exist where the game has repeated behavior, not where a future engine might want an extension point.

The raycaster does not know about menus. The HUD does not know about wall rendering. The mask does not know about enemies. The DOM can still be used for layout where layout is the problem, and Canvas can be used where pixels are the problem.

This was one of the advantages of writing directly against the browser. The browser already had multiple materials. HTML was good for screens and menus. CSS was good for layering and transitions. Canvas was good for the game view. There was no need to force all of it through one abstraction.

Turning Images Into Game Data

The resource loader is small but important.

Wolfenstein-style rendering wants textures in predictable square cells. The port uses sprite sheets for walls and objects, then cuts them into 128-by-128 images at load time. An offscreen canvas draws one cell from the sheet, turns it into a data URL, and stores it in a lookup table.

After that, the renderer can ask for things in the shape it needs:

resources.walls[textureIndex][side]
resources.sprites[itemIndex][frameIndex]

That preprocessing step keeps the inner loop simple. The renderer does not want to think about sprite sheet layout every time it draws a wall column. It wants a texture image, a texture coordinate, and a destination column.

This is one of those small decisions that tells you what kind of program it is. The code does not build a generic asset pipeline. It prepares the assets into the exact form the renderer wants. Work that can happen once at startup is moved out of the frame loop. The frame loop stays narrow.

Again, the implementation is direct: generate the pixels, cache the pixels, put the pixels on screen.

Bitmap Font Rendering

The text rendering deserves its own place because it is one of those details that makes the port feel like a game instead of a web page.

The menus and HUD could not simply use browser fonts. A browser font would have been easy, but it would also have been wrong in a very visible way. Font rendering differed between platforms, antialiasing differed between browsers, and the original game had a very specific bitmap character. If the menus used normal DOM text, the illusion would break before the player even reached the first level.

So the port renders text as images.

There are two paths in the font system. font.BitmapFont uses a bitmap image that contains glyphs laid out horizontally. A small map tells the renderer where each character begins and how wide it is. When text is needed, the font renderer draws the matching glyph slices into an offscreen canvas, then turns that canvas into a data URL and inserts it into the DOM as an image.

The other path, font.Font, is even more direct. The small and large menu fonts are stored as compact bitmap data in JavaScript. Each glyph is a set of integer rows. Rendering a character means walking those bits, painting colored pixels into an ImageData buffer, and writing the result to the offscreen canvas.

That gives the code several useful properties.

Text can be colored without needing separate image files for every color. The same glyph data can become grey menu text, selected menu text, disabled text, read-this text, or high score text. It also means the game can process ordinary HTML fragments after loading them. A screen can contain readable text in the source, and game.processFont() replaces that text with generated bitmap images when the screen enters the game.

The renderer also caches the result by font and text value. Once a phrase has been turned into pixels, the same generated image can be reused. That matters because menus redraw and update, but the words themselves usually do not change. The port pays the text-rendering cost when it has to, not every frame.

It is a small system, but it captures the spirit of the whole port. The browser is still doing the work. There is still no external text rendering library. But the program refuses to let the browser’s default presentation leak into the game. It takes the platform primitive, Canvas, and uses it to produce the exact kind of pixels the game needs.

Getting the Game Data Into Browser Shape

The browser port also needed the original game data in a form the JavaScript runtime could use.

That part did not happen by hand. The assets were converted from a Wolfenstein 3D editor format using a custom C# converter utility. The converter extracted the images and maps and turned them into browser-friendly files: PNG sprite sheets and JavaScript map data that the static app could load directly.

This was an important part of keeping the runtime simple. The browser code did not need to understand the original game data formats. It did not need a binary parser for every source file. It did not need to do expensive conversion work during startup. By the time the browser saw the assets, they were already shaped for the port.

Images became image files the browser could load normally. Wall textures and sprites became sheets the resource loader could slice into 128-by-128 cells. Maps became JavaScript files that populated wolf3d.maps, which meant a level could be loaded with a simple XHR request and evaluated into the same data structures the engine already expected.

That split made the architecture cleaner. The C# converter was an offline tool. The browser runtime was the game. The conversion step absorbed the awkwardness of extraction, while the game stayed static and dependency-free.

The open-source repository intentionally does not include those extracted game assets. The point of mentioning the converter is architectural: the port was not loading original game files directly in the browser. It used an offline conversion step to transform the data into web-native assets, then kept the runtime focused on playing the game.

The Raycaster at the Center

The heart of the port is the raycaster.

Wolfenstein 3D is built on a grid. The world is not arbitrary polygons. It is cells: empty space, walls, doors, secrets, objects, enemies. That makes it a perfect fit for a column renderer. For every vertical column of the screen, the engine casts a ray from the player into the grid, walks cell by cell until it hits something solid, computes the distance, and draws one vertical slice of texture.

The camera is just two vectors: the direction the player is facing, and the camera plane used to spread rays across the screen.

var cameraX = 2 * x / width - 1;
var rayDirectionX = player.direction.x + player.plane.x * cameraX;
var rayDirectionY = player.direction.y + player.plane.y * cameraX;

That is the beautiful thing about this kind of renderer. Once the math is in place, the world becomes very cheap to describe. A wall is a number in a map array. A ray walks the array. A texture slice becomes a one-pixel crop stretched vertically with drawImage().

The renderer does this hundreds of times per frame, once per screen column. It also records the distance of each wall hit in a z-buffer. That z-buffer becomes the truth for everything drawn after the walls. Sprites are projected into screen space, sorted from far to near, and each visible stripe is tested against the wall distance for that column.

This is how enemies disappear behind walls. It is also how weapons know what they are aiming at.

When an enemy sprite is visible in a column, the renderer stores that enemy in the player’s fireBuffer for that stripe. A hitscan weapon does not need a separate picking engine. It looks near the center of the screen, checks the same visibility information the renderer already computed, and damages the first visible target.

That reuse is one of my favorite parts of the implementation. The renderer is not only drawing. It is producing spatial knowledge. The weapon system uses that knowledge instead of inventing a parallel world.

Doors Are Not Walls

In a simple raycaster, a wall is just the first solid cell the ray hits.

Wolfenstein doors complicate that. A door is visually inside a grid cell, but it slides open. It can be partly open, fully open, closing, blocked by the player, blocked by an enemy, or locked behind a key. Treating it as a normal wall would be too crude.

The port gives doors their own state: position, texture, slide amount, action, delay, and optional key. During raycasting, doors encountered before the final wall hit are kept in a buffer. If a door is closer than the wall and its slide amount still blocks the ray, the renderer draws the visible door slice and writes that distance into the z-buffer.

The door has not moved through the map. Its texture has moved inside the cell.

Secret walls are the opposite. They really move through the map. When pushed, the starting cell becomes empty. The secret wall advances by a fractional state until it crosses into the next cell, then continues until it has moved two cells or is blocked. The map keeps separate secret-wall state so collision and rendering can both understand the wall while it is in motion.

This is where a lot of game code lives: not in grand abstractions, but in the difference between two things that look similar until you implement them. A door and a secret wall are both moving obstructions. Architecturally, they are not the same thing at all.

The implementation lets them be different.

The Main Loop Is a Set of Small Clocks

The game loop is driven by requestAnimationFrame, with vendor-prefixed fallbacks and finally setTimeout. The target tick is roughly 33 updates per second. Each frame calculates how much time has passed and turns that into a capped timeFactor.

The cap matters. If the browser stalls for a moment, the game should not respond by letting a guard sprint through the map or a door jump through its entire animation. Time compensation is useful only until it becomes a bug.

Inside the frame, the code updates the world in a straightforward order. Doors process. Secret walls process. Animations process. AI processes. One-frame script events are cleared. Positional audio events are played at a volume based on distance. Controls are processed. The player HUD updates. The raycaster renders.

There is no big scheduler.

There are arrays:

doors.process
secret.process
animation.process
ai.process
audio.process

If a door is moving, it is in doors.process. If an animation is active, it is in animation.process. If a sound should be played from a map position, it is queued in audio.process until the frame loop turns distance into volume.

This is the kind of simplicity that is easy to underestimate. A more general engine might have a unified entity lifecycle. This port does not need one. A door is not an enemy. A sound event is not a sprite. An animation is not a map script. Each small system gets the loop it needs.

That does not make the game less complete. It makes the completeness local.

Enemies Are Sprites With Intent

The enemy system starts from sprites and adds intent.

An enemy has a position, a texture resource, an animation set, hit points, a direction, a current action, and a process method. The current action is just a function reference: ready, moving, attack, fire, dying, dead, or a specialized behavior for a boss or projectile.

That turns each enemy into a small state machine.

The shared base handles the things every enemy needs: calculating which sprite direction should face the player, testing line of sight, choosing movement directions, walking through the grid, opening doors, taking damage, and advancing the current action.

The visibility check is not magic. It walks a line through the grid between enemy and player. If a wall or a closed door blocks the line, the player is not visible. If the enemy sees the player, or hears activity in the same floor area, it can wake up and begin attacking.

Movement is similarly local. Enemies choose chase, dodge, or run directions from nearby cells. They avoid blocked tiles, avoid reversing direction when appropriate, consider doors, and use a bit of randomness. It is not a general pathfinding library. It is the kind of movement model a Wolfenstein 3D enemy needs.

That is the point. The AI code is not trying to be generally intelligent. It is trying to create the pressure the game needs: guards noticing you, moving through corridors, opening doors, firing when visible, reacting to damage, and becoming part of the scene the raycaster knows how to draw.

The enemy is both a visual thing and a behavioral thing. It is a sprite with intent.

Input Is State, Not Events

Keyboard input is translated into game actions and stored as active or inactive key state.

That distinction matters because games do not usually want to respond to a key event once. They want to know what is currently true. Is the player holding forward? Is run active? Is strafe active? Was fire released? Should left/right mean rotation, or should they mean strafing because Alt is down?

The controller maps key codes to named actions, then the game loop processes those actions. Some actions are continuous. Some are single-fire. Some cancel each other. Some have release behavior.

Touch input uses the same idea. The game view contains a transparent table of control regions. Entering or touching a region activates actions like forward, left, right, or combinations. The input surface is crude, but it has one important property: it feeds the same controller state as the keyboard.

The rest of the game does not need to care where the input came from.

That is the right kind of abstraction. It does not invent a framework. It simply normalizes the one fact the game needs: which actions are active right now.

Audio Had to Be Pragmatic

Browser audio in 2012 was not one clean API.

The port uses WebKit AudioContext when it is available. In that path, sound effects are requested as array buffers and decoded into audio buffers. If that API is not available, the code falls back to HTML audio elements and checks whether the browser can play MP4 or OGG.

Music is handled as a looping audio element with multiple sources. Sound effects are loaded lazily and cached. Positional sound is deliberately simple: game systems queue a sound with map coordinates, and the main loop compares that position to the player. The farther away the event is, the lower the volume.

That is enough.

The game does not need a full audio engine. It needs doors to sound like they are nearby or far away. It needs enemies to make noise from their position. It needs music to loop. It needs guns, pickups, doors, secrets, and death sounds to happen at the right time.

The implementation solves exactly that.

State Lives in the Browser

A static game still needs persistence.

The port uses localStorage for settings, controls, graphics detail, sound volume, music volume, save slots, highscores, and zoom state. Save games serialize enough information to reconstruct the level: map name, difficulty, player position and direction, health, ammo, lives, score, keys, weapons, map and hit state, sprites removed or added, secrets, doors, and other level progress.

That choice preserved the static deployment model. No backend. No account. No database. No server session. The browser already had a small persistence layer, and the game used it.

This is another place where directness helped. The storage format did not have to serve multiple clients, sync across devices, support migrations for a live service, or survive hostile input. It had to remember a local game. The shape of the solution matched the shape of the problem.

Why No Dependencies Worked

No dependencies worked because the project was narrow.

It was not trying to create a reusable game engine. It was not trying to support arbitrary maps from arbitrary games. It was not trying to become a framework for browser shooters. It was trying to port one game to the browser, with enough fidelity that the browser disappeared and the game remained.

That narrowness made the absence of dependencies practical.

Canvas handled pixels. XHR handled files. HTML handled screens. CSS handled layout and fades. Audio APIs handled sound. localStorage handled persistence. JavaScript objects handled systems. The missing part was not a library; it was the code that made those browser materials behave like Wolfenstein 3D.

This is the line I still find important.

Dependencies are often useful when they provide a material or a stable primitive. But the core shape of a game is not a generic problem. The renderer, loop, collision, enemy behavior, resource model, and input feel define the game. If those layers come from a general engine too early, the game begins by negotiating with someone else’s idea of what a game should be.

Here, the negotiation was with the browser itself.

That is a very different relationship. The browser gives you primitives, not a game theory. Canvas does not tell you what an enemy is. XHR does not tell you what a map is. localStorage does not tell you how saving should work. Those decisions remain yours.

The port feels old in some ways because it belongs to its time. But it also feels unusually durable because there is so little around it that can expire. There is no abandoned framework version trapped underneath it. There is no engine upgrade path to follow. There is no transitive dependency tree to audit before the source can be understood.

It is just the program and the platform.

What I Would Preserve

If I were polishing the repository today, I would not rewrite it into modern JavaScript.

That would make the code look more familiar to current readers, but it would also remove part of what makes the project valuable. The source shows how a complete browser game was structured when the platform had become capable enough, but before the modern frontend stack became the default answer to every problem.

The globals matter. The script order matters. The direct XHR matters. The vendor-prefixed requestAnimationFrame fallback matters. The audio fallback matters. The little local process arrays matter. They show the shape of the browser as it was, and the shape of a program built close to it.

What I would add is explanation around the code, not a new identity inside the code.

Because the interesting thing is not that the implementation is perfect. It is not. The interesting thing is that the game is complete, understandable, and still runnable as static files. It owns its important layers. It can be opened without resurrecting a vanished ecosystem.

That is rare now.

What I Would Use Today

If I were building the same port from scratch today, I would not reproduce the 2012 structure exactly.

I would still keep the project small. I would still avoid a game engine. I would still avoid a framework. The core of the game would remain mine: the raycaster, the loop, the map representation, the resource model, the input layer, the enemy behavior, the save format. Those are the parts that define the game, and I would still want them shaped by the game rather than by a general-purpose engine.

But I would use modern packaging where it helps without changing the architecture.

I would use Vite. Not because the game needs a framework, but because Vite is a good way to run a local development server, use native-style imports, and produce a clean static output. The important distinction is that Vite would be tooling around the source, not the architecture inside the source. The game should still be understandable as browser code. The build tool should make development smoother, not become the thing the program is written for.

I would use imported modules instead of global objects and script order. In 2012, script tags were the simplest cross-browser module system available for this target. Today, import is the obvious way to make dependencies explicit. The same subsystems would probably still exist: renderer, resources, maps, player, AI, audio, input, screens. They would just be connected by imports instead of shared globals.

I would also use a formatter and a linter.

That was not really part of the workflow when this port was written. The code carries the habits and unevenness of that time: manual formatting, local conventions, and whatever discipline I brought to each file by hand. Today I would not rely on that. A formatter would remove style decisions from the work, and a linter would catch the boring mistakes before they became debugging sessions. That kind of tooling would not change the architecture of the game. It would simply make the source easier to maintain.

I would not use TypeScript.

That is not because TypeScript is bad. For many projects, especially larger application codebases, it is valuable. But for this kind of small, self-contained game, I would rather keep the source close to the runtime language. The important correctness boundaries here are not mostly type boundaries. They are behavioral and spatial: does the ray hit the right wall, does the sprite sort correctly, does the door block the player, does the enemy see through a closed door, does the save state restore the map exactly? TypeScript can document some shapes, but it would not be the thing that makes those systems true.

Plain JavaScript would also keep the source closer to the spirit of the port. The browser is the material. Vite and modules would clean up the development experience, but I would avoid turning the project into a modern stack demonstration.

The version I would build today would be more modular, easier to navigate, and easier to serve locally. But it would still be a static browser game. It would still be Canvas. It would still have no runtime dependencies. It would still choose ownership over generality at the layers where the game becomes itself.

The Port Still Runs

The open-source release does not include copyrighted game data. The source references images, maps, sounds, and music by relative path, but those files are intentionally excluded from the repository.

The source is available on GitHub at lazarv/wolf3d.

For local testing, the folder can still be served with a static server that proxies missing files to the hosted copy:

npx http-server . -p 8080 -c-1 --proxy https://wolf3d.wadcmd.com/

That keeps the source boundary clean. The repository contains the browser port. The game data stays outside it.

And the architecture still works because it was always just static files and browser APIs.

The Thing I Still Like About It

Looking back at this port, the thing I like most is not the raycaster, although I still like the raycaster. It is not the no-dependency choice by itself. It is not the fact that the code still runs.

It is the relationship between the program and its materials.

The implementation does not float above the browser. It touches it directly. It asks Canvas for pixels, HTML for screens, CSS for layering, XHR for files, audio for sound, localStorage for persistence, and JavaScript for the small machines that hold the game together.

There is very little pretending.

That kind of software has a particular feeling. You can follow it down. You can see where time is spent. You can see where state lives. You can see why a door opens, why a wall renders, why an enemy sees you, why a shot hits, why a sound is quiet, why a save game restores the room you left behind.

Modern software often hides those answers behind tools that are powerful, useful, and sometimes necessary. But there is still value in a program where the answers are close to the surface.

This port is one of those programs.

It is a game built from the browser, not from a stack around the browser. It belongs to April-May 2012, but it still says something I believe now: sometimes the best architecture is not the one with the most reusable layers. Sometimes it is the one that lets the thing you are building stay closest to the material it is made from.

For this port, that material was the browser.

And the browser was enough.

I had two blog posts ranking against each other. Here’s the Next.js fix

A few weeks ago I noticed two posts on my Next.js blog were dragging each other down in Google. Both targeted the same keyword. Both got ~80% of the way to page 1 and stalled. Neither ranked for anything else either.

Classic keyword cannibalization. I’d written them three weeks apart, didn’t remember the first when I wrote the second, and Google couldn’t decide which one to surface, so it surfaced neither.

This post walks through how I diagnosed and fixed it, plus the date-handling gotcha that almost made things worse. Code at the bottom.

How to spot cannibalization

Open Google Search Console – Performance – Queries. Click any keyword you care about. Look at the “Pages” tab.

If two URLs show impressions for the same query, with neither clearly dominating, you have cannibalization. Google is split, it can’t tell which one is canonical for that intent.

The symptom: both posts hover around position 8–15. Neither breaks through.
The diagnosis in my case took 30 seconds:

  • /blog/podcast-to-linkedin-posts (published March 7)
  • /blog/podcast-to-linkedin-posts-guide (published March 25)

Same primary keyword. I’d literally forgotten about the older post when I wrote the newer one.

The decision: merge, not delete

Three options when you have two competing posts:

  1. Delete the loser, bad idea, you lose every backlink pointing at it
  2. Add noindex to the loser, Google ignores it, but inbound link equity is wasted
  3. Merge into one canonical URL + permanent redirect, the only option that preserves everything

The keep/redirect decision: I picked the older URL as the survivor. Three reasons:

  • Older posts usually have more inbound links
  • Older URL has longer SERP history with Google
  • If the newer post has substantially better content, you merge the content INTO the older URL — not the other way around

So: the survivor’s URL stays. The content gets the best of both posts. The loser URL gets a permanent redirect.

The architecture problem

Here’s where it got interesting. My blog isn’t markdown files on disk. It’s inline JSX in src/app/blog/[slug]/page.tsx — one big posts: Record<string, BlogPost> map.
So “merging” wasn’t a file operation. It was:

  1. Edit the survivor’s entry in the map
  2. Remove the loser’s entry
  3. Configure a redirect at the framework level, not at the post level

For Next.js, the cleanest way to handle that is next.config.ts:

// next.config.ts
const nextConfig = {
  async redirects() {
    return [
      {
        source: '/blog/podcast-to-linkedin-posts-guide',
        destination: '/blog/podcast-to-linkedin-posts',
        permanent: true,
      },
    ]
  },
}

export default nextConfig

One gotcha worth knowing: permanent: true emits a 308, not a 301. Both are “permanent redirect” from Google’s perspective and link equity transfers identically, Google confirmed this back in 2016. But if you have monitoring rules that specifically check for 301, you’ll need to adjust them. I verified the behavior in production: Google indexed the redirect correctly within 48 hours.

The date-handling gotcha

This is where I almost made things worse. When you update a post substantially, you want to signal freshness to Google. The naive fix:

// DON'T do this
{
  slug: 'podcast-to-linkedin-posts',
  date: '2026-04-25', // overwriting the original publishedAt
  // ...
}

Bad move. You just:

  • Broke your RSS feed (subscribers see a “new” post that’s not actually new)
  • Lost the publishedAt signal that Google’s freshness algorithm relies on for trust history
  • Confused anything that depends on chronological ordering (sitemap-sort, related-posts logic, archive pages)

The right fix is to add an updatedAt field separately:

// post interface
interface BlogPost {
  slug: string
  title: string
  date: string          // never change after publish
  updatedAt?: string    // bump on substantial edits
  // ...
}

And in the JSON-LD render:

// app/blog/[slug]/page.tsx
const jsonLd = {
  '@context': 'https://schema.org',
  '@type': 'Article',
  datePublished: post.date,
  dateModified: post.updatedAt ?? post.date, // <-- the key line
  // ...
}

In the masthead, render the “Updated” line conditionally:

<time>{formatDate(post.date)}</time>
{post.updatedAt && (
  <>
    {' · '}
    <time>Updated {formatDate(post.updatedAt)}</time>
  </>
)}

Now Google sees both signals cleanly: when the post was first published (rank-trust history) and when it was last meaningfully updated (freshness boost).

The cleanup grep

Before celebrating, find every internal link pointing at the old URL and update it:

# find every reference to the loser slug
rg "podcast-to-linkedin-posts-guide"

Don’t trust your memory. I had 4 internal links scattered across other blog posts that pointed at the loser. Without updating them, every reader gets an extra redirect hop, and a little link equity leaks each time.

Also check:

  • Footer / nav components
  • Any sitemap generator (mine iterates the posts array, so it self-cleaned — but check yours)
  • Schema markup generators
  • Any hardcoded references using the full https:// URL form
  • Middleware (in case you have other redirect logic that could conflict)

The verification

After deploying:

next build, make sure nothing breaks
Hit the loser URL in browser, watch DevTools Network tab, should be 308 to survivor
Hit the survivor URL, should render with both datePublished and dateModified in JSON-LD (verify with Google Rich Results Test)
Request indexing in GSC for the survivor URL, speeds up Google’s re-evaluation from weeks to days

What to expect

Realistically: 2–4 weeks before you see ranking movement. Google needs to:

  • Crawl the redirect
  • Consolidate signals onto the survivor
  • Re-evaluate ranking based on the unified, deeper content

In my case I closed three cannibalization pairs in one session on my blog at castnova.app. I’ll know in three weeks whether the consolidation lifts the affected URLs into page 1.

The bigger lesson

Cannibalization compounds. Two posts – both stall. Five posts on overlapping intents – your whole topical cluster underperforms, because Google can’t form a clean ranking signal for any single one.

The fix is mechanical (merge + redirect + grep), but the prevention is editorial: before publishing a new post, search your own blog for related keywords. If something already exists, extend it instead of writing a parallel post.

If you’re running a Next.js blog with inline posts (not MDX files), the merge operation is even simpler than the markdown file case, one map edit, one config entry, done. The hard part isn’t the code, it’s catching yourself before you write the duplicate post in the first place.

OpenAI Open-Sources Symphony, a SPEC.md for Autonomous Coding Agent Orchestration

OpenAI Symphony is an agent orchestrator that uses project-management tools, like issue trackers, as a control plan to coordinate multiple coding agents. Instead of developers managing interactive coding sessions, Symphony manages “tasks” by assigning each one to a dedicated agent that works autonomously to completion. Once a task is finished, a human is in charge to review the resulting output.

By Sergio De Simone

CONNECTING POWER BI TO SQL

Open Power BI Desktop

Connecting Power BI to PostgreSQL Databases for Business Intelligence

Businesses generate massive amounts of data every day, but raw data alone is not useful unless it can be transformed into meaningful insights. This is where Power BI and SQL databases become essential tools for modern business intelligence and analytics.

Organizations use Power BI dashboards to monitor sales performance, customer behavior, inventory levels and operational KPIs in real time. By connecting Power BI to SQL databases such as PostgreSQL, analysts can work with structured, scalable and reliable data sources to support data-driven decision-making.

What is Power BI?

Power BI is a Microsoft business analytics platform that enables users to connect to multiple data sources, transform raw data and create interactive visualizations and dashboards.

It combines software services, applications and connectors that work together to transform unrelated sources of data into coherent, visually immersive and interactive insights.

Power BI allows users to:

  • Connect to multiple data sources such as Excel, SQL databases and cloud platforms
  • Clean and transform data using Power Query
  • Build interactive dashboards and reports
  • Track KPIs in real time
  • Share reports securely across teams and organizations

The data source can range from a simple Excel spreadsheet to large-scale cloud databases.

Why Do Companies Connect Power BI to Databases?

Companies connect Power BI to databases because databases provide structured, centralized and scalable storage for analytical data.

Some key benefits include:

  • Access to real-time operational data
  • Improved data consistency across teams
  • Faster processing of large datasets
  • Better performance compared to spreadsheets
  • Automated scheduled refreshes
  • Enhanced security and governance

Unlike Excel, databases can efficiently manage millions of rows of data while maintaining high performance and reliability.

Using scheduled refresh in Power BI ensures that dashboards always reflect the latest available data without requiring manual updates.

Importance of SQL Databases for Analytical Data

SQL databases are crucial for storing and managing analytical data because they provide structured, reliable and standardized methods for data storage and retrieval.

Some major advantages include:

  • Ensuring data accuracy through ACID compliance
  • Supporting efficient complex queries
  • Enabling business intelligence and reporting
  • Allowing seamless integration from multiple data sources
  • Improving data consistency and integrity
  • Optimizing large-scale data management

Popular SQL databases include PostgreSQL, MySQL, Microsoft SQL Server and Oracle Database.

Connecting Power BI to a Local PostgreSQL Database

Step 1: Open Power BI Desktop

Launch the Power BI Desktop application on your computer.

Open Power BI Desktop

Step 2: Select “Get Data”

On the top-left section of the screen, select Get Data.

From the dropdown menu, click More.

Get Data Option

Step 3: Choose PostgreSQL Database

A new window will open displaying different data source categories.

Select:

Database → PostgreSQL Database

PostgreSQL Database Option

Step 4: Enter Connection Details

You will be prompted to enter the following:

  • Server Name: localhost
  • Database Name
  • Authentication credentials

Then click OK.

Connection Details

Step 5: Load the Tables

After a successful connection, Power BI will display the available database tables.

Select the tables you want to load into Power BI.

Selecting Tables

Step 6: Transform or Load Data

If your data requires cleaning or transformation, select Transform Data to open Power Query Editor.

Power Query allows users to:

  • Remove duplicates
  • Rename columns
  • Change data types
  • Filter rows
  • Merge tables

Power Query Editor

If the data is already clean, click Load to import the tables directly into Power BI.

Loaded Data

Connecting Power BI to Aiven PostgreSQL (Cloud Database)

Cloud databases allow organizations to access and manage data remotely without maintaining physical servers.

One popular cloud PostgreSQL provider is Aiven for PostgreSQL.

Step 1: Obtain Connection Details from Aiven

After creating your PostgreSQL service in Aiven, navigate to the service dashboard and retrieve the following connection details:

  • Host
  • Port
  • Database name
  • Username
  • Password

These credentials are required when connecting Power BI to the cloud database.

Step 2: Download the SSL Certificate

Aiven requires secure SSL connections for database access.

From the Aiven dashboard:

  • Navigate to the service overview
  • Download the CA certificate
  • Save the certificate file on your computer

This certificate helps Power BI establish a secure encrypted connection with the database server.

Step 3: Connect Power BI to Aiven PostgreSQL

In Power BI:

  1. Select Get Data
  2. Choose PostgreSQL Database
  3. Enter the following:
    • Host name
    • Port number
    • Database name
  4. Provide your username and password

When prompted for SSL settings:

  • Enable SSL connection
  • Select the downloaded SSL certificate file

Once authenticated successfully, Power BI will connect securely to the cloud PostgreSQL database.

Why SSL Certificates Are Important

SSL stands for Secure Socket Layer.

An SSL certificate encrypts communication between systems, ensuring that sensitive information such as usernames, passwords and database queries cannot be intercepted by unauthorized users.

SSL certificates are important because they:

  • Protect sensitive business data
  • Prevent unauthorized access
  • Encrypt network communication
  • Improve database security
  • Ensure secure cloud connectivity

In cloud database environments, SSL is essential for maintaining secure and trusted connections.

Loading Tables into Power BI

After establishing the connection, the database tables can be imported into Power BI.

For this project, the following tables were loaded:

  • Customers
  • Products
  • Sales
  • Inventory

These tables provide the foundation for creating analytical dashboards and business reports.

Creating Relationships Between Tables

After loading the tables, relationships can be created in the Model View inside Power BI.

Example relationships include:

  • sales.customer_id → customers.customer_id
  • sales.product_id → products.product_id
  • inventory.product_id → products.product_id

These relationships allow Power BI to correctly aggregate, filter and analyze data across multiple tables.

Without proper relationships, dashboard calculations and visualizations may produce inaccurate results.

Understanding Data Modeling in Power BI

A data model is a visual and logical representation of data elements and the relationships between them.

Data modeling helps organize data in a way that supports efficient analysis and reporting.

Data modeling is important because it:

  • Defines relationships between tables
  • Ensures data consistency and integrity
  • Improves dashboard performance
  • Supports accurate business analysis
  • Enables efficient querying and filtering

Good data modeling practices help analysts build scalable and reliable Power BI solutions.

Using SQL for Data Analysis

SQL skills are extremely valuable for Power BI analysts because SQL helps retrieve, clean and prepare data before visualization.

Example SQL query:

SELECT 
    product_name,
    SUM(sales_amount) AS total_sales
FROM sales
GROUP BY product_name
ORDER BY total_sales DESC;

This query aggregates product sales data before importing it into Power BI for dashboard creation.

SQL can also be used to:

  • Filter datasets
  • Perform aggregations
  • Join multiple tables
  • Clean raw data
  • Improve report performance

Conclusion

Power BI becomes significantly more powerful when combined with SQL databases such as PostgreSQL. While Power BI handles visualization and dashboard creation, SQL databases provide the foundation for storing, managing and retrieving analytical data efficiently.

Connecting Power BI to both local and cloud PostgreSQL databases enables analysts to build secure, scalable and real-time business intelligence solutions.

For Power BI analysts, SQL skills are essential because they allow analysts to:

  • Retrieve relevant datasets
  • Clean and transform data efficiently
  • Perform joins and aggregations
  • Optimize dashboard performance
  • Work with enterprise-scale databases

Understanding database connectivity, data modelling and SQL querying is a critical step toward becoming an effective business intelligence professional.

xAI open-sourced their ranker. It doesn’t compile.

On 2026-05-15, xAI pushed an update to xai-org/x-algorithm — the recommender behind X’s For You feed. Three years after Twitter’s original 2023 release, under different leadership.

I cloned it and ran analysis on it.

The released code does not compile.

What’s missing

There’s no Cargo.toml anywhere in the repo. The Rust crates reference crate::params::FAVORITE_WEIGHT, crate::params::REPORT_WEIGHT, crate::params::OON_WEIGHT_FACTOR — sixty-plus named symbols — but crate::params is not in the tree. Same for crate::clients::* (referenced 58 times, all Prod*Client implementations missing), the entire xai_feature_switches plane, and the training code.

The values were withheld. That’s the headline read.

The seams of the sanitization

What’s interesting is the unevenness of the pass. The redaction ran out of energy in specific, telling ways:

const TWEET_EVENT_TOPIC: &str = "";

The topic name variable is preserved. The topic string is empty. A mechanical search-and-replace on string literals left the variable intact.

std::env::var("")

Same shape — the env-var-reading call survives; the env-var name is empty-stringed out.

if ( and self.deluxe ...

Actual invalid Python in grox/classifiers/content/safety_ptos.py. A predicate was deleted; the and was left dangling. The file does not parse as shipped. Nobody re-ran the test suite after the cut.

ModelName.EAPI_REASONING_INTERNAL

Preserved verbatim in a public file. An internal model identifier with _INTERNAL in its name, shipped to a public release. That’s a survivor.

PTOS_CUTOFF_TWEET_ID = 2_054_275_414_225_846_272

A real Snowflake date cutoff for “tweets after this date require PTOS_REVIEWED label or default to MediumRisk.” That’s a policy boundary, in plaintext, in a public file.

Zero TODO, zero FIXME, zero XXX across two hundred source files. Someone swept the comments. Combined with the syntax errors, the empty strings, and the _INTERNAL leak, it reads as a hurried mechanical pass over code that wasn’t structured for public release.

If you’re keeping score, this looks like a textbook screw-up: they shipped less than 2023 Twitter did (which shipped actual numeric weights — favorite=0.5, reply=13.5, reply_engaged_by_author=75, report=-369), the redaction was sloppy, and the consolation prize is that nothing in the released form is even runnable.

What the redaction did not touch

But hold that read next to this.

Every weight has a symbol. Every threshold has a symbol. Every feature flag has a symbol. The values are gone. The schema is fully public.

Now think about who the competitor audience actually is. Meta, TikTok, Pinterest, Reddit, Discord — companies with their own users, their own engagement data, their own A/B testing infrastructure. What do they lack?

They don’t lack data. They have their own users. What they lack is the list of dials xAI considers worth having.

“FAVORITE_WEIGHT = 1.0” is a single data point on someone else’s product. But the set:

FAVORITE_WEIGHT, REPORT_WEIGHT, OON_WEIGHT_FACTOR, NEW_USER_OON_WEIGHT_FACTOR,
AUTHOR_DIVERSITY_DECAY, AUTHOR_DIVERSITY_FLOOR, MAX_POST_AGE, ...

That’s not data. That’s the design of the search space. The values are recoverable from there if you have your own data — which a serious competitor does.

Search space matters more than search values when you have your own engagement data. The release shipped the search space.

So which is it?

Reading A — the oopsie. Mechanical sanitization stripped numeric values. The same script left symbol names alone because it didn’t have to scrub them. The schema leak is a side-effect. They thought they were hiding the operational details. They didn’t realize that for serious competitors, the architecture is most of the operational details.

Reading B — the play. Someone at xAI knew exactly what they were doing. The values get you sued, regulated, front-paged. The names signal architectural sophistication, anchor recruiting conversations, lock competitors into your conceptual framework, and let you claim transparency without leaking what actually matters. The schema is meant to be visible.

You can’t tell which is right from the artifact alone. But the diagnostic question generalizes.

Read the function, not the artifact

A partial disclosure is one observation. The interesting object is the function that produces the artifact — and that function shows itself across releases, not within one.

Reading A predicts: the next release will have more leaks of the same kind, because the underlying process is mechanical and the codebase keeps evolving.

Reading B predicts: the next release will close some of the current leaks and open new disclosure surfaces, because there’s a calibration loop.

They diverge over a release cadence, not over a single release.

If I had to bet right now, I’d bet both are partially true: leadership chose architectural-transparency-without-operational-disclosure as the release shape (deliberate), and the actual execution of that shape was a sanitization pipeline that wasn’t entirely careful (accidental). The empty-string Kafka topics and the broken Python are sloppy execution of a deliberate strategy.

The operational consequence is identical either way: the schema is public. What matters is what comes next.

What I’d take from this

When you can’t grep for numbers, grep for symbol names. The set of names tells you what the operator considers a tunable axis. That’s design intent, and it’s hard to fake or obscure once it’s in source.

The redaction’s seams are diagnostic. Mechanical sanitization leaves footprints: empty strings, broken syntax, surviving _INTERNAL identifiers, zero-TODO source trees. If the seams are obvious, the redaction was probably mechanical and the disclosure was probably not curated artifact-by-artifact. If there are no seams, somebody was paying attention.

Originally posted at bjro.dev. Co-authored with Claude Opus 4.7.

Configuration in Go Should Be Typed: Introducing confkit

Every Go application eventually needs configuration.

At the beginning, it is usually innocent:

port := os.Getenv("PORT")

Then the application grows.

You add a database URL. Then a Redis address. Then timeouts. Then feature flags. Then a YAML file for local development. Then environment variables for production. Then CLI flags for internal tools. Then Kubernetes secrets. Then maybe Vault, AWS Secrets Manager, or another infrastructure-specific source.

At some point, configuration stops being a few environment variables.

It becomes part of your application architecture.

And if you are not careful, it becomes a pile of string parsing, default values, manual validation, unclear precedence rules, and startup errors that only make sense to the person who originally wrote the code.

That is the problem I wanted to solve with confkit.

confkit is a typed configuration loading library for Go. It lets you define application configuration as a regular Go struct, load it from multiple sources, apply defaults, validate fields, and safely redact secrets from logs and error messages.

The short version:

type Config struct {
    Port     int    `env:"PORT" default:"8080" validate:"min=1,max=65535"`
    Database string `env:"DATABASE_URL" validate:"required" secret:"true"`
}

cfg, err := confkit.Load[Config](
    confkit.FromEnv(),
    confkit.FromYAML("config.yaml"),
)
if err != nil {
    log.Fatal(confkit.Explain(err))
}

No maps.

No manual parsing.

No hidden stringly-typed configuration layer.

No accidental secret leaks in startup logs.

Why configuration deserves more attention

Configuration code is rarely the most exciting part of a project.

Nobody starts a new backend service thinking:

I can’t wait to write a robust configuration layer today.

Usually, we just want to get to the actual application logic.

But configuration is one of the first things your application touches when it starts. If it is wrong, everything else becomes unreliable.

Bad configuration handling can lead to:

  • services starting with empty required values
  • invalid ports, timeouts, or limits
  • production secrets accidentally printed to logs
  • unclear source priority between files, env vars, and flags
  • duplicated parsing logic across services
  • late runtime failures instead of fast startup failures
  • CI pipelines that cannot validate config before deployment

In Go, this often ends up as a mix of os.Getenv, helper functions, YAML unmarshalling, manual validation, and a few comments explaining which value overrides which.

That works for small projects. It gets painful when the project grows.

I wanted configuration to feel more like a contract:

This is the shape of the config.

These fields are required.

These defaults are safe.

These values are secrets.

These validation rules must pass before the application starts.

In confkit, that contract is a Go struct.

The struct is the contract

Here is a slightly more complete example:

package main

import (
    "log"
    "time"

    "github.com/MimoJanra/confkit"
)

type Config struct {
    Host    string        `env:"HOST" default:"localhost"`
    Port    int           `env:"PORT" default:"8080" validate:"min=1,max=65535"`
    Timeout time.Duration `env:"TIMEOUT" default:"30s"`

    DB struct {
        DSN      string `env:"DSN" validate:"required" secret:"true"`
        MaxConns int    `env:"MAX_CONNS" default:"10" validate:"min=1,max=100"`
    } `prefix:"DB_"`
}

func main() {
    cfg, err := confkit.Load[Config](
        confkit.FromFlags(nil),
        confkit.FromEnv(),
        confkit.FromYAML("config.yaml"),
    )
    if err != nil {
        log.Fatal(confkit.Explain(err))
    }

    log.Printf("listening on %s:%d", cfg.Host, cfg.Port)
}

This struct describes quite a lot:

  • HOST has a default value
  • PORT has a default value and must be between 1 and 65535
  • TIMEOUT is parsed as time.Duration
  • DB_DSN is required
  • DB_DSN is also marked as secret
  • DB_MAX_CONNS has a default and a validation range
  • nested config fields can use a prefix

The application code receives a typed Config.

That means the rest of the app does not need to know where configuration came from. It does not care if a value came from YAML, an environment variable, a CLI flag, or a secret manager.

It just receives a normal Go value.

Explicit source priority

One thing I wanted to avoid is magical source precedence.

Configuration priority should be visible in code.

With confkit, sources are checked in the order you pass them:

cfg, err := confkit.Load[Config](
    confkit.FromFlags(nil),          // highest priority
    confkit.FromEnv(),               // overrides files
    confkit.FromYAML("config.yaml"), // fallback
)

This makes the priority easy to reason about:

flags -> environment variables -> config.yaml -> defaults

This is useful for common deployment patterns:

  • local development uses config.yaml
  • production overrides values through environment variables
  • CLI tools override specific values through flags
  • defaults keep non-critical fields simple

There is no need to hide this behavior in documentation or in a custom helper function. The order is visible where the config is loaded.

Clear validation errors

A configuration library should fail early.

If a required value is missing, the application should not start and then fail later when some database client, HTTP server, or queue consumer receives an empty string.

For example:

type Config struct {
    DatabaseURL string `env:"DATABASE_URL" validate:"required" secret:"true"`
}

If DATABASE_URL is missing, confkit gives a readable error:

Invalid configuration:

  DatabaseURL
    error: field is required
    source: env (DATABASE_URL)

That is the kind of error I want during startup.

It tells me:

  • which field failed
  • what went wrong
  • where the value was expected to come from

The goal is not just to return an error. The goal is to return an error that is useful when the service fails to start and someone needs to fix the deployment quickly.

Defaults without extra files

Defaults live next to the fields:

type Config struct {
    Host     string        `env:"HOST" default:"localhost"`
    Port     int           `env:"PORT" default:"8080" validate:"min=1,max=65535"`
    LogLevel string        `env:"LOG_LEVEL" default:"info" validate:"oneof=debug info warn error"`
    Timeout  time.Duration `env:"TIMEOUT" default:"30s"`
}

For me, this is much easier to maintain than scattering defaults across startup code.

The struct tells the full story:

  • field name
  • environment variable name
  • default value
  • validation rule
  • secret status, if needed

This makes configuration review easier too. You can look at the struct and understand what the application expects.

Secret redaction by default

Configuration often contains sensitive values:

type Config struct {
    APIKey   string `env:"API_KEY" validate:"required" secret:"true"`
    Password string `env:"DB_PASSWORD" secret:"true"`
}

If a field is marked with secret:"true", confkit redacts it in formatted errors and config dumps.

This matters because startup errors are often logged everywhere:

  • local terminal output
  • Docker logs
  • Kubernetes logs
  • CI/CD output
  • Slack alerts
  • monitoring systems
  • screenshots in support threads

A configuration library should make the safe behavior the easy behavior.

The developer should not have to remember to manually hide every token before logging an error.

Loading from files

confkit supports common file-based configuration formats:

cfg, err := confkit.Load[Config](
    confkit.FromYAML("config.yaml"),
)

There are also JSON and TOML sources:

cfg, err := confkit.Load[Config](
    confkit.FromJSON("config.json"),
    confkit.FromTOML("config.toml"),
)

A typical local config.yaml might look like this:

host: localhost
port: 8080
timeout: 30s

db:
  dsn: postgres://user:password@localhost:5432/app
  max_conns: 10

And the Go struct can still be the main contract:

type Config struct {
    Host    string        `yaml:"host" env:"HOST" default:"localhost"`
    Port    int           `yaml:"port" env:"PORT" default:"8080" validate:"min=1,max=65535"`
    Timeout time.Duration `yaml:"timeout" env:"TIMEOUT" default:"30s"`

    DB struct {
        DSN      string `yaml:"dsn" env:"DSN" validate:"required" secret:"true"`
        MaxConns int    `yaml:"max_conns" env:"MAX_CONNS" default:"10"`
    } `yaml:"db" prefix:"DB_"`
}

This gives you a nice local workflow without giving up production-friendly environment variable overrides.

Environment variables for production

Environment variables are still one of the most common ways to configure services in production.

confkit keeps this simple:

type Config struct {
    Host string `env:"HOST" default:"localhost"`
    Port int    `env:"PORT" default:"8080"`
}

Then:

HOST=0.0.0.0 PORT=3000 ./app

For nested structs, prefixes help avoid awkward names:

type Config struct {
    Database struct {
        Host string `env:"HOST" default:"localhost"`
        Port int    `env:"PORT" default:"5432"`
    } `prefix:"DB_"`
}

This maps to:

DB_HOST
DB_PORT

I like this pattern because nested structs are natural in Go, while environment variables are naturally flat. Prefixes connect the two without making the code weird.

CLI flags for tools

Configuration is not only for web services.

CLI tools often need a mix of files, environment variables, and flags.

Example:

type Config struct {
    Input   string `flag:"input" validate:"required"`
    Output  string `flag:"output" default:"out.json"`
    Verbose bool   `flag:"verbose" default:"false"`
}

cfg, err := confkit.Load[Config](
    confkit.FromFlags(nil),
    confkit.FromEnv(),
)
if err != nil {
    log.Fatal(confkit.Explain(err))
}

Now the CLI can be configured through flags, while still allowing environment variable fallback if you want it.

That gives you one config model instead of separate parsing logic for each source.

Safe config dumps

Sometimes you need to see the final configuration after all sources, defaults, and overrides were applied.

This is useful for debugging:

log.Println(confkit.DumpString(cfg))

If the config contains secret fields, they are redacted by default.

Example:

type Config struct {
    Host     string `env:"HOST" default:"localhost"`
    Password string `env:"PASSWORD" secret:"true"`
}

A safe dump should not print the real password.

That sounds obvious, but it is very easy to get wrong when every service has its own custom config logging code.

confkit gives you dump helpers for JSON and YAML output, with secret redaction enabled by default.

Validate configuration in CI

One feature I find especially useful is ValidateOnly.

The idea is simple: run the full loading and validation pipeline without triggering side effects such as hooks, audit logging, or reload-related behavior.

That makes it useful for CI checks.

For example:

_, err := confkit.ValidateOnly[Config](
    context.Background(),
    confkit.WithSource(confkit.FromYAML("config.prod.yaml")),
)
if err != nil {
    log.Fatal(confkit.Explain(err))
}

This lets you validate configuration before deployment.

A broken YAML file, missing required value, or invalid timeout should be caught before the application reaches production.

Optional infrastructure integrations

Not every application needs Vault.

Not every CLI tool needs AWS.

Not every small service should pull Kubernetes dependencies into the binary.

That is why confkit keeps the core package focused and provides optional integrations separately.

The core package covers common local/runtime sources such as:

  • environment variables
  • CLI flags
  • YAML
  • JSON
  • TOML

Optional integrations can be used when needed, including:

  • Kubernetes
  • Vault
  • Consul
  • etcd
  • AWS SSM / Secrets Manager

This keeps the basic library lightweight while still allowing production systems to load configuration from real infrastructure.

Install the core package:

go get github.com/MimoJanra/confkit@latest

Install optional integrations only when you need them:

go get github.com/MimoJanra/confkit/vault@latest
go get github.com/MimoJanra/confkit/consul@latest
go get github.com/MimoJanra/confkit/etcd@latest
go get github.com/MimoJanra/confkit/aws@latest

Hot reload

Some applications need to react to configuration changes without restarting.

confkit supports file watching:

cfg, watcher, err := confkit.LoadWithWatcher[Config](
    "config.yaml",
    confkit.FromYAML("config.yaml"),
    confkit.FromEnv(),
)
if err != nil {
    log.Fatal(confkit.Explain(err))
}

watcher.AddListener(func(oldCfg, newCfg any, err error) {
    if err != nil {
        log.Printf("config reload failed: %v", err)
        return
    }

    log.Println("config reloaded")
})

watcher.Start()
defer watcher.Stop()

_ = cfg

This is useful for long-running services where some operational values can safely change at runtime.

Of course, not every config value should be hot-reloadable. Database credentials, server ports, and deeply structural settings often still require a restart.

But when you do need reload behavior, it is useful to have it integrated with the same configuration model.

Custom sources

Configuration backends are often company-specific.

Maybe your team has an internal configuration service. Maybe you load values from a platform API. Maybe secrets come from a custom encrypted file format.

confkit supports custom sources through a small interface:

type Source interface {
    Name() string
    Lookup(ctx context.Context, field *FieldInfo) (any, bool, error)
}

That means the built-in sources are not a closed world.

You can keep the same typed struct, validation rules, defaults, and redaction behavior while adding your own configuration backend.

Schema and documentation generation

One useful side effect of treating configuration as a struct contract is that the same struct can be used to generate documentation.

confkit includes schema-related functionality for generating references from config structs.

That can be useful when you want to document:

  • supported environment variables
  • default values
  • required fields
  • validation constraints
  • CLI options
  • config file structure

In other words, the config struct can become the source of truth not only for the application, but also for the documentation around that application.

Why not just use another config library?

There are already good Go configuration libraries.

You might use:

  • envconfig for environment variables
  • koanf for modular configuration composition
  • Viper for a broad and established configuration system

confkit is not trying to pretend those projects do not exist.

The design is focused on a specific style:

Go struct + tags + typed loading + defaults + validation + safe diagnostics

A rough rule of thumb:

Use envconfig if you only need environment variables.

Use koanf if you want a flexible toolkit and prefer composing behavior yourself.

Use Viper if you need a large, established configuration system with many built-in behaviors.

Use confkit if you want your configuration contract to be a Go struct, with validation and safe errors built in from the beginning.

API stability

Configuration loading sits very close to application startup.

If it breaks, the application may not start at all.

That is why API stability matters.

confkit has a v1 stability contract for the core public API. The intention is that v1.x releases can add functionality without breaking existing users, while breaking API changes require a future v2 release.

For a configuration library, this is important. The API should be boring in the best possible way: stable, predictable, and safe to update.

Complete minimal example

Here is a small working example:

package main

import (
    "log"

    "github.com/MimoJanra/confkit"
)

type Config struct {
    Host string `env:"HOST" default:"localhost"`
    Port int    `env:"PORT" default:"8080" validate:"min=1,max=65535"`

    DatabaseURL string `env:"DATABASE_URL" validate:"required" secret:"true"`
}

func main() {
    cfg, err := confkit.Load[Config](
        confkit.FromEnv(),
        confkit.FromYAML("config.yaml"),
    )
    if err != nil {
        log.Fatal(confkit.Explain(err))
    }

    log.Printf("starting server on %s:%d", cfg.Host, cfg.Port)
}

Example config.yaml:

host: localhost
port: 8080

Run with:

DATABASE_URL=postgres://user:pass@localhost:5432/app go run .

The application gets a typed config struct, while secrets remain protected in logs and errors.

Final thoughts

Configuration is not glamorous.

But it is one of the most important startup paths in any service.

It decides whether your application starts safely, fails clearly, validates its assumptions, and protects sensitive values.

confkit is my attempt to make that layer simple and explicit:

  • define config as a Go struct
  • load it from multiple sources
  • apply defaults
  • validate before startup
  • redact secrets automatically
  • explain errors clearly
  • support real production sources when needed

One struct.

Multiple sources.

Typed values.

Readable errors.

Safe logs.

That is the whole idea.

You can find the project here:

github.com/MimoJanra/confkit

De Data-Chaos a Data-Driven Decisions: Pipeline ETL Automatizado en Microsoft Fabric y PowerBI

Dicen que lo más difícil de detectar es aquello que tenemos delante constantemente.

Las grandes organizaciones generan millones de datos cada día, pero disponer de información, no siempre significa comprenderla. Muchos datos permanecen aislados, dispersos o infrautilizados, dificultando la identificación de patrones, riesgos y oportunidades estratégicas.

Porque un dato informa, pero millones, transforman. Bajo esta idea nace ORIONTECH, un proyecto académico basado en un pipeline analítico end-to-end desarrollado con Microsoft Fabric y Power BI, orientado al reporting ejecutivo, control financiero y monitorización operacional.

El proyecto implementa una arquitectura Medallion (Bronze, Silver y Gold) capaz de transformar datos raw en información estratégica mediante procesos automatizados de ingestión, limpieza, modelado y visualización. A través de PySpark Notebooks, Lakehouse y modelos analíticos en Power BI, ORIONTECH permite centralizar KPIs financieros, riesgos operacionales, desviaciones presupuestarias y métricas de productividad dentro de una misma plataforma diseñada con enfoque enterprise.

Actualmente, la solución trabaja sobre un dataset sintético inspirado en estructuras financieras y operacionales reales de mi entorno profesional, ya que la utilización de información corporativa real no es posible por motivos de confidencialidad y protección de datos.

El dataset incluye aproximadamente 30.000 registros con variables relacionadas con ingresos, costes operativos, presupuestos, forecast financiero, productividad, departamentos y regiones internacionales.

Uno de los principales retos fue garantizar la consistencia y calidad del dato, ya que existían categorías duplicadas, nomenclaturas inconsistentes, valores nulos y diferencias de formato entre variables numéricas y textuales. Para resolverlo, fue necesario construir procesos de limpieza y estandarización dentro de la capa Silver antes de modelar la información para su consumo analítico en Power BI.

Sobre este modelo se desarrollaron distintos dashboards orientados a diferentes perspectivas del negocio:


1. Executive Overview

2. Operational Risk

3. Financial Performance

4. Controlling Report

Uno de los aspectos más interesantes del proyecto fue comprobar cómo determinados departamentos concentraban gran parte de los costes ocultos y de la exposición al riesgo operacional, impactando directamente sobre la rentabilidad global de la organización.

Como evolución futura, ORIONTECH podrá incorporar comparaciones en los KPIS vs budget y vs forecast; implementar el análisis predictivo y la monitorización en tiempo real mediante inteligencia artificial.

Este proyecto ha sido diseñado con un enfoque escalable y orientado a su posible implantación en entornos corporativos reales, permitiendo automatizar procesos repetitivos, optimizar el reporting ejecutivo y fomentar una cultura data-driven dentro de las distintas áreas de negocio.

Github Repository: https://github.com/evolve-space/Proyecto-Master-DataScience-Evolve-MariaMonedero.git

Proyecto desarrollado durante el Máster en Data Science & Artificial Intelligence de Evolve: https://evolve.es/

Title: I Built a Production GPU Energy Optimizer in One Day — From My Phone

I Built a Production GPU Energy Optimizer in One Day — From My Phone

Not from a MacBook. Not from a cloud VM. From my Android phone,
using Termux.

Here’s what shipped by end of day:

  • Real-time GPU energy dashboard
  • DESYNC & GHOST power anomaly detection
  • 17 cloud provider support
  • Per-user API keys
  • Time-series metrics scaling to 100+ GPUs
  • 18/18 smoke tests passing
  • 60-second Docker install

The Problem

GPU providers lie. Not intentionally — but telemetry desync is real.

Two failure modes kill your energy budget:

DESYNC — GPU drawing 420W but reporting 8% utilization.
You’re paying full price for a GPU doing nothing useful.

GHOST power — GPU reporting 98% utilization at 40W draw.
Physically impossible. Your scheduler is making decisions on
fake data.

We found both in the wild across AWS and Vast.ai during testing.

The Solution

An open validation stack that:

  1. Detects DESYNC and GHOST anomalies automatically
  2. Works across 17 GPU cloud providers
  3. Evicts bad workloads via Kubernetes or Run:ai
  4. Alerts via Slack
  5. Stores time-series data for 100+ GPUs

What We Built

Component Status
CEI Formal Specification ✅
Grafana Dashboard ✅
GPU Agent Script ✅
Per-user API Keys ✅
Time-series DB ✅
17-Provider Validator ✅
Smoke Test 18/18 ✅
Docker one-liner ✅

Why Termux

No laptop. No cloud IDE. Just an Android phone with Termux.

This matters because it proves the stack is lightweight enough
to run anywhere. If it builds and runs on a phone, it runs on
any bare metal server, VPS, or edge node.

60-Second Install


bash
 Install Docker (skip if already installed)
curl -fsSL https://get.docker.com | sh

 Clone and run
git clone https://github.com/mikebains41-debug/ai-gpu-energy-optimizer-
cd ai-gpu-energy-optimizer-
docker-compose up