<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Kishore K]]></title><description><![CDATA[kishorek.dev is a blog focused on software engineering, AI, backend development, scalable architectures, microservices, cloud, and modern developer workflows. E]]></description><link>https://blogs.kishorek.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6a00c971e3eebc2e20b385b4/2178070c-3f8c-40c6-ad99-4793fd0db5bc.png</url><title>Kishore K</title><link>https://blogs.kishorek.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 09 Sep 2026 03:36:15 GMT</lastBuildDate><atom:link href="https://blogs.kishorek.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[WebTransport vs WebSockets: The Modern Low-Latency Pipe]]></title><description><![CDATA[Picture a fast-paced multiplayer game where two things are flying between the browser and the server at once: a chat message a player just typed, and a stream of position updates telling everyone wher]]></description><link>https://blogs.kishorek.dev/webtransport-vs-websockets-the-modern-low-latency-pipe</link><guid isPermaLink="true">https://blogs.kishorek.dev/webtransport-vs-websockets-the-modern-low-latency-pipe</guid><category><![CDATA[webtransport ]]></category><category><![CDATA[websockets]]></category><category><![CDATA[http3]]></category><category><![CDATA[webdev]]></category><category><![CDATA[Real Time]]></category><category><![CDATA[Web3]]></category><dc:creator><![CDATA[Kishore K Sharma]]></dc:creator><pubDate>Wed, 08 Jul 2026 17:46:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/af687bd2-49c5-44ac-a79c-b279009b3a6d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Picture a fast-paced multiplayer game where two things are flying between the browser and the server at once: a chat message a player just typed, and a stream of position updates telling everyone where that player is standing. Now one network packet — carrying a single position update — gets dropped somewhere on the internet. On a WebSocket, that dropped packet doesn't just delay the position update. It freezes the chat message too. And the next position update. And everything else, until the lost packet is re-sent and re-delivered. A byte nobody even cares about anymore is holding the entire connection hostage.</p>
<p>That's not a bug in your code. It's baked into what a WebSocket <em>is</em>: a single TCP connection carrying one strictly-ordered stream of bytes. TCP's core promise — deliver everything, in order, no gaps — is exactly what makes it stall. WebTransport is the newer browser API that steps around that promise, and once you see the shape of the problem, you can't unsee how often WebSocket was quietly the wrong tool.</p>
<h2>What a WebSocket actually is</h2>
<p>A WebSocket feels like magic when you first meet it: <code>new WebSocket(url)</code>, an <code>onmessage</code> handler, a <code>send()</code>, and you've got a live two-way channel. That simplicity is real and it's why WebSockets have run the real-time web for a decade — chat, notifications, live dashboards, collaborative editors.</p>
<p>But under that friendly surface, a WebSocket is one <strong>TCP</strong> connection, and it inherits everything TCP believes about the world. TCP treats your data as a single ordered stream of bytes and refuses to hand byte number 500 to your application until bytes 1 through 499 have all arrived. That guarantee is wonderful for a file download, where a gap would corrupt the result. It is a straitjacket for real-time data.</p>
<p>This gives a WebSocket two hard limits that no amount of clever application code can fully escape:</p>
<ul>
<li><strong>Head-of-line blocking.</strong> Because everything shares one ordered stream, one lost packet stalls <em>everything</em> queued behind it — even logically unrelated messages — until that packet is retransmitted.</li>
<li><strong>One flavor of delivery.</strong> A WebSocket gives you exactly one channel, and it's always reliable and ordered. You have no way to tell it "this particular message is fine to drop if it's late." Everything gets the full retransmit-until-delivered treatment, whether it deserves it or not.</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/eb3fac07-0dee-4fdc-bf70-66a7c2755181.png" alt="WebSocket rides a single TCP connection with one strictly-ordered stream, so a lost packet triggers head-of-line blocking; WebTransport runs over QUIC on UDP with many independent streams plus unreliable datagrams, and a loss on one stream never stalls another." style="display:block;margin:0 auto" />

<h2>Enter WebTransport, riding on QUIC</h2>
<p>WebTransport is built on <strong>HTTP/3</strong>, which runs on <strong>QUIC</strong>, which runs on <strong>UDP</strong> instead of TCP. That swap is the whole story. UDP doesn't insist on one ordered byte-stream, so QUIC gets to reinvent delivery on its own terms — and it does two things a TCP-bound WebSocket simply can't.</p>
<p><strong>First: multiple independent streams over one connection.</strong> With WebTransport you open as many streams as you like inside a single QUIC connection, and QUIC keeps ordering <em>per stream</em> rather than across the whole connection. A lost packet on stream A only holds up stream A. Stream B, carrying completely different data, keeps flowing as if nothing happened. The head-of-line blocking that was unavoidable on TCP is now scoped to just the one stream that actually lost a packet.</p>
<p><strong>Second: unreliable datagrams.</strong> Alongside those reliable streams, WebTransport gives you datagrams — fire-and-forget messages with <em>no retransmission</em>. Send one, and if it's lost, it's gone. No re-send, no waiting, no stalling. That sounds like a downgrade until you realize how much real-time data is worthless the moment it's stale. A player's position from 80 milliseconds ago is garbage; you already have a newer one. Re-sending the old one is worse than useless — it costs latency to deliver data you'd immediately throw away. Datagrams let you say "don't bother," which is exactly the right call for that data.</p>
<h2>Streams or datagrams: pick per data type</h2>
<p>The mental model that makes WebTransport click is that you now choose the delivery guarantee <em>per piece of data</em>, instead of accepting one guarantee for the whole connection.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/c30d4697-5d54-4db3-b1c2-deeb165befd4.png" alt="Reliable ordered streams are for must-arrive data like files and chat messages; unreliable datagrams are for drop-if-late data like position updates, live media frames, and high-frequency telemetry — you pick per data type over one connection." style="display:block;margin:0 auto" />

<p>The split is refreshingly intuitive:</p>
<ul>
<li><strong>Reliable, ordered streams</strong> for anything that <em>must arrive</em>: a file or asset transfer, chat messages, a game action that changes state, any command where a gap would break correctness. This is the WebSocket-style guarantee — you just get many independent copies of it.</li>
<li><strong>Unreliable datagrams</strong> for anything that's <em>worthless once stale</em>: player position updates, live audio/video frames, cursor movements, high-frequency sensor telemetry. Miss one? The next one is already on its way and it's fresher anyway.</li>
</ul>
<p>Notice that a single game connection wants both at once — reliable streams for "player fired a weapon," datagrams for "player is standing here now." WebSocket forces all of it down the one reliable pipe. WebTransport lets each kind of data travel the way it should.</p>
<h2>The head-of-line blocking picture</h2>
<p>It's worth making the stall concrete, because it's the single clearest reason to reach for WebTransport.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/d3e23e0c-dfc3-4a93-83e7-24747f8b2cac.png" alt="On TCP and WebSocket a dropped packet stalls every multiplexed message behind it because bytes must be delivered in order; on QUIC and WebTransport only the stream that lost a packet waits, while the other streams keep flowing." style="display:block;margin:0 auto" />

<p>On the WebSocket side, packets 1 through 5 share one pipe. Packet 2 drops. Packets 3, 4, and 5 may have already physically arrived — but TCP won't release them to your app, because releasing them would mean delivering data out of order. So they sit in a buffer, waiting for a retransmit of packet 2, and your "live" connection is briefly frozen.</p>
<p>On the QUIC side, stream A loses a packet and has to wait for its retransmit — but stream B is a genuinely separate ordered sequence, so its packets sail straight through to the application. The loss is contained. On a lossy mobile network, where packet loss is routine, this is the difference between a connection that hitches constantly and one that glides.</p>
<h2>Sending data: the code</h2>
<p>WebTransport's API is lower-level than WebSocket's, and that's the honest tradeoff for the extra power. Here's a bidirectional stream plus a datagram over one connection:</p>
<pre><code class="language-js">// Open the connection (needs an HTTP/3 server on the other end).
const transport = new WebTransport("https://example.com:4433/game");
await transport.ready;

// A reliable, ordered stream — for must-arrive data.
const stream = await transport.createBidirectionalStream();
const writer = stream.writable.getWriter();
await writer.write(new TextEncoder().encode("player fired weapon"));

// Read whatever the server sends back on that stream.
const reader = stream.readable.getReader();
const { value } = await reader.read();
console.log("server said:", new TextDecoder().decode(value));

// A datagram — fire-and-forget, no retransmit. Perfect for position.
const dgram = transport.datagrams.writable.getWriter();
await dgram.write(new TextEncoder().encode(JSON.stringify({ x: 12, y: 30 })));
</code></pre>
<p>Compared with <code>ws.send("...")</code> and <code>ws.onmessage</code>, that's clearly more machinery — promises, readers, writers, byte encoding, explicit stream creation. But every extra line is buying you something a WebSocket can't offer: which stream this rides on, and whether it's reliable at all.</p>
<h2>The honest caveats</h2>
<p>WebTransport is genuinely newer, and "newer" comes with a bill.</p>
<ul>
<li><strong>It needs HTTP/3 on the server.</strong> WebTransport can't run over your existing TCP stack; the server has to speak HTTP/3 over QUIC. That's a real infrastructure requirement, not a library import.</li>
<li><strong>Browser and tooling support is still maturing.</strong> It's shipping and usable, but it isn't the decade-hardened, works-literally-everywhere baseline that WebSocket has become.</li>
<li><strong>The API is lower-level.</strong> As the code above shows, you're managing streams and byte buffers yourself. For a simple "push me a notification" use case, that's overkill, and a WebSocket is still the right, boring choice.</li>
<li><strong>You'll often keep WebSocket as a fallback.</strong> A common pattern is WebTransport where it's available and beneficial, with a WebSocket path for clients or networks that can't do HTTP/3.</li>
</ul>
<p>So this isn't "rip out every WebSocket this sprint." WebTransport earns its complexity specifically when latency and loss matter: real-time multiplayer games, low-latency live media, cloud gaming, high-frequency telemetry, anything where a stall or a stale packet is a felt problem.</p>
<h2>The one-line takeaway</h2>
<p>Here's the whole thing compressed: <strong>WebSocket gave you one ordered pipe. WebTransport gives you a bundle of independent pipes, plus a lossy express lane for data that's better dropped than delivered late.</strong> A WebSocket had to be reliable-and-ordered about everything, because TCP left it no choice. WebTransport lets you match each piece of data to the delivery it actually wants — and stops one unlucky packet from freezing the rest.</p>
]]></content:encoded></item><item><title><![CDATA[Passkeys Explained: Why Passwords Are Becoming Obsolete]]></title><description><![CDATA[Here's a sentence that should bother you more than it does: a password is a secret you have to share. You invent something only you know, and then — in order for it to be useful — you hand it to a ser]]></description><link>https://blogs.kishorek.dev/passkeys-explained-why-passwords-are-becoming-obsolete</link><guid isPermaLink="true">https://blogs.kishorek.dev/passkeys-explained-why-passwords-are-becoming-obsolete</guid><category><![CDATA[passkeys]]></category><category><![CDATA[#webauthn]]></category><category><![CDATA[Security]]></category><category><![CDATA[authentication]]></category><category><![CDATA[webdev]]></category><category><![CDATA[#fido2]]></category><dc:creator><![CDATA[Kishore K Sharma]]></dc:creator><pubDate>Wed, 08 Jul 2026 17:38:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/a93210c3-7804-4d6b-82b4-ce871eccff9a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Here's a sentence that should bother you more than it does: a password is a secret you have to share. You invent something only you know, and then — in order for it to be useful — you hand it to a server, which writes it down (hopefully hashed), stores it in a database, and trusts that you'll type it back later. The one thing a secret is supposed to do, stay secret, is the one thing a password can't do, because sharing it is the entire point.</p>
<p>That contradiction is the root cause of basically every authentication disaster you've read about. Database dumps full of password hashes. Phishing pages that look exactly like the real login. People reusing <code>hunter2</code> across forty sites so one breach unlocks their whole life. None of these are bugs in a specific app. They're all downstream of the same design flaw: <strong>the secret is shared, so it lives in too many places, and any of those places can leak or be tricked into giving it up.</strong></p>
<p>Passkeys are the fix, and the fix is almost aggressively simple: stop sending a secret. A passkey is a public/private key pair — the same asymmetric cryptography that's been securing HTTPS for decades — pointed at the problem of logging in. Your device generates the pair, keeps the private key locked away where even <em>you</em> can't easily extract it, and hands the server only the public key. The server files that public key next to your account and, from then on, never holds anything worth stealing.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/37eeecdb-2ee5-4be9-8453-ffd5c44285b2.png" alt="Left, a password: a shared secret that travels the wire and is stored (hashed) on the server, making it phishable, breachable, and reused across sites. Right, a passkey: the private key stays on the device and only the public key reaches the server, so a database dump is useless to a thief." style="display:block;margin:0 auto" />

<h2>The cast: FIDO2, WebAuthn, and where the key actually lives</h2>
<p>Two names get thrown around and it's worth pinning them down once. <strong>WebAuthn</strong> is the browser API — <code>navigator.credentials.create()</code> and <code>.get()</code> — that web pages call to make and use passkeys. <strong>FIDO2</strong> is the broader FIDO Alliance standard WebAuthn is part of, covering the protocol between the browser and the <em>authenticator</em> (the thing that actually holds the key). Together they define a system where the private key is generated and used inside hardware designed to never let it out.</p>
<p>That hardware is the important part. On your phone it's the <strong>secure enclave</strong>; on a laptop it's the <strong>TPM</strong>; on a hardware key like a YubiKey it's the key's own chip. The private key is created <em>inside</em> that chip and mathematically cannot be exported in the clear. When something needs signing, the data goes into the chip, a signature comes out, and the key itself never appears in your app's memory, your browser, or the network. That's the structural difference from a password manager, which stores a secret and then hands it back to you to transmit. A passkey's private key is never handed back to anyone.</p>
<h2>Registration: the device makes a key, keeps half of it</h2>
<p>Signing up with a passkey is a short conversation. The server (the "relying party," in spec-speak) sends the browser a <strong>challenge</strong> — a random blob — along with some info about itself, most importantly its <strong>relying party ID</strong>, which is basically its domain: <code>paypal.com</code>.</p>
<p>Your device then generates a brand-new key pair <em>specifically for that site</em>. The private key goes into the secure enclave and stays there, welded to that origin. The public key, plus a <strong>credential ID</strong> (a handle so the server knows which key to ask for later), goes back to the server. The server stores those two things against your account. That's it. There is no password to hash, no secret to guard, nothing in the database that helps an attacker who steals it.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/51d86e56-92eb-4f79-9982-2cac5c636580.png" alt="Registration flow: the server sends a challenge; the device generates a key pair bound to the site's origin, keeping the private key in its secure enclave; the public key and credential ID are returned and the server stores them, holding no secret of its own." style="display:block;margin:0 auto" />

<p>Notice what the server database looks like now. If an attacker dumps it, they get a list of public keys. Public keys are, by design, safe to publish on a billboard — they can <em>verify</em> a signature but can't <em>produce</em> one. The breach that would have been catastrophic with password hashes is a shrug with passkeys, because there's nothing there to reuse.</p>
<h2>Authentication: sign a random number, prove it's you</h2>
<p>Logging back in is a <strong>challenge-response</strong>, and it's where the whole thing clicks. The server sends a fresh random challenge. Your browser asks the device to sign it — but the private key won't budge until you perform a <strong>user gesture</strong>: Face ID, a fingerprint, or a PIN. That gesture unlocks the key locally, on your hardware; the biometric never leaves your device and never touches the server.</p>
<p>The device signs the challenge with the private key and sends the <strong>signature</strong> back. The server takes the public key it stored during registration, verifies the signature, and — if the math checks out — logs you in. At no point did a secret cross the wire. What crossed the wire was a signature over a one-time random number, useless to anyone who intercepts it, because the next login uses a different challenge.</p>
<pre><code class="language-js">// Authentication, roughly, from the browser's side
const assertion = await navigator.credentials.get({
  publicKey: {
    challenge: serverRandomBytes,      // fresh every time
    rpId: "paypal.com",                // the origin the key is bound to
    allowCredentials: [{ id: credentialId, type: "public-key" }],
    userVerification: "required",      // forces the Face ID / PIN gesture
  },
});
// assertion.response.signature -&gt; sent to the server, which verifies
// it against the stored public key. No secret is ever transmitted.
</code></pre>
<p>Two properties fall out of this for free. There's <strong>no shared secret in flight</strong>, so there's nothing to sniff. And because each challenge is random and single-use, a captured signature can't be replayed. Compare that to a password, which is the <em>same</em> string every single time — capture it once and you own it forever.</p>
<h2>Why this actually kills phishing</h2>
<p>This is the part worth tattooing on the inside of your eyelids, because it's the property passwords could never have.</p>
<p>Remember that the key was generated bound to a relying party ID — <code>paypal.com</code>. The browser enforces that binding. When a site asks for a passkey signature, the browser checks that the site's actual origin matches the <code>rpId</code> the key was created for. If you land on <code>paypa1.com</code> (that's a <em>one</em>, not an <em>L</em>) — a pixel-perfect phishing clone — and it tries to trigger your PayPal passkey, the browser looks at the origin, sees it doesn't match, and <strong>refuses to sign</strong>. Not "warns you." Refuses. There is no dialog for the user to click through, no signature produced, nothing for the fake site to steal.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/1182589c-eaf8-4f1e-aab1-aafa6b647556.png" alt="Authentication as challenge-response: the server sends a random challenge, a Face ID or PIN gesture unlocks the private key, the device signs the challenge and the server verifies it with the stored public key. Below, a phishing attempt on the lookalike origin paypa1.com fails because the credential is bound to paypal.com and the browser refuses to sign." style="display:block;margin:0 auto" />

<p>Why is this impossible with passwords? A password is just a string; <em>you</em> decide which site to type it into, and humans are terrible at reading URLs under time pressure. The security depended on a tired person correctly parsing <code>paypa1.com</code> at 11pm. Passkeys move that check from your fallible eyeballs to the browser's origin comparison, which does not get tired and does not misread a <code>1</code> for an <code>l</code>. Phishing doesn't get <em>harder</em>. For passkey logins, it stops being a thing.</p>
<h2>Platform passkeys vs. roaming keys</h2>
<p>There are two flavors, and the difference is where the key lives and how it travels.</p>
<p><strong>Platform passkeys</strong> are stored by your operating system and synced across your devices through a provider — iCloud Keychain for Apple, Google Password Manager for Android/Chrome, and others. Create a passkey on your phone and it's there on your laptop, encrypted end-to-end in transit. This is the mainstream experience, and it's the one that makes passkeys usable for normal humans: you're not tied to a single physical device.</p>
<p><strong>Roaming authenticators</strong> are hardware keys like a YubiKey. The private key never leaves the physical fob; you carry it and tap or plug it in to authenticate. Nothing syncs anywhere. This is the high-assurance option — favored by people whose threat model includes "what if my cloud account itself is compromised" — at the cost of, well, having to carry a physical object and buy a backup.</p>
<h2>The honest caveats</h2>
<p>Passkeys are genuinely better, and I'm not going to pretend they're frictionless.</p>
<p><strong>Account recovery is the hard problem.</strong> With passwords, "recovery" was a reset email — which, notice, was always a phishable backdoor that undercut your nice strong password anyway. Passkeys force the industry to build <em>real</em> recovery: a second passkey on another device, a hardware key in a drawer, or a provider-level recovery flow. Set up more than one credential from day one. A single passkey on a single device is a single point of failure.</p>
<p><strong>Cross-ecosystem syncing is still awkward.</strong> Apple's sync doesn't natively flow into Google's, and vice versa. It's improving, and QR-code cross-device sign-in (use your phone to log in on someone else's laptop) papers over a lot, but if you live half in Apple's world and half in Google's, you'll feel the seams.</p>
<p><strong>"What if I lose my device?"</strong> is the question everyone asks, and the honest answer is: if it was a synced platform passkey, it's already on your other devices and in your provider's encrypted backup — losing the phone doesn't lose the key. If it was a lone hardware key with no backup, you're locked out, the same way you'd be locked out of a safe whose only key you dropped in the ocean. The mitigation is the same either way: register more than one authenticator.</p>
<p>The through-line is this: passwords weren't a good idea that got poorly implemented. They were the <em>wrong primitive from the start</em> — a secret you're required to share, then somehow keep secret from everyone you shared it with. Passkeys don't patch that; they retire it. The server holds something public, your device holds something that never moves, and the only "secret" left in the system is a fingerprint that never travels anywhere. Once you've logged in with a glance at your phone and no password at all, the old way starts to look less like security and more like a thirty-year-long game of hoping nobody was listening.</p>
]]></content:encoded></item><item><title><![CDATA[Running AI Inside Your Browser: The Built-in AI APIs]]></title><description><![CDATA[Here's a claim that would have sounded unhinged two years ago: you can now run a language model with zero API keys, zero network calls, and zero dollars per token — from a <script> tag. No fetch to Op]]></description><link>https://blogs.kishorek.dev/running-ai-inside-your-browser-the-built-in-ai-apis</link><guid isPermaLink="true">https://blogs.kishorek.dev/running-ai-inside-your-browser-the-built-in-ai-apis</guid><category><![CDATA[AI]]></category><category><![CDATA[Browsers]]></category><category><![CDATA[browser]]></category><category><![CDATA[webdev]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Kishore K Sharma]]></dc:creator><pubDate>Wed, 08 Jul 2026 17:29:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/d4a7eb46-f0af-4430-9d66-eba85a70fc6a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Here's a claim that would have sounded unhinged two years ago: you can now run a language model with zero API keys, zero network calls, and zero dollars per token — from a <code>&lt;script&gt;</code> tag. No <code>fetch</code> to OpenAI, no server, no bill. Just a browser API that hands you a model that's already sitting on the user's machine.</p>
<p>That's the pitch of the <strong>built-in AI APIs</strong>. Chrome (and, increasingly, other browsers) ships a small language model — Google's <strong>Gemini Nano</strong> — down to the device, and exposes it through a set of JavaScript APIs. When your code calls one, the inference happens <em>locally</em>. The prompt never leaves the laptop. The response comes back without a single packet crossing the network.</p>
<p>Once you internalize that, a lot of features you'd previously have dismissed as "too expensive to run on every keystroke" or "can't, privacy" suddenly become a few lines of JavaScript. Let's look at why local inference changes the math, what the API family actually looks like, and — the part everyone skips — where it falls flat on its face.</p>
<h2>Why "on the device" is the whole story</h2>
<p>Every time you call a cloud LLM, the same chain of events plays out. Your user's text leaves their machine, travels to a server you don't own, gets run through a model, and the answer travels back. That chain buys you a genuinely powerful model. But it also buys you four bills you might not want to pay.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/eb8c11ee-f055-4f65-8961-45ed91607117.png" alt="On-device inference versus cloud inference: the cloud path sends your data across the network to a server model, costing an API bill, a network dependency, and round-trip latency; the on-device path keeps the data local, runs for free, works offline, and answers with low latency." style="display:block;margin:0 auto" />

<p>Run the model on the device and all four bills go to zero at once:</p>
<ul>
<li><strong>Privacy.</strong> The data never leaves. For a "summarize this private note" or "clean up this half-written email" feature, that's not a nice-to-have — it's the difference between shippable and a compliance meeting.</li>
<li><strong>Cost.</strong> There's no per-token meter. You can run inference on every keystroke, on every item in a list, on background tabs, and your invoice doesn't move.</li>
<li><strong>Offline.</strong> A subway, a plane, a dead conference-center Wi-Fi — the model still works, because it's already there.</li>
<li><strong>Latency.</strong> No round trip. For small tasks the answer starts appearing almost immediately, because you skipped the slowest part: the network.</li>
</ul>
<p>None of this is magic. It's just where the computation happens. Move it from a datacenter to the <code>navigator</code> and the entire cost structure of the feature flips.</p>
<h2>The API family: one model, many doors</h2>
<p>You don't talk to Gemini Nano directly. The browser wraps it in a family of APIs, and picking the right one is most of the skill.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/e4ed7b39-61d2-451f-9d22-800620c8f164.png" alt="The browser built-in AI API surface: a general Prompt API for free-form use sits over one model, alongside task-specific APIs — Summarizer, Translator, Language Detector, Writer, and Rewriter — all backed by a single on-device Gemini Nano download." style="display:block;margin:0 auto" />

<p>At the bottom is the general <strong>Prompt API</strong> — free-form, you send text (and, increasingly, images or audio) and get text back. It's the "I'll do whatever you describe" door:</p>
<pre><code class="language-js">// Always check first — availability is not guaranteed.
const availability = await LanguageModel.availability();

if (availability !== "unavailable") {
  const session = await LanguageModel.create({
    initialPrompts: [
      { role: "system", content: "You are a terse, friendly assistant." },
    ],
  });

  const reply = await session.prompt("Give me three name ideas for a coffee app.");
  console.log(reply);
}
</code></pre>
<p>On top of that sit the <strong>task-specific APIs</strong>, each a purpose-built door for one common job:</p>
<ul>
<li><strong>Summarizer</strong> — turn long text into a headline, a <code>tl;dr</code>, or bullet points.</li>
<li><strong>Translator</strong> — translate between languages, on-device.</li>
<li><strong>Language Detector</strong> — figure out <em>what</em> language a chunk of text is (the natural partner to Translator).</li>
<li><strong>Writer / Rewriter</strong> — draft new text from a prompt, or rework existing text to be shorter, longer, or more formal.</li>
</ul>
<p>The Summarizer is a good example of how little code these take:</p>
<pre><code class="language-js">const summarizer = await Summarizer.create({
  type: "tl;dr",
  format: "plain-text",
  length: "short",
});

const summary = await summarizer.summarize(longArticleText);
</code></pre>
<p>Why bother with the narrow APIs when the Prompt API can technically do all of it? Because they're tuned for their job, their options are structured instead of buried in prompt phrasing, and they give the browser room to optimize a known task. Rule of thumb: <strong>reach for the specific API when one fits, and drop to the Prompt API only when nothing else does.</strong></p>
<h2>The honest part: it's small, and it's not always there</h2>
<p>Now the caveats, because a post that only lists upsides is marketing, not engineering.</p>
<p><strong>The model is small.</strong> Gemini Nano is measured in a couple of gigabytes, not the hundreds you'd need for a frontier model. It is nowhere near GPT-class on reasoning, nuance, world knowledge, or long, multi-step problems. Ask it to summarize a paragraph and it shines. Ask it to debug a subtle race condition across five files, reason through a legal argument, or write a novel chapter that holds together, and you'll feel the ceiling immediately. This is a small model doing small-model things well — not a pocket-sized genius.</p>
<p><strong>There's a one-time download.</strong> The model isn't shipped with the browser binary; it's fetched on demand, and it's big enough that you cannot pretend the download is instant. The very first time a user hits your feature, the model may still be arriving. Which leads to the caveat that actually shows up in your code:</p>
<p><strong>Availability is gated, experimental, and must be feature-detected.</strong> These APIs are new, partly behind origin trials or flags, and only present on capable hardware. So you never assume they exist. You check — and you specifically handle the "still downloading" state, because it's a real state a real user will land in:</p>
<pre><code class="language-js">const availability = await LanguageModel.availability();

switch (availability) {
  case "unavailable":
    // No model on this device — fall back to a cloud call or hide the feature.
    useCloudFallback();
    break;
  case "downloadable":
  case "downloading":
    // Model isn't ready yet. Show progress; don't block the UI on it.
    const session = await LanguageModel.create({
      monitor(m) {
        m.addEventListener("downloadprogress", (e) =&gt; {
          console.log(`Downloading model: ${Math.round(e.loaded * 100)}%`);
        });
      },
    });
    break;
  case "available":
    // Ready to go right now.
    break;
}
</code></pre>
<p>Feature-detect, handle "unavailable," handle "downloading." If you skip that, your feature works beautifully on your machine and throws on a stranger's. That's not a footnote; it's the difference between a demo and a product.</p>
<h2>So when do you actually use it?</h2>
<p>The framing that makes all of this click: <strong>on-device AI is not a replacement for cloud AI. It's a new tier.</strong> You don't pick one forever — you route each task to the tier that fits it.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/ecc9c643-ad6b-4d21-b3f1-1a8cd0441225.png" alt="When to reach for on-device versus cloud AI: on-device fits private, offline, high-volume, simple, and cost-sensitive tasks; cloud fits hard reasoning, large context, top-quality output, and complex or novel work." style="display:block;margin:0 auto" />

<p>Reach for <strong>on-device</strong> when the task is:</p>
<ul>
<li><strong>Private or sensitive</strong> — the data shouldn't leave the machine.</li>
<li><strong>Offline-capable</strong> — it has to keep working with no connection.</li>
<li><strong>High-volume</strong> — you're running it constantly and a per-token bill would hurt.</li>
<li><strong>Simple and bounded</strong> — classify, tag, detect language, summarize a paragraph, tidy a sentence.</li>
</ul>
<p>Reach for the <strong>cloud</strong> when you need what a small model can't give you: deep reasoning, a large context window, genuinely high-quality long-form output, or anything complex and novel. An agent that plans across many steps, an analysis that has to be <em>right</em>, a code-generation task — that's still cloud territory, and pretending otherwise just ships a worse feature.</p>
<p>The best designs use both. On-device handles the constant, cheap, private stuff — instant language detection, an offline "summarize this page" button, a rewrite-my-sentence helper that fires on every edit — while a cloud call is reserved for the moments the user explicitly asks for heavy lifting. You get the responsiveness and privacy of local inference for the 90% of tasks that are easy, and the raw capability of a big model for the 10% that aren't.</p>
<h2>The browser as an AI runtime</h2>
<p>Step back and the trend line is hard to miss. The browser used to be a client: it rendered your UI and forwarded requests to servers that did the real work. Then it got a GPU (WebGPU), real threads, a filesystem, and now a language model baked into the platform, callable from plain JavaScript.</p>
<p>That last one is a quiet turning point. Inference — the thing we've spent two years assuming lives in someone else's datacenter behind an API key — is becoming a <em>local capability</em>, as ordinary to reach for as <code>localStorage</code> or <code>fetch</code>. It won't be the tier you use for everything, and it shouldn't be. But for the enormous class of small, private, high-frequency tasks, the model is now sitting right there on the device, free and offline and instant.</p>
<p>The browser is quietly becoming an AI runtime, not just a client. Worth building for.</p>
]]></content:encoded></item><item><title><![CDATA[The HTTP QUERY Method: A GET That's Allowed to Have a Body]]></title><description><![CDATA[Every search endpoint you've ever built is lying about what it does. Type a query into a search box, hit enter, and the browser fires a POST /search. POST. The verb that means create a thing, change t]]></description><link>https://blogs.kishorek.dev/the-http-query-method-a-get-that-s-allowed-to-have-a-body</link><guid isPermaLink="true">https://blogs.kishorek.dev/the-http-query-method-a-get-that-s-allowed-to-have-a-body</guid><category><![CDATA[http]]></category><category><![CDATA[api]]></category><category><![CDATA[webdev]]></category><category><![CDATA[backend]]></category><category><![CDATA[backend developments]]></category><category><![CDATA[REST API]]></category><category><![CDATA[http query method, query vs get vs post, safe idempotent http, cacheable query, api design, rest search endpoint]]></category><category><![CDATA[http-query-method]]></category><dc:creator><![CDATA[Kishore K Sharma]]></dc:creator><pubDate>Wed, 08 Jul 2026 04:09:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/f2211a40-0298-4910-956c-8e72e07a7c0e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every search endpoint you've ever built is lying about what it does. Type a query into a search box, hit enter, and the browser fires a <code>POST /search</code>. <code>POST</code>. The verb that means <em>create a thing, change the world, do it once and be careful</em>. You didn't create anything. You asked a question. You could ask it a thousand times and the answer would be the same each time. But the request on the wire says <code>POST</code>, and every cache, proxy, and retry policy between you and the database now treats a harmless read as a dangerous write.</p>
<p>This isn't a bug in your code. It's a thirty-year-old gap in HTTP itself, and you've been papering over it your whole career without noticing. The method that <em>should</em> carry a search — <code>GET</code> — isn't allowed to have a request body. And the method that's allowed to have a body — <code>POST</code> — carries all the wrong semantics. So we pick <code>POST</code>, shrug, and give up caching and idempotency as the price of being able to send a real query.</p>
<p>QUERY is the proposed HTTP method that closes the gap. It's the one you'd have designed if you were building HTTP today: safe and idempotent like <code>GET</code>, but with a request body like <code>POST</code>. It is currently an <a href="https://datatracker.ietf.org/doc/draft-ietf-httpbis-safe-method-w-body/">IETF Internet-Draft</a> adopted by the HTTP working group — not yet a finished RFC, but far enough along that it's worth understanding now, because it renames a problem you already have.</p>
<h2>Why GET can't do the job</h2>
<p>The instinct is: "just put the query in the URL." And for simple cases, <code>GET /search?q=cats&amp;sort=new</code> is perfect. That's exactly what <code>GET</code> is for. The trouble starts the moment your query stops being a couple of key-value pairs.</p>
<p>Real search is structured. A faceted product search, a GraphQL-style selection, an Elasticsearch bool query with nested filters, a geospatial polygon — these are JSON objects with arrays and nesting, sometimes kilobytes of them. You have three bad options for cramming that into a URL:</p>
<ul>
<li><strong>Query string.</strong> URL-encode a whole JSON blob into <code>?filter=%7B%22...</code>. It works until it hits the wall — and there is a wall. Browsers, proxies, and servers all cap URL length somewhere around 2,000–8,000 characters, and none of them agree on where. Cross that invisible line and you get a <code>414 URI Too Long</code>, sometimes only from <em>one</em> proxy in the chain, sometimes only in production.</li>
<li><strong>The URL leaks.</strong> Everything in the URL gets logged. Access logs, proxy logs, browser history, the <code>Referer</code> header sent to the next site, analytics. Put a customer's search terms — or worse, an auth token someone stuffed in a query param — in the URL and you've quietly written it to a dozen places you don't control.</li>
<li><strong>Encoding pain.</strong> Deeply nested state doesn't map cleanly to flat <code>key=value</code> pairs. You end up inventing a bracket convention (<code>filter[price][gte]=10</code>) that every client and server has to agree on and parse by hand.</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/c907e36a-6b5c-4a2a-a502-2caee3136f74.png" alt="Three ways to send a complex query: cramming JSON into the GET query string hits the URL length wall and leaks into logs; POST carries the body fine but is neither safe nor cacheable; QUERY carries the body and stays safe, idempotent, and cacheable." style="display:block;margin:0 auto" /> 

<p>The obvious fix — "let GET have a body" — is a dead end, and not for a good reason, just an old one. <a href="https://www.rfc-editor.org/rfc/rfc9110">RFC 9110</a> technically permits a body on a <code>GET</code>, but immediately warns that doing so has no defined meaning and will break things: caches, proxies, and libraries throughout the ecosystem were built assuming <code>GET</code> bodies don't exist, and many silently drop them. So a <code>GET</code> body is a trap. Which is how everyone ends up at <code>POST</code>.</p>
<h2>What POST actually costs you</h2>
<p><code>POST</code> works. The body goes through. That's the whole appeal, and it's also where the honesty ends.</p>
<p>The word "safe" in HTTP is a technical term with teeth. A <strong>safe</strong> method is one the spec promises has no side effects the client is responsible for — a read, essentially. <code>GET</code>, <code>HEAD</code>, and <code>OPTIONS</code> are safe. A crawler will happily fire safe requests all day because it knows it isn't changing anything. <strong>Idempotent</strong> means making the same request twice has the same effect as making it once — so a client, proxy, or load balancer can safely <em>retry</em> it after a timeout without fear of doing the thing twice.</p>
<p><code>POST</code> is neither. It's the method that specifically means "this might change something, and repeating it might change something <em>again</em>." So the entire infrastructure that speaks HTTP treats your <code>POST /search</code> accordingly:</p>
<ul>
<li><strong>Caches refuse it.</strong> A <code>POST</code> response is essentially uncacheable by default. Your identical search from a thousand users can't be served from a shared cache, because the cache has no way to know the request was harmless.</li>
<li><strong>Retries are dangerous.</strong> A proxy or client library will retry a timed-out <code>GET</code> automatically. It won't retry a <code>POST</code>, because it can't tell your safe search from an unsafe "charge the card." So a blip becomes a user-facing error instead of a transparent retry.</li>
<li><strong>The semantics lie.</strong> Anyone reading your access logs, your API, or your traces sees <code>POST /search</code> and has to <em>know</em>, out of band, that it's actually a read. The protocol is no longer telling the truth about your system.</li>
</ul>
<p>You didn't choose any of that. You chose it by elimination, because <code>POST</code> was the only method left standing that could carry the body.</p>
<h2>What QUERY changes</h2>
<p>QUERY is deliberately boring, and that's the point. Take everything good about <code>GET</code> and add the one thing it lacks.</p>
<p>A QUERY request is <strong>safe</strong> and <strong>idempotent</strong> — the spec says so, the same way it says <code>GET</code> is. So every retry policy, every crawler, every cache that special-cases safe methods can treat QUERY exactly like a read, because it <em>is</em> one. And a QUERY request carries a <strong>body</strong>, with a <code>Content-Type</code> that describes it. Your query is <code>application/json</code>, or a GraphQL document, or an SQL-ish DSL — whatever you want — sent as content, not smuggled into a URL.</p>
<pre><code class="language-http">QUERY /products HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json

{
  "filter": {
    "category": "keyboards",
    "price": { "gte": 40, "lte": 150 },
    "tags": { "any": ["mechanical", "wireless"] }
  },
  "sort": [{ "field": "price", "order": "asc" }],
  "limit": 50
}
</code></pre>
<p>No length ceiling. Nothing sensitive in the URL, so nothing sensitive in your access logs or browser history. No bracket-encoding convention to invent. And the method itself announces, to every piece of software on the path, that this is a safe, repeatable read.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/8e0f39f8-b0e3-455b-b470-4fc812f99465.png" alt="The same faceted product search sent two ways: as POST it is unsafe, non-idempotent, and uncacheable; as QUERY it is safe, idempotent, and cacheable, with the body untouched." style="display:block;margin:0 auto" />

<h2>The clever part: caching a request that has a body</h2>
<p>Here's the piece that makes QUERY more than "POST with better manners," and it's the detail worth remembering.</p>
<p>A normal cache keys on the method and the URL. That's <em>why</em> <code>GET</code> is cacheable and <code>POST</code> isn't — a <code>GET</code>'s full input lives in the URL, so the URL is a complete cache key. But a QUERY's input is in the <em>body</em>. Two QUERY requests to the same <code>/products</code> URL can ask completely different questions. So a cache that ignored the body would be catastrophically wrong: it'd serve one user's search results to another user's totally different search.</p>
<p>QUERY's answer is that the <strong>request body becomes part of the cache key</strong>. A cache stores the response against the combination of method, URI, <em>and</em> the query content. Fire the identical QUERY again — same URL, same body — and a shared cache can return the stored result without ever touching your database. Change one filter and it's a different key, so you get a fresh result. You get <code>GET</code>-style caching for requests whose input was far too big and complex to ever fit in a <code>GET</code>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/d6010e6b-b6fa-4b44-b1f5-b3c1f26c36ed.png" alt="How a cache stores a QUERY: the key is method plus URI plus the request body, so identical queries hit the cache and a changed body misses it and reaches the origin." style="display:block;margin:0 auto" />

<p>The draft also lets the server point at the results with a <code>Content-Location</code> response header — effectively saying "the answer to that query also lives at this stable URL" — so a follow-up plain <code>GET</code> can fetch the same result the normal, fully-cacheable way. It's the bridge back to classic HTTP caching for the results themselves.</p>
<h2>Will it replace POST? No — and that's the right question</h2>
<p>QUERY replaces exactly one thing: the <code>POST</code> you use when you aren't posting. Search, filtered list endpoints, complex reporting reads, GraphQL-over-HTTP queries, any "give me data based on this complicated criteria" call. Everywhere your <code>POST</code> is a read wearing a write's coat, QUERY is the honest version.</p>
<p>It does <strong>not</strong> touch the <code>POST</code> that actually creates and mutates. Placing an order, sending a message, uploading a file — those have side effects, they're not idempotent, and <code>POST</code> is exactly right for them. Nothing about QUERY changes that. The line is simple: <strong>is this request a read?</strong> If yes, QUERY is what you were reaching for. If it changes state, it stays <code>POST</code>.</p>
<p>The honest caveat: it's a draft. Client libraries, server frameworks, proxies, and CDNs are only starting to grow first-class support, and until the whole path understands the method, an intermediary that doesn't recognize QUERY may reject or mishandle it. So this isn't "rewrite your search endpoints this sprint." It's "understand the shape of the fix, watch the draft, and know exactly which of your <code>POST</code>s are lying." Because once you see it, you can't unsee it: half the <code>POST</code>s in your codebase were always secretly a <code>GET</code> that just needed somewhere to put its body.</p>
]]></content:encoded></item><item><title><![CDATA[Put a Login on Swagger and Actuator (Before Someone Else Does)]]></title><description><![CDATA[Two endpoints ship with your Spring Boot app that you never wrote and probably stopped thinking about months ago. One hands any visitor a complete, machine-readable map of your entire API. The other w]]></description><link>https://blogs.kishorek.dev/secure-swagger-actuator-spring-boot</link><guid isPermaLink="true">https://blogs.kishorek.dev/secure-swagger-actuator-spring-boot</guid><category><![CDATA[Spring Boot]]></category><category><![CDATA[Security]]></category><category><![CDATA[Java]]></category><category><![CDATA[spring security]]></category><category><![CDATA[backend]]></category><dc:creator><![CDATA[Kishore K Sharma]]></dc:creator><pubDate>Thu, 25 Jun 2026 20:22:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/f7fd4028-b337-4861-ab5b-6ab2f2d49279.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Two endpoints ship with your Spring Boot app that you never wrote and probably stopped thinking about months ago. One hands any visitor a complete, machine-readable map of your entire API. The other will, on request, send them a copy of your application's memory — tokens, passwords, connection strings and all. They're called Swagger and Actuator, you almost certainly turned them on for a good reason in development, and the unpleasant part is that "development" and "production" share the same config more often than anyone admits.</p>
<p>Neither is a bug. Both are features you opted into. The mistake is leaving them standing open to the internet because they were open on your laptop and nothing ever yelled at you to close them.</p>
<p>Let's look at what's actually behind each door, then lock them properly — not with one flag, but in layers.</p>
<h2>What you're actually exposing</h2>
<img src="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/7695edef-5785-45bb-9edb-a0a5defbc78a.png" alt="Swagger UI / api-docs hands out your full API surface and schemas; Actuator exposes env, beans, heapdump, loggers and shutdown — config, secrets, and control." style="display:block;margin:0 auto" />

<p><strong>Swagger / OpenAPI.</strong> <code>springdoc</code> serves <code>/swagger-ui/index.html</code> for humans and <code>/v3/api-docs</code> as raw JSON for machines. That JSON is the whole thing: every route, every method, every parameter, every request and response schema. For a legitimate consumer it's documentation. For an attacker it's reconnaissance they didn't have to do — your undocumented internal endpoints, your admin routes, your "we'll secure that later" controller, all neatly listed.</p>
<p><strong>Actuator.</strong> This one's worse, because some of its endpoints don't just <em>describe</em> the app, they <em>operate</em> it. A quick tour of the sharp ones:</p>
<ul>
<li><code>/actuator/env</code> and <code>/configprops</code> — your configuration, including a lot of things that were never meant to leave the server.</li>
<li><code>/actuator/beans</code> and <code>/mappings</code> — your whole bean graph and URL map, i.e. the internal architecture.</li>
<li><code>/actuator/heapdump</code> — downloads a full heap dump. Whatever secrets were sitting in memory are now a file on the attacker's disk.</li>
<li><code>/actuator/loggers</code> — lets you change log levels at runtime via POST. Crank a package to <code>DEBUG</code> and watch the secrets scroll.</li>
<li><code>/actuator/shutdown</code> — exactly what it says. Disabled by default, but people enable it and forget.</li>
</ul>
<p>Good news first: by default Boot only exposes <code>health</code> over HTTP, and <code>env</code> sanitizes obvious keys. The danger is the line that's in half the tutorials on the internet:</p>
<pre><code class="language-yaml">management:
  endpoints:
    web:
      exposure:
        include: "*"   # &lt;- exposes every actuator endpoint. great in a demo, a gift in prod.
</code></pre>
<p>Ship that, leave it unauthenticated, and every endpoint above is one <code>curl</code> away.</p>
<h2>Lock it in layers, not with one switch</h2>
<p>The instinct is to find the single "secure it" setting. There isn't one, and that's fine — defense in depth means each layer assumes the one above it failed.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/a5be0681-fe8d-45ff-9056-61f262bc091b.png" alt="Four layers: expose only what you need, authenticate, authorize with ROLE_ADMIN, and isolate onto a separate management port." style="display:block;margin:0 auto" />

<h3>Layer 1 — expose less</h3>
<p>The cheapest fix is the one you skip: don't publish what you don't need. Pin the actuator exposure list to the endpoints you actually use, and never <code>*</code> in production.</p>
<pre><code class="language-yaml">management:
  endpoints:
    web:
      exposure:
        include: health,info     # not "*"
  endpoint:
    health:
      show-details: when-authorized   # full health only after login; anonymous sees just UP/DOWN
</code></pre>
<p>And Swagger genuinely does not need to exist in production for most apps. Turn it off per profile:</p>
<pre><code class="language-yaml"># application-prod.yml
springdoc:
  api-docs:
    enabled: false
  swagger-ui:
    enabled: false
</code></pre>
<p>If you do keep it in prod — internal tools, a partner API — then it has to get the same auth as everything else below. An endpoint that doesn't exist can't be attacked; that's always the strongest version of "secured."</p>
<h3>Layers 2 and 3 — authenticate, then authorize</h3>
<p>Add <code>spring-boot-starter-security</code>, and the framework gives you a login out of the box. But "logged in" is not the bar for these endpoints — <em>admin</em> is. The distinction matters: a regular user account that gets phished shouldn't come with a heap dump button.</p>
<p>Spring Security's <code>EndpointRequest</code> matchers know about actuator, so you don't hand-write the paths:</p>
<pre><code class="language-java">@Bean
SecurityFilterChain security(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -&gt; auth
            // health + info stay open for load balancers and uptime checks
            .requestMatchers(EndpointRequest.to(HealthEndpoint.class, InfoEndpoint.class)).permitAll()
            // every OTHER actuator endpoint: admins only
            .requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole("ADMIN")
            // Swagger UI + the raw OpenAPI doc: admins only
            .requestMatchers("/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**").hasRole("ADMIN")
            // your real application rules
            .anyRequest().authenticated()
        )
        .httpBasic(Customizer.withDefaults())   // or formLogin for a browser UI
        // actuator POSTs (e.g. /loggers) are not browser forms — exempt them from CSRF
        .csrf(csrf -&gt; csrf.ignoringRequestMatchers(EndpointRequest.toAnyEndpoint()));
    return http.build();
}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/ffa70efc-6967-4916-b7e7-1b256e7e13f7.png" alt="The filter chain: health permitAll, toAnyEndpoint hasRole ADMIN, swagger paths hasRole ADMIN, anyRequest your app rules — most-specific matcher first." style="display:block;margin:0 auto" />

<p>One thing the diagram is quietly insisting on: <strong>order matters.</strong> Spring Security takes the first matcher that matches, so the narrow <code>health</code> rule has to come <em>before</em> the broad <code>toAnyEndpoint()</code> — flip them and <code>toAnyEndpoint()</code> swallows health and your load balancer starts getting 401s. Most-specific first, every time.</p>
<p>And the credentials behind <code>ROLE_ADMIN</code> have to be real. Not the random password Boot prints to the console on startup, not <code>user</code> / <code>password</code> in a properties file. A proper user — from your database or directory — with a <strong>BCrypt-hashed</strong> secret:</p>
<pre><code class="language-java">@Bean
PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}
</code></pre>
<h3>Layer 4 — isolate the port</h3>
<p>The last layer is network, not code. Move actuator off your public application port entirely:</p>
<pre><code class="language-yaml">management:
  server:
    port: 9001          # actuator lives here...
    address: 127.0.0.1  # ...and only answers on loopback / your internal net
</code></pre>
<p>Now even a misconfigured rule upstream doesn't help an outside attacker, because the port they can reach doesn't serve actuator at all. Your monitoring, which lives on the same box or inside the same network, still gets in. This pairs naturally with a firewall or security group that simply never routes <code>9001</code> to the outside world.</p>
<h2>The short version</h2>
<p>Swagger and Actuator aren't dangerous because they're insecure. They're dangerous because they're <em>useful</em>, which is exactly why you turned them on and then stopped seeing them. Treat them like any other privileged surface:</p>
<ul>
<li><strong>Expose less</strong> — pin the actuator list, and turn Swagger off in prod unless you have a reason not to.</li>
<li><strong>Authenticate</strong> — put Spring Security in front of both.</li>
<li><strong>Authorize</strong> — <code>hasRole("ADMIN")</code>, not merely "logged in," with real BCrypt-backed credentials.</li>
<li><strong>Isolate</strong> — separate management port, bound to the internal network.</li>
</ul>
<p>No single one of these is the answer. Stacked, they mean that the day one layer is misconfigured — and someday one will be — the other three are still standing between a stranger and a copy of your app's memory.</p>
]]></content:encoded></item><item><title><![CDATA[HTTP Security Headers: The Cheapest Security Your API Will Ever Ship]]></title><description><![CDATA[Some of the strongest security on your API isn't in your code at all. It's in a handful of headers — a few lines you attach to every response, which the browser then enforces on your behalf, for free,]]></description><link>https://blogs.kishorek.dev/http-security-headers-the-cheapest-security-your-api-will-ever-ship</link><guid isPermaLink="true">https://blogs.kishorek.dev/http-security-headers-the-cheapest-security-your-api-will-ever-ship</guid><category><![CDATA[Security]]></category><category><![CDATA[http]]></category><category><![CDATA[webdev]]></category><category><![CDATA[backend]]></category><category><![CDATA[api]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Kishore K Sharma]]></dc:creator><pubDate>Thu, 25 Jun 2026 20:17:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/04ac5754-f424-4af6-94ed-43c56ebc800a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Some of the strongest security on your API isn't in your code at all. It's in a handful of headers — a few lines you attach to every response, which the browser then enforces on your behalf, for free, on hardware you'll never see. Skip them and nothing breaks. The app works fine in the demo, ships fine, runs fine for a year. That's exactly why they're so easy to never set.</p>
<p>There are two halves to this, and most people only ever think about one. Response headers are orders you give the browser. Request headers are claims the client makes about itself — and the client can lie about every single one. Get that trust direction backwards and you end up building an auth check on top of a field anyone can forge with a one-line <code>curl</code>.</p>
<p>Here's the whole picture, then the specifics.</p>
<h2>Two kinds of header, two levels of trust</h2>
<p>A request header is set by whoever made the request. Your frontend, a browser, <code>curl</code>, a scraper, an attacker with a terminal — all equal, all anonymous. <code>User-Agent</code>, <code>Host</code>, <code>Origin</code>, <code>X-Forwarded-For</code>: every one of them is free text the sender chose to type. Treat them as evidence to verify, never as proof of anything.</p>
<p>A response header is set by you, the server. The interesting ones are instructions the browser is more or less contractually obliged to follow: only ever talk to me over HTTPS, refuse to run inline script, don't let this page be put in a frame. You write one line; the browser enforces it for every user who visits.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/8a917199-85d8-416d-b6b5-7899946bbea5.png" alt="Request headers are set by the client and can all be forged, so they're evidence to verify; response headers are set by the server and the browser obeys them." style="display:block;margin:0 auto" />

<p>So the rule has two sides and they're mirror images. Outbound: send the headers that switch on the browser's built-in defenses. Inbound: assume every header is hostile until you've checked the thing it claims.</p>
<h2>The defensive set</h2>
<p>These are the response headers worth attaching to basically everything. None of them cost you anything at runtime, and you set them in one place, not per route.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/0fddb835-929d-456b-8aff-50da5f1a3a3b.png" alt="The defensive set: HSTS, CSP, X-Content-Type-Options, frame-ancestors, Referrer-Policy, Permissions-Policy, and dropping Server / X-Powered-By — each mapped to the attack it stops." style="display:block;margin:0 auto" />

<p><strong>Strict-Transport-Security.</strong> HSTS. <code>max-age=63072000; includeSubDomains; preload</code>. It tells the browser "for the next two years, only ever reach me over HTTPS" — so the downgrade trick, where someone on the network quietly answers your first plain-HTTP request before the redirect kicks in, stops working. It's only honored over HTTPS in the first place. The <code>preload</code> flag opts you into a list baked straight into browsers, but it's sticky and a pain to back out of, so don't add it until you mean it.</p>
<p><strong>Content-Security-Policy.</strong> The big one, and the fiddly one. CSP tells the browser which sources of script, style, images and so on it's allowed to load and run — so even if an attacker manages to inject a <code>&lt;script&gt;</code> into your page, the browser simply refuses to execute it. <code>default-src 'self'</code> is the strict starting point. Roll it out with <code>Content-Security-Policy-Report-Only</code> first, so you get reports of what it <em>would</em> block without actually breaking your own app. This is the header that neuters most XSS, which is why it's worth the tuning it asks for.</p>
<p><strong>X-Content-Type-Options: nosniff.</strong> One value, no options. It stops the browser second-guessing your <code>Content-Type</code> and "helpfully" deciding that the thing you served as plain text is actually executable script. Cheap to set, closes a whole class of MIME-confusion bugs.</p>
<p><strong>X-Frame-Options / frame-ancestors.</strong> Clickjacking defense — it stops other sites loading your page inside an invisible iframe and tricking your logged-in users into clicking things they can't see. <code>X-Frame-Options: DENY</code> is the old header; CSP's <code>frame-ancestors 'none'</code> is the modern replacement. Set both for now — old browsers only understand the first.</p>
<p><strong>Referrer-Policy.</strong> Controls how much of your URL leaks in the <code>Referer</code> header when a user clicks a link out to another site. <code>strict-origin-when-cross-origin</code> is a sane default; reach for <code>no-referrer</code> if your URLs carry anything sensitive (tokens in query strings — which you shouldn't have, but here we all are).</p>
<p><strong>Permissions-Policy.</strong> Switches off browser features you don't use. <code>geolocation=(), camera=(), microphone=()</code> says "nothing on this origin may even ask for these" — so an injected script can't quietly try, either.</p>
<p>And one in reverse: <strong>stop advertising your stack.</strong> Drop <code>Server</code> and <code>X-Powered-By</code>. They won't get you owned on their own, but they hand any passing scanner your framework and version for free, which is the first step of every "find a known CVE for this exact version" script. In Express it's one line: <code>app.disable("x-powered-by")</code>.</p>
<p>You don't hand-set all of this on every route. Put it in one place — middleware, or better, the reverse proxy / CDN at the edge — so it's on by default and can't be forgotten. In Express, Helmet does most of the set for you:</p>
<pre><code class="language-js">import helmet from "helmet";
app.use(helmet());        // HSTS, nosniff, frame, referrer, a baseline CSP, and more
app.disable("x-powered-by");
</code></pre>
<p>Then check your work from the outside. Point securityheaders.com or Mozilla Observatory at a live URL and it'll grade the lot in seconds and tell you what's missing.</p>
<h2>The headers you must never trust</h2>
<p>Now the inbound side, where the bugs are quieter and a lot meaner. Every item here is a request header someone can set to whatever they like.</p>
<p><strong>X-Forwarded-For.</strong> The classic. Behind a proxy, the client's real IP gets put here, so people reach for it to rate-limit, geo-block, or allowlist. The problem: the client can simply <em>send their own</em> <code>X-Forwarded-For</code>, and now your "trusted IP" is whatever string they pasted in. The only value you can trust is the one your <em>own</em> proxy appended — which means telling your framework exactly how many proxies sit in front of you and reading only that hop.</p>
<pre><code class="language-js">// You run exactly one proxy you control (your load balancer).
// Trust that one hop — no more, no less.
app.set("trust proxy", 1);
// Now req.ip is the value your proxy vouched for, not whatever the client claimed.
</code></pre>
<p>Trust the whole header blindly and your IP allowlist becomes a polite suggestion.</p>
<p><strong>Host.</strong> A real browser sets it honestly; an attacker is under no such obligation. If you build absolute URLs out of the <code>Host</code> header — password-reset links are the textbook case — someone can send a forged <code>Host</code>, and the reset email you generate now points your victim at the attacker's server. Validate <code>Host</code> against an allowlist of domains you actually serve, and never splice it straight into a link.</p>
<p><strong>Origin and Referer.</strong> Useful <em>signals</em>, not authentication. They tell you where a browser thinks a request came from, which is handy as one input to a CSRF check — but they can be absent, and outside a browser they're whatever the sender wants. Use them as a hint, never as the lock.</p>
<p><strong>Content-Type.</strong> A claim about the body, not a guarantee about it. Don't let it decide how far you trust the payload, and don't lean on it as CSRF protection — "we only accept <code>application/json</code>" falls over the second someone sends <code>text/plain</code> with a JSON body in it.</p>
<p>The thread through all of these: a request header is the sender describing themselves. Verify the <em>thing</em> it claims — the token's signature, the IP your proxy vouched for, the domain on your allowlist — and never trust the claim on its own.</p>
<h2>The two CORS lines that quietly open the door</h2>
<p>CORS gets its own section, because it's the one people most often "fix" straight into a hole. The setup: a browser won't let <code>evil.com</code>'s JavaScript read a response from <code>your-api.com</code> unless your API explicitly says it's allowed, via <code>Access-Control-Allow-Origin</code>.</p>
<p>Here's the bug nearly everyone ships at least once:</p>
<pre><code>Access-Control-Allow-Origin: &lt;reflects whatever Origin the request sent&gt;
Access-Control-Allow-Credentials: true
</code></pre>
<p>Reflecting the request's <code>Origin</code> straight back means <em>every</em> site is allowed. Add <code>Allow-Credentials: true</code> on top and you've also told the browser to send cookies — so any malicious page your logged-in user happens to visit can now make authenticated calls to your API and read the answers. You didn't open a door; you took down the wall.</p>
<p>The fix is an explicit allowlist, and only ever echoing an origin that's on it:</p>
<pre><code class="language-js">const ALLOWED = new Set(["https://app.example.com"]);

app.use((req, res, next) =&gt; {
  const origin = req.headers.origin;
  if (ALLOWED.has(origin)) {
    res.setHeader("Access-Control-Allow-Origin", origin);
    res.setHeader("Access-Control-Allow-Credentials", "true");
    res.setHeader("Vary", "Origin"); // so a cache can't serve one origin's header to another
  }
  next();
});
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/df11afbd-45f7-48d9-9651-2499671f3772.png" alt="The CORS hole: reflecting any Origin plus Allow-Credentials true lets any site read authenticated responses. The fix: echo only allowlisted origins, add Vary, and keep real authorization in the handler." style="display:block;margin:0 auto" />

<p>And the thing CORS is <em>not</em>: it is not server-side access control. All it governs is whether a browser hands the response back to the page's script. The request still arrived, your handler still ran, your database was still queried — <code>curl</code> ignores CORS entirely. So CORS protects your users' browsers from leaking data cross-origin; it does nothing to stop a direct attacker hitting your endpoint. Your real authorization has to live in the handler, every time.</p>
<h2>"But mine's a pure JSON API"</h2>
<p>Fair, and worth being honest about. A couple of these — CSP, X-Frame-Options — really only bite when a browser renders your response as a page. If your API exclusively returns JSON to server-side clients that no browser ever touches, those two are belt-and-suspenders.</p>
<p>But HSTS still matters the moment anyone reaches you over HTTP. <code>nosniff</code> still matters. CORS absolutely still matters as soon as any browser talks to you. And <em>every</em> request-header trap above applies no matter who's calling, browser or not. So the inbound rules aren't optional just because you skipped the HTML — those are the ones that quietly cost you the most.</p>
<h2>Ship it</h2>
<p>Put the response set in one place — middleware or the edge — so it's on by default and can't be dropped per route. Send HSTS, nosniff, a real CSP (report-only first), frame protection, Referrer-Policy and Permissions-Policy, and strip the stack-advertising headers. On the way in: pick a trusted-proxy count and read only that hop, validate <code>Host</code>, treat <code>Origin</code> and <code>Content-Type</code> as hints, and verify tokens instead of just decoding them. Then run the live URL through a header scanner and fix whatever it grades you down for.</p>
<p>The whole thing is maybe twenty lines and one afternoon. It's the cheapest security you'll ever ship — which is precisely why it's the most often skipped.</p>
]]></content:encoded></item><item><title><![CDATA[Claude Fable 5: The Model That Thinks So Hard You Can't Turn It Off]]></title><description><![CDATA[Claude Fable 5 has three features that sound like bugs. You can't turn off its thinking. It costs more than Opus. And it slices the same text into about 30% more tokens than you're used to. All three ]]></description><link>https://blogs.kishorek.dev/claude-fable-5-the-model-that-thinks-so-hard-you-can-t-turn-it-off</link><guid isPermaLink="true">https://blogs.kishorek.dev/claude-fable-5-the-model-that-thinks-so-hard-you-can-t-turn-it-off</guid><category><![CDATA[ai, coding, claude, software-engineering, productivity, developer-tools]]></category><category><![CDATA[AI]]></category><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[claude]]></category><category><![CDATA[claude-code]]></category><category><![CDATA[opus]]></category><category><![CDATA[fable 5]]></category><category><![CDATA[gemini]]></category><category><![CDATA[copilot]]></category><dc:creator><![CDATA[Kishore K Sharma]]></dc:creator><pubDate>Thu, 11 Jun 2026 05:52:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/01bf9bcd-72cd-452f-bfc6-5b4695c62d69.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Claude Fable 5 has three features that sound like bugs. You can't turn off its thinking. It costs more than Opus. And it slices the same text into about 30% more tokens than you're used to. All three are on purpose, and once you see why, the model makes a lot more sense.</p>
<p>So here's the tour: what Fable 5 actually is, the facts that'll trip you up the first time, what genuinely got better, and the part nobody puts in the launch post: when you should <em>not</em> reach for it.</p>
<h2>What it even is</h2>
<p>Fable 5 is Anthropic's most capable widely released model, and it's built for the hard end of the work. Overnight agent runs. First-shot builds of a system you've specified well. The kind of debugging that used to need a human babysitting the loop. It carries a 1M token context window (that's the default, not just a ceiling you can opt into) and can write up to 128K tokens back.</p>
<p>The part most people get wrong on day one: it's not a drop-in upgrade for everything. If your whole ask is "give me the latest and greatest," the sensible move is Opus 4.8. Fable 5 is the model you reach for <em>on purpose</em>, for the jobs that were genuinely out of reach before. Think of it as the specialist you call in, not the one who sits at the front desk.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/2735238d-9d59-4c5c-af43-c44e34fba060.png" alt="Claude Fable 5 at a glance: 1M context, 128K output, \(10/\)50 pricing, always-on thinking, a new tokenizer, and a 30-day data-retention requirement." style="display:block;margin:0 auto" />

<h2>The facts that'll trip you up first</h2>
<p>This is the fun part, because every one of these has quietly broken someone's afternoon.</p>
<p><strong>Thinking is always on.</strong> On older models you flipped thinking on, set a token budget, or switched it off. On Fable 5 you do none of that. The reasoning is always running, and trying to disable it is a flat 400. The old <code>budget_tokens</code> knob is gone too. What you get instead is one dial: <code>effort</code>.</p>
<pre><code class="language-ts">import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();

// Thinking is always on. You don't request it, and you can't turn it off.
// Steer how deep it goes with effort, not a token budget.
const res = await client.messages.create({
  model: "claude-fable-5",
  max_tokens: 16000,
  output_config: { effort: "high" }, // low | medium | high | xhigh | max
  messages: [{ role: "user", content: "..." }],
});
// Sending thinking: { type: "disabled" } here? That's a 400.
</code></pre>
<p><strong>You never see the raw reasoning.</strong> Fable 5 thinks in full behind the scenes and hands you a summary, or nothing at all. The unfiltered chain of thought is sealed and never returned. Ask for the summarized view if you want the gist; otherwise the thinking field comes back empty while the model still thought (and still billed) for the work. One sharp edge: if you're continuing a conversation on the same model, pass those thinking blocks back exactly as you got them. Edit them and the API rejects the turn. Hand them to a <em>different</em> model and they're quietly dropped, no charge, no error.</p>
<p><strong>The token math you memorized is wrong now.</strong> Fable 5 ships a new tokenizer. The paragraph that was 1,000 tokens on Opus lands closer to 1,300 here. Nothing you wrote changed; the ruler did. So every <code>max_tokens</code> you hand-tuned, every cost estimate sitting in a spreadsheet, every "this fits in the window" assumption needs a fresh measurement. Run <code>count_tokens</code> with <code>model: "claude-fable-5"</code> and it'll hand you the count under both tokenizers, so you can see the gap before it shows up on the invoice.</p>
<p><strong>A refusal is a 200, not a crash.</strong> This one gets everybody once. Fable 5 runs safety classifiers on the way in, mostly around biology and cybersecurity, and benign-adjacent work can trip them too. When one declines, you don't get an exception. You get a cheerful HTTP 200 with <code>stop_reason</code> set to <code>"refusal"</code> and, often, an empty content array. Code that grabs <code>content[0]</code> without looking will throw on thin air.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/145085b2-d557-4881-aec0-2eb5f14cfd0d.png" alt="A refused request comes back as an HTTP 200 with stop_reason &quot;refusal&quot; and usually empty content; a configured fallback retries it on Opus 4.8 in the same round trip." style="display:block;margin:0 auto" />

<pre><code class="language-ts">const res = await client.beta.messages.create({
  model: "claude-fable-5",
  max_tokens: 16000,
  betas: ["server-side-fallback-2026-06-01"],
  fallbacks: [{ model: "claude-opus-4-8" }], // refusals retry here, same request
  messages: [{ role: "user", content: "..." }],
});

// Check stop_reason BEFORE you read content.
if (res.stop_reason === "refusal") {
  // a classifier declined; content is empty (pre-output) or partial (mid-stream)
}
</code></pre>
<p>The nice part: wire up that fallback and a refusal quietly retries on Opus 4.8 in the same round trip, and you only pay for the answer that actually comes back.</p>
<p><strong>It wants your data for 30 days.</strong> Fable 5 isn't available under zero data retention. If your org is set to ZDR, every request 400s no matter how clean the payload is. Worth knowing before you burn an afternoon debugging a request that was fine all along.</p>
<p><strong>A single call can run for minutes.</strong> On a genuinely hard task at high effort, one request can chew for several minutes. A fifteen-minute call isn't a hang; it's the model gathering context, building, and checking its own work. Plan for it. Stream the response, show progress, and let people wander off and come back instead of staring at a spinner that looks frozen.</p>
<h2>What actually got better</h2>
<p>The headline is long-horizon work. Fable 5 is built to run far without a hand on its shoulder: big refactors, multi-step builds, the overnight kind of task. The trick to getting the most out of it is boring but real. Give it the whole spec up front in one clear turn, set effort high, and let it go. It plans more before it acts, and that front-loaded thinking usually means fewer wrong turns, not more.</p>
<p>The surprising win is at the <em>cheap</em> end of the dial.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/29b3dd62-d2a5-4c1f-a741-135559fdc147.png" alt="The effort dial runs low to max; Fable 5 at low effort often beats older models running at xhigh, so the premium isn't only about cranking it to max." style="display:block;margin:0 auto" />

<p>You'd expect a premium model to shine at max effort, and it does. But the bigger story is that Fable 5 at <code>low</code> or <code>medium</code> often beats older models running flat out. So "worth the premium" doesn't have to mean "crank everything to max and eat the bill." A lot of work runs fine on a low setting and still comes out ahead.</p>
<p>Debugging got noticeably sharper. It finds real bugs instead of plausible-looking ones, and it's better at the worst kind: the intermittent flake, where weaker models run the test once, see green, and declare victory. (Fair warning, that bug-finding strength doesn't stretch into security analysis, where those same classifiers tend to step in.)</p>
<p>It's a better delegator, too. Where older models would spawn a sub-agent and then sit there blocked until it finished, Fable 5 keeps long-running sub-agents alive and talks to them while it works on something else. If your harness fans work out across agents, that changes how much you can keep in flight at once.</p>
<p>It can also read a bad photo. Flipped, blurry, low-light, noisy. It's trained to reach for crop and zoom tools instead of squinting and guessing, which helps a lot on the screenshot-and-document side of vision.</p>
<p>And here's the twist that catches careful prompt engineers off guard: your hard-won scaffolding can hurt. All those "FIRST do X, THEN do Y, ALWAYS verify Z" prompts you tuned for older models tend to over-constrain Fable 5 and drag its output quality down. The better move is to state the goal and the constraints, then get out of the way. Years of prompt-wrangling instinct, and the new advice is mostly "say less."</p>
<h2>The catch</h2>
<p>None of this is free, and I mean that literally. Fable 5 runs \(10 per million input tokens and \)50 per million output, against Opus 4.8's \(5 and \)25. Stack the higher rate on top of a tokenizer that counts more tokens for the same text, and an unchanged workload can cost a good bit more than your gut expects. It can also refuse work that's perfectly legitimate but happens to sit near a sensitive area. And it won't run at all under zero retention.</p>
<p>This is a specialist. For everyday traffic, Opus 4.8 is still the one to reach for. Fable 5 earns its keep on the problems that were actually out of reach before, not the ones you've already solved twice.</p>
<p>(One footnote for completeness: if you're in Anthropic's Project Glasswing, you'll meet the same model wearing the name Claude Mythos 5. Same capabilities, same price, different label.)</p>
<h2>So when do you actually use it</h2>
<p>Hand it the problem you haven't been able to crack. Give it the full picture in one go, not a trickle of follow-ups. Set the effort to match how much the answer matters, wire a fallback for the occasional refusal, and let it run while you go do something else.</p>
<p>Just don't ask it to stop thinking. That's the one thing it won't do for you.</p>
]]></content:encoded></item><item><title><![CDATA[How I Actually Use AI to Write Code (Without Wrecking the Codebase)]]></title><description><![CDATA[AI can write code fast. It can also write a bug fast, with total confidence and a beautifully worded commit message. That's the part the demos leave out.
You've seen the videos. Someone types one sent]]></description><link>https://blogs.kishorek.dev/how-i-actually-use-ai-to-write-code-without-wrecking-the-codebase</link><guid isPermaLink="true">https://blogs.kishorek.dev/how-i-actually-use-ai-to-write-code-without-wrecking-the-codebase</guid><category><![CDATA[ai, coding, claude, software-engineering, productivity, developer-tools]]></category><category><![CDATA[AI]]></category><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[vibe coding]]></category><category><![CDATA[gemini]]></category><category><![CDATA[claude]]></category><category><![CDATA[github copilot]]></category><dc:creator><![CDATA[Kishore K Sharma]]></dc:creator><pubDate>Wed, 10 Jun 2026 15:13:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/0b4b7e94-1fdf-4c12-b133-bfe9591878fe.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>AI can write code fast. It can also write a bug fast, with total confidence and a beautifully worded commit message. That's the part the demos leave out.</p>
<p>You've seen the videos. Someone types one sentence, an app appears, the crowd claps. What's never on camera is the next six months: the part where somebody has to read that code, change it, and explain to a customer why it did something nobody intended. Generating code was never the hard part of this job. Living with it is.</p>
<p>I lean on AI every day. Claude Code, Cursor, the usual suspects. But I use it the way you'd use a strong, fast junior engineer who started this morning, has read nothing, remembers nothing tomorrow, and will confidently do exactly the wrong thing if you let them. Handled like that, it's a huge multiplier. Handled like a senior you can hand the keys to, it quietly rots your codebase while everyone's impressed by the velocity.</p>
<p>Here's how I actually run it.</p>
<h2>The vibe-coding trap</h2>
<p>"Vibe coding" is fun right up to the moment it isn't. You prompt, it produces, it runs, you ship. Speed feels incredible. Then a bug shows up in code you didn't write and don't understand, and now you're debugging a stranger's work with none of the context that would've come from writing it yourself.</p>
<p>That's the trap. The code looking plausible is not the same as the code being correct, and AI is <em>extremely</em> good at plausible. It writes things that pass the eye test and fail the edge case. Confident, well-formatted, subtly wrong. The worst kind of pull request, basically, except it arrives every ninety seconds.</p>
<p>The fix is the same one that works for AI-assisted writing, and honestly the same one that works for junior engineers: you don't hand over a blank canvas and hope. You hand over a rulebook and a leash.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/24b673c7-199e-4bb3-a4a7-98b597f0dbb9.png" alt="" style="display:block;margin:0 auto" />

<h2>The rulebook lives in the repo</h2>
<p>For code, the rulebook isn't a prompt you retype. It's a file that sits in the repository and the agent reads automatically every time it starts: <code>CLAUDE.md</code>, or <code>AGENTS.md</code>, or your editor's rules file. Same idea whatever the tool calls it.</p>
<p>Think of it as the onboarding doc you wish every new hire actually read, except this one does, every single session, without complaint.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/3af0289b-b249-4691-9274-27c78895f542.png" alt="" style="display:block;margin:0 auto" />

<p>Without it, the agent guesses. It puts files wherever, invents a naming scheme, picks a library you'd never approve, and writes business logic into a controller because nothing told it not to. With it, the guessing stops. Here's the shape of mine:</p>
<pre><code class="language-markdown"># CLAUDE.md

## Project map
- HTTP controllers live in `src/api`, thin. Business logic in `src/services`.
- Persistence in `src/repositories`. No SQL outside that folder.

## Conventions
- Constructor injection only. No field @Autowired.
- DTOs in/out at the boundary. Never leak entities to the API.

## Commands (run these, don't claim "done" until they pass)
- Build:  ./mvnw -q compile
- Test:   ./mvnw -q test
- Lint:   ./mvnw -q spotless:check

## Do NOT
- Add a dependency without asking.
- Touch the `payments` module.
- Commit anything under `secrets/` or any `.env`.
</code></pre>
<p>That last block is the most valuable part. Every "do not" is a class of disaster the agent now can't wander into. You write it once and you stop re-explaining it forever.</p>
<h2>Tests are the leash</h2>
<p>The rulebook keeps the agent pointed in the right direction. Tests are what stop it from lying to you about whether it got there.</p>
<p>This is the single thing that makes AI coding safe past toy size: you don't trust the code, you trust the test that proves the code. So the agent doesn't get to declare victory because the diff looks reasonable. It declares victory when the suite is green, and not one second before.</p>
<p>In practice I either write the failing test first, or I make the agent write it before the implementation and I check that the test is actually testing the real thing (they love a test that asserts <code>true == true</code> and calls it a day). Then the loop is simple: red, write code, green, or back to the agent. The test is the leash. It's exactly as long as your coverage is honest.</p>
<pre><code class="language-java">// The contract, written before the code:
@Test
void rejectsTransferWhenBalanceTooLow() {
    var account = new Account(money("10.00"));
    assertThrows(InsufficientFundsException.class,
        () -&gt; account.transfer(money("25.00")));
}
// Now the agent can write transfer(). It isn't "done" until this is green.
</code></pre>
<p>No test, no trust. If a change can't be pinned down by a test, that's usually a sign it's the kind of change I shouldn't be handing off in the first place. Which brings up the real question.</p>
<h2>Know what to hand over</h2>
<p>Not everything is safe to give away, and pretending otherwise is how the velocity demos turn into incident reports.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/139c5baf-5639-45f6-ba85-42c2faf81485.png" alt="" style="display:block;margin:0 auto" />

<p>The left column is where AI earns its keep: boilerplate, CRUD endpoints, the fifteenth mapper between two nearly identical shapes, unit tests for code that already exists, mechanical renames across forty files, "explain what this legacy function does." Work that's tedious, fast to generate, and easy to verify. Hand it over and don't feel a thing.</p>
<p>The right column is where I keep my hands on the wheel. System and data architecture. Anything touching auth, security, or money. Concurrency, locking, ordering, the stuff that works fine until it's 2am and a race condition is eating your weekend. And the one thing the model fundamentally can't do for you: figuring out what the actual problem is. The agent can build the thing. It can't tell you you're building the wrong thing.</p>
<p>The boundary isn't fixed, by the way. As your tests and your rulebook get stronger, more work safely slides left. But it slides because you earned the safety, not because you got tired of reviewing.</p>
<h2>What the agent can't do</h2>
<p>It can't hold your whole system in its head. It sees the files in front of it and a summary, not the four years of context about why that weird workaround exists and what breaks if you "clean it up."</p>
<p>It can't own the consequences. When the thing it wrote goes sideways in production, it's not on the call. You are. No skin in the game means no judgment about risk, just confident output either way.</p>
<p>And it doesn't know what "good" means for your domain. Good code for a banking ledger and good code for a throwaway internal dashboard are different in ways the model will flatten into the same tidy average unless you tell it otherwise.</p>
<p>So the senior skill in all this isn't writing code anymore. It's reading a diff quickly, smelling what's off, and saying "no, not like that" with a reason. The job shifted from typing to reviewing, and reviewing well is harder than it looks. You're the editor. The agent is a very fast, very forgetful typist.</p>
<h2>The honest version</h2>
<p>This makes me genuinely faster, and I'm not going to pretend it doesn't. On the 70% of the work that's mechanical, it's a different speed of working entirely. That's real, and it frees up attention for the 30% that's actually judgment, which is the part that was always worth my time.</p>
<p>But it is not hands-off, and the people selling it as hands-off are describing a codebase I wouldn't want to inherit. I read every diff. I write the tests, or I check the ones it wrote like I don't trust them, because I don't. The rulebook and the leash aren't bureaucracy. They're the only reason the speed doesn't turn into a slow-motion mess six months out.</p>
<p>The tool will change. It's Claude Code today, something better next year, and your <code>CLAUDE.md</code> and your test suite move over almost untouched, because they're about <em>your</em> codebase, not the model. That's the asset. The model is just the engine you bolt it to.</p>
<p>So: scope it small, write down the rules, make the tests the gate, and review like it's a junior's PR, because that's precisely what it is. Then merge it under your name and mean it.</p>
]]></content:encoded></item><item><title><![CDATA[From Code to Architecture: Lessons from Six Years of Shipping]]></title><description><![CDATA[The hardest thing about backend engineering isn't writing code. It's deciding what code not to write — and what discipline to add around the code you do.
I've spent six and a half years building produ]]></description><link>https://blogs.kishorek.dev/from-code-to-architecture</link><guid isPermaLink="true">https://blogs.kishorek.dev/from-code-to-architecture</guid><category><![CDATA[architecture]]></category><category><![CDATA[Architecture Design]]></category><category><![CDATA[Microservices]]></category><category><![CDATA[production]]></category><dc:creator><![CDATA[Kishore K Sharma]]></dc:creator><pubDate>Tue, 12 May 2026 15:17:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a00c971e3eebc2e20b385b4/453577e7-abe9-4647-a581-a20ce8f5d2ca.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The hardest thing about backend engineering isn't writing code. It's deciding what code <em>not</em> to write — and what discipline to add around the code you do.</p>
<p>I've spent six and a half years building production systems across telecom, fintech, govtech and edtech. The languages changed. The frameworks changed. The cloud providers changed. Five patterns kept showing up.</p>
<h2>1. The boundary between services is a contract, not a suggestion</h2>
<p>Every microservice migration I've seen go badly started with the same mistake: treating service boundaries as code-organization, not contract-organization.</p>
<p>The test is simple. If a downstream team can ship a breaking change without your service knowing about it, you don't have a contract. You have an internal call masquerading as a network call — the worst of both worlds. You pay the latency cost of HTTP and the coupling cost of a shared module.</p>
<p>What it looks like when it's right:</p>
<ul>
<li>Schemas live in a versioned, language-agnostic format (OpenAPI, protobuf)</li>
<li>Breaking changes get a <code>/v2</code>, never a same-version mutation</li>
<li>Consumers can pin a version; producers can't yank one out from under them</li>
<li>A test suite proves you serialize what you say you serialize</li>
</ul>
<p>This is boring. It's also the reason your platform doesn't catch fire on a Friday.</p>
<h2>2. Idempotency is a feature, not a hope</h2>
<p>Networks fail. Retries happen. The question isn't whether your service will see the same request twice. The question is whether the second time hurts.</p>
<p>The cheapest implementation is a request-id header that the caller generates and the server caches the result against. If the same id arrives twice, the server returns the cached response and skips the side effect. Costs you a Redis key and a hash check. Saves you a duplicate charge or a duplicate row.</p>
<p>I've watched teams without this build elaborate compensation flows to undo what the second request did. They never work as well as not doing the work twice in the first place.</p>
<h2>3. Retries are a contract too</h2>
<p>Retries between services without a budget are how cascading failures start. Service A retries B three times, B retries C three times — that's nine attempts on C from one logical call. C is already struggling; now it's seeing 9× load.</p>
<p>Two rules I won't break:</p>
<ul>
<li><strong>Bound the retry budget end-to-end</strong>, not per-hop. Pass a deadline header. Each hop checks it before retrying.</li>
<li><strong>Circuit-break aggressively</strong>. After N failures in a window, stop calling. Surface a 503 fast. The system recovers faster from a clean failure than from slow degradation.</li>
</ul>
<p>Spring Cloud's circuit breaker (Resilience4j under the hood) does this in three lines of config. Most Node teams hand-roll something with a global counter and call it a day. Both work; the question is whether you're explicit about the budget or just hoping.</p>
<h2>4. Observability before incidents, not during</h2>
<p>The single biggest predictor of how a system survives an incident is whether the operator can see what's happening <em>right now</em>. Not yesterday's logs. Now.</p>
<p>The minimum viable trio:</p>
<ul>
<li><strong>Structured logs</strong> with a request-id propagated through every service touched</li>
<li><strong>One latency dashboard</strong> with p50 / p95 / p99 per route, refreshed in seconds</li>
<li><strong>Error rate per dependency</strong>, not just per service — so you know which downstream is the problem before users do</li>
</ul>
<p>Add this <em>before</em> you have the incident. The pattern I've seen kill teams: rolling out fancy distributed tracing two weeks after a P0, while the lessons are still in postmortem format.</p>
<h2>5. If it doesn't move a metric, don't ship it</h2>
<p>Every system I've owned shipped with a number attached — throughput lifts, manual-effort reductions, faster cycle times, integrations going live across enterprise systems.</p>
<p>Not because metrics make engineers feel important. Because the discipline of choosing a metric <em>before</em> you start coding forces you to know what "done" means. Every one of those numbers existed in a spec before the first line of code did. The code was the cheap part. The agreement on the metric was the expensive part.</p>
<p>The corollary: if you can't articulate the metric, you don't understand the work yet. Go back and ask.</p>
<hr />
<p>Six years compresses to this: code is the easy part. The hard part is the discipline you build <em>around</em> the code — boundaries, idempotency, retries, observability, metrics — that lets the code keep working when the world gets noisy.</p>
<p>Everything else is a tool to serve that.</p>
]]></content:encoded></item></channel></rss>