<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://kkumaresan.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://kkumaresan.com/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-08-28T08:31:56+05:30</updated><id>https://kkumaresan.com/feed.xml</id><title type="html">Karuppuswamy Kumaresan</title><subtitle>Principal Architect with 22+ years translating business strategy into platform architecture — legacy modernization, cloud economics, AI enablement and architecture governance across enterprise SaaS.</subtitle><author><name>Karuppuswamy Kumaresan</name><email>im@kkumaresan.com</email></author><entry><title type="html">Aze: a Full-Stack Starter I Kept Rebuilding, So I Saved It</title><link href="https://kkumaresan.com/writing/aze-a-full-stack-starter/" rel="alternate" type="text/html" title="Aze: a Full-Stack Starter I Kept Rebuilding, So I Saved It" /><published>2026-08-28T00:00:00+05:30</published><updated>2026-08-28T00:00:00+05:30</updated><id>https://kkumaresan.com/writing/aze-a-full-stack-starter</id><content type="html" xml:base="https://kkumaresan.com/writing/aze-a-full-stack-starter/"><![CDATA[<p>Every side project I start begins the same way. Not with the idea, with the
plumbing. A monorepo layout, an API with auth, a database with migrations, a
cache, Docker files, CI, and at some point the question “how does this get
deployed?” By the time the plumbing works, the weekend is over and the idea
hasn’t been touched.</p>

<p>So eventually I put all of that plumbing into one repository and published it.
It is called <a href="https://github.com/aruzone/aze-mini">Aze</a> (the repo is
<code class="language-plaintext highlighter-rouge">aze-mini</code>), it is MIT licensed, and it is a full-stack starter. Clone it,
delete the demo code, and start building the thing you actually meant to
build.</p>

<!--more-->

<h2 id="what-is-in-it">What is in it</h2>

<p>The stack, in one breath: an <strong>Nx monorepo</strong> holding a <strong>Next.js</strong> client and
a <strong>NestJS</strong> API, with <strong>Prisma</strong> on <strong>Postgres</strong>, a <strong>Redis</strong> cache, Docker
images, a <strong>Helm</strong> chart, and an <strong>Argo CD</strong> application. TypeScript
everywhere.</p>

<p>That sentence describes a lot of starters. What makes this one different is
not the list of technologies. It is that the boring decisions have already
been made, and written down where you can argue with them.</p>

<h2 id="opinionated-on-purpose">Opinionated on purpose</h2>

<p>Most starters hand you a pile of choices and get out of the way. That sounds
friendly, but it means you spend your first evening re-deciding things that
have standard answers. Aze makes the decisions and documents each one in an
<a href="https://github.com/aruzone/aze-mini/tree/main/docs/adr">ADR</a>. A few examples:</p>

<ul>
  <li><strong>One database.</strong> Postgres only. There is no file-based fallback that works
in dev and behaves differently in production.</li>
  <li><strong>Auth fails closed.</strong> Every API route requires a token unless it explicitly
opts out with a decorator. A new route is protected by default, not exposed
by default.</li>
  <li><strong>The cache fails open.</strong> If Redis is down, the API gets slower. It does
not break. Those are two different failure modes and they should not be
confused.</li>
  <li><strong>One error shape.</strong> Every refusal comes back as
<code class="language-plaintext highlighter-rouge">{ statusCode, timestamp, path, message }</code>, so a client has exactly one
thing to read.</li>
  <li><strong>The token never reaches browser JavaScript.</strong> The client calls the API
from its own server, and the JWT lives in an httpOnly cookie.</li>
</ul>

<p>You own your clone. If you disagree with any of these, that is fine. The
point of writing them down is that a change you make later is a decision you
made on purpose, not an accident.</p>

<h2 id="platform-and-demo">Platform and Demo</h2>

<p>The repository is deliberately split into two tiers:</p>

<ul>
  <li><strong>Platform</strong> is what you keep: auth, the request perimeter, the cache, the
error handling, the session, the CI.</li>
  <li><strong>Demo</strong> is a small product catalogue with a seeded user, there to show
each pattern once. Read it, then delete it.</li>
</ul>

<p>Deleting the demo is a supported operation, not an afterthought.
<a href="https://github.com/aruzone/aze-mini/blob/main/docs/demo.md">docs/demo.md</a> is
a removal guide: the exact paths to delete, the order to work in, and how to
check that what is left still works. Lint rules are set up so platform code
cannot quietly depend on demo code, which means the demo is genuinely
removable rather than tangled in.</p>

<h2 id="clone-and-own-no-strings">Clone and own, no strings</h2>

<p>One thing worth saying plainly, because most starters hide it: <strong>there is no
update path</strong>. When you clone Aze, you own the result. Fixes made to the
starter later (including security fixes) will not reach your project, and
there is no supported way to pull them in.</p>

<p>This is written down in an
<a href="https://github.com/aruzone/aze-mini/blob/main/docs/adr/0004-clone-and-own-no-update-path.md">ADR</a>
because it shapes everything else. Since the security posture you clone is
the posture you keep, the parts that matter, like auth, tokens and headers,
are held to a production bar, and the parts that are <em>not</em> held to that bar
say so out loud.</p>

<h2 id="built-to-be-read-by-coding-agents">Built to be read by coding agents</h2>

<p>A lot of the code I write now gets written with AI agents in the loop, and
agents work from what a repository tells them. So Aze carries:</p>

<ul>
  <li><strong>AGENTS.md</strong> is the working brief: every command, the module layout, what
each file is for. <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> just points at it, so no tool reads a
different version of the truth.</li>
  <li><strong>CONTEXT.md</strong> is the project’s vocabulary, including words to avoid, so an
agent’s output uses the same terms the code does.</li>
  <li><strong>Commands that need no local knowledge</strong>: <code class="language-plaintext highlighter-rouge">npm run test</code>, <code class="language-plaintext highlighter-rouge">npm run lint</code>,
<code class="language-plaintext highlighter-rouge">npm run build</code> all work for an agent that has never seen the repo.</li>
  <li><strong>A documentation checker</strong>: <code class="language-plaintext highlighter-rouge">npm run check:docs</code> fails if a document
names a file that does not exist or describes a route the API does not
serve. Docs that cannot quietly go stale.</li>
</ul>

<h2 id="getting-started">Getting started</h2>

<p>If you have Docker, this is the whole setup:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/aruzone/aze-mini.git
<span class="nb">cd </span>aze-mini

docker compose up <span class="nt">-d</span> <span class="nt">--build</span> <span class="nt">--wait</span>

<span class="c"># optional: seed the demo catalogue and a user to sign in as</span>
docker compose run <span class="nt">--rm</span> migrate npx prisma db seed
</code></pre></div></div>

<p>Client at <code class="language-plaintext highlighter-rouge">localhost:3000</code>, API at <code class="language-plaintext highlighter-rouge">localhost:3030/api</code>, and an interactive
OpenAPI page at <code class="language-plaintext highlighter-rouge">localhost:3030/api/docs</code>.</p>

<p>For day-to-day work there is a local-toolchain path too (Node 24, with Docker
for Postgres and Redis only). Both are written up as runbooks in
<a href="https://github.com/aruzone/aze-mini/tree/main/docs/agents">docs/agents/</a>.</p>

<p>One note: the compose file carries committed public credentials for local
use. They are not secrets and are not fit to be. Replace them before running
that file anywhere but your own machine.</p>

<h2 id="what-it-does-not-do">What it does not do</h2>

<p>The starter runs, migrates and deploys. Carrying real users’ data asks for a
few more decisions, and rather than leaving you to discover them in
production, they are listed in
<a href="https://github.com/aruzone/aze-mini/blob/main/docs/deployment.md">docs/deployment.md</a>:
TLS and an Ingress, database backups, a general rate limit beyond login,
token revocation, shared throttle counts across replicas, and real secret
management. A summary table says what is already handled and what is yours.</p>

<h2 id="that-is-it">That is it</h2>

<p>No dashboard, no CLI, no paid tier. A repository with the plumbing done, the
reasoning written down, and the demo code waiting to be deleted. If that
sounds like a useful starting point, take a look:</p>

<p><strong><a href="https://github.com/aruzone/aze-mini">github.com/aruzone/aze-mini</a></strong>. It is
MIT licensed; use it for whatever you like.</p>]]></content><author><name>Karuppuswamy Kumaresan</name><email>im@kkumaresan.com</email></author><category term="starter" /><category term="fullstack" /><category term="nestjs" /><category term="nextjs" /><category term="devops" /><summary type="html"><![CDATA[I published aze-mini, an MIT-licensed full-stack starter with Next.js, NestJS, Postgres, Redis, Docker and Helm. Here is what it is, why it is opinionated on purpose, and what you get when you clone it.]]></summary></entry><entry><title type="html">The Corporate Website Is Dead. The Web Is Not.</title><link href="https://kkumaresan.com/writing/the-corporate-website-is-dead/" rel="alternate" type="text/html" title="The Corporate Website Is Dead. The Web Is Not." /><published>2026-08-17T00:00:00+05:30</published><updated>2026-08-17T00:00:00+05:30</updated><id>https://kkumaresan.com/writing/the-corporate-website-is-dead</id><content type="html" xml:base="https://kkumaresan.com/writing/the-corporate-website-is-dead/"><![CDATA[<p>I spent five years as a creative director before I moved into architecture,
and eleven running a digital services practice that built these things for a
living. The pitch back then was simple enough that we never had to defend it:
your website is where your customers go. Everything else — the ads, the
listings, the print — existed to send people there.</p>

<p>That sentence stopped being true a while ago. Most companies have not
noticed, because the site still gets traffic and the traffic still converts,
so nothing looks broken. What changed is quieter than a traffic collapse.
The website is still there. Its job is not the one it was designed for.</p>

<!--more-->

<h2 id="the-clicks-are-down-and-the-visits-matter-more">The clicks are down and the visits matter more</h2>

<p>Two pieces of research from the past year point in opposite directions, and
the contradiction is the whole story.</p>

<p>Pew Research tracked what people actually do on a Google results page. When
an AI summary appears, users click a traditional link on 8% of those pages,
against 15% when there is no summary. Roughly half the clicks, gone. Only 1%
click a source cited inside the summary itself. People also stop searching
altogether more often — a session ends on 26% of pages carrying a summary,
against 16% of ordinary ones. Google’s own position, stated by search chief
Nick Fox in July, is that AI features send billions of clicks to websites
every week. That may well be true; the company has not published a baseline
or a denominator, so it is hard to weigh against Pew’s numbers.</p>

<p>Now the other direction. Yext’s 2026 consumer research asked what happens
<em>after</em> someone gets a recommendation from an AI tool. More than nine in ten
take at least one verification step before acting. 62% go and search Google.
58% go directly to the business’s own website. 52% click through to the
sources the AI cited. And this holds steady whether or not the person says
they trust the AI — the ones who rate their trust 5 out of 5 verify at
essentially the same rate as the ones who are sceptical.</p>

<p>So: fewer clicks per search, and more deliberate visits per decision. Those
are not in conflict. They describe a funnel that has changed shape. Casual
browsing traffic is being absorbed by the summary layer, which was never
worth much anyway. What still reaches the site is someone checking whether a
recommendation holds up.</p>

<p>That is a different visitor with a different question, and most corporate
sites are still built to answer the old one.</p>

<h2 id="owned-infrastructure-borrowed-land">Owned infrastructure, borrowed land</h2>

<p>It helps to be blunt about what each channel is actually for. Once you write
it out, the website’s remaining job gets easier to see.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  Instagram, YouTube  ──▶  "Why should I pay attention to you?"
  LinkedIn            ──▶  "Are you credible in your field?"
  Google              ──▶  "Who does this?"
  AI assistants       ──▶  "Who should I consider?"
  Reviews, community  ──▶  "What do others say happened?"
  ─────────────────────────────────────────────────────────
  Website             ──▶  "Is any of this actually true?"
  ─────────────────────────────────────────────────────────
  WhatsApp, email     ──▶  "Let's talk."
  CRM, commerce       ──▶  "Let's do business."
</code></pre></div></div>

<p>Everything above that line runs on somebody else’s platform, under rules
that change without consulting you and reach that can be throttled at will.
Everything on it is yours. That distinction used to be a talking point for
selling websites. It is now the reason the website survives at all: it is
the only node in the chain where a company controls both the claim and the
evidence behind it.</p>

<p>The website has become a verification layer. Not the destination — the thing
people check the destination against.</p>

<h2 id="which-is-why-the-brochure-structure-fails">Which is why the brochure structure fails</h2>

<p>The default corporate site is still shaped like a company org chart:</p>

<blockquote>
  <p>Home · About Us · Services · Products · Careers · Contact Us</p>
</blockquote>

<p>That structure answers “what would you like to know about us?” — a question
nobody arrives with any more. Someone who has just been handed a shortlist
by ChatGPT arrives with something sharper: <em>is this lot any good, and can
they do the specific thing I need?</em></p>

<p>Take an industrial automation firm, the kind of business where a single
contract runs into years. The brochure version of its site says it delivers
world-class Industry 4.0 solutions. Every competitor’s site says that too,
so the sentence carries no information at all.</p>

<p>The useful version says what it fixes — machine downtime, energy
consumption, retrofitting equipment that is older than the engineers
maintaining it. Then it shows the receipts: how many plants it has done this
in, what the measured savings were, which PLC families it has integrated
against, what the architecture looks like when deployed. Then it says who
this is for, because a plant manager and a procurement head are reading for
completely different reasons. And then it lets someone act on any of it
without a form: run the numbers, read a comparable deployment, check
compatibility, talk to an engineer.</p>

<p>None of that is new as advice. What is new is that it is no longer optional,
because the summary layer above the website has already given the visitor
the generic answer. Generic content is precisely the part machines can now
produce on demand. What they cannot produce is your evidence.</p>

<p>The shift is from a site organised around navigation to one organised around
intent. The homepage stops being a poster and becomes something closer to a
decision interface.</p>

<h2 id="nobody-starts-at-the-homepage">Nobody starts at the homepage</h2>

<p>This is the part I think designers underrate most.</p>

<p>The 2005 path was Google, homepage, navigation, page. The 2026 paths look
like an AI answer straight to a specific page, or a LinkedIn post straight
to a case study, or Maps to reviews to a pricing page. The homepage is
increasingly the thing people visit <em>second</em>, if at all, to work out who
they have landed on.</p>

<p>Which means every page that matters has to stand on its own. A case study
buried three levels under <code class="language-plaintext highlighter-rouge">/resources</code> may well be the first and only thing
a buyer sees. If it opens mid-thought, assumes the reader already knows what
the company does, and ends without a next step, it has wasted the only
impression it was going to get.</p>

<p>Every significant page needs to carry its own context, its own credibility,
its own evidence and its own exit. That sounds like a content problem. It is
really an information architecture problem, which is why it tends to be
nobody’s job.</p>

<h2 id="the-second-reader-is-a-machine">The second reader is a machine</h2>

<p>Here is the change that I think matters most, and the one that pulls this
out of design and into architecture.</p>

<p>We used to design for a human with a browser. Increasingly the chain is a
human, then a model, then the web. The model reads the site, decides whether
the company is a credible answer to a question, and either passes it on or
does not. It is a reader with no patience for implication, no ability to
infer from a nice layout, and no interest in your brand film.</p>

<p>So the site now needs to be legible to four audiences at once: people,
search crawlers, language models, and other software that consumes it
through feeds and APIs. Semantic markup, structured data, schema.org types
for the things a company actually sells, clean and explicit information
architecture, named entities, documented products, real FAQs — these have
been filed under SEO for twenty years, treated as a technical chore handed
to a specialist after the design was signed off.</p>

<p>They are user experience decisions now. If a model cannot parse what a
company does, it cannot recommend it, and the buyer never reaches the
beautifully art-directed page at all.</p>

<p>LinkedIn’s research with Bain this June puts a number on how early this
happens: 94% of B2B buying groups use a large language model before they
speak to anyone in sales. The same study found 40% of deals collapse not
over price or product but because the buyer cannot build a case they are
willing to defend internally — and that a recommendation from a comparable
company is worth about ten times more, in defensibility, than an argument
about being cheaper or more innovative.</p>

<p>That is a fairly precise brief for what a website should contain. Proof
from people who look like the buyer, structured so a machine can find it and
a nervous human can forward it to their boss.</p>

<h2 id="what-this-actually-is-now">What this actually is now</h2>

<p>Drawn out, the thing stops looking like a website and starts looking like a
system with a website in the middle of it.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      LinkedIn   Instagram   YouTube   Google   AI assistants
          └──────────┴───────────┼────────┴──────────┘
                                 ▼
                        ┌────────────────┐
                        │    WEBSITE     │
                        │  knowledge     │
                        │  evidence      │
                        │  products      │
                        │  tools         │
                        └───────┬────────┘
                                │
                 ┌──────────────┼──────────────┐
                 ▼              ▼              ▼
              WhatsApp         CRM         commerce

    resting on:  content · structured data · search
                 analytics · AI discoverability · APIs
</code></pre></div></div>

<p>Designing that is not web design in the sense I learned it. The decisions
that determine whether it works are about content models, entity
definitions, integration points and measurement — the same decisions I now
spend my time on in enterprise platform work, applied to a smaller and much
more public surface.</p>

<p>I would not tell a company to stop investing in its website. I would tell it
to stop investing in a brochure. The two have been the same object for so
long that the distinction sounds like hair-splitting, right up until you
watch an AI assistant summarise a company’s entire value proposition from
its homepage and get it wrong, because the homepage never actually said
anything.</p>

<p>The standalone website is dying. The web as a business interface is not —
it is quietly becoming the layer everything else has to check itself
against.</p>

<hr />

<p>Sources: <a href="https://www.pewresearch.org/short-reads/2025/07/22/google-users-are-less-likely-to-click-on-links-when-an-ai-summary-appears-in-the-results/">Pew Research Center on AI summaries and
clicks</a>,
the <a href="https://www.yext.com/resources/consumer-search-behaviors-findings">Yext 2026 Consumer Search Behaviors
Report</a>,
Google’s <a href="https://searchengineland.com/google-says-ai-search-features-sending-billions-of-clicks-to-websites-each-week-482599">claim on AI Search
clicks</a>,
and <a href="https://ppc.land/the-b2b-buying-formula-linkedin-says-ai-just-scrambled/">LinkedIn and Bain on B2B
buying</a>.</p>]]></content><author><name>Karuppuswamy Kumaresan</name><email>im@kkumaresan.com</email></author><category term="ux" /><category term="ai" /><category term="architecture" /><summary type="html"><![CDATA[Fewer people arrive at a company's homepage, and the ones who do already know what they want. What that does to the job of a corporate website, and to designing one.]]></summary></entry><entry><title type="html">A Soldering Iron and a Message Bus</title><link href="https://kkumaresan.com/writing/a-soldering-iron-and-a-message-bus/" rel="alternate" type="text/html" title="A Soldering Iron and a Message Bus" /><published>2026-08-11T00:00:00+05:30</published><updated>2026-08-11T00:00:00+05:30</updated><id>https://kkumaresan.com/writing/a-soldering-iron-and-a-message-bus</id><content type="html" xml:base="https://kkumaresan.com/writing/a-soldering-iron-and-a-message-bus/"><![CDATA[<p><a href="/writing/the-case-for-a-homelab/">Part one</a> argued that a homelab is a
production system with real users. This is the part below that argument: the
devices themselves. A Raspberry Pi in Chennai runs four containers — Home
Assistant, Mosquitto, Portainer and a reverse SSH tunnel — and everything
interesting hangs off the second of those. Most of what publishes to that
broker is not a product. It is a board I built at the dining table, flashed
and screwed into a wall. None of it can be redeployed from a laptop. Some of
it is glued behind an air conditioner’s grille, in a flat where the ambient
temperature sits above thirty degrees for eight months of the year.</p>

<p>That constraint shapes every decision that follows.</p>

<!--more-->

<h2 id="what-is-actually-on-the-walls">What is actually on the walls</h2>

<p>The device tier is deliberately boring, and mostly two chips.</p>

<figure class="figure-wide">
  <img src="/assets/img/fig-02-device-layer.png" width="1600" height="1600" loading="lazy" decoding="async" alt="Block diagram of the device layer. Home Assistant, the automation plane, runs a scheduler that re-asserts the desired state of every room on a fixed interval. Below it an MQTT broker carries paired state and command topics under a zone/device/attribute contract, plus a last-will availability topic. Hanging off the broker: a hand-built ESP8266 or ESP32 IR controller for the air conditioners, running C++ firmware that sends full-state frames; a hand-assembled relay board flashed with Tasmota for fans, heaters and lighting; and door, PIR, BME280, HTU21D and current-clamp sensors." />
  <figcaption>Fig. 02 — Device layer. Everything below the broker is a dumb
  appliance with a small computer taped to it.</figcaption>
</figure>

<p>The air conditioner controllers are mine end to end: an ESP — 8266 on the
older boards, ESP32 on everything I have built since — an IR LED, a driver
transistor and a temperature sensor, on a board I assembled, running
firmware I wrote in C++. More on that below, because it is the part of this
system I am least willing to hand-wave.</p>

<p>The fan and heater controllers are hand-built too — relay boards I put
together and flashed with Tasmota, where stock firmware was genuinely enough
and writing my own would have been ego rather than engineering. Off-the-shelf
hardware fills the rest: door and motion sensors watching the main entrance,
BME280 and HTU21D sensors for temperature, humidity and pressure, and
current transformers clamped around the heavy circuits. All of it lands on
the same bus, in the same shape, whatever it is underneath.</p>

<p>That split is the honest summary of the build. Write the firmware where the
problem is genuinely unsolved; flash someone else’s where it isn’t. Nothing
here is a smart appliance. Every one of these is a dumb appliance with a
small computer I built taped to it, and that is the point: the intelligence
is central and version-controlled, and the edge is replaceable for the price
of a board and an evening.</p>

<h2 id="the-firmware-i-had-to-write">The firmware I had to write</h2>

<p>Air conditioners were the part nobody could sell me a solution for, so I
wrote one in C++ against Espressif’s own SDK rather than the Arduino layer
over it — the ESP8266 RTOS SDK on the early boards, ESP-IDF proper on the
ESP32s I have built since. Same idioms, same FreeRTOS underneath, so the port
cost me an afternoon. That choice costs you a weekend of build system and
menuconfig before a single LED blinks, and buys you a device that behaves
predictably for years.</p>

<p>The problem is that an AC remote speaks a protocol its manufacturer does not
publish. You point an IR receiver at the remote, capture the carrier and
stare at pulse timings until structure appears: a header burst, a bit
encoding where a one and a zero differ only in the length of the gap after an
identical mark, a payload and a checksum at the end that has to be derived
by pressing buttons in sequence and watching which byte moves. Then you emit
it back — a timing array shifted out of a GPIO pin at 38 kHz. Getting the
checksum wrong on the first brand cost me an evening of an AC that beeped and
did nothing. The second brand took an hour, because by then I understood the
shape of the problem.</p>

<p>The design constraint that mattered most only became obvious mid-way: these
remotes do not send deltas. Press “temperature up” and the remote transmits
the <em>entire</em> state — mode, setpoint, fan speed, swing, timer — in one frame.
The air conditioner is a receiver with no memory of the conversation, which
means the board on the wall has to be the thing that remembers. So the
firmware carries a state struct, mutates one field when a command arrives,
and retransmits the whole frame every time. That single fact shapes
everything upstream: it is <em>why</em> there is a separate state topic and command
topic, and why the scheduler can safely reassert the same state every ten
minutes without confusing the unit.</p>

<p>The rest of the firmware is unglamorous and matters just as much, and this is
where working close to the SDK pays for itself. Separate FreeRTOS tasks for
the IR transmit path, the network client and the sensor sampling, talking
through queues, so nothing blocks anything else — the IR frame has
microsecond-sensitive timing, and a network stall in the middle of a
transmission produces a command the air conditioner silently ignores. WiFi
and broker reconnection with backoff, since a board that reboots at 2 AM must
rejoin and re-announce itself without anyone noticing. A last-will message
registered at connect time, so the broker reports the board’s death if it
cannot. Static allocation and no clever abstractions, because the thing has
to run for months untouched behind a grille in forty-degree heat, and heap
fragmentation on a device you cannot reach is not a bug you get to fix.</p>

<p>Writing it myself is also what made the rest of the design possible. A device
whose firmware I control can publish exactly the topics my architecture
wants, in the shape my architecture wants, rather than forcing the platform
to adapt to whatever a vendor decided to emit.</p>

<h2 id="the-topic-scheme-is-the-architecture">The topic scheme is the architecture</h2>

<p>If you take one thing from this post: on an IoT network, your MQTT topic
scheme <em>is</em> your API, and you will live with it far longer than you expect.</p>

<p>The shape worth copying is this one:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;prefix&gt;/&lt;zone&gt;/&lt;device&gt;/&lt;attribute&gt;/&lt;direction&gt;

  …/&lt;device&gt;/mode/state         →  what the unit is doing
  …/&lt;device&gt;/mode/command       →  what I have asked it to do
  …/&lt;device&gt;/temperature/{…}    →  one pair per attribute
  …/&lt;zone&gt;/ambient/temperature  →  what the room actually reads
  …/&lt;device&gt;/availability       →  online / offline, via last will
</code></pre></div></div>

<p>Three properties earn their keep. Every controllable attribute is split into
a state topic and a command topic, so the system never confuses “what I asked
for” with “what is true” — the single most valuable line in the whole design.
Ambient temperature comes from an independent sensor rather than from the air
conditioner’s own reading, so the control loop is closed against the room,
not against the appliance’s optimism. And every device registers a last-will
message with the broker, so when a board drops off the WiFi the broker
announces the death on the device’s behalf. Availability is not something the
automation platform has to infer from silence.</p>

<p>The scheme also carries an honest scar, and I would rather describe it than
show it. My production topics use two different top-level prefixes, split
along a boundary that made sense on the evening I introduced it and makes no
sense now, and the house has a third name again inside Home Assistant. Each
choice was locally reasonable. Together they mean I keep a mental translation
table to debug my own network. Renaming would require reflashing boards that
are now behind furniture and inside grilles, so the drift is permanent — the
cheapest possible lesson in why naming conventions are load-bearing
infrastructure, learned at the price of never being able to fix it.</p>

<h2 id="scheduling-is-a-control-loop-not-a-rule">Scheduling is a control loop, not a rule</h2>

<p>The obvious way to automate an air conditioner is a rule: at 22:00, turn on.
I started there. It fails within a week, because rules are edge-triggered and
houses are stateful. A command lost to WiFi contention is lost forever. A
device that reboots at 22:01 never gets the message. Someone turns the unit
off manually and the system never notices.</p>

<p>So the climate system is a loop instead. Every ten minutes — plus immediately
on any control-switch change — an automation calls a scheduler script with a
list of rooms. For each room it reads three schedule slots out of Home
Assistant helpers: a start time, an end time, a target temperature between 18
and 30, and a fan-overlap toggle. Then it asserts the correct state, whatever
the current one happens to be.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>every 10 min ─→ for each room ─→ inside a window?  → cool @ target
                                  in the overlap?  → AC off, fan on
                                  otherwise        → AC off, fan off
</code></pre></div></div>

<p>The loop is idempotent, so a dropped IR command self-heals on the next tick,
and the worst-case cost of any single failure is ten minutes of wrong
temperature. Three slots per room exist because a Chennai night is three
different problems: the evening pre-cool, the deep-night hold and the
pre-dawn hour when the outside air finally drops below the setpoint.</p>

<p>The fan overlap is my favourite piece of the whole system and the least
technical. When the AC’s window closes, the ceiling fan runs on for another
seven minutes — the default, tunable per room — to move the cold air already
in the room instead of paying the compressor to make more. It is one number
in a config file, it is worth real money over a Chennai summer, and it exists
only because someone in the house complained about waking up cold at 3 AM.</p>

<h2 id="amps-to-kilowatt-hours">Amps to kilowatt-hours</h2>

<p>The energy pipeline is four transformations, each one cheap and each one
auditable.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CT clamp → amps (MQTT) → × 220 V → watts → Riemann sum → kWh → utility meter (day/week/month)
</code></pre></div></div>

<p>A clamp on the circuit publishes amperage. A template sensor multiplies by
mains voltage to get power. An integration sensor performs a left Riemann sum
over time to accumulate energy. Utility meters slice that total into daily,
weekly and monthly cycles that reset on their own. Per appliance, per room.</p>

<p>It is worth being clear-eyed about the accuracy. Multiplying amps by a
nominal 220 V assumes a power factor of one, which is false for anything with
a motor in it — which is to say, false for every air conditioner in the
house. The absolute numbers are wrong. The <em>comparative</em> numbers are useful,
and comparison is what actually changes behaviour: which room costs the most,
whether the fan overlap paid for itself, what happened to consumption when
the setpoint moved by one degree. Precision I do not have; direction I do.</p>

<p>The staged version adds three-phase whole-home monitoring on R, Y and B,
which is where per-appliance measurement stops being a curiosity and starts
reconciling against the utility bill.</p>

<h2 id="the-part-with-real-stakes">The part with real stakes</h2>

<p>Air conditioners are comfort. Heaters are not: a resistive element left on is
a fire, and everything else in this post is a hobby by comparison.</p>

<p>So the utility scheduler is not a scheduler at all, it is a state machine
with a physical feedback signal. Each appliance is declared with four
references — a manual control switch, an automation-enable switch, the relay
that actually carries current, and a current sensor. It runs a load timer
that caps continuous operation, a reset timer that enforces a cool-down
before the next cycle, and a tolerance threshold on the current reading.</p>

<p>That current sensor is the whole design. Without it, “off” means the system
believes it sent an off command. With it, “off” means the circuit is drawing
under a tenth of an amp. A welded relay, a stuck Tasmota, a command that
never landed — all of them look identical from the software side and
completely different from the clamp. On an overcurrent or a state mismatch,
the system cuts the relay and pushes a notification to the four phones in the
family group. Nobody has to be watching a dashboard.</p>

<p>This is the pattern I would defend most strongly to anyone building the same
thing: for any device that can hurt someone, close the loop with a physical
measurement, not with your own command history.</p>

<h2 id="what-the-soldering-iron-taught-the-architect">What the soldering iron taught the architect</h2>

<p>None of this is exotic hardware. It is a handful of cheap boards, a message
bus and a loop that asserts the same thing every ten minutes until the house
agrees. What made it worth building is that every layer punishes a different
kind of sloppiness, and it punishes you at home, where the blast radius is
one flat.</p>

<p>The firmware taught me that a protocol you cannot read is just a timing
diagram you have not been patient enough with. The topic scheme taught me
that an interface outlives the code on both sides of it, and that you should
design it as though you will never be able to change it — because on devices
sealed behind a grille, you cannot. The scheduler taught me that state you
assert beats state you announce. The current clamp taught me that trust in
your own commands is not evidence.</p>

<p>Ten minutes of wrong temperature is a survivable mistake. That is the whole
reason to practise here.</p>]]></content><author><name>Karuppuswamy Kumaresan</name><email>im@kkumaresan.com</email></author><category term="homelab" /><category term="iot" /><category term="mqtt" /><category term="architecture" /><summary type="html"><![CDATA[Boards I built by hand, C++ firmware I wrote to drive them, and a scheduler that runs every ten minutes. What the automation tier actually does, and what it taught me about designing interfaces you cannot redeploy.]]></summary></entry><entry><title type="html">The Case for a Homelab</title><link href="https://kkumaresan.com/writing/the-case-for-a-homelab/" rel="alternate" type="text/html" title="The Case for a Homelab" /><published>2026-08-06T00:00:00+05:30</published><updated>2026-08-06T00:00:00+05:30</updated><id>https://kkumaresan.com/writing/the-case-for-a-homelab</id><content type="html" xml:base="https://kkumaresan.com/writing/the-case-for-a-homelab/"><![CDATA[<p>My home network runs two fibre connections from competing ISPs, load-balanced
50/50 by a MikroTik router that also hosts the network’s DNS filter in a
container. Behind it, a Raspberry Pi runs the automation stack: Home
Assistant, an MQTT broker, a container management plane. The whole thing
rebuilds from a git repository and one bootstrap script.</p>

<p>That is more infrastructure than a household strictly needs. It is also the
cheapest architecture education I have ever given myself. The argument of
this post, the first in a series, is that a system like this belongs in more
homes and small offices than you might expect: not as a hobby, but as a
working scale model of the disciplines that matter everywhere else.</p>

<!--more-->

<h2 id="what-actually-runs-here">What actually runs here</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>     Airtel fibre              Jio fibre
          │                        │
    ether1-airtel            ether2-jio
          └───────────┬────────────┘
              PCC load balancing
           50/50, connection-sticky
                      │
             MikroTik L009UiGS
              LAN 10.1.0.1/24
                      │
       ┌──────────────┼──────────────┐
       │              │              │
  LAN bridge     Pi-hole DNS    Raspberry Pi
  wired and      (container     ├ Home Assistant
  wireless        on the        ├ Mosquitto MQTT
  clients         router)       └ Portainer
</code></pre></div></div>

<p>Three planes, each with one job.</p>

<p>The edge is the router, with both fibre links terminated on it. New
connections are split evenly across the ISPs by per-connection
classification, so a video call stays pinned to one link while bulk traffic
spreads across both. Failover uses recursive routing: each link is
continuously health-checked against well-known internet addresses, and when
the checks fail, that ISP’s routes are withdrawn and everything shifts to the
surviving link.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> check addresses      virtual next-hop     ISP gateway
 1.0.0.1, 8.8.8.8 ──▶   10.1.1.1  ──ping──▶  Airtel
 8.8.4.4, 1.1.1.1 ──▶   10.2.2.2  ──ping──▶  Jio

 pings fail → virtual hop unreachable → route withdrawn
            → traffic moves to the surviving link
</code></pre></div></div>

<p>Nobody in the house notices an ISP outage. I read about it afterwards, in
the email the router sends when an interface changes state.</p>

<p>DNS for every device flows through Pi-hole, so advertising and tracker
domains are filtered at the network layer with no per-device configuration.
The filter runs as a container on the router itself: the appliance at the
edge is also a small container host.</p>

<p>The automation plane is a Raspberry Pi running Home Assistant, a Mosquitto
MQTT broker and Portainer under Docker. Air conditioners, fans, lights and
heaters publish state over MQTT; Home Assistant schedules them and meters
energy consumption per room.</p>

<p>The toolset, by role:</p>

<ul>
  <li><strong>RouterOS / WinBox</strong> — routing, firewall and management on the MikroTik
edge; the entire configuration is scriptable and exportable.</li>
  <li><strong>Pi-hole</strong> — network-wide advertising and tracker blocking at the DNS
layer, for every device with no client configuration.</li>
  <li><strong>Unbound</strong> — recursive DNS resolution behind Pi-hole, answering from the
root servers instead of handing the query log to a public resolver.</li>
  <li><strong>Home Assistant</strong> — the automation brain: schedules, scenes and per-room
energy metering.</li>
  <li><strong>Mosquitto</strong> — the MQTT broker every sensor and switch speaks through.</li>
  <li><strong>Nginx Proxy Manager</strong> — one front door for the service UIs, with proper
names and TLS instead of a notebook of IP-and-port pairs.</li>
  <li><strong>Portainer</strong> — visibility and management for the container stack.</li>
</ul>

<p>The part I care most about is invisible in the diagram: the entire stack is
declared in a git repository. A <code class="language-plaintext highlighter-rouge">.env</code> file holds the instance-specific
values, a bootstrap script installs Docker, generates the secrets and lays
out the directory tree, and <code class="language-plaintext highlighter-rouge">docker compose up</code> does the rest. The Pi is
replaceable hardware, not a snowflake.</p>

<h2 id="why-a-household-needs-an-architecture">Why a household needs an architecture</h2>

<p>A homelab is usually defended as a learning environment. That undersells it.
It is a production system with real users, and the SLOs are enforced by your
family. When the internet drops during a school exam or the lights refuse an
automation at midnight, no framing of “it’s just a lab” survives contact
with the user base.</p>

<p>That pressure is precisely what makes it valuable. Every discipline that
matters at enterprise scale has an honest scale model here:</p>

<ul>
  <li><strong>Redundancy</strong> — two commodity fibre links and deliberate routing buy the
availability that one “business grade” link promises.</li>
  <li><strong>Failure domains</strong> — the router, the DNS path and the automation plane
fail independently, and the design has to answer for each.</li>
  <li><strong>Reproducibility</strong> — if the Pi dies, recovery is a script and a restore,
not an afternoon of archaeology.</li>
  <li><strong>Constraint budgeting</strong> — the router has 512 MB of RAM shared with its
DNS container. Capacity planning is not optional at any scale.</li>
</ul>

<p>The blast radius is a household. The lessons are not. A homelab is the
cheapest place I know to learn expensive lessons.</p>

<h2 id="the-same-pattern-small-office-sized">The same pattern, small-office sized</h2>

<p>Nothing above requires enterprise budget. Two consumer fibre plans and a
router in the price range of a mid-tier phone deliver load-balanced,
self-healing internet. For a ten-person office, a clinic or a studio, that
is business continuity at consumer prices: an ISP outage becomes a log line
instead of a lost morning.</p>

<p>The rest of the stack translates just as directly. Network-wide DNS
filtering removes a class of malware and advertising without touching a
single client device. Local automation handles scheduling and energy
metering without a cloud subscription or a vendor dependency. And because
the configuration lives in git with secrets kept out, the setup survives
the failure of any single box, including the person who built it being
unavailable, which is the failure mode small offices plan for least.</p>

<h2 id="security-has-to-be-designed-in-then-audited">Security has to be designed in, then audited</h2>

<p>A homelab is also where security stops being abstract. The perimeter here is
default-drop: nothing reaches the router from the WAN side unless a rule
explicitly allows it, and management services are reachable only from the
LAN. DNS is a control point, not just a convenience. The repository
publishes structure, never credentials: secrets are generated at bootstrap
and excluded from version control.</p>

<p>Then I did what I would do to any production system: exported the router’s
configuration and ran a cold findings review against it. Twenty findings,
three of them critical. A container password sitting in plain text in the
config export. Management services I had never explicitly restricted,
because the firewall in front of them made it feel unnecessary. A VPN server
stub configured years ago, unused, and quietly widening the attack surface.</p>

<p>Each finding is a small embarrassment and a better teacher than any
checklist. Defence-in-depth exists because the layer you trust will one day
be misconfigured. Configuration exports are secrets and deserve the same
handling. And an unused service is not neutral; it is surface. Auditing your
own work with the same coldness you would apply to a client’s is a
discipline, and a homelab gives you somewhere consequential to practise it.</p>

<h2 id="where-this-series-goes">Where this series goes</h2>

<p>This post is the overview. The ones that follow will each take one plane
apart properly: the dual-WAN design (per-connection classification, recursive
routes and the mangle table that makes failover boring), the reproducible
Raspberry Pi stack (the <code class="language-plaintext highlighter-rouge">.env</code> contract, the bootstrap script, migration and
backup), and the hardening pass that closes out the audit findings.</p>

<p>None of it is exotic hardware or heroic configuration. That is the point.
The homelab’s real output is not uptime; it is judgment, practised where the
stakes are survivable.</p>]]></content><author><name>Karuppuswamy Kumaresan</name><email>im@kkumaresan.com</email></author><category term="homelab" /><category term="architecture" /><category term="security" /><summary type="html"><![CDATA[Dual-WAN failover, filtered DNS, and an automation stack that rebuilds from one script. Why production discipline belongs at home, and in small offices.]]></summary></entry><entry><title type="html">Architecture Debt Is a Cost Model, Not a Backlog</title><link href="https://kkumaresan.com/writing/architecture-debt-is-a-cost-model/" rel="alternate" type="text/html" title="Architecture Debt Is a Cost Model, Not a Backlog" /><published>2026-08-03T00:00:00+05:30</published><updated>2026-08-03T00:00:00+05:30</updated><id>https://kkumaresan.com/writing/architecture-debt-is-a-cost-model</id><content type="html" xml:base="https://kkumaresan.com/writing/architecture-debt-is-a-cost-model/"><![CDATA[<p>Most organisations track architecture debt as a backlog: a list of deferred
refactors, each with a rough estimate, each perpetually outranked by feature
work. The framing guarantees the outcome. A backlog item competes for capacity.
A cost model competes for budget — and budget conversations happen at a level
where architecture decisions actually get made.</p>

<h2 id="what-the-backlog-framing-hides">What the backlog framing hides</h2>

<p>A deferred refactor has a carrying cost that compounds across three dimensions
at once:</p>

<ul>
  <li><strong>Infrastructure</strong> — over-provisioned services that were sized for an
architecture that no longer exists.</li>
  <li><strong>Delivery</strong> — every feature routed through the compromised boundary pays a
coordination tax.</li>
  <li><strong>Risk</strong> — the compliance and resilience surface widens quietly.</li>
</ul>

<p>None of these appear on a ticket. All of them appear on a P&amp;L.</p>

<h2 id="reframing-the-conversation">Reframing the conversation</h2>

<!--more-->

<p>When I work with leadership teams on modernisation, the first artefact is
rarely a target architecture. It is a model that attaches a monthly number to
the current one. Once the carrying cost is visible, sequencing stops being an
engineering argument and becomes an economic one.</p>

<p>That shift is what makes modernisation fundable — and what keeps it funded
after the first quarter of work stops producing visible features.</p>]]></content><author><name>Karuppuswamy Kumaresan</name><email>im@kkumaresan.com</email></author><category term="architecture" /><category term="finops" /><summary type="html"><![CDATA[Most organisations track technical debt as a list of deferred tasks. Treating it as a compounding cost model changes which decisions get funded.]]></summary></entry></feed>