<?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" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 21 Jul 2026 20:09:44 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ "Relaxation and its Role in Vision": The 1977 PhD Thesis That Helped Shape Modern AI Research ]]>
                </title>
                <description>
                    <![CDATA[ When people think of Geoffrey Hinton, they usually think of backpropagation, Boltzmann Machines, Deep Belief Networks, or the deep learning revolution that transformed artificial intelligence. But few ]]>
                </description>
                <link>https://www.freecodecamp.org/news/relaxation-and-its-role-in-vision-the-1977-phd-thesis-that-helped-shape-modern-ai-research/</link>
                <guid isPermaLink="false">6a5faeb5d735fe8d9b8a5d84</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Mohammed Fahd Abrah ]]>
                </dc:creator>
                <pubDate>Tue, 21 Jul 2026 17:39:01 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/f1fe2707-f951-401a-9b08-54c5e1c2a3d9.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>When people think of Geoffrey Hinton, they usually think of backpropagation, Boltzmann Machines, Deep Belief Networks, or the deep learning revolution that transformed artificial intelligence.</p>
<p>But few people look further back to the beginning of his research career.</p>
<p>In 1977, nearly a decade before the famous backpropagation paper, Hinton completed his PhD thesis at the University of Edinburgh titled "Relaxation and its Role in Vision." At first glance, it seems to be a thesis about computer vision and relaxation methods. That was exactly what I expected when I began reading it.</p>
<p>As I worked through the thesis, however, I realized that it was about much more than a vision algorithm. Many of the ideas that would later define Hinton's research were already taking shape. The terminology was different, the math was simpler, and neural networks hadn't yet become the focus of his work. But the same way of thinking was already there.</p>
<p>This review isn't a chapter-by-chapter summary of the thesis. Instead, it focuses on the ideas that stood out to me while reading it and explores how many of them reappeared in Hinton's later work. Some of these ideas became central to modern AI, while others remain surprisingly overlooked despite being discussed nearly fifty years ago.</p>
<p>Looking back, what impressed me most was not that the thesis predicted specific algorithms. It was that it introduced a consistent way of thinking about intelligence, perception, and computation that would continue to shape Hinton's research for decades.</p>
<p>I hope this review encourages more people to read this remarkable thesis, not simply as a historical document, but as the starting point of one of the most influential research journeys in artificial intelligence.</p>
<h2 id="heading-thesis-overview">Thesis Overview</h2>
<p>In this review, we'll explore Geoffrey Hinton's 1977 PhD thesis, "Relaxation and its Role in Vision", completed at the University of Edinburgh.</p>
<p>We'll begin by looking at the central problem Hinton set out to solve and the ideas that motivated his relaxation approach. From there, we'll explore how the thesis represents uncertainty, reasons about competing hypotheses, and searches for globally consistent interpretations.</p>
<p>Next, we'll examine the puppet program, the relaxation operator, the role of schemas and stored knowledge, the SETTLE system, and Hinton's comparisons with other approaches of the time. We'll also discuss the limitations he identified in his own method and why they mattered.</p>
<p>Finally, we'll look at how many of the ideas introduced in this thesis reappeared throughout Hinton's later work and helped shape the development of modern AI.</p>
<p>If you'd like to follow along, you can also read the original thesis:</p>
<p><a href="https://era.ed.ac.uk/items/02e3dc47-0325-4574-a4d9-b09c3063b4ee"><strong>Geoffrey Hinton. <em>Relaxation and its Role in Vision</em>. PhD thesis, University of Edinburgh, 1977.</strong></a></p>
<p>Here is an infographic gives a quick overview of Geoffrey Hinton's 1977 PhD thesis. It summarizes the main ideas, how the relaxation method works, its applications, its limitations, and why many of these ideas still matter today.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69ce92860ff860b6de01ed93/255a53c5-2e86-4fa5-b80d-07deb770dda0.png" alt="Infographic summarizing Geoffrey Hinton's 1977 PhD thesis, Relaxation and Its Role in Vision. It highlights the thesis's main ideas, methodology, key findings, applications, limitations, and its influence on modern AI." style="display:block;margin:0 auto" width="1024" height="1536" loading="lazy">

<h2 id="heading-table-of-contents">Table of Contents:</h2>
<ul>
<li><p><a href="#heading-the-core-challenge-why-visual-systems-cant-afford-to-guess-too-soon">The Core Challenge: Why Visual Systems Can't Afford to Guess Too Soon</a></p>
</li>
<li><p><a href="#heading-the-first-appearance-of-thinking-as-optimization">The First Appearance of Thinking as Optimization</a></p>
</li>
<li><p><a href="#heading-vision-is-inference-not-pattern-matching">Vision Is Inference, Not Pattern Matching</a></p>
</li>
<li><p><a href="#heading-why-perception-requires-hypotheses">Why Perception Requires Hypotheses</a></p>
</li>
<li><p><a href="#heading-from-binary-decisions-to-degrees-of-belief">From Binary Decisions to Degrees of Belief</a></p>
</li>
<li><p><a href="#heading-distributed-computation-before-neural-networks">Distributed Computation Before Neural Networks</a></p>
</li>
<li><p><a href="#heading-parallelism-as-the-natural-way-to-compute">Parallelism as the Natural Way to Compute</a></p>
</li>
<li><p><a href="#heading-constraint-propagation">Constraint Propagation</a></p>
</li>
<li><p><a href="#heading-local-rules-can-produce-global-intelligence">Local Rules Can Produce Global Intelligence</a></p>
</li>
<li><p><a href="#heading-why-local-consistency-is-not-enough">Why Local Consistency Is Not Enough</a></p>
</li>
<li><p><a href="#heading-relaxation-as-a-way-of-reasoning">Relaxation as a Way of Reasoning</a></p>
</li>
<li><p><a href="#heading-the-importance-of-equilibrium">The Importance of Equilibrium</a></p>
</li>
<li><p><a href="#heading-from-symbolic-decisions-to-numerical-reasoning">From Symbolic Decisions to Numerical Reasoning</a></p>
</li>
<li><p><a href="#heading-why-perception-is-a-search-problem">Why Perception Is a Search Problem</a></p>
</li>
<li><p><a href="#heading-beyond-pattern-recognition-why-internal-representations-matter-more-than-the-final-output">Beyond Pattern Recognition: Why Internal Representations Matter More Than the Final Output</a></p>
</li>
<li><p><a href="#heading-the-importance-of-intermediate-and-hierarchical-representations">The Importance of Intermediate and Hierarchical Representations</a></p>
</li>
<li><p><a href="#heading-schemas-and-stored-knowledge">Schemas and Stored Knowledge</a></p>
</li>
<li><p><a href="#heading-the-settle-system">The SETTLE System</a></p>
</li>
<li><p><a href="#heading-uncertainty-and-ambiguity-as-the-foundation-of-reasoning">Uncertainty and Ambiguity as the Foundation of Reasoning</a></p>
</li>
<li><p><a href="#heading-the-whole-picture">The Whole Picture</a></p>
</li>
<li><p><a href="#heading-a-consistent-philosophy-across-five-decades">A Consistent Philosophy Across Five Decades</a></p>
</li>
<li><p><a href="#heading-permission-to-publish">Permission to Publish</a></p>
</li>
<li><p><a href="#heading-further-reading">Further Reading</a></p>
</li>
</ul>
<h2 id="heading-the-core-challenge-why-visual-systems-cant-afford-to-guess-too-soon">The Core Challenge: Why Visual Systems Can't Afford to Guess Too Soon</h2>
<p>Before exploring the ideas in Hinton's thesis, it helps to understand the problem he set out to solve. The opening chapter asks a deceptively simple question: <strong>How can a visual system choose the correct interpretation when a single image may support many plausible explanations?</strong></p>
<p>This is the central challenge of visual perception. Real-world scenes are often ambiguous or partially hidden, so a system can't afford to commit to one interpretation too early. A premature decision can introduce errors that spread through the rest of the reasoning process and lead to an incorrect understanding of the entire scene.</p>
<p>The real challenge is to keep multiple plausible interpretations alive until there is enough evidence to determine which one is most consistent.</p>
<p>Hinton argues that the common approaches of the 1970s didn't solve this problem. One approach, known as the <strong>principle of least commitment</strong>, delayed decisions by leaving information unspecified. According to Hinton, this simply postponed the real issue because it offered no way to compare competing hypotheses or determine how they should become consistent with one another.</p>
<p>Another approach assigned fixed meanings to low-level visual features. But since the meaning of a feature depends on its surrounding context, these rigid definitions often failed when objects were partially hidden or appeared in different situations.</p>
<p>The infographic below summarizes the central challenge Hinton identifies at the beginning of his thesis. Rather than committing to the first plausible interpretation of a visual scene, he argues that a vision system should maintain many competing hypotheses simultaneously and allow them to interact until they converge on a single, globally consistent explanation.</p>
<p>It also highlights two contemporary approaches that Hinton rejects, the <em>principle of least commitment</em> and <em>rigid feature semantics</em>, because, in his view, they avoid the core problem instead of solving it.</p>
<p>This framing establishes the motivation for the relaxation framework developed throughout the rest of the thesis.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69ce92860ff860b6de01ed93/2e04e5d9-5ad0-4a2b-8e87-c117b28d6b34.png" alt="Infographic on Hinton 1977 PhD thesis explaining parallel relaxation." style="display:block;margin:0 auto" width="1536" height="1024" loading="lazy">

<h2 id="heading-the-first-appearance-of-thinking-as-optimization">The First Appearance of Thinking as Optimization</h2>
<p>One of the most interesting ideas in Hinton's thesis is that perception isn't a matter of instantly recognizing an object. Instead, he treats it as a process of finding the best explanation for what the eyes are seeing.</p>
<p>Rather than committing to a single interpretation from the start, the system considers many possible hypotheses at the same time. Some support each other, others compete, and their confidence changes as they interact. Through repeated updates, weak explanations gradually disappear while the strongest and most consistent interpretation emerges.</p>
<p>Although Hinton applies this idea to visual perception, the underlying principle reaches far beyond computer vision. It introduces a way of thinking about intelligence as an optimization problem: many possible explanations compete until the system settles on the one that best fits the available evidence.</p>
<p>Looking back, this idea feels surprisingly familiar. The same general philosophy later appeared in probabilistic inference, energy-based models, Conditional Random Fields (CRFs), Boltzmann Machines, and many other approaches where intelligence emerges by searching for the most consistent solution rather than making a single immediate decision.</p>
<h2 id="heading-vision-is-inference-not-pattern-matching">Vision Is Inference, Not Pattern Matching</h2>
<p>One idea that stands out throughout the thesis is Hinton's view of what it actually means to see. He argues that vision is not simply recognizing patterns or assigning an image to a category. Instead, perception is the process of building an internal explanation of the scene.</p>
<p>A visual system doesn't immediately know what it's looking at. It must decide which objects are present, how they relate to one another, and which interpretation best explains the available evidence. In other words, seeing is a process of inference, not just recognition.</p>
<p>Hinton also rejects the idea that perception works by simply comparing an input with a collection of stored templates. He argues that this view is too limited to explain how we understand complex and unfamiliar scenes.</p>
<p>Instead, perception is presented as a constructive process. The system builds an interpretation by combining evidence, relationships, and prior knowledge until a coherent explanation emerges. It's not retrieving an answer from memory but actively constructing one.</p>
<p>Reading this today is striking because it closely resembles ideas that became popular decades later. Modern generative models and latent variable methods are also built around the idea of explaining observations by inferring the hidden structure that produced them.</p>
<p>These ideas also feel remarkably close to modern representation learning, where the goal isn't to memorize examples but to learn meaningful internal representations that can explain new observations.</p>
<p>Hinton was exploring these ways of thinking in 1977, long before they became a central theme in modern AI.</p>
<h2 id="heading-why-perception-requires-hypotheses">Why Perception Requires Hypotheses</h2>
<p>Hinton argues that perception can't be a purely reactive process. A visual system often receives incomplete, ambiguous, or even misleading information, so it can't simply accept the first interpretation that comes to mind.</p>
<p>Instead, it must begin with several possible explanations. As more evidence is considered, some hypotheses become more convincing while others are weakened or rejected. The final interpretation is reached only after this process of evaluation and refinement.</p>
<p>Although Hinton doesn't describe it using modern Bayesian terminology, the underlying idea is remarkably similar. Rather than making an immediate decision, the system continuously updates its beliefs as evidence accumulates until the most consistent explanation remains.</p>
<h2 id="heading-from-binary-decisions-to-degrees-of-belief">From Binary Decisions to Degrees of Belief</h2>
<p>Another idea that feels remarkably modern is Hinton's decision to avoid treating hypotheses as simply true or false. Instead, every hypothesis is assigned a value between 0 and 1 that reflects how strongly the system currently believes it. As the relaxation process unfolds, these values are updated repeatedly until the most consistent interpretation stands out while the others gradually fade away.</p>
<p>Today, we use different terms for similar concepts, including probabilities, belief values, confidence scores, activations, and logits. The terminology has evolved over the years, but the underlying idea remains the same: intelligence often depends on representing uncertainty instead of making immediate, irreversible decisions.</p>
<p>The infographic below illustrates how Hinton's relaxation process operates after hypotheses have been assigned continuous belief values.</p>
<p>Rather than selecting a single answer immediately, the system repeatedly updates all competing hypotheses in parallel, using both numerical constraints and individual preferences until one coherent interpretation gradually emerges.</p>
<p>By replacing rigid yes-or-no decisions with continuous optimization, the relaxation framework makes it possible to search efficiently for a globally consistent solution.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69ce92860ff860b6de01ed93/4ab39c8f-8a0d-41db-a2a5-3f0669c52010.png" alt="Infographic showing Hinton's idea of giving each hypothesis a confidence value between 0 and 1 instead of a true-or-false decision. It illustrates how these values are repeatedly updated until the most consistent interpretation remains." style="display:block;margin:0 auto" width="1536" height="1024" loading="lazy">

<h2 id="heading-distributed-computation-before-neural-networks">Distributed Computation Before Neural Networks</h2>
<p>One of the most forward-looking ideas in the thesis is that intelligence shouldn't depend on a single central controller making every decision. Instead, Hinton describes a system made up of many local hypotheses that interact with one another at the same time. Each contributes a small part of the final solution, and together they produce a coherent interpretation.</p>
<p>Instead of focusing on individual components, Hinton emphasizes how these connections allow information to flow through the system until a consistent interpretation emerges.</p>
<p>This way of thinking feels surprisingly familiar today. Modern neural networks are also built on the idea that complex behavior can emerge from the combined activity of many simple units rather than from one component directing the entire process.</p>
<p>The terminology is different from modern deep learning, but the emphasis on networks, interactions, and distributed computation is already clearly visible.</p>
<h2 id="heading-parallelism-as-the-natural-way-to-compute">Parallelism as the Natural Way to Compute</h2>
<p>Another idea that stands out is Hinton's emphasis on parallel computation. At a time when most computers were designed to execute instructions one after another, he argued that perception is better viewed as many processes working simultaneously and influencing one another.</p>
<p>Looking back, this was an unusually forward-looking perspective. Decades before massively parallel hardware became common, Hinton was already describing computation in a way that closely resembles how modern neural networks run today, with many simple operations happening at the same time rather than one step after another.</p>
<h2 id="heading-constraint-propagation">Constraint Propagation</h2>
<p>A recurring idea throughout the thesis is that no hypothesis should be evaluated in isolation. Instead, each one influences the others through a network of constraints. When the confidence of one hypothesis changes, that change spreads across the network, strengthening compatible explanations and weakening conflicting ones.</p>
<p>This idea later became a common theme in several areas of AI. Graphical models, factor graphs, message passing, and belief propagation all rely on the same basic intuition: local interactions can gradually lead to a globally consistent solution.</p>
<p>Although these methods were developed later and use different mathematical frameworks, it's not difficult to see the conceptual connection.</p>
<p>To demonstrate how constraints interact during relaxation, Hinton chose a deliberately simplified vision problem instead of real photographs.</p>
<p>A user first drew several transparent, overlapping rectangles on a graphics terminal. Some rectangles represented genuine parts of a stick-figure puppet, such as the torso, arms, or legs, while others acted as irrelevant distractors.</p>
<p>Every overlap between rectangles became a candidate joint, and the system generated competing hypotheses about which rectangles belonged to the puppet and which overlaps represented real connections. Its goal was to identify the interpretation with the greatest number of mutually consistent instantiated joints, while remaining robust to missing body parts and irrelevant clutter.</p>
<p>By removing the complexity of natural images, Hinton isolated the combinatorial challenge of visual interpretation while keeping the problem mathematically manageable.</p>
<p>The infographic below illustrates how Hinton used this simplified puppet domain to evaluate the relaxation framework. By reducing vision to identifying consistent body parts and joints, the example isolates the core challenge of combining many competing local hypotheses into a single globally consistent interpretation.</p>
<p>Although intentionally simple, the puppet experiment captures the essential reasoning problem of computer vision: many local hypotheses compete simultaneously, constraints propagate between them, and only the globally most consistent interpretation survives.</p>
<p>Hinton presents the domain as a controlled laboratory for studying these interactions before extending the same relaxation principles to more realistic vision problems.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69ce92860ff860b6de01ed93/f62ac4d9-75bb-448a-9485-b3bf8ee26284.png" alt="Infographic showing Hinton's puppet test problem, where overlapping rectangles represent possible body parts. The system uses constraints and relaxation to identify the combination of rectangles that forms the most consistent stick-figure puppet." style="display:block;margin:0 auto" width="1536" height="1024" loading="lazy">

<h2 id="heading-local-rules-can-produce-global-intelligence">Local Rules Can Produce Global Intelligence</h2>
<p>One of the ideas I enjoyed most in this thesis is how a complex solution emerges from simple local interactions. Each hypothesis only needs to communicate with the hypotheses directly connected to it. There's no central component that knows the correct answer or controls the entire process.</p>
<p>As information flows through the network, the system gradually settles on a consistent interpretation. The final result emerges from cooperation rather than command.</p>
<p>This same principle continues to appear throughout AI research. Neural networks, swarm intelligence, graph neural networks, and belief propagation all demonstrate how complex behavior can arise from many simple components following local rules.</p>
<p>The puppet task wasn't just a toy example. It was a complete program with its own processing pipeline. Starting from a drawing of overlapping rectangles, the system generated hypotheses, applied constraints, repeatedly updated their confidence, and finally selected the most consistent interpretation.</p>
<p>The infographic below illustrates how Hinton's entire relaxation framework fits together as a complete computational pipeline. It shows how hypothesis generation, constraint construction, iterative relaxation, and final selection work as successive stages of a single reasoning process.</p>
<p>Rather than relying on a central controller or modern end-to-end training, the system reaches a coherent solution through repeated local interactions among competing hypotheses. This illustrates how simple local rules can produce globally consistent behavior.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69ce92860ff860b6de01ed93/888228de-11a4-46f9-9759-efb24fbe2742.png" alt="Infographic showing the end-to-end pipeline of Hinton's puppet program. Starting from overlapping rectangles, the system generates hypotheses, builds constraints, applies the relaxation operator, iteratively updates hypothesis values, and produces the most consistent puppet interpretation." style="display:block;margin:0 auto" width="1024" height="1536" loading="lazy">

<h2 id="heading-why-local-consistency-is-not-enough">Why Local Consistency Is Not Enough</h2>
<p>One important point Hinton makes is that solving small local conflicts doesn't necessarily produce the best overall interpretation. A hypothesis may fit well with its immediate neighbors while still contributing to an incorrect explanation of the entire scene.</p>
<p>For that reason, the system must evaluate how all the hypotheses work together rather than judging each one independently.</p>
<p>This shift from local agreement to finding the best overall solution is a key theme throughout the thesis. It also reflects a broader direction that AI would later take, where many problems are formulated as global optimization rather than a collection of isolated local decisions.</p>
<h2 id="heading-relaxation-as-a-way-of-reasoning">Relaxation as a Way of Reasoning</h2>
<p>As I read the thesis, I began to see relaxation as more than just an algorithm. It is a way of approaching difficult problems. Instead of trying to reach the correct answer in a single step, the system starts with tentative beliefs, refines them through repeated interactions, and continues until the solution becomes stable.</p>
<p>This idea feels surprisingly familiar today. Although the mathematics is different, many modern methods follow the same pattern. Gradient descent improves parameters step by step, the Expectation-Maximization (EM) algorithm alternates between refinement stages, belief propagation repeatedly exchanges information, and diffusion models generate samples through a sequence of gradual updates.</p>
<p>The methods are different, but the underlying philosophy is remarkably similar: good solutions often emerge through many small improvements rather than one decisive computation.</p>
<p>So how does relaxation actually work? Hinton's answer is surprisingly simple. During each update, the system balances two forces: one keeps the hypotheses consistent with the constraints, while the other gently pushes them toward better explanations. Repeating this process eventually leads to a stable solution.</p>
<p>The infographic below illustrates how Hinton translates this reasoning process into a concrete computational procedure. Rather than making a single decision, the relaxation operator repeatedly updates all hypotheses in parallel, applying the same two-force rule until the entire network settles into a stable, globally consistent state.</p>
<p>The process shows how simple local updates can collectively produce a coherent global interpretation.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69ce92860ff860b6de01ed93/caae92a0-9e65-4519-81d7-f1c8d36ec239.png" alt="Infographic explaining Hinton's relaxation operator. Each update balances a constraint force that keeps hypotheses consistent with a preference force that nudges them toward better interpretations, repeating until the system reaches a stable solution." style="display:block;margin:0 auto" width="1536" height="1024" loading="lazy">

<h2 id="heading-the-importance-of-equilibrium">The Importance of Equilibrium</h2>
<p>Another idea that appears throughout the thesis is the importance of allowing the system to reach a stable state. The final interpretation isn't imposed by a central controller or chosen by a fixed rule. Instead, it emerges naturally as the hypotheses interact until no further changes are needed.</p>
<p>This idea became a recurring theme in Hinton's later work. Hopfield Networks, Boltzmann Machines, and energy-based models all rely on systems evolving toward stable configurations through their own internal dynamics. Although the models are different, the underlying intuition is much the same: a good solution is one the system naturally settles into.</p>
<p>Hinton didn't just describe what worked. He also analyzed the situations where relaxation could break down, discussing both the possibility of converging to ambiguous intermediate solutions and the architectural limitations of the overall framework.</p>
<p>The infographic below illustrates these two limitations. During relaxation, the system may converge to a stable solution that lies between discrete interpretations rather than fully committing to a single one.</p>
<p>It also highlights a broader architectural weakness: hypothesis generation and hypothesis selection remain separate stages, preventing later reasoning from influencing which candidate hypotheses are created in the first place.</p>
<p>Hinton openly presents these limitations, which later AI systems addressed through more integrated and end-to-end learning approaches.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69ce92860ff860b6de01ed93/428377bb-a51b-4a23-ad7b-9843346d1cb8.png" alt="Infographic explaining a limitation of Hinton's relaxation method. It shows how the system can converge to a stable non-integer solution between two valid interpretations instead of reaching a single clear answer." style="display:block;margin:0 auto" width="1536" height="1024" loading="lazy">

<h2 id="heading-from-symbolic-decisions-to-numerical-reasoning">From Symbolic Decisions to Numerical Reasoning</h2>
<p>One of the subtle but important shifts in the thesis is the move away from treating knowledge as simply true or false. Instead of relying on rigid symbolic decisions, Hinton represents beliefs with numerical values that can increase or decrease as new evidence is considered.</p>
<p>This may seem like a small design choice, but it reflects a much broader change in how intelligent systems can reason. Rather than forcing early decisions, the system keeps track of uncertainty and adjusts its beliefs over time. Looking back, this is the same direction that much of modern machine learning would eventually follow.</p>
<p>Hinton didn't develop these ideas in isolation. In the thesis, he evaluates his relaxation framework alongside other prominent approaches of the time, highlighting their different ways of representing uncertainty and reasoning about visual scenes.</p>
<p>The infographic below compares three approaches side by side. Using the same line-labeling problem as a common benchmark, it shows how Waltz's filtering algorithm, fuzzy-weight models, and Hinton's relaxation framework represent uncertainty, update hypotheses, and enforce consistency.</p>
<p>Hinton argues that his supposition-value framework provides a more principled way to reason under uncertainty by combining continuous confidence values with explicit numerical constraints, allowing competing interpretations to evolve toward a globally consistent solution.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69ce92860ff860b6de01ed93/84706725-4aa3-444d-89f4-13cbac4b4920.png" alt="Infographic comparing Hinton's L.P. relaxation method with Waltz filtering and fuzzy-weight models. It highlights how the three approaches represent uncertainty and why Hinton argued that his supposition-value framework provided a stronger solution." style="display:block;margin:0 auto" width="1536" height="1024" loading="lazy">

<h2 id="heading-why-perception-is-a-search-problem">Why Perception Is a Search Problem</h2>
<p>One of the ideas that repeatedly appears throughout the thesis is that perception is fundamentally a search process. A visual system isn't simply recognizing an object from what it sees. Instead, it's searching through many possible explanations to find the one that best fits the available evidence.</p>
<p>This distinction is more important than it might first appear. Recognition suggests that the answer is already obvious and only needs to be retrieved. Search assumes that the correct interpretation must be discovered by exploring alternatives and resolving uncertainty.</p>
<p>Even today, that way of thinking continues to shape many approaches to artificial intelligence.</p>
<p>If perception is a search problem, the next question becomes what the system is actually searching through. Hinton answers this by introducing a geometric view of the search process, where every possible interpretation occupies a position within a structured space of feasible solutions.</p>
<p>The infographic below illustrates this geometric perspective. It represents all valid hypothesis assignments as points inside a feasible search space, where the corners correspond to clean all-or-nothing interpretations and intermediate points represent uncertain or partial beliefs.</p>
<p>Rather than searching directly among discrete solutions, the relaxation process moves continuously through this space, gradually improving the current state until it converges on the highest-scoring feasible interpretation.</p>
<p>This geometric perspective provides an intuitive way to understand how relaxation transforms a difficult combinatorial search into a continuous optimization problem.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69ce92860ff860b6de01ed93/28b9bb53-2961-4818-affc-5812a5d26ee9.png" alt="Infographic showing Hinton's view of perception as a search through many possible interpretations. It illustrates a search space of feasible states and how hill climbing gradually moves the system from uncertain states to the best consistent interpretation." style="display:block;margin:0 auto" width="1536" height="1024" loading="lazy">

<h2 id="heading-beyond-pattern-recognition-why-internal-representations-matter-more-than-the-final-output">Beyond Pattern Recognition: Why Internal Representations Matter More Than the Final Output</h2>
<p>One of the most thought-provoking parts of the thesis is Hinton's criticism of viewing perception as nothing more than pattern recognition. He argues that recognizing visual features alone can't explain how we understand a scene. A vision system must also determine how objects are related, how their parts fit together, and how those relationships combine into a coherent interpretation of the world.</p>
<p>This emphasis on relationships is central to Hinton's view of perception. Understanding a scene requires more than identifying individual objects. The system must represent objects, their constituent parts, and the structural relationships that connect them into larger wholes. In other words, perception is fundamentally about building a structured representation of the scene rather than recognizing isolated patterns.</p>
<p>As I read the thesis, one theme kept resurfacing: Hinton is less interested in the final answer than in the internal representation the system builds while interpreting a scene. The goal isn't simply to assign a label to an image, but to construct a structured description that captures the relationships between its components. The final decision is simply the outcome of this richer reasoning process.</p>
<p>Looking back, this perspective was remarkably forward-looking. Many modern AI systems have moved beyond simple classification toward learning internal representations that capture structure, relationships, and context. Although today's models use very different mathematical tools, the underlying intuition is strikingly similar.</p>
<p>Throughout his later career, Hinton consistently emphasized that the quality of an intelligent system depends less on its final output than on the representations it learns along the way.</p>
<h2 id="heading-the-importance-of-intermediate-and-hierarchical-representations">The Importance of Intermediate and Hierarchical Representations</h2>
<p>One idea that caught my attention is Hinton's discussion of intermediate-level hypotheses. Rather than moving directly from visual input to a final interpretation, he argues that perception benefits from intermediate representations that bridge the gap between raw observations and complete understanding.</p>
<p>Understanding a scene happens at multiple levels. Simple elements combine to form larger structures, and those structures become part of an even richer interpretation. Perception is built gradually through a hierarchy rather than all at once.</p>
<p>Looking back, these ideas feel strikingly familiar. Modern deep learning is built on the principle of intermediate-level hypotheses, with each layer learning increasingly abstract representations before reaching a final prediction.</p>
<p>And the idea of hierarchical perception would continue to appear throughout Hinton's later research. Whether in Deep Belief Nets, Capsule Networks, or hierarchical generative models, the same principle remains: meaningful representations are built layer by layer, with each level capturing patterns that the previous one could not.</p>
<p>The terminology has once again changed, but the intuition is much the same: complex understanding is achieved through a hierarchy of intermediate representations, not in a single step.</p>
<h2 id="heading-schemas-and-stored-knowledge">Schemas and Stored Knowledge</h2>
<p>In the later chapters, Hinton introduces the idea of schemas as a way to organize knowledge and connect it to perception. Rather than treating perception and stored knowledge as separate processes, he shows how they can work together to interpret what the system observes.</p>
<p>One of the most interesting ideas in these chapters is Hinton's view of schemas. Instead of storing exact examples, he argues that knowledge should capture the rules, relationships, and constraints that define a category. This allows the system to interpret new situations by reasoning about their underlying structure rather than simply matching what it has already seen.</p>
<p>Reading this today, it's easy to see why the idea remains relevant. Although the terminology has changed, many modern AI systems also rely on learned internal representations that support generalization instead of memorization. In that sense, Hinton's discussion of schemas can be seen as an early step toward concepts that later evolved into latent representations and internal world models.</p>
<p>The infographic below illustrates Hinton's contrast between schema-based reasoning and template matching. Instead of relying on stored examples, schemas represent structural knowledge through roles, relationships, and constraints that guide interpretation. New observations are understood by satisfying these structural relationships rather than by finding an exact match to a memorized template.</p>
<p>This perspective foreshadows later developments in representation learning, where successful generalization depends on learning underlying structure instead of memorizing individual examples.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69ce92860ff860b6de01ed93/f69efb55-9f33-4a4a-95ae-c1fb624f1e3a.png" alt="Infographic explaining Hinton's idea of schemas. It compares schema-based knowledge with template matching, showing how rules and relationships help a system understand new examples instead of relying on exact stored patterns." style="display:block;margin:0 auto" width="1536" height="1024" loading="lazy">

<h2 id="heading-the-settle-system">The SETTLE System</h2>
<p>One part of the thesis that deserves far more attention is <strong>SETTLE</strong>, an experimental reasoning system Hinton developed to combine schemas, inference rules, and relaxation into a single computational framework. It's often overshadowed by the earlier chapters on relaxation, but it reveals how Hinton was already thinking about integrating multiple forms of reasoning rather than treating them as separate processes.</p>
<p>Instead of applying rules independently or storing knowledge in isolation, SETTLE allows schemas, inference rules, relaxation, and dynamic network construction to cooperate while the system gradually builds the most consistent interpretation from uncertain evidence.</p>
<p>Looking back, SETTLE is interesting not because it resembles modern AI systems in detail, but because it reflects Hinton's early effort to integrate knowledge, reasoning, and inference into a unified computational process.</p>
<p>The infographic below illustrates how these components interact within SETTLE. Inference rules generate candidate conclusions while the relaxation process continuously evaluates their consistency. This allows evidence, rules, and competing hypotheses to influence one another until they converge on the most coherent interpretation.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69ce92860ff860b6de01ed93/9b7dd4e4-482e-4da3-ae00-dbe617d1e124.png" alt="Infographic explaining Hinton's SETTLE system. It shows how schemas, inference rules, and relaxation work together to evaluate evidence, make inferences, and build the most consistent interpretation." style="display:block;margin:0 auto" width="1536" height="1024" loading="lazy">

<h2 id="heading-uncertainty-and-ambiguity-as-the-foundation-of-reasoning">Uncertainty and Ambiguity as the Foundation of Reasoning</h2>
<p>One theme that runs throughout the thesis is that uncertainty isn't a problem to avoid but a natural starting point for perception. A visual system doesn't begin with complete knowledge or immediate confidence. Instead, it starts with tentative assumptions that are gradually strengthened, weakened, or discarded as more evidence is taken into account.</p>
<p>Closely related to this is Hinton's treatment of ambiguity. Rather than viewing multiple possible interpretations as a failure of perception, he accepts them as an unavoidable consequence of incomplete information.</p>
<p>A visual scene can support several plausible explanations, and the system should allow those possibilities to coexist until enough evidence is available to distinguish between them. Instead of forcing an early decision, it gradually moves toward the most consistent interpretation.</p>
<p>Although Hinton's thesis predates modern probabilistic graphical models and Bayesian inference methods, its underlying perspective is remarkably close to probabilistic thinking.</p>
<p>The mathematical tools would evolve considerably over the following decades, but the central idea remained the same: intelligent systems should reason under uncertainty rather than expect complete and perfect information from the start. Looking back, it's not difficult to see why this perspective became one of the defining principles of modern AI.</p>
<h2 id="heading-the-whole-picture">The Whole Picture</h2>
<p>The infographic below provides an overview of Hinton's thesis by bringing together its main contributions in a single view. It compares his relaxation framework with other approaches available in the late 1970s, outlines the different application domains explored throughout the thesis (from simplified vision tasks to schema-based reasoning and the SETTLE system) and concludes with the key limitations Hinton openly identifies.</p>
<p>Taken together, these elements show that the thesis is not only a proposal for a relaxation algorithm, but also a broader research program for reasoning under uncertainty that influenced many ideas developed in later AI systems.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69ce92860ff860b6de01ed93/0299d21d-cf99-4afb-b5ec-b9892ba68e59.png" alt="Infographic summarizing Hinton's 1977 thesis. It compares his relaxation method with contemporary approaches, outlines the main application domains explored in the thesis, and highlights the key open problems Hinton identified for future research." style="display:block;margin:0 auto" width="1024" height="1536" loading="lazy">

<h2 id="heading-a-consistent-philosophy-across-five-decades">A Consistent Philosophy Across Five Decades</h2>
<p>After finishing the thesis, what impressed me most was not a single algorithm or experiment. It was the continuity of Hinton's thinking. Many of the ideas introduced in 1977 reappear throughout the rest of his career, even though the mathematical tools and models changed dramatically.</p>
<p>The thesis begins with relaxation, competing hypotheses, distributed constraints, optimization, and stable solutions. Later came Boltzmann Machines, backpropagation, Deep Belief Networks, AlexNet, the Forward-Forward algorithm, and more recently, his ideas on mortal computation.</p>
<p>At first glance, these contributions seem very different. Yet they're connected by a remarkably consistent research philosophy.</p>
<p>Throughout his career, Hinton has viewed intelligence as something that emerges from the interaction of many simple computational elements rather than from a central controller. He has consistently emphasized distributed knowledge over isolated symbols, inference over simple recognition, rich internal representations over final outputs, optimization as the mechanism for intelligent behavior, and uncertainty as something to be represented and refined rather than ignored.</p>
<p>Looking back from today, Hinton's 1977 thesis feels less like an isolated piece of early research and more like the beginning of an intellectual journey that would shape nearly five decades of artificial intelligence research.</p>
<p>This final infographic illustrates these conceptual connections. Rather than presenting Hinton's later work as direct implementations of his thesis, it shows how many of its central ideas, including continuous confidence values, optimization-based perception, structured knowledge representations, and integrated reasoning, continued to reappear in later developments such as Boltzmann Machines, backpropagation, Deep Belief Networks, and energy-based models.</p>
<p>The emphasis isn't on a single line of technical development, but on the remarkable continuity of the research philosophy that connects Hinton's earliest work to many of his later contributions.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69ce92860ff860b6de01ed93/42ecd143-9088-4541-9473-b1f32987b844.png" alt="Infographic summarizing the main ideas from Hinton's 1977 thesis and showing how they reappeared in later work, including Boltzmann Machines, backpropagation, Deep Belief Networks, and energy-based models, highlighting the continuity of his research philosophy." style="display:block;margin:0 auto" width="1578" height="997" loading="lazy">

<h2 id="heading-permission-to-publish">Permission to Publish</h2>
<p>Before writing this review, I contacted Professor Geoffrey Hinton to request permission to publish an educational review of his thesis. Professor Hinton kindly granted permission for the publication of this review.</p>
<p>The article is written entirely in my own words and reflects my own interpretation of the thesis, with full acknowledgment of the original work.</p>
<h2 id="heading-further-reading">Further Reading</h2>
<ul>
<li><p><a href="https://era.ed.ac.uk/handle/1842/8121">Geoffrey E. Hinton (1977) – <em>Relaxation and its Role in Vision</em></a></p>
</li>
<li><p><a href="https://mitpress.mit.edu/9780262514620/vision/">David Marr (1982) – <em>Vision: A Computational Investigation into the Human Representation and Processing of Visual Information</em></a></p>
</li>
<li><p><a href="https://shop.elsevier.com/books/probabilistic-reasoning-in-intelligent-systems/pearl/978-0-08-051489-5">Judea Pearl (1988) – <em>Probabilistic Reasoning in Intelligent Systems</em></a></p>
</li>
<li><p><a href="https://www.nature.com/articles/323533a0">Rumelhart, Hinton &amp; Williams (1986) – <em>Learning Representations by Back-Propagating Errors</em></a></p>
</li>
<li><p><a href="https://mitpress.mit.edu/9780262680530/parallel-distributed-processing-volume-1/">Rumelhart &amp; McClelland (1986) – <em>Parallel Distributed Processing: Explorations in the Microstructure of Cognition</em></a></p>
</li>
<li><p><a href="https://pmc.ncbi.nlm.nih.gov/articles/PMC346238/">John J. Hopfield (1982) – <em>Neural Networks and Physical Systems with Emergent Collective Computational Abilities</em></a></p>
</li>
<li><p><a href="https://www.cs.toronto.edu/~hinton/absps/fastnc.pdf">Hinton, Osindero &amp; Teh (2006) – <em>A Fast Learning Algorithm for Deep Belief Nets</em></a></p>
</li>
<li><p><a href="https://papers.nips.cc/paper/4824-imagenet-classification-with-deep-convolutional-neural-networks">Krizhevsky, Sutskever &amp; Hinton (2012) – <em>ImageNet Classification with Deep Convolutional Neural Networks</em></a></p>
</li>
<li><p><a href="https://books.google.com.my/books/about/Deep_Learning.html?hl=id&amp;id=omivDQAAQBAJ&amp;redir_esc=y">Goodfellow, Bengio &amp; Courville (2016) – <em>Deep Learning</em></a></p>
</li>
<li><p><a href="https://www.microsoft.com/en-us/research/wp-content/uploads/2006/01/Bishop-Pattern-Recognition-and-Machine-Learning-2006.pdf">Christopher M. Bishop (2006) – <em>Pattern Recognition and Machine Learning</em></a></p>
</li>
<li><p><a href="https://arxiv.org/abs/2006.11239">Ho, Jain &amp; Abbeel (2020) – <em>Denoising Diffusion Probabilistic Models</em></a></p>
</li>
<li><p><a href="https://arxiv.org/abs/2212.13345">Geoffrey Hinton (2022) – <em>The Forward-Forward Algorithm: Some Preliminary Investigations</em></a></p>
</li>
</ul>
<p><strong>Contact Me</strong></p>
<ul>
<li><p><a href="https://github.com/MOHAMMEDFAHD"><strong>Github</strong></a></p>
</li>
<li><p><a href="https://x.com/programmingoce"><strong>X</strong></a></p>
</li>
<li><p><a href="https://www.linkedin.com/in/mohammed-abrah-6435a63ba/"><strong>Linkedin</strong></a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Serve a Multi-User AI Agent with FastAPI and Streamlit ]]>
                </title>
                <description>
                    <![CDATA[ In this tutorial, I’ll show you how to serve a multi-user local AI agent as a REST API using FastAPI, then add a lightweight Streamlit UI on top. Instead of interacting with the agent through a termin ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-serve-a-multi-user-ai-agent-with-fastapi-and-streamlit/</link>
                <guid isPermaLink="false">6a5e9c35892c69a16fdf27df</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ai-agent ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ollama ]]>
                    </category>
                
                    <category>
                        <![CDATA[ #qwen ]]>
                    </category>
                
                    <category>
                        <![CDATA[ FastAPI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ api ]]>
                    </category>
                
                    <category>
                        <![CDATA[ streamlit ]]>
                    </category>
                
                    <category>
                        <![CDATA[ UI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ llm ]]>
                    </category>
                
                    <category>
                        <![CDATA[ streaming ]]>
                    </category>
                
                    <category>
                        <![CDATA[ chatgpt ]]>
                    </category>
                
                    <category>
                        <![CDATA[ agentic ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Streaming API ]]>
                    </category>
                
                    <category>
                        <![CDATA[ langgraph ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Darsh Shah ]]>
                </dc:creator>
                <pubDate>Mon, 20 Jul 2026 22:07:49 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/e5bf4093-e618-4388-954c-f1a49bc87cfe.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this tutorial, I’ll show you how to serve a multi-user local AI agent as a REST API using FastAPI, then add a lightweight Streamlit UI on top.</p>
<p>Instead of interacting with the agent through a terminal, we’ll expose it over HTTP so multiple users can access it through a chat-style frontend interface. Each session will maintain its own conversation history and streamed responses.</p>
<p>The local AI agent will be built with LangChain v1, Ollama, Qwen, and Python, running on your own machine and ready to plug into larger applications without any per-call model API charges.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-background">Background</a></p>
</li>
<li><p><a href="#heading-what-is-fastapi">What is FastAPI</a>?</p>
</li>
<li><p><a href="#heading-what-is-streamlit">What is Streamlit</a>?</p>
</li>
<li><p><a href="#heading-what-is-multi-user-support">What Is Multi-User Support</a>?</p>
</li>
<li><p><a href="#heading-motivation-and-architecture">Motivation and Architecture</a></p>
</li>
<li><p><a href="#heading-step-1-install-ollama-and-pull-the-model">Step 1: Install Ollama and Pull the Model</a></p>
</li>
<li><p><a href="#heading-step-2-install-python-dependencies">Step 2: Install Python Dependencies</a></p>
</li>
<li><p><a href="#heading-step-3-build-the-agent-and-api-layer-with-fastapi">Step 3: Build the agent and API layer with FastAPI</a></p>
</li>
<li><p><a href="#heading-step-4-build-streamlit-ui">Step 4: Build Streamlit UI</a></p>
</li>
<li><p><a href="#heading-step-5-run-the-backend-app">Step 5: Run the backend app</a></p>
</li>
<li><p><a href="#heading-step-6-run-the-frontend-app">Step 6: Run the frontend app</a></p>
</li>
<li><p><a href="#heading-sample-output">Sample Output</a></p>
</li>
<li><p><a href="#heading-what-to-improve-before-production">What to Improve Before Production</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-background">Background</h2>
<p>Many AI agents start out as simple Python scripts that run in a command-line terminal. You type a message, the agent responds, and everything happens in a single local session.</p>
<p>That setup is great for development and testing, but it becomes limiting when you want other people or applications to interact with the agent.</p>
<p>To make an AI agent truly useful, we need to expose it through an interface that other users can access. A REST API is a practical way to do that.</p>
<p>To follow this tutorial, you'll need Ollama installed on your machine. The tutorial works on macOS, Windows, and Linux. I'm using a MacBook Pro with 32 GB of RAM, but you can run this on a lower-memory machine by choosing a smaller Qwen model from Ollama.</p>
<h2 id="heading-what-is-fastapi"><strong>What is FastAPI?</strong></h2>
<p><a href="https://github.com/fastapi/fastapi">FastAPI</a> is a Python web framework for building APIs. In this tutorial, it gives us a simple way to expose the agent over HTTP so other apps, scripts, or services can call it.</p>
<p>FastAPI is a good fit for AI apps because it gives us a clean boundary around the system. We define the request and response models in Python, FastAPI validates them automatically, and it turns HTTP requests into Python objects and Python objects back into JSON. It also generates interactive API docs for free and supports async endpoints, which is useful for AI workloads that may take longer to respond.</p>
<h2 id="heading-what-is-streamlit"><strong>What is Streamlit?</strong></h2>
<p><a href="https://streamlit.io">Streamlit</a> is a Python framework for building lightweight web interfaces with minimal frontend work. It lets us create interactive browser-based apps using normal Python code instead of HTML, CSS, and JavaScript.</p>
<p>In this tutorial, Streamlit sits on top of the FastAPI backend as a thin client. FastAPI exposes the AI agent over HTTP, and Streamlit gives us a simple UI for calling that API and displaying the results. That separation keeps the backend reusable while still making the agent easy to use in the browser.</p>
<h2 id="heading-what-is-multi-user-support"><strong>What Is Multi-User Support?</strong></h2>
<p>Multi-user support means the AI agent can handle requests from more than one user while keeping each user’s session separate.</p>
<p>For example, User 1&nbsp;asks the agent one question and User 2&nbsp;asks a different question. The agent should remember the correct context for each user independently. Without multi-user support, all users may end up sharing the same conversation state, which can lead to mixed responses, incorrect memory, or overwritten context.</p>
<h2 id="heading-motivation-and-architecture"><strong>Motivation and Architecture</strong></h2>
<p>Turning an AI agent into an API is the natural next step after building it locally. A Python script is great for experimenting, but an API makes the agent reusable. And adding multi-user support makes the agent extensible to be used by others.</p>
<p>To keep things simple, we’ll use a small local agent powered by Ollama and Qwen. The agent has two tools: one for checking the current time and another for counting words.</p>
<p>FastAPI provides the HTTP layer by exposing one endpoint called <code>/chat/stream</code>. When the request comes in with a user message, Pydantic validates the request, LangChain handles the agent loop and tool calling, and the final answer is returned as stream. Streamlit sits on top of that API and acts as a frontend that sends requests to the API and displays the results.</p>
<img src="https://cdn.hashnode.com/uploads/covers/684c95e159698b4bf6a0e4be/21a2b03d-b4c3-4211-82b1-aa265ac6fb1e.png" alt="image showing the sequence diagram of user calling the streamlit UI. The it goes to FastAPI layer, then to AI agent and finally Qwen and tool calls" style="display:block;margin:0 auto" width="1478" height="1000" loading="lazy">

<p>Example request:</p>
<pre><code class="language-json">{ 
    "message": "How many words are in: LangChain makes tool calling easier",
    "user_id":"123e4567-e89b-12d3-a456-426614174000"
 }
</code></pre>
<p>Example response:</p>
<pre><code class="language-json">{
  "answer": "There are **5** words in LangChain makes tool calling easier."
}
</code></pre>
<p>The model runs locally through Ollama, so there are no per-call model API charges.</p>
<h2 id="heading-step-1-install-ollama-and-pull-the-model"><strong>Step 1: Install Ollama and Pull the Model</strong></h2>
<p>To get started, install the Ollama application for your platform.</p>
<p>We’ll use Qwen as the chat model. I’m using <code>qwen3.5:4b</code>. If your machine has less RAM, you can use <code>qwen3.5:0.8b</code> instead.</p>
<pre><code class="language-plaintext">ollama pull qwen3.5:4b
</code></pre>
<h2 id="heading-step-2-install-python-dependencies"><strong>Step 2: Install Python Dependencies</strong></h2>
<p>Create a virtual environment and install the required packages:</p>
<pre><code class="language-plaintext">python3 -m venv venv
source venv/bin/activate

pip install fastapi uvicorn streamlit requests langchain langchain-core langchain-ollama langgraph
</code></pre>
<p>If tutorial requires LangChain &gt;= 1.0.0.</p>
<h2 id="heading-step-3-build-the-agent-and-api-layer-with-fastapi">Step 3: <strong>Build the Agent and API Layer with FastAPI</strong></h2>
<p>This application has three main responsibilities. FastAPI exposes the HTTP endpoint, Pydantic validates the incoming request data, and LangChain runs the agent, including tool calling and short-term memory.</p>
<p>The <code>user_id</code> sent with each request is used as the thread identifier, allowing the checkpointer to keep each user’s conversation history separate. This memory is per session. So every new session will have its own memory.</p>
<p>Another important detail is that the agent is created only once at startup with <code>agent = build_agent()</code>. Reusing the same agent instance avoids rebuilding the model and tool list for every request, which reduces overhead and improves response times while still supporting multiple users.</p>
<p>Inside the <code>/chat/stream</code> endpoint, the backend uses <a href="https://docs.langchain.com/oss/python/langchain/event-streaming">LangChain’s</a> <code>stream_events(..., version="v3")</code> to generate the response as a stream instead of waiting for the full answer all at once. FastAPI then wraps that stream in a <code>StreamingResponse</code>, so the frontend can receive the output gradually as it's produced. This makes the app feel much more interactive, because users can start reading the answer immediately while the rest is still being generated.</p>
<p>Put together, this gives you a lightweight backend that validates input, preserves separate memory for each user, and streams responses to the UI in real time.</p>
<p>Save the following code as <code>app.py</code>:</p>
<pre><code class="language-python">from datetime import datetime
from uuid import UUID

from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse

from pydantic import BaseModel

from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_ollama import ChatOllama
from langgraph.checkpoint.memory import InMemorySaver

CHAT_MODEL = "qwen3.5:4b"

SYSTEM_PROMPT = (
    "You are a helpful assistant with access to tools for getting the current time "
    "and counting words in text. "
    "Use tools when needed. If the question does not need a tool, answer directly."
)

# -----------------------------
# Request model
# -----------------------------

class ChatRequest(BaseModel):
    user_id: UUID
    message: str

# -----------------------------
# Tools
# -----------------------------

@tool
def current_time() -&gt; str:
    """Return the current local date and time."""
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")


@tool
def word_count(text: str) -&gt; int:
    """Count the number of words in a piece of text."""
    return len(text.split())


# -----------------------------
# Agent + checkpoint memory
# -----------------------------

# Store conversation history in short term memory
checkpointer = InMemorySaver()

def build_agent():
    model = ChatOllama(model=CHAT_MODEL, temperature=0)
    return create_agent(
        model=model,
        tools=[current_time, word_count],
        system_prompt=SYSTEM_PROMPT,
        checkpointer=checkpointer,
    )


agent = build_agent()

# -----------------------------
# Streaming endpoint
# -----------------------------

app = FastAPI()

@app.post("/chat/stream")
def chat_stream(req: ChatRequest):
    def generate():
        run = agent.stream_events(
            {
                "messages": [{"role": "user", "content": req.message}],
            },
            config={
                "configurable": {
                    # Keep each user's short-term memory isolated
                    # by using their user_id as the thread ID.
                    "thread_id": str(req.user_id),
                }
            },
            version="v3",
        )

        for message in run.messages:
            for token in message.text:
                yield token

    return StreamingResponse(generate(), media_type="text/plain")
</code></pre>
<h2 id="heading-step-4-build-streamlit-ui">Step 4: Build Streamlit UI</h2>
<p>The Streamlit code creates a simple chat interface for the AI agent and keeps each browser session tied to a unique user_id.</p>
<p>When the app first loads, it generates and stores a UUID in st.session_state, which is later sent to the backend so the agent can keep that user’s conversation history separate from other users. It also creates a chat_history list in session state so previous messages remain visible every time Streamlit reruns the script. The app then loops through that saved history and displays each message in a chat-style format using st.chat_message().</p>
<p>When the user enters a new message through st.chat_input(), the app immediately saves and displays it, then sends it to the backend API with a POST request to <code>http://127.0.0.1:8001/chat/stream</code> along with the session’s user_id.</p>
<p>The request is made with stream=True, which allows the response to arrive gradually instead of all at once. As each chunk of text is received from the backend, the code appends it to full_answer and updates a placeholder on the page, creating a live streaming effect. Once the response is complete, the final assistant message is stored in chat_history so it remains part of the conversation on the page</p>
<p>Save the below as <code>streamlit_app.py</code></p>
<pre><code class="language-python">import uuid
import requests
import streamlit as st

API_URL = "http://127.0.0.1:8001/chat/stream"

st.title("Local AI Agent")

if "user_id" not in st.session_state:
    st.session_state.user_id = str(uuid.uuid4())

if "chat_history" not in st.session_state:
    st.session_state.chat_history = []

# Show previous messages
for item in st.session_state.chat_history:
    with st.chat_message(item["role"]):
        st.markdown(item["content"])

message = st.chat_input("Enter a message")

if message:
    # Save and show user message
    st.session_state.chat_history.append({"role": "user", "content": message})
    with st.chat_message("user"):
        st.markdown(message)

    # Stream assistant response
    full_answer = ""
    with st.chat_message("assistant"):
        placeholder = st.empty()

        # Send the reqeust to backend API via POST request
        with requests.post(
            API_URL,
            json={
                "message": message,
                "user_id": st.session_state.user_id,
            },
            stream=True,
        ) as response:
            response.raise_for_status()

            for chunk in response.iter_content(chunk_size=None, decode_unicode=True):
                if chunk:
                    full_answer += chunk
                    placeholder.markdown(full_answer)

    # Save final assistant response
    st.session_state.chat_history.append(
        {"role": "assistant", "content": full_answer}
    )
</code></pre>
<h2 id="heading-step-5-run-the-backend-app">Step 5: Run the Backend App</h2>
<p>Start the server with Uvicorn:</p>
<pre><code class="language-bash">uvicorn app:app --reload --port 8001
</code></pre>
<p>Once the application starts, open:</p>
<ul>
<li><p><code>http://127.0.0.1:8001/</code></p>
</li>
<li><p><code>http://127.0.0.1:8001/docs</code></p>
</li>
</ul>
<p>The <code>/docs</code> endpoint is automatically generated by FastAPI using your Pydantic models. It provides an interactive interface where you can test the API without writing any client code.</p>
<img src="https://cdn.hashnode.com/uploads/covers/684c95e159698b4bf6a0e4be/5cf32ff0-273c-47cd-80be-ebf807e4443d.png" alt="Api docs that was generated by FastAPI. It includes /chat/stream  endpoint and schema" style="display:block;margin:0 auto" width="2712" height="1034" loading="lazy">

<p>You can send requests directly from <code>curl</code>. In your terminal, run these commands to invoke the API for the AI agent and check the output:</p>
<pre><code class="language-bash">$ curl -X POST http://127.0.0.1:8001/chat/stream \
  -H "Content-Type: application/json" \
  -d '{"message":"What time is it?","user_id":"123e4567-e89b-12d3-a456-426614174000"}'

$ curl -X POST http://127.0.0.1:8001/chat/stream \
  -H "Content-Type: application/json" \
  -d '{"message":"How many words are in: LangChain makes tool calling easier","user_id":"123e4567-e89b-12d3-a456-426614174000"}'

$ curl -X POST "http://127.0.0.1:8001/chat/stream" \
-H "Content-Type: application/json" \
-d '{"message":"What is the capital of France?","user_id":"123e4567-e89b-12d3-a456-426614174000"}'
</code></pre>
<p>To stop the server, press Ctrl+C in the terminal.</p>
<h2 id="heading-step-6-run-the-frontend-app"><strong>Step 6: Run the Frontend App</strong></h2>
<p>In another terminal, go to the project directory:</p>
<pre><code class="language-plaintext">source venv/bin/activate
streamlit run streamlit_app.py
</code></pre>
<p>That opens the frontend in your browser at <code>http://localhost:8501/</code>. Try the example prompts like "What is the capital of France". You should see the answer in a chat style interface.</p>
<img src="https://cdn.hashnode.com/uploads/covers/684c95e159698b4bf6a0e4be/1030735a-49ed-43e1-995d-07b122c2c965.png" alt="Streamlit UI provides a simple chat frontend for the local AI agent" style="display:block;margin:0 auto" width="1848" height="1710" loading="lazy">

<p>The UI is calling the FastAPI endpoint and invoking the AI agent. You now have a working end to end application for your local AI agent that you can play with.</p>
<p>To stop the server, press Ctrl+C in the terminal.</p>
<h2 id="heading-sample-output">Sample Output</h2>
<p>The image below show two browser sessions of the app running side by side on the same endpoint. Each session is assigned a unique id, which allows the backend to maintain a separate conversation history for each user.</p>
<p>Even though both users ask the same question, “Who am I?”, the responses are different because each session’s answer is based on its own prior messages.</p>
<img src="https://cdn.hashnode.com/uploads/covers/684c95e159698b4bf6a0e4be/b97b8efa-6fca-4e80-9c0a-d0d2601fc2b6.png" alt="Image showing two sessions with the agent and it gives different answers based on the the conversation history" style="display:block;margin:0 auto" width="2914" height="1906" loading="lazy">

<h2 id="heading-what-to-improve-before-production">What to Improve Before Production</h2>
<p>Although this application is fully functional, it's still intentionally minimal. It already supports a reusable FastAPI backend, a Streamlit chat interface, per-user conversation history, and streaming responses.</p>
<p>If you wanted to take it further, the next steps would be adding authentication, persistent storage, structured logging, monitoring, and more robust deployment setup.</p>
<p>It's also worth noting that if your goal is simply to get a polished self-hosted chat UI up and running quickly, you may not need to build the frontend yourself. Projects like <a href="https://www.librechat.ai/">LibreChat</a> and <a href="https://docs.openwebui.com/">Open WebUI</a> already provide richer interfaces and broader features out of the box.</p>
<p>This tutorial takes a different approach: instead of adopting a full platform, it shows how to build a lightweight custom stack yourself so you can better understand the architecture and have more control over how the agent is exposed.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, we took a local AI agent, wrapped it in a FastAPI app, and used Streamlit UI on top of it.</p>
<p>This transforms the AI agent from a standalone script into a reusable service. Instead of only working in a terminal, it can now be accessed through a simple HTTP endpoint by other apps, scripts, or internal tools.</p>
<p>By assigning each session a unique id, the service can also maintain separate conversation history for multiple users, making it possible to support a chat-style interface with isolated memory per session.</p>
<p>From here, you can continue extending the same service by adding authentication or production-ready features. Happy tinkering!</p>
<p>If you enjoyed this tutorial, you can find more of my writing on my&nbsp;<a href="https://darshshah.org/blog/">blog</a>&nbsp;(recent posts include system design paper series), my work on my&nbsp;<a href="https://darshshah.org/">personal website</a>, and updates on&nbsp;<a href="https://www.linkedin.com/in/darshs">LinkedIn</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use Apple’s Foundation Models in a Web App with a macOS Companion ]]>
                </title>
                <description>
                    <![CDATA[ Not every AI feature needs a cloud model, with its per-token bills, network round-trips, and private data leaving your machine. If you're on a modern Mac, a capable language model is already on your d ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-apple-s-foundation-models-in-a-web-app-with-a-macos-companion/</link>
                <guid isPermaLink="false">6a5e92afe12aa31dae6e8a79</guid>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ macOS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Swift ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Balogun Wahab ]]>
                </dc:creator>
                <pubDate>Mon, 20 Jul 2026 21:27:11 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/7f0e2343-7394-46b5-a4c8-3ef0fecfa57a.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Not every AI feature needs a cloud model, with its per-token bills, network round-trips, and private data leaving your machine. If you're on a modern Mac, a capable language model is already on your disk.</p>
<p><strong>Foundation Models</strong> is Apple's Swift framework for working with large language models. It's the on-device model behind Apple Intelligence, Apple's Private Cloud Compute, or another provider's server model.</p>
<p>This tutorial targets the on-device model: you send it a prompt and it runs entirely on the Mac's own hardware locally, free-per-call, and offline-friendly.</p>
<p>Paired with Apple Vision for reading images on device, that's enough to build real AI features like summaries, classification, and structured extraction without the data ever leaving your machine.</p>
<h2 id="heading-table-of-contents">Table Of Contents</h2>
<ul>
<li><p><a href="#heading-what-you-will-build">What You Will Build</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-why-a-macos-companion-app">Why a macOS Companion App?</a></p>
</li>
<li><p><a href="#heading-foundation-models-cant-read-images-directly">Foundation Models Can't Read Images Directly</a></p>
</li>
<li><p><a href="#heading-project-structure">Project Structure</a></p>
</li>
<li><p><a href="#heading-build-the-react-app">Build the React App</a></p>
<ul>
<li><p><a href="#heading-check-companion-health">Check Companion Health</a></p>
</li>
<li><p><a href="#heading-convert-the-image-to-base64">Convert the Image to Base64</a></p>
</li>
<li><p><a href="#heading-analyze-immediately-after-upload">Analyze Immediately After Upload</a></p>
</li>
<li><p><a href="#heading-send-the-image-to-the-companion">Send the Image to the Companion</a></p>
</li>
<li><p><a href="#heading-render-the-json-output">Render the JSON Output</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-build-the-macos-companion-app">Build the macOS Companion App</a></p>
</li>
<li><p><a href="#heading-check-foundation-models-availability">Check Foundation Models Availability</a></p>
</li>
<li><p><a href="#heading-extract-text-with-apple-vision">Extract Text with Apple Vision</a></p>
</li>
<li><p><a href="#heading-ask-foundation-models-to-explain-the-vision-output">Ask Foundation Models to Explain the Vision Output</a></p>
</li>
<li><p><a href="#heading-return-json-to-the-browser">Return JSON to the Browser</a></p>
</li>
<li><p><a href="#heading-run-the-app">Run the App</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-resources">Resources</a></p>
</li>
</ul>
<h2 id="heading-what-you-will-build">What You Will Build</h2>
<p>You'll build <strong>Vision Bridge</strong>, a web app that sends an image to a local macOS companion. The companion reads the image with Apple Vision, reasons about it with Foundation Models, and returns structured JSON to the browser: private, on-device AI behind a plain web interface.</p>
<p>You can find the complete source code in this GitHub repository: <a href="http://github.com/03balogun/vision-bridge">github.com/03balogun/vision-bridge</a>.</p>
<p>The goal isn't to build a giant product but rather to understand the architecture behind how this works.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5db93b3da2342e8354088115/6d18db01-e921-4291-bb2e-26be2c02b304.png" alt="Screenshot of the Vision Bridge app, with image upload on the left and JSON output on the right" style="display:block;margin:0 auto" width="3024" height="1714" loading="lazy">

<p>Vision Bridge has two parts:</p>
<ul>
<li><p>A React app with a split-screen interface.</p>
</li>
<li><p>A macOS companion app that exposes a local API.</p>
</li>
</ul>
<p>The React app has:</p>
<ul>
<li><p>An image upload area</p>
</li>
<li><p>An image preview</p>
</li>
<li><p>Automatic analysis after upload</p>
</li>
<li><p>A JSON output viewer</p>
</li>
<li><p>A companion health status indicator</p>
</li>
</ul>
<p>The macOS companion app has:</p>
<ul>
<li><p><code>GET /v1/health</code></p>
</li>
<li><p><code>POST /v1/analyze-image</code></p>
</li>
<li><p>Apple Vision OCR</p>
</li>
<li><p>Foundation Models availability checks</p>
</li>
<li><p>Foundation Models reasoning over Vision output</p>
</li>
</ul>
<p>The final response looks like this:</p>
<pre><code class="language-json">{
  "support": {
    "visionAvailable": true,
    "foundationModelAvailable": true,
    "foundationModelStatus": "available"
  },
  "image": {
    "filename": "screenshot.png",
    "contentType": "image/png",
    "byteCount": 1048576,
    "width": 1440,
    "height": 900
  },
  "vision": {
    "detectedText": [
      {
        "text": "Build failed",
        "confidence": 0.96,
        "boundingBox": {
          "x": 0.12,
          "y": 0.31,
          "width": 0.45,
          "height": 0.08
        }
      }
    ]
  },
  "model": {
    "summary": "The image appears to show a software build failure.",
    "description": "A developer tool window is showing an error state with diagnostic text.",
    "suggestedTags": ["screenshot", "developer-tool", "error"],
    "possibleUses": [
      "Generate alt text",
      "Summarize screenshots",
      "Extract document data"
    ]
  }
}
</code></pre>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along, you need:</p>
<ul>
<li><p>macOS 26 or newer</p>
</li>
<li><p>Xcode with the macOS 26 SDK</p>
</li>
<li><p>Node.js 20 or newer</p>
</li>
<li><p>Basic React knowledge</p>
</li>
<li><p>Basic Swift knowledge</p>
</li>
<li><p>A Mac that supports Apple Intelligence</p>
</li>
</ul>
<p>Foundation Models availability depends on the Mac, the OS version, and Apple Intelligence settings. The companion checks this at runtime, which we'll cover below.</p>
<h2 id="heading-why-a-macos-companion-app">Why a macOS Companion App?</h2>
<p>You can't write this in a regular React app:</p>
<pre><code class="language-ts">import FoundationModels from "apple-frameworks";
</code></pre>
<p>That API doesn't exist in the browser. A native macOS app, however, can use any Apple framework, so the companion acts as a local bridge. The same pattern works for any native capability the web platform doesn't expose.</p>
<h2 id="heading-foundation-models-cant-read-images-directly">Foundation Models Can't Read Images Directly</h2>
<p>The public Foundation Models framework is a language model interface. It doesn't currently expose direct image input the way a multimodal cloud model might, so this tutorial never sends the image to the model. Instead, the companion feeds the Vision OCR observations and image metadata into the prompt. The model reasons over structured text, never the original pixels.</p>
<p>That split plays to each framework's strength: Vision is excellent at pulling machine-readable information out of images, and Foundation Models turns that information into summaries, labels, explanations, and structured output.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5db93b3da2342e8354088115/a5c11ad4-dcac-4690-bc6d-08b27fd6fed8.png" alt="Vision Bridge architecture: the browser sends the image over localhost to the Swift companion, which runs Apple Vision OCR, feeds the observations to Foundation Models, and returns structured JSON" style="display:block;margin:0 auto" width="2492" height="1572" loading="lazy">

<p>The above diagram shows the round trip that the rest of this tutorial builds. The browser sends the uploaded image as base64 JSON over localhost to the Swift companion. Inside the companion, Apple Vision runs OCR on the image and produces text observations: the recognized strings, their confidence scores, and their bounding boxes.</p>
<p>Those observations, not the image itself, are formatted into a prompt for Foundation Models, which generates a summary, description, and tags. The companion then bundles the Vision output and the model output into one JSON response and returns it to the browser.</p>
<h2 id="heading-project-structure">Project Structure</h2>
<p>Create a project with this structure:</p>
<pre><code class="language-text">vision-bridge/
  apps/
    web/
      src/
        main.tsx
        styles.css
      package.json
      vite.config.ts
    macos-companion/
      Package.swift
      Sources/
        VisionBridgeCompanion/
          main.swift
  package.json
  README.md
</code></pre>
<p>The root <code>package.json</code> gives us a few convenient commands:</p>
<pre><code class="language-json">{
  "scripts": {
    "dev": "npm --workspace apps/web run dev",
    "build": "npm --workspace apps/web run build",
    "companion": "swift run --package-path apps/macos-companion VisionBridgeCompanion"
  },
  "workspaces": ["apps/web"]
}
</code></pre>
<h2 id="heading-build-the-react-app">Build the React App</h2>
<p>The web app is intentionally simple. It has one job: let the user pick an image and show the JSON returned by the companion.</p>
<p>The web app uses Vite, React, Lucide icons, and a JSON viewer:</p>
<pre><code class="language-json">{
  "dependencies": {
    "@vitejs/plugin-react": "^6.0.3",
    "lucide-react": "^0.468.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-json-view-lite": "^2.5.0",
    "vite": "^8.1.3"
  }
}
</code></pre>
<p>After defining the dependencies, install them:</p>
<pre><code class="language-plaintext">npm install
</code></pre>
<p>The API base URL points to the local companion:</p>
<pre><code class="language-ts">const API_BASE_URL = "http://127.0.0.1:43119";
</code></pre>
<h3 id="heading-check-companion-health">Check Companion Health</h3>
<p>The web app pings the companion so the UI can show whether the native bridge is online:</p>
<pre><code class="language-ts">async function checkHealth() {
  setHealthError(null);

  try {
    const response = await fetch(`${API_BASE_URL}/v1/health`);
    if (!response.ok) {
      throw new Error(`Health check failed with ${response.status}`);
    }

    const payload = await response.json();
    setHealth(payload);
  } catch (error) {
    setHealth(null);
    setHealthError(error instanceof Error ? error.message : "Companion unavailable");
  }
}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/5db93b3da2342e8354088115/dc3c37eb-1d9c-4b44-82db-f38adada4f19.png" alt="Screenshot of the companion online status pill" style="display:block;margin:0 auto" width="732" height="212" loading="lazy">

<h3 id="heading-convert-the-image-to-base64">Convert the Image to Base64</h3>
<p>When the user selects a file, the app converts it to base64 so it can be sent as JSON:</p>
<pre><code class="language-ts">function readFileAsBase64(file: File) {
  return new Promise&lt;string&gt;((resolve, reject) =&gt; {
    const reader = new FileReader();
    reader.onload = () =&gt; {
      const result = String(reader.result);
      resolve(result.includes(",") ? result.split(",")[1] : result);
    };
    reader.onerror = () =&gt; reject(reader.error);
    reader.readAsDataURL(file);
  });
}
</code></pre>
<p>This isn't the only way to upload files. You could also use <code>multipart/form-data</code>, but JSON keeps the demo easy to inspect.</p>
<h3 id="heading-analyze-immediately-after-upload">Analyze Immediately After Upload</h3>
<p>The app starts analysis as soon as an image is uploaded:</p>
<pre><code class="language-ts">async function handleFile(file: File) {
  if (!file.type.startsWith("image/")) {
    setError("Choose a PNG, JPEG, HEIC, or another browser-readable image.");
    return;
  }

  const base64 = await readFileAsBase64(file);
  const nextImage = {
    file,
    previewUrl: URL.createObjectURL(file),
    base64,
  };

  setSelectedImage(nextImage);
  setAnalysis(null);
  setError(null);
  setCopied(false);

  analyzeImage(nextImage);
}
</code></pre>
<p><code>handleFile</code> does the preparation work for every new image. It rejects anything that isn't a browser-readable image, converts the file to base64, and builds a single object holding everything the rest of the flow needs: the original <code>File</code> (for its name and MIME type), an object URL for the preview, and the base64 payload for the API call.</p>
<p>It then clears out the previous run the old analysis, any error message, and the "copied" indicator so the UI never shows results from the last image next to a new one. Finally, it kicks off <code>analyzeImage(nextImage)</code> immediately.</p>
<p>Note that it passes the fresh object directly instead of relying on the <code>selectedImage</code> state: React state updates don't apply until the next render, so reading the state here would still give you the <em>previous</em> image.</p>
<p>The <code>Analyze</code> button still exists in the UI, but it works as a manual rerun button.</p>
<h3 id="heading-send-the-image-to-the-companion">Send the Image to the Companion</h3>
<p>Here's the core request:</p>
<pre><code class="language-ts">const analysisRequestId = useRef(0);

async function analyzeImage(image = selectedImage) {
  if (!image) {
    setError("Choose an image first.");
    return;
  }

  const requestId = analysisRequestId.current + 1;
  analysisRequestId.current = requestId;

  setRequestState("loading");
  setError(null);
  setCopied(false);

  try {
    const response = await fetch(`${API_BASE_URL}/v1/analyze-image`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        filename: image.file.name,
        mimeType: image.file.type || "application/octet-stream",
        base64: image.base64,
      }),
    });

    const payload = await response.json();

    if (requestId !== analysisRequestId.current) {
      return;
    }

    if (!response.ok) {
      throw new Error(payload.error?.message ?? `Analysis failed with ${response.status}`);
    }

    setAnalysis(payload);
    setRequestState("success");
  } catch (error) {
    if (requestId !== analysisRequestId.current) {
      return;
    }

    setRequestState("error");
    setError(error instanceof Error ? error.message : "Could not analyze image");
  }
}
</code></pre>
<p>This function is the entire client side of the bridge. It flips <code>requestState</code> to <code>loading</code> (which drives the spinner and disables the button), then sends a <code>POST</code> to <code>/v1/analyze-image</code> with a JSON body containing three fields: the filename, the MIME type, and the base64 image data. That body maps one-to-one onto the <code>AnalyzeImageRequest</code> struct the Swift companion decodes later.</p>
<p>Notice that the response is parsed as JSON <em>before</em> checking <code>response.ok</code>. That's deliberate: when the companion rejects a request (bad base64, oversized image), it still returns a JSON body with an <code>error.message</code> field, so the UI can show the companion's own explanation instead of a generic status code. On success, the payload goes straight into state, and the JSON viewer re-renders with the result.</p>
<p>The <code>requestId</code> bookkeeping guards against stale responses. If a user uploads a second image while the first is still analyzing, whichever request finishes <em>last</em> would win, and OCR plus model generation takes long enough that responses can genuinely arrive out of order. So every call increments a counter stored in a ref and remembers its own ID.</p>
<p>After the <code>await</code>, it checks whether it's still the newest request; if a newer upload started in the meantime, the older response is silently discarded instead of overwriting the latest image's result. The same check runs in the <code>catch</code> block, so an old failure can't clobber a newer success either. If you also want to cancel the in-flight HTTP request rather than just ignore its result, an <code>AbortController</code> is the natural next step.</p>
<h3 id="heading-render-the-json-output">Render the JSON Output</h3>
<p>The output pane uses <code>react-json-view-lite</code>:</p>
<pre><code class="language-tsx">&lt;JsonView
  data={jsonData}
  shouldExpandNode={allExpanded}
  style={jsonViewTheme}
/&gt;
</code></pre>
<h2 id="heading-build-the-macos-companion-app">Build the macOS Companion App</h2>
<p>The companion is a Swift command-line app. It exposes a small local HTTP API.</p>
<p>If you come from the web side, the mapping is simple: Swift Package Manager is Swift's npm, <code>Package.swift</code> is its <code>package.json</code>, and <code>swift run</code> is its <code>npm start</code>. It ships with Xcode, so there's nothing extra to install.</p>
<p>The <code>Package.swift</code> file looks like this:</p>
<pre><code class="language-swift">// swift-tools-version: 6.0

import PackageDescription

let package = Package(
    name: "VisionBridgeCompanion",
    platforms: [
        .macOS("26.0")
    ],
    products: [
        .executable(
            name: "VisionBridgeCompanion",
            targets: ["VisionBridgeCompanion"]
        )
    ],
    targets: [
        .executableTarget(
            name: "VisionBridgeCompanion"
        )
    ]
)
</code></pre>
<p>The companion imports the Apple frameworks it needs:</p>
<pre><code class="language-swift">import Foundation
import FoundationModels
import ImageIO
import Network
import Vision
</code></pre>
<p>It listens on <code>127.0.0.1:43119</code>:</p>
<pre><code class="language-swift">private let defaultPort: UInt16 = 43119
</code></pre>
<p>The app exposes two routes:</p>
<pre><code class="language-swift">switch (request.method, request.path) {
case ("GET", "/v1/health"):
    let health = HealthResponse(support: ModelSupport.current)
    return try json(health)

case ("POST", "/v1/analyze-image"):
    let payload = try JSONDecoder().decode(AnalyzeImageRequest.self, from: request.body)
    let response = try await service.analyze(payload)
    return try json(response)

default:
    return try json(
        ErrorResponse(error: APIErrorPayload(message: "Route not found")),
        status: .notFound
    )
}
</code></pre>
<p>This <code>switch</code> is the companion's entire routing layer — no web framework, just pattern matching on the method and path.</p>
<p>The two routes split the work cleanly:</p>
<ul>
<li><p><code>GET /v1/health</code> is the cheap, read-only route. It runs no analysis, it just reports whether Vision and Foundation Models are usable on this Mac via <code>ModelSupport.current</code> (covered in the next section). The React app calls it on load to render the online/offline status pill, so the user knows the bridge is up before they upload anything.</p>
</li>
<li><p><code>POST /v1/analyze-image</code> is where the real work happens. It decodes the request body into an <code>AnalyzeImageRequest</code> (with the same <code>filename</code>, <code>mimeType</code>, and <code>base64</code> fields the browser sent) and hands it to the analysis service. This validates the image, runs Vision OCR, prompts Foundation Models, and returns the combined result. The <code>try await</code> matters here: analysis is asynchronous, and the route simply waits for it before serializing the response.</p>
</li>
</ul>
<p>Anything else falls through to a JSON 404, so even unknown routes respond in the same format the browser already knows how to parse.</p>
<p>Errors work the same way: thrown errors are caught in one place and converted into JSON error responses with an appropriate status code, which is exactly what the web app's <code>payload.error?.message</code> check reads.</p>
<p>One practical detail: because the browser calls the companion from a different origin (the Vite dev server), every response also carries CORS headers, and the router answers preflight <code>OPTIONS</code> requests with an empty <code>204</code>. Without that, the browser would block the <code>fetch</code> before it ever reached these routes.</p>
<h2 id="heading-check-foundation-models-availability">Check Foundation Models Availability</h2>
<p>The companion shouldn't assume that the model is available. Check it first:</p>
<pre><code class="language-swift">private struct ModelSupport: Encodable {
    let visionAvailable: Bool
    let foundationModelAvailable: Bool
    let foundationModelStatus: String

    static var current: ModelSupport {
        let model = SystemLanguageModel.default

        switch model.availability {
        case .available:
            return ModelSupport(
                visionAvailable: true,
                foundationModelAvailable: true,
                foundationModelStatus: "available"
            )

        case .unavailable(let reason):
            return ModelSupport(
                visionAvailable: true,
                foundationModelAvailable: false,
                foundationModelStatus: "unavailable.\(reason.description)"
            )

        @unknown default:
            return ModelSupport(
                visionAvailable: true,
                foundationModelAvailable: false,
                foundationModelStatus: "unavailable.unknown"
            )
        }
    }
}
</code></pre>
<p>A user might have an unsupported Mac, Apple Intelligence might be disabled, or the model might not be ready yet. The response tells the browser which case it's dealing with.</p>
<h2 id="heading-extract-text-with-apple-vision">Extract Text with Apple Vision</h2>
<p>The companion decodes the base64 image, checks its metadata, then runs Vision OCR.</p>
<p>Here's the text recognition flow:</p>
<pre><code class="language-swift">private func recognizeText(in imageData: Data) async throws -&gt; [DetectedText] {
    var request = RecognizeTextRequest()
    request.recognitionLevel = .accurate
    request.automaticallyDetectsLanguage = true
    request.usesLanguageCorrection = true

    let observations = try await request.perform(on: imageData)

    var detectedText: [DetectedText] = []

    for observation in observations {
        guard let candidate = observation.topCandidates(1).first else {
            continue
        }

        let bounds = NormalizedBox.from(points: [
            observation.topLeft,
            observation.topRight,
            observation.bottomRight,
            observation.bottomLeft
        ])

        detectedText.append(DetectedText(
            text: candidate.string,
            confidence: Double(candidate.confidence),
            boundingBox: bounds
        ))
    }

    return detectedText
}
</code></pre>
<p>Vision gives us structured observations:</p>
<ul>
<li><p>recognized text</p>
</li>
<li><p>confidence scores</p>
</li>
<li><p>normalized bounding boxes</p>
</li>
</ul>
<p>Those observations become the model’s context.</p>
<h2 id="heading-ask-foundation-models-to-explain-the-vision-output">Ask Foundation Models to Explain the Vision Output</h2>
<p>Now the companion creates a prompt from the image metadata and OCR results.</p>
<p>Notice the instruction:</p>
<pre><code class="language-text">You cannot see the original image. Use only the metadata and OCR observations below.
</code></pre>
<p>That keeps the model honest. It shouldn't pretend to see pixels it never received.</p>
<p>Here's the prompt shape:</p>
<pre><code class="language-swift">let textPreview = detectedText
    .prefix(30)
    .map { "- \($0.text) (confidence: \(String(format: "%.2f", $0.confidence)))" }
    .joined(separator: "\n")

let prompt = """
You are summarizing Apple Vision OCR output for a developer tool named Vision Bridge.
You cannot see the original image. Use only the metadata and OCR observations below.

Image:
- filename: \(image.filename)
- content type: \(image.contentType)
- size: \(image.width ?? 0)x\(image.height ?? 0)

OCR observations:
\(textPreview.isEmpty ? "- No text detected." : textPreview)

Return a compact JSON object with these exact keys:
summary: one sentence
description: one short paragraph
suggestedTags: 3 to 6 short tags
possibleUses: 3 to 5 practical use cases for this kind of image analysis
"""
</code></pre>
<p>Then call the model:</p>
<pre><code class="language-swift">let session = LanguageModelSession(
    model: .default,
    instructions: "Return valid JSON only. Do not include Markdown fences."
)

let response = try await session.respond(to: prompt)
let raw = response.content.trimmingCharacters(in: .whitespacesAndNewlines)
</code></pre>
<p>Even when you ask for JSON, always validate the output. Models can still return Markdown fences or malformed text. The sample app strips simple Markdown code fences and falls back to a raw response if parsing fails.</p>
<h2 id="heading-return-json-to-the-browser">Return JSON to the Browser</h2>
<p>The companion combines the support state, image metadata, Vision results, and model output:</p>
<pre><code class="language-swift">return AnalyzeImageResponse(
    support: support,
    image: metadata,
    vision: VisionPayload(detectedText: detectedText),
    model: modelInsight
)
</code></pre>
<p>The browser doesn't need to know how Vision or Foundation Models work. It just receives JSON. The native app owns the native capabilities, while the web app owns the interface.</p>
<p>It's worth pausing on what each of the four blocks actually gives you, because they're not all the same kind of data:</p>
<ul>
<li><p><code>support</code> tells you what was possible on this Mac. If <code>foundationModelAvailable</code> is <code>false</code>, the <code>model</code> block still exists but contains a fallback message rather than real analysis, and the <code>foundationModelStatus</code> string (for example, <code>unavailable.appleIntelligenceNotEnabled</code>) tells the UI <em>why</em>, so it can explain rather than silently degrade.</p>
</li>
<li><p><code>image</code> echoes back the file's metadata plus the measured pixel dimensions. It's useful as a sanity check, and you need the width and height to do anything spatial with the Vision results.</p>
</li>
<li><p><code>vision</code> is the ground truth. Each entry in <code>detectedText</code> is a string Vision actually found, with a confidence score between 0 and 1 and a normalized bounding box: coordinates expressed as fractions of the image size, so <code>x: 0.12, width: 0.45</code> means "starts 12% from the left and spans 45% of the width." Because the boxes are normalized, you can draw highlight overlays on the preview at any display size by multiplying by the rendered dimensions. Low-confidence entries are worth filtering or flagging before you trust them.</p>
</li>
<li><p><code>model</code> is interpretation, not observation. The <code>summary</code>, <code>description</code>, <code>suggestedTags</code>, and <code>possibleUses</code> fields are generated by the language model from the OCR text. This is useful as alt text, captions, or tag suggestions, but they inherit whatever the OCR missed and should be treated as a draft, not a fact. When the model's output can't be parsed as JSON, <code>rawResponse</code> carries the unparsed text so nothing is lost.</p>
</li>
</ul>
<p>For a screenshot of a failed build, the model block might come back like this:</p>
<pre><code class="language-json">{
  "model": {
    "summary": "The image appears to show a software build failure.",
    "description": "A developer tool window is showing an error state with diagnostic text.",
    "suggestedTags": ["screenshot", "developer-tool", "error"],
    "possibleUses": [
      "Generate alt text",
      "Summarize screenshots",
      "Extract document data"
    ]
  }
}
</code></pre>
<p>That combination (exact text with positions from Vision, plus a human-readable interpretation from the model) is enough to build real features on top of a searchable screenshot library indexed by <code>detectedText</code> and <code>suggestedTags</code>, automatic alt text for uploaded images, or click-to-highlight overlays powered by the bounding boxes.</p>
<p>And because the prompt lives in the companion, changing what comes back (say, extracting line items from receipts instead of tagging screenshots) is a prompt edit, not an architecture change.</p>
<h2 id="heading-run-the-app">Run the App</h2>
<p>Start the companion:</p>
<pre><code class="language-sh">npm run companion
</code></pre>
<p>In another terminal, start the web app:</p>
<pre><code class="language-sh">npm run dev
</code></pre>
<p>Open the Vite URL:</p>
<pre><code class="language-text">http://127.0.0.1:5173
</code></pre>
<p>If that port is busy, Vite will choose another one.</p>
<p>The companion should be available at:</p>
<pre><code class="language-text">http://127.0.0.1:43119
</code></pre>
<p>You can test it directly:</p>
<pre><code class="language-sh">curl http://127.0.0.1:43119/v1/health
</code></pre>
<p>Expected response:</p>
<pre><code class="language-json">{
  "app": "Vision Bridge Companion",
  "ok": true,
  "support": {
    "foundationModelAvailable": true,
    "foundationModelStatus": "available",
    "visionAvailable": true
  },
  "version": "0.1.0"
}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/5db93b3da2342e8354088115/76a47c9a-934c-4133-ba7c-e2a9c6b6dad4.png" alt="Screenshot of terminal running companion" style="display:block;margin:0 auto" width="1448" height="556" loading="lazy">

<h2 id="heading-conclusion">Conclusion</h2>
<p>You now have a React interface that uploads an image, a Swift companion that analyzes it with Apple-native frameworks, and structured JSON flowing between them.</p>
<p>Vision Bridge is intentionally small, but the bridge itself is reusable. Once you have a trusted native companion, a web app can do more than send prompts to a remote model: it can ask the Mac to work with local context, use any Apple framework, and return structured data the browser can render, store, or sync.</p>
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p><a href="https://developer.apple.com/documentation/foundationmodels">Apple Foundation Models documentation</a></p>
</li>
<li><p><a href="https://developer.apple.com/documentation/vision">Apple Vision documentation</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Optimize Enterprise Application Performance with T-SQL Query Tuning and Indexing Strategies ]]>
                </title>
                <description>
                    <![CDATA[ In this article, you'll learn how to optimize SQL Server performance using T-SQL query tuning, indexing strategies, execution plans, and real-world optimization techniques for enterprise applications. ]]>
                </description>
                <link>https://www.freecodecamp.org/news/optimize-enterprise-app-performance-with-t-sql-query-tuning-and-indexing-strategies/</link>
                <guid isPermaLink="false">6a5e89d9dfc3c99e37bbdef0</guid>
                
                    <category>
                        <![CDATA[ SQL Query Performance ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Enterprise T-SQL ]]>
                    </category>
                
                    <category>
                        <![CDATA[ SQL Query Tuning ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Query Performance ]]>
                    </category>
                
                    <category>
                        <![CDATA[ SQL Performance Tuning ]]>
                    </category>
                
                    <category>
                        <![CDATA[ SQL Server Execution Plan ]]>
                    </category>
                
                    <category>
                        <![CDATA[ optimization ]]>
                    </category>
                
                    <category>
                        <![CDATA[ performance ]]>
                    </category>
                
                    <category>
                        <![CDATA[ SQL ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Databases ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Gopinath Karunanithi ]]>
                </dc:creator>
                <pubDate>Mon, 20 Jul 2026 20:49:29 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/1df4fdd1-4c5d-4f0b-a0e6-f40a75de6a8e.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this article, you'll learn how to optimize SQL Server performance using T-SQL query tuning, indexing strategies, execution plans, and real-world optimization techniques for enterprise applications.</p>
<p>Slow SQL queries are one of the biggest bottlenecks in enterprise applications. This guide demonstrates how to analyze execution plans, design effective indexes, rewrite inefficient T-SQL queries, optimize joins and aggregations, and monitor performance using SQL Server tools.</p>
<p>By working through several practical examples, you'll learn how to build faster, scalable, and more maintainable SQL Server workloads.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a href="#heading-introduction">Introduction</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-why-query-performance-matters-in-enterprise-applications">Why Query Performance Matters in Enterprise Applications</a></p>
</li>
<li><p><a href="#heading-how-sql-server-executes-queries">How SQL Server Executes Queries</a></p>
</li>
<li><p><a href="#heading-understanding-execution-plans">Understanding Execution Plans</a></p>
</li>
<li><p><a href="#heading-common-execution-plan-operators">Common Execution Plan Operators</a></p>
</li>
<li><p><a href="#heading-finding-slow-queries">Finding Slow Queries</a></p>
</li>
<li><p><a href="#heading-writing-efficient-where-clauses">Writing Efficient WHERE Clauses</a></p>
</li>
<li><p><a href="#heading-optimizing-join-operations">Optimizing JOIN Operations</a></p>
</li>
<li><p><a href="#heading-optimizing-aggregations">Optimizing Aggregations</a></p>
</li>
<li><p><a href="#heading-common-table-expressions-vs-temporary-tables">Common Table Expressions vs. Temporary Tables</a></p>
</li>
<li><p><a href="#heading-avoiding-common-t-sql-performance-anti-patterns">Avoiding Common T-SQL Performance Anti-Patterns</a></p>
</li>
<li><p><a href="#heading-measuring-before-and-after-optimization">Measuring Before and After Optimization</a></p>
</li>
<li><p><a href="#heading-monitoring-query-performance">Monitoring Query Performance</a></p>
</li>
<li><p><a href="#heading-real-world-example-optimizing-a-reporting-query">Real-World Example: Optimizing a Reporting Query</a></p>
</li>
<li><p><a href="#heading-when-not-to-optimize-prematurely">When NOT to Optimize Prematurely</a></p>
</li>
<li><p><a href="#heading-best-practices-for-enterprise-t-sql-optimization">Best Practices for Enterprise T-SQL Optimization</a></p>
</li>
<li><p><a href="#heading-future-trends-in-sql-performance-optimization">Future Trends in SQL Performance Optimization</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-introduction"><strong>Introduction</strong></h2>
<p>Enterprise application performance often depends more on the database than the application itself. Whether you're building with ASP.NET Core, Java Spring Boot, or Node.js, inefficient database queries can lead to slow API responses, page load delays, timeout errors, and increased infrastructure costs.</p>
<p>While adding CPU, memory, or database replicas may temporarily improve performance, the root cause is often inefficient T-SQL queries, poorly designed indexes, outdated statistics, or suboptimal execution plans. Since the same queries may execute thousands of times per minute, even small optimizations can significantly reduce latency and resource consumption.</p>
<p>In enterprise environments, where databases often contain millions of records and support highly concurrent workloads, query tuning becomes essential for maintaining scalability and responsiveness.</p>
<p>In this article, you'll learn how SQL Server executes queries, how to analyze execution plans, optimize T-SQL, design effective indexing strategies, and apply practical techniques to improve database performance in real-world applications.</p>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<p>To get the most from this tutorial, you should be familiar with:</p>
<ul>
<li><p>Basic SQL and T-SQL syntax</p>
</li>
<li><p>Microsoft SQL Server fundamentals</p>
</li>
<li><p>Primary keys and foreign keys</p>
</li>
<li><p>Basic understanding of indexes</p>
</li>
<li><p>SQL Server Management Studio (SSMS) or Azure Data Studio</p>
</li>
<li><p>Basic knowledge of relational database concepts</p>
</li>
</ul>
<h2 id="heading-why-query-performance-matters-in-enterprise-applications"><strong>Why Query Performance Matters in Enterprise Applications</strong></h2>
<p>Database performance directly affects every layer of an enterprise application. Even if the frontend is highly optimized and the application servers are properly scaled, slow database operations quickly become the limiting factor.</p>
<p>Consider a typical enterprise architecture:</p>
<img src="https://cdn.hashnode.com/uploads/covers/695f02b68a3eda4408ac22af/bfd4d87f-dc93-4411-a066-1375a4344d6d.png" alt="A high-level system architecture diagram illustrating the application flow from the Client to the ASP.NET Core API, then to the Business Services layer, and finally to the SQL Server Database. The components are connected sequentially with arrows, representing the request and data flow through the application layers." style="display:block;margin:0 auto" width="754" height="119" loading="lazy">

<p>Figure 1. High-Level Architecture Showing Client, ASP.NET Core API, Business Services, and SQL Server Database</p>
<p>Figure 1 illustrates the high-level architecture of a typical ASP.NET Core application. Client requests are received by the ASP.NET Core API, which serves as the application's entry point. The API forwards these requests to the Business Services layer, where the core business logic is executed. The Business Services layer then interacts with the SQL Server Database to retrieve or persist data. The sequential flow of requests through these components is represented by the arrows in the diagram.</p>
<p>Although this architecture separates responsibilities and improves maintainability, its overall performance is often constrained by the database. Every request that requires data eventually reaches the SQL Server database. If the database responds slowly, every upstream component (including the Business Services layer, the API, and ultimately the client) must wait for the query to complete.</p>
<p>Consider an order management system in which a dashboard displays customer information, recent orders, invoices, inventory levels, and shipment status. Loading this dashboard may require several independent database queries. While these queries may execute concurrently, the user perceives the combined response time. Consequently, even a small number of poorly optimized queries can significantly increase page load times and degrade the overall user experience.</p>
<p>As database size and application usage grow, performance issues often become increasingly apparent.</p>
<p>Common symptoms include:</p>
<ul>
<li><p>APIs that gradually become slower as data grows</p>
</li>
<li><p>High CPU utilization on the SQL Server</p>
</li>
<li><p>Excessive disk I/O</p>
</li>
<li><p>Blocking between concurrent transactions</p>
</li>
<li><p>Deadlocks during peak usage</p>
</li>
<li><p>Timeout exceptions in application logs</p>
</li>
</ul>
<p>Many of these problems originate from inefficient SQL rather than insufficient hardware.</p>
<p>For example, suppose a customer table contains ten million records. Searching for customers by email without an appropriate index forces SQL Server to examine every row.</p>
<pre><code class="language-sql">SELECT *
FROM Customers
WHERE Email = 'john@example.com';
</code></pre>
<p>Without an index on the Email column, SQL Server performs a table scan, reading every page before locating the desired row.</p>
<p>Adding a properly designed index transforms the same query into an index seek, allowing SQL Server to locate the record almost immediately.</p>
<p>As enterprise datasets continue growing, these differences become increasingly significant.</p>
<h2 id="heading-how-sql-server-executes-queries"><strong>How SQL Server Executes Queries</strong></h2>
<p>Understanding SQL Server's execution process is essential before attempting optimization.</p>
<p>Every query passes through several stages before data is returned.</p>
<h3 id="heading-step-1-parsing">Step 1: Parsing</h3>
<p>SQL Server first validates the syntax.</p>
<pre><code class="language-sql">SELECT Name
FROM Customers;
</code></pre>
<p>If the statement contains syntax errors, execution stops immediately.</p>
<h3 id="heading-step-2-binding">Step 2: Binding</h3>
<p>Next, SQL Server verifies that referenced tables, columns, functions, and objects exist.</p>
<p>For example,</p>
<pre><code class="language-sql">SELECT CustomerName
FROM Customers;
</code></pre>
<p>If CustomerName doesn't exist, SQL Server reports an error before optimization begins.</p>
<h3 id="heading-step-3-query-optimization">Step 3: Query Optimization</h3>
<p>The SQL Server Query Optimizer evaluates multiple possible execution strategies.</p>
<p>It estimates the cost of various approaches, including table scans, index seeks, different join algorithms, parallel execution, and sorting methods.</p>
<p>The optimizer chooses the plan with the lowest estimated cost based on available statistics.</p>
<p>Importantly, developers don't tell SQL Server <em>how</em> to execute a query. They specify <em>what</em> data they need.</p>
<h3 id="heading-step-4-execution-plan-generation">Step 4: Execution Plan Generation</h3>
<p>The optimizer then generates an execution plan.</p>
<p>The execution plan acts as a blueprint describing every operation required to satisfy the query.</p>
<p>For example:</p>
<pre><code class="language-sql">SELECT *
FROM Orders
WHERE CustomerID = 1250;
</code></pre>
<p>Depending on available indexes, SQL Server may choose either clustered Index Seek, Nonclustered Index Seek, Index Scan, or Table Scan. Understanding these operators is the foundation of effective tuning.</p>
<h2 id="heading-understanding-execution-plans"><strong>Understanding Execution Plans</strong></h2>
<p>Execution plans reveal how SQL Server actually processes a query. Rather than guessing why a query performs poorly, execution plans identify the most expensive operations directly.</p>
<p>SQL Server provides two primary plan types:</p>
<ol>
<li><p><strong>Estimated Execution Plan:</strong> Generated without executing the query. It predicts the optimizer's chosen strategy using available statistics.</p>
</li>
<li><p><strong>Actual Execution Plan:</strong> Generated after the query runs, showing the real execution path along with runtime statistics such as row counts and operator costs.</p>
</li>
</ol>
<p>For performance tuning, the actual execution plan is generally more valuable because it exposes differences between estimated and actual behavior.</p>
<p>In SQL Server Management Studio, you can enable the actual execution plan by selecting <strong>Include Actual Execution Plan</strong> before running your query.</p>
<h2 id="heading-common-execution-plan-operators"><strong>Common Execution Plan Operators</strong></h2>
<p>Understanding a handful of common operators makes execution plans much easier to interpret.</p>
<h3 id="heading-table-scan">Table Scan</h3>
<p>A table scan reads every row in a table.</p>
<p><code>Customers ──► Table Scan</code></p>
<p>This is acceptable for small lookup tables but becomes increasingly expensive as tables grow.</p>
<h3 id="heading-index-scan">Index Scan</h3>
<p>An index scan reads every entry within an index.</p>
<p>Although better than scanning the full table, it still processes every index page.</p>
<p><strong>Index Seek</strong></p>
<p>An index seek navigates directly to matching rows.</p>
<p>CustomerID Index<br>&nbsp; &nbsp; &nbsp; &nbsp; │<br>&nbsp; &nbsp; &nbsp; &nbsp; ▼<br>&nbsp; Index Seek</p>
<p>This is generally the most efficient access method for selective queries.</p>
<h3 id="heading-nested-loop-join">Nested Loop Join</h3>
<p>Nested Loop joins perform well when one input contains relatively few rows.</p>
<p>Customers<br>&nbsp; │<br>&nbsp; ▼<br>Nested Loop<br>&nbsp; ▲</p>
<p>│<br>Orders</p>
<p>They're commonly used for OLTP workloads.</p>
<h3 id="heading-hash-match">Hash Match</h3>
<p>Hash joins excel when processing large datasets with no useful indexes. But keep in mind that they consume more memory and may spill to disk if insufficient memory is available.</p>
<h3 id="heading-merge-join">Merge Join</h3>
<p>Merge joins require sorted inputs but can process large result sets efficiently. They're often selected when both datasets are already indexed appropriately.</p>
<h3 id="heading-key-lookup">Key Lookup</h3>
<p>One operator that frequently surprises developers is the <strong>Key Lookup</strong>.</p>
<p>Suppose an index contains only the <code>CustomerID</code> column, but the query also requests <code>Address</code> and <code>PhoneNumber</code>.</p>
<p>SQL Server first performs an Index Seek to locate matching rows, then executes additional lookups against the clustered index to retrieve missing columns.</p>
<p>Although acceptable for a few rows, thousands of key lookups can significantly degrade performance.</p>
<p>In many cases, creating a covering index eliminates these extra lookups entirely. This is a topic we'll explore later in the article.</p>
<h2 id="heading-finding-slow-queries"><strong>Finding Slow Queries</strong></h2>
<p>SQL Server provides several built-in tools that help locate performance bottlenecks in production environments.</p>
<h3 id="heading-using-query-store">Using Query Store</h3>
<p>Query Store records query history, execution plans, runtime statistics, and performance trends over time. Rather than relying on temporary monitoring sessions, it continuously captures valuable performance information, making it one of the most useful features for enterprise SQL Server deployments.</p>
<p>For example, if an application suddenly becomes slower after a deployment, Query Store can compare execution plans before and after the change to determine whether the optimizer selected a less efficient plan.</p>
<p>Typical metrics available include:</p>
<ul>
<li><p>Average execution time</p>
</li>
<li><p>CPU consumption</p>
</li>
<li><p>Logical reads</p>
</li>
<li><p>Execution count</p>
</li>
<li><p>Query plan history</p>
</li>
</ul>
<p>This historical view helps identify regressions that may otherwise be difficult to reproduce.</p>
<h3 id="heading-using-dynamic-management-views-dmvs">Using Dynamic Management Views (DMVs)</h3>
<p>Dynamic Management Views expose internal SQL Server performance information while the server is running.</p>
<p>One commonly used DMV is:</p>
<pre><code class="language-sql">SELECT TOP 10
    qs.execution_count,
    qs.total_worker_time,
    qs.total_elapsed_time,
    SUBSTRING(
        qt.text,
        qs.statement_start_offset / 2,
        (
            CASE
                WHEN qs.statement_end_offset = -1
                THEN LEN(CONVERT(NVARCHAR(MAX), qt.text)) * 2
                ELSE qs.statement_end_offset
            END - qs.statement_start_offset
        ) / 2
    ) AS QueryText
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
ORDER BY qs.total_worker_time DESC;
</code></pre>
<p>This query identifies statements consuming the most CPU time, helping prioritize optimization efforts.</p>
<h3 id="heading-measuring-io-and-execution-time">Measuring I/O and Execution Time</h3>
<p>SQL Server also provides lightweight commands for measuring query performance.</p>
<pre><code class="language-sql">SET STATISTICS IO ON;
SET STATISTICS TIME ON;
</code></pre>
<p>After enabling these options, executing a query displays additional information such as:</p>
<ul>
<li><p>Logical reads</p>
</li>
<li><p>Physical reads</p>
</li>
<li><p>CPU time</p>
</li>
<li><p>Total elapsed time</p>
</li>
</ul>
<p>Consider the following query:</p>
<pre><code class="language-sql">SELECT *
FROM Orders
WHERE CustomerID = 1025;
</code></pre>
<p>The output might resemble:</p>
<p><code>Table 'Orders'.</code></p>
<p><code>Logical reads: 4832</code></p>
<p>SQL Server Execution Times:<br><code>CPU time = 215 ms</code></p>
<p><code>Elapsed time = 287 ms</code></p>
<p>After adding an appropriate index, the same query could produce:</p>
<p><code>Logical reads: 6</code></p>
<p><code>CPU time = 3 ms</code></p>
<p><code>Elapsed time = 5 ms</code></p>
<p>These measurements provide objective evidence that an optimization has improved performance.</p>
<h2 id="heading-writing-efficient-where-clauses"><strong>Writing Efficient WHERE Clauses</strong></h2>
<p>One of the simplest ways to improve query performance is to write <strong>SARGable</strong> predicates. A query is considered SARGable (Search ARGument Able) when SQL Server can efficiently use an index to locate matching rows.</p>
<p>Many developers unintentionally prevent index usage by applying functions directly to indexed columns.</p>
<p>Consider this example:</p>
<pre><code class="language-sql">SELECT *
FROM Orders
WHERE YEAR(OrderDate) = 2025;
</code></pre>
<p>Although the logic is correct, SQL Server must evaluate the YEAR() function for every row before performing the comparison. As a result, it can't efficiently seek into an index on <code>OrderDate</code>.</p>
<p>A better approach is to compare the column directly.</p>
<pre><code class="language-sql">SELECT *
FROM Orders
WHERE OrderDate &gt;= '2025-01-01'
AND OrderDate &lt; '2026-01-01';
</code></pre>
<p>This version allows SQL Server to perform an index seek rather than scanning the entire table.</p>
<p>Similarly, avoid implicit data type conversions.</p>
<p>Instead of:</p>
<p><code>WHERE CustomerID = '100'</code></p>
<p>prefer:</p>
<p><code>WHERE CustomerID = 100</code></p>
<p>Matching the column's data type eliminates unnecessary conversions during query execution.</p>
<h2 id="heading-optimizing-join-operations"><strong>Optimizing JOIN Operations</strong></h2>
<p>Enterprise applications rarely query a single table. Most business operations involve combining data from multiple related tables, making joins one of the most important optimization areas.</p>
<p>Consider an order management system:</p>
<pre><code class="language-sql">SELECT
    c.Name,
    o.OrderDate,
    o.TotalAmount
FROM Customers c
INNER JOIN Orders o
    ON c.CustomerID = o.CustomerID;
</code></pre>
<p>When both <code>CustomerID</code> columns are indexed, SQL Server can efficiently join the tables.</p>
<p>But poor indexing often forces SQL Server to scan one or both tables, dramatically increasing execution time.</p>
<h4 id="heading-exists-vs-in"><code>EXISTS</code> vs. <code>IN</code></h4>
<p>Another common optimization involves replacing <code>IN</code> with <code>EXISTS</code> for large subqueries.</p>
<p>Less efficient:</p>
<pre><code class="language-sql">SELECT *
FROM Customers
WHERE CustomerID IN (
    SELECT CustomerID
    FROM Orders
);
</code></pre>
<p>Better:</p>
<pre><code class="language-sql">SELECT *
FROM Customers c
WHERE EXISTS (
    SELECT 1
    FROM Orders o
    WHERE o.CustomerID = c.CustomerID
);
</code></pre>
<p>For correlated lookups involving large datasets, <code>EXISTS</code> often enables more efficient execution plans.</p>
<h4 id="heading-eliminate-unnecessary-joins">Eliminate Unnecessary Joins</h4>
<p>Sometimes queries include tables whose data is never used.</p>
<p>For example:</p>
<pre><code class="language-sql">SELECT
    o.OrderID,
    c.Name
FROM Orders o
INNER JOIN Customers c
    ON o.CustomerID = c.CustomerID
INNER JOIN Regions r
    ON c.RegionID = r.RegionID;
</code></pre>
<p>If no columns from Regions are selected or filtered, removing the join reduces unnecessary work and simplifies the execution plan.</p>
<h2 id="heading-optimizing-aggregations"><strong>Optimizing Aggregations</strong></h2>
<p>Aggregations become increasingly expensive as datasets grow. Reporting systems frequently summarize millions of rows using functions such as <code>SUM()</code>, <code>COUNT()</code>, <code>AVG()</code>, and <code>MAX()</code>.</p>
<p>A straightforward aggregation might look like this:</p>
<pre><code class="language-sql">SELECT
    CustomerID,
    SUM(TotalAmount)
FROM Orders
GROUP BY CustomerID;
</code></pre>
<p>Although simple, performance depends heavily on indexing and data distribution.</p>
<p>If the query repeatedly scans millions of rows, consider creating an index on <code>CustomerID</code>.</p>
<p>Window functions often provide a cleaner alternative to complex subqueries.</p>
<p>For example, identifying each customer's most recent order:</p>
<pre><code class="language-sql">SELECT
    CustomerID,
    OrderDate,
    ROW_NUMBER() OVER (
        PARTITION BY CustomerID
        ORDER BY OrderDate DESC
    ) AS RowNum
FROM Orders;
</code></pre>
<p>Window functions allow SQL Server to calculate rankings and running totals without complicated self-joins.</p>
<p>Whenever possible, avoid unnecessary sorting operations, since sorting large result sets consumes considerable CPU and memory.</p>
<h2 id="heading-common-table-expressions-vs-temporary-tables"><strong>Common Table Expressions vs. Temporary Tables</strong></h2>
<p>Both Common Table Expressions (CTEs) and temporary tables help simplify complex queries, but they serve different purposes.</p>
<p>A CTE provides a readable way to structure intermediate query logic.</p>
<pre><code class="language-sql">WITH RecentOrders AS
(
    SELECT *
    FROM Orders
    WHERE OrderDate &gt;= DATEADD(DAY, -30, GETDATE())
)
SELECT *
FROM RecentOrders;
</code></pre>
<p>CTEs improve readability and maintainability but aren't materialized automatically. SQL Server may execute the underlying logic multiple times depending on the execution plan.</p>
<p>Temporary tables, on the other hand, physically store intermediate results.</p>
<pre><code class="language-sql">SELECT *
INTO #RecentOrders
FROM Orders
WHERE OrderDate &gt;= DATEADD(DAY, -30, GETDATE());

SELECT *
FROM #RecentOrders;
</code></pre>
<p>Temporary tables become particularly useful when:</p>
<ul>
<li><p>Intermediate results are reused multiple times</p>
</li>
<li><p>Large datasets need additional indexing</p>
</li>
<li><p>Complex joins benefit from breaking queries into stages</p>
</li>
</ul>
<p>Choosing between the two depends on workload characteristics rather than personal preference.</p>
<h2 id="heading-avoiding-common-t-sql-performance-anti-patterns">Avoiding Common T-SQL Performance Anti-Patterns</h2>
<p>Many performance issues stem from common coding habits rather than complex database problems.</p>
<h3 id="heading-avoid-select">Avoid <code>SELECT *</code></h3>
<p>Fetching every column increases network traffic, memory consumption, and I/O.</p>
<p>Instead of this:</p>
<pre><code class="language-sql">SELECT *
FROM Customers;
</code></pre>
<p>Retrieve only the required columns:</p>
<pre><code class="language-sql">SELECT
    CustomerID,
    Name,
    Email
FROM Customers;
</code></pre>
<p>This reduces both data transfer and execution costs.</p>
<h3 id="heading-avoid-scalar-functions-in-where-clauses">Avoid Scalar Functions in <code>WHERE</code> Clauses</h3>
<p>Scalar functions execute once per row, preventing efficient index usage.</p>
<p>Instead of:</p>
<pre><code class="language-sql">WHERE UPPER(Name) = 'JOHN'
</code></pre>
<p>store normalized values or use case-insensitive collations where appropriate.</p>
<h3 id="heading-avoid-cursors-for-row-by-row-processing">Avoid Cursors for Row-by-Row Processing</h3>
<p>Cursors process records sequentially.</p>
<pre><code class="language-sql">DECLARE CustomerCursor CURSOR
FOR
SELECT CustomerID
FROM Customers;
</code></pre>
<p>Although sometimes necessary, cursor-based solutions rarely scale well for enterprise workloads.</p>
<p>Most cursor logic can be rewritten using set-based operations.</p>
<p>For example:</p>
<p>Instead of updating rows individually:</p>
<pre><code class="language-sql">UPDATE Customers
SET Status = 'Active'
WHERE LastLogin &gt;= DATEADD(DAY, -30, GETDATE());
</code></pre>
<p>SQL Server processes the entire set efficiently rather than iterating row by row.</p>
<h3 id="heading-reduce-correlated-subqueries">Reduce Correlated Subqueries</h3>
<p>Correlated subqueries execute repeatedly for each outer row.</p>
<p>For example:</p>
<pre><code class="language-sql">SELECT
    CustomerID,
    (
        SELECT COUNT(*)
        FROM Orders o
        WHERE o.CustomerID = c.CustomerID
    ) AS OrderCount
FROM Customers c;
</code></pre>
<p>Rewriting this using joins and aggregation often produces more efficient execution plans.</p>
<pre><code class="language-sql">SELECT
    c.CustomerID,
    COUNT(o.OrderID) AS OrderCount
FROM Customers c
LEFT JOIN Orders o
    ON c.CustomerID = o.CustomerID
GROUP BY c.CustomerID;
</code></pre>
<p>The rewritten version allows SQL Server to process the data in a single pass rather than executing thousands of nested queries.</p>
<h2 id="heading-measuring-before-and-after-optimization"><strong>Measuring Before and After Optimization</strong></h2>
<p>Effective tuning always follows the same cycle:</p>
<ol>
<li><p>Measure the original query using Query Store or SET STATISTICS.</p>
</li>
<li><p>Analyze the execution plan.</p>
</li>
<li><p>Identify expensive operators such as scans, sorts, or key lookups.</p>
</li>
<li><p>Apply one targeted optimization, such as rewriting the query or adding an index.</p>
</li>
<li><p>Measure again using the same workload.</p>
</li>
</ol>
<p>This iterative approach ensures that every optimization is evidence-based rather than relying on assumptions. In enterprise environments, even small improvements to frequently executed queries can significantly reduce CPU usage, disk I/O, and response times.</p>
<h2 id="heading-monitoring-query-performance"><strong>Monitoring Query Performance</strong></h2>
<p>Query tuning is an ongoing process rather than a one-time optimization effort. As enterprise databases grow, data distributions change, index fragment, and application workloads evolve. Queries that once performed well may gradually become inefficient.</p>
<p>SQL Server provides several built-in tools for identifying performance issues.</p>
<h3 id="heading-query-store">Query Store</h3>
<p>Query Store records query history, execution statistics, execution plans, and runtime information.</p>
<p>It helps answer questions such as:</p>
<ul>
<li><p>Which queries consume the most CPU?</p>
</li>
<li><p>Which execution plans changed recently?</p>
</li>
<li><p>Which query became slower after deployment?</p>
</li>
<li><p>Which indexes are no longer being used?</p>
</li>
</ul>
<p>Enable Query Store:</p>
<pre><code class="language-sql">ALTER DATABASE SalesDB
SET QUERY_STORE = ON;
</code></pre>
<p>View top resource-consuming queries:</p>
<pre><code class="language-sql">SELECT
    qt.query_sql_text,
    rs.avg_duration,
    rs.avg_cpu_time
FROM sys.query_store_query_text qt
JOIN sys.query_store_query q
    ON qt.query_text_id = q.query_text_id
JOIN sys.query_store_plan p
    ON q.query_id = p.query_id
JOIN sys.query_store_runtime_stats rs
    ON p.plan_id = rs.plan_id
ORDER BY rs.avg_duration DESC;
</code></pre>
<p>Instead of relying on user complaints, administrators can proactively detect regressions before they affect production workloads.</p>
<h3 id="heading-dynamic-management-views-dmvs">Dynamic Management Views (DMVs)</h3>
<p>SQL Server exposes runtime statistics through Dynamic Management Views.</p>
<p>Example:</p>
<pre><code class="language-sql">SELECT TOP 10
    qs.execution_count,
    qs.total_elapsed_time / qs.execution_count AS AvgTime,
    st.text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
ORDER BY AvgTime DESC;
</code></pre>
<p>This query highlights expensive SQL statements currently cached by SQL Server.</p>
<h3 id="heading-actual-execution-plans">Actual Execution Plans</h3>
<p>Execution plans remain one of the most valuable tuning tools.</p>
<p>When reviewing plans, look for:</p>
<ul>
<li><p>Table scans</p>
</li>
<li><p>Index scans on large tables</p>
</li>
<li><p>Key lookups</p>
</li>
<li><p>Sort operators</p>
</li>
<li><p>Hash Match operations</p>
</li>
<li><p>Missing index recommendations</p>
</li>
<li><p>Large memory grants</p>
</li>
</ul>
<p>The graphical execution plan often pinpoints the exact operator responsible for poor performance.</p>
<h2 id="heading-real-world-example-optimizing-a-reporting-query"><strong>Real-World Example: Optimizing a Reporting Query</strong></h2>
<p>Consider an enterprise reporting system that generates monthly sales summaries.</p>
<p>Original query:</p>
<pre><code class="language-sql">SELECT
    CustomerName,
    SUM(TotalAmount)
FROM Orders
WHERE YEAR(OrderDate) = 2025
GROUP BY CustomerName;
</code></pre>
<p>Although simple, this query performs poorly because <code>YEAR()</code> prevents index seeks and the entire Orders table must be scanned.</p>
<p>Rewrite it like this:</p>
<pre><code class="language-sql">SELECT
    CustomerName,
    SUM(TotalAmount)
FROM Orders
WHERE OrderDate &gt;= '2025-01-01'
AND OrderDate &lt; '2026-01-01'
GROUP BY CustomerName;
</code></pre>
<p>Then create an appropriate index:</p>
<pre><code class="language-sql">CREATE INDEX IX_Orders_OrderDate
ON Orders(OrderDate)
INCLUDE (CustomerName, TotalAmount);
</code></pre>
<p>Performance improvements may include:</p>
<ul>
<li><p>Index Seek instead of Table Scan</p>
</li>
<li><p>Lower logical reads</p>
</li>
<li><p>Reduced CPU utilization</p>
</li>
<li><p>Faster execution time</p>
</li>
<li><p>Better scalability under concurrent reporting workloads</p>
</li>
</ul>
<p>This illustrates that query rewriting and indexing typically produce much larger gains than simply adding hardware.</p>
<h2 id="heading-when-not-to-optimize-prematurely"><strong>When NOT to Optimize Prematurely</strong></h2>
<p>Performance optimization should be driven by evidence, not assumptions. Premature or unnecessary tuning can increase complexity, make queries harder to maintain, and sometimes even reduce overall system performance.</p>
<p>Before making changes, use tools such as Query Store, execution plans, and SQL Server DMVs to identify the actual bottlenecks.</p>
<h3 id="heading-avoid-optimizing-without-profiling">Avoid Optimizing Without Profiling</h3>
<p>Don't rewrite queries simply because they look inefficient. Measure execution time, logical reads, CPU usage, and execution plans first so that optimization efforts target real performance problems rather than perceived ones.</p>
<h3 id="heading-dont-create-indexes-for-every-query">Don't Create Indexes for Every Query</h3>
<p>While indexes can dramatically improve read performance, every additional index increases storage requirements and slows <code>INSERT</code>, <code>UPDATE</code>, and <code>DELETE</code> operations. Create indexes only for frequently executed queries that demonstrate a measurable benefit.</p>
<h3 id="heading-dont-force-query-hints-unnecessarily">Don't Force Query Hints Unnecessarily</h3>
<p>Query hints such as <code>OPTION (FORCE ORDER)</code> or <code>OPTION (RECOMPILE)</code> can override SQL Server's optimizer. They should be used only after careful testing, as they may solve one problem while causing performance regressions elsewhere.</p>
<pre><code class="language-sql">SELECT *
FROM Orders
WHERE CustomerID = @CustomerID
OPTION (RECOMPILE);
</code></pre>
<h3 id="heading-dont-over-normalize-or-de-normalize-without-evidence">Don't Over-Normalize or De-Normalize Without Evidence</h3>
<p>Highly normalized schemas may require expensive joins, while excessive denormalization can introduce redundant data and update anomalies. Choose the appropriate design based on actual workload characteristics rather than assumptions.</p>
<h3 id="heading-balance-read-and-write-performance">Balance Read and Write Performance</h3>
<p>An optimization that accelerates reporting queries may slow transactional workloads due to additional index maintenance. Always evaluate how tuning changes affect both read-heavy and write-heavy operations before deploying them to production.</p>
<h2 id="heading-best-practices-for-enterprise-t-sql-optimization"><strong>Best Practices for Enterprise T-SQL Optimization</strong></h2>
<p>Successful tuning is about applying consistent engineering practices rather than isolated optimizations. We've already discussed some of these best practices, but I'll list them all here for review and completeness (and as a quick reference):</p>
<h3 id="heading-design-indexes-around-queries">Design Indexes Around Queries</h3>
<p>Indexes should reflect actual application workloads.</p>
<p>Instead of indexing every column, identify common <code>WHERE</code> clauses, frequently joined columns, <code>ORDER BY</code> columns, and <code>GROUP BY</code> columns.</p>
<p>Build indexes that support these operations efficiently.</p>
<h3 id="heading-avoid-over-indexing">Avoid Over-Indexing</h3>
<p>More indexes are not always better.</p>
<p>Every <code>INSERT</code>, <code>UPDATE</code>, and <code>DELETE</code> operation must maintain every index.</p>
<p>Too many indexes increase storage, write latency, maintenance time, and fragmentation. Keep only indexes that provide measurable value.</p>
<h3 id="heading-keep-statistics-updated">Keep Statistics Updated</h3>
<p>Outdated statistics lead to poor execution plans.</p>
<p>Update statistics regularly:</p>
<p><code>UPDATE STATISTICS Orders;</code></p>
<p>Or update the entire database:</p>
<p><code>EXEC sp_updatestats;</code></p>
<p>Many performance issues disappear after SQL Server receives accurate distribution statistics.</p>
<h3 id="heading-monitor-index-fragmentation">Monitor Index Fragmentation</h3>
<p>Indexes become fragmented as data changes.</p>
<p>Check fragmentation:</p>
<pre><code class="language-sql">SELECT
    avg_fragmentation_in_percent,
    page_count
FROM sys.dm_db_index_physical_stats
(
    DB_ID(),
    OBJECT_ID('Orders'),
    NULL,
    NULL,
    'LIMITED'
);
</code></pre>
<h3 id="heading-avoid-select">Avoid SELECT *</h3>
<p>Retrieve only required columns.</p>
<p>Instead of:</p>
<pre><code class="language-sql">SELECT *
FROM Customers;
</code></pre>
<p>Use this:</p>
<pre><code class="language-sql">SELECT CustomerID,
       CustomerName,
       Email
FROM Customers;
</code></pre>
<p>Benefits include smaller network payloads, better covering index usage, lower memory consumption, and reduced I/O.</p>
<h3 id="heading-test-with-production-like-data">Test with Production-Like Data</h3>
<p>Queries that perform well on development databases containing thousands of rows may behave very differently against production systems with hundreds of millions of records.</p>
<p>Always validate execution plans, memory grants, CPU usage, parallelism, and logical reads using realistic datasets.</p>
<h2 id="heading-future-trends-in-sql-performance-optimization"><strong>Future Trends in SQL Performance Optimization</strong></h2>
<h3 id="heading-1-intelligent-query-processing">1. Intelligent Query Processing</h3>
<p>Modern versions of SQL Server include features such as adaptive query processing, memory grant feedback, and automatic plan correction. These capabilities allow the query optimizer to adjust execution strategies based on actual workload patterns, improving performance without manual tuning.</p>
<h3 id="heading-2-cloud-native-database-optimization">2. Cloud-Native Database Optimization</h3>
<p>Cloud database platforms provide built-in capabilities such as automatic indexing recommendations, continuous performance monitoring, and self-tuning features. These services reduce administrative overhead while helping maintain consistent query performance as workloads grow.</p>
<h3 id="heading-3-ai-assisted-performance-tuning">3. AI-Assisted Performance Tuning</h3>
<p>Artificial intelligence is becoming a valuable assistant for database optimization. AI-powered tools can analyze execution plans, recommend indexes, identify inefficient queries, and even suggest T-SQL rewrites, enabling developers to resolve performance issues earlier in the development lifecycle.</p>
<h3 id="heading-4-performance-engineering-by-default">4. Performance Engineering by Default</h3>
<p>Database optimization is shifting from reactive troubleshooting to proactive performance engineering. By incorporating query analysis, indexing reviews, and performance testing into CI/CD pipelines, teams can detect regressions before they reach production.</p>
<h3 id="heading-strong-fundamentals-still-matter">Strong Fundamentals Still Matter</h3>
<p>Despite advances in automation, understanding execution plans, indexing strategies, query design, and statistics remains essential. Automated tools provide recommendations, but experienced developers and DBAs are still needed to validate trade-offs and ensure optimizations align with business requirements.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>Effective T-SQL performance optimization isn't about applying isolated tricks or adding indexes indiscriminately. It requires understanding how SQL Server executes queries, accesses data, and chooses execution plans.</p>
<p>By combining efficient query design with well-planned indexing strategies, accurate statistics, and continuous monitoring, you can dramatically reduce latency, lower resource consumption, and improve scalability across enterprise applications.</p>
<p>Rather than waiting for performance problems to appear in production, teams should make query tuning a routine part of the development lifecycle. Regularly reviewing execution plans, monitoring workload patterns through Query Store, validating indexes against real application behavior, and testing with production-scale datasets creates a foundation for predictable and reliable database performance.</p>
<p>As enterprise systems continue to grow in complexity and data volume, organizations that treat performance optimization as an ongoing engineering discipline will be better equipped to deliver responsive, scalable, and cost-effective applications.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Browser-Based PDF Signature Tool Using JavaScript ]]>
                </title>
                <description>
                    <![CDATA[ PDF documents are commonly used for agreements, forms, approvals, invoices, reports, applications, and other documents that may need a signature or additional text before they are shared. A traditiona ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-pdf-signature-tool-javascript/</link>
                <guid isPermaLink="false">6a5e89518186f4c5817d466b</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdf ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Tutorial ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Hashnode ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Bhavin Sheth ]]>
                </dc:creator>
                <pubDate>Mon, 20 Jul 2026 20:47:13 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/175ab1f9-2917-4588-9e67-50607f6fa5a1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>PDF documents are commonly used for agreements, forms, approvals, invoices, reports, applications, and other documents that may need a signature or additional text before they are shared.</p>
<p>A traditional workflow often involves printing the document, signing it by hand, scanning it again, and sending the new file. For a simple electronic signature, that process adds unnecessary steps.</p>
<p>In this tutorial, you'll build a browser-based PDF Signature Tool using JavaScript. Users will be able to upload a PDF, preview and navigate its pages, and add content directly to the document.</p>
<p>The application will support two main element types: <strong>Signature</strong> and <strong>Text/Stamp</strong>.</p>
<p>For signatures, users can draw directly in the browser, type their name and choose a signature style, or upload an existing signature image. For text-based elements, they can enter custom text or use preset stamps such as <strong>APPROVED</strong>, <strong>CONFIDENTIAL</strong>, <strong>DRAFT</strong>, and <strong>PAID</strong>.</p>
<p>After creating an element, users can position it on the PDF preview and adjust properties such as scale, rotation, opacity, font size, and color. The element can then be applied to the current page, every page, or a specific set of pages.</p>
<p>Once processing is complete, the application generates a new PDF for review. Users can preview the result, rename the output file, check its page count and file size, and download it directly from the browser.</p>
<p>The project uses PDF.js for document rendering and PDF-lib for modifying and generating the final PDF.</p>
<p>By the end of this tutorial, you'll understand how to build an interactive PDF editing workflow that combines canvas-based input, image embedding, text placement, coordinate conversion, page selection, and client-side file generation.</p>
<h3 id="heading-what-well-cover">What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-what-this-pdf-signature-tool-can-do">What This PDF Signature Tool Can Do</a></p>
</li>
<li><p><a href="#heading-electronic-signatures-vs-digital-signatures">Electronic Signatures vs Digital Signatures</a></p>
</li>
<li><p><a href="#heading-how-the-browser-based-workflow-works">How the Browser-Based Workflow Works</a></p>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a href="#heading-what-libraries-are-we-using">What Libraries Are We Using?</a></p>
</li>
<li><p><a href="#heading-uploading-and-previewing-the-pdf">Uploading and Previewing the PDF</a></p>
</li>
<li><p><a href="#heading-choosing-an-element-to-add">Choosing an Element to Add</a></p>
</li>
<li><p><a href="#heading-creating-a-signature">Creating a Signature</a></p>
</li>
<li><p><a href="#heading-drawing-a-signature">Drawing a Signature</a></p>
</li>
<li><p><a href="#heading-typing-a-signature">Typing a Signature</a></p>
</li>
<li><p><a href="#heading-uploading-a-signature-image">Uploading a Signature Image</a></p>
</li>
<li><p><a href="#heading-adding-text-and-preset-stamps">Adding Text and Preset Stamps</a></p>
</li>
<li><p><a href="#heading-positioning-and-styling-the-element">Positioning and Styling the Element</a></p>
</li>
<li><p><a href="#heading-applying-the-element-to-selected-pages">Applying the Element to Selected Pages</a></p>
</li>
<li><p><a href="#heading-applying-and-finalizing-the-pdf">Applying and Finalizing the PDF</a></p>
</li>
<li><p><a href="#heading-generating-the-signed-pdf">Generating the Signed PDF</a></p>
</li>
<li><p><a href="#heading-previewing-the-final-pdf">Previewing the Final PDF</a></p>
</li>
<li><p><a href="#heading-renaming-and-downloading-the-final-pdf">Renaming and Downloading the Final PDF</a></p>
</li>
<li><p><a href="#heading-demo-how-the-pdf-signature-tool-works">Demo: How the PDF Signature Tool Works</a></p>
</li>
<li><p><a href="#heading-handling-signature-transparency">Handling Signature Transparency</a></p>
</li>
<li><p><a href="#heading-important-notes-and-common-mistakes">Important Notes and Common Mistakes</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-this-pdf-signature-tool-can-do">What This PDF Signature Tool Can Do</h2>
<p>The application provides a single editing workflow for adding signatures, text, and common document stamps to PDF pages.</p>
<p>When <strong>Signature</strong> is selected, users can create the signature in three different ways.</p>
<ol>
<li><p>The <strong>Draw</strong> option provides a canvas where the user can write a signature using a mouse, trackpad, stylus, or touch input.</p>
</li>
<li><p>The <strong>Type</strong> option converts entered text into a signature-style element. Users can type their name, adjust the size, and choose from the available signature styles.</p>
</li>
<li><p>The <strong>Upload</strong> option accepts an existing signature image. This is useful for someone who already has a transparent PNG or another supported image of their handwritten signature.</p>
</li>
</ol>
<p>The second element type is <strong>Text/Stamp</strong>. Users can enter custom text such as:</p>
<pre><code class="language-text">Signed on: 08-09-2025
</code></pre>
<p>They can also quickly choose a predefined stamp:</p>
<pre><code class="language-text">APPROVED
CONFIDENTIAL
DRAFT
PAID
</code></pre>
<p>After an element has been created, the application provides controls for its placement and appearance. Users can move it to the required location and adjust its scale, rotation, opacity, and position.</p>
<p>Text and stamp elements can additionally use configurable font sizes and colors.</p>
<p>The page controls determine where the selected element will be applied. A signature may belong only on the final page of a contract, while a <code>CONFIDENTIAL</code> stamp may need to appear on every page.</p>
<p>The application therefore supports:</p>
<pre><code class="language-text">Current page only
All pages
Specific pages
</code></pre>
<p>The goal is to provide one consistent workflow for several common PDF editing tasks without requiring separate tools for each element type.</p>
<h2 id="heading-electronic-signatures-vs-digital-signatures">Electronic Signatures vs Digital Signatures</h2>
<p>Before building the application, it's important to distinguish between an <strong>electronic signature</strong> and a <strong>digital signature</strong>.</p>
<p>The tool in this tutorial creates an electronic signature workflow.</p>
<p>A drawn signature, typed signature, or uploaded signature image is placed visually onto the PDF page. This is similar to signing a document by hand and inserting a visible representation of that signature into the file.</p>
<p>For example, a user might draw a signature on a canvas:</p>
<pre><code class="language-javascript">const signatureImage =
    signatureCanvas.toDataURL("image/png");
</code></pre>
<p>The generated image can then be embedded into the PDF.</p>
<p>A digital signature is technically different.</p>
<p>Certificate-based digital signatures use cryptographic methods to help verify document integrity and the identity associated with a signing certificate. They may involve digital certificates, private keys, signature validation, and trust chains.</p>
<p>Simply placing a handwritten signature image on a PDF doesn't create that type of cryptographic verification.</p>
<p>This distinction matters because the terms are sometimes used interchangeably in everyday conversation even though the underlying technologies are different.</p>
<p>The project we're building focuses on <strong>visual electronic signatures and document elements</strong>. It doesn't create certificate-based cryptographic digital signatures.</p>
<p>Keeping that distinction clear makes it easier to understand exactly what the application does and what would require a more advanced signing system.</p>
<h2 id="heading-how-the-browser-based-workflow-works">How the Browser-Based Workflow Works</h2>
<p>The process begins when a user selects a PDF file.</p>
<p>PDF.js loads the document and renders the current page into a browser canvas. Previous and next buttons allow the user to navigate through the PDF before choosing where to place an element.</p>
<p>The user then selects one of two element types:</p>
<pre><code class="language-text">Signature
Text/Stamp
</code></pre>
<p>If <strong>Signature</strong> is selected, the application provides three creation methods:</p>
<pre><code class="language-text">Draw
Type
Upload
</code></pre>
<p>The selected signature is converted into an element that can be displayed over the PDF preview.</p>
<p>If <strong>Text/Stamp</strong> is selected, the application instead creates a text element using either custom content or one of the predefined stamp values.</p>
<p>The complete workflow looks like this:</p>
<pre><code class="language-text">Upload PDF
    ↓
Render and Navigate Pages
    ↓
Choose Signature or Text/Stamp
    ↓
Create the Element
    ↓
Position and Style It
    ↓
Choose Target Pages
    ↓
Apply &amp; Finalize
    ↓
Generate the New PDF
    ↓
Preview the Result
    ↓
Rename and Download
</code></pre>
<p>During editing, the element displayed over the PDF preview is only a browser-side representation. Its position must later be translated into coordinates that match the actual PDF page.</p>
<p>For example, the application may store an element like this:</p>
<pre><code class="language-javascript">const element = {
    type: "signature",
    x: 622,
    y: 496,
    scale: 1.14,
    rotation: 0,
    opacity: 1
};
</code></pre>
<p>When the user clicks <strong>Apply &amp; Finalize</strong>, those values are used to calculate the final placement inside the PDF.</p>
<p>This separation between the interactive preview and the final PDF generation is the foundation of the project. It allows users to visually prepare the document first and create the modified PDF only after the placement is ready.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>To keep the project easy to understand, we'll use three main files:</p>
<pre><code class="language-text">pdf-signature-tool/
│
├── index.html
├── style.css
└── script.js
</code></pre>
<p>The HTML file contains the upload interface, PDF preview, editing controls, final preview, and download section.</p>
<p>The CSS file handles the layout and visual states.</p>
<p>The JavaScript file manages PDF loading, page rendering, signature creation, text and stamp elements, positioning, final PDF generation, and downloading.</p>
<p>Start with the basic HTML structure:</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;

    &lt;meta charset="UTF-8"&gt;

    &lt;meta
        name="viewport"
        content="width=device-width, initial-scale=1.0"&gt;

    &lt;title&gt;PDF Signature Tool&lt;/title&gt;

    &lt;link
        rel="stylesheet"
        href="style.css"&gt;

&lt;/head&gt;

&lt;body&gt;

    &lt;main class="pdf-signature-tool"&gt;

        &lt;section id="uploadSection"&gt;

            &lt;h1&gt;PDF Signature Tool&lt;/h1&gt;

            &lt;p&gt;
                Upload your PDF to add your
                electronic signature.
            &lt;/p&gt;

            &lt;div id="dropZone"&gt;

                &lt;p&gt;Drag &amp; Drop PDF Here&lt;/p&gt;

                &lt;p&gt;Or click to browse file&lt;/p&gt;

                &lt;button id="selectPdfButton"&gt;
                    Select PDF
                &lt;/button&gt;

                &lt;input
                    type="file"
                    id="pdfInput"
                    accept="application/pdf"
                    hidden&gt;

            &lt;/div&gt;

        &lt;/section&gt;

        &lt;section
            id="editorSection"
            hidden&gt;

            &lt;div class="pdf-preview"&gt;

                &lt;div id="previewContainer"&gt;

                    &lt;canvas id="pdfCanvas"&gt;&lt;/canvas&gt;

                    &lt;div id="elementLayer"&gt;&lt;/div&gt;

                &lt;/div&gt;

                &lt;div class="page-navigation"&gt;

                    &lt;button id="previousPage"&gt;
                        &amp;lt;
                    &lt;/button&gt;

                    &lt;span id="pageInfo"&gt;
                        Page 1 of 1
                    &lt;/span&gt;

                    &lt;button id="nextPage"&gt;
                        &amp;gt;
                    &lt;/button&gt;

                &lt;/div&gt;

            &lt;/div&gt;

            &lt;aside id="editorControls"&gt;

                &lt;!-- Signature and text controls
                     will be added here --&gt;

            &lt;/aside&gt;

        &lt;/section&gt;

        &lt;section
            id="resultSection"
            hidden&gt;

            &lt;!-- Final preview and download
                 controls will be added here --&gt;

        &lt;/section&gt;

    &lt;/main&gt;

    &lt;script src="script.js"&gt;&lt;/script&gt;

&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>The <code>previewContainer</code> is especially important.</p>
<p>It contains two layers:</p>
<pre><code class="language-text">PDF Canvas
    +
Interactive Element Layer
</code></pre>
<p>The PDF page is rendered onto the canvas, while signatures, text, and stamps are displayed in a separate overlay.</p>
<p>This allows users to move and style an element without modifying the original PDF every time they make a small adjustment.</p>
<p>The overlay should match the dimensions and position of the PDF canvas.</p>
<pre><code class="language-css">#previewContainer {
    position: relative;
    display: inline-block;
}

#pdfCanvas {
    display: block;
}

#elementLayer {
    position: absolute;
    inset: 0;
    pointer-events: none;
}
</code></pre>
<p>Individual signature and text elements can later enable their own pointer interactions.</p>
<pre><code class="language-css">.pdf-element {
    position: absolute;
    cursor: move;
    pointer-events: auto;
    transform-origin: center;
}
</code></pre>
<p>This layered structure becomes the foundation of the interactive editor.</p>
<h2 id="heading-what-libraries-are-we-using">What Libraries Are We Using?</h2>
<p>This project uses two JavaScript libraries for different parts of the PDF workflow.</p>
<h3 id="heading-pdfjs-for-rendering-and-previewing">PDF.js for Rendering and Previewing</h3>
<p>PDF.js is responsible for reading the uploaded document and rendering its pages inside the browser.</p>
<p>A page can be loaded like this:</p>
<pre><code class="language-javascript">const page =
    await pdfDocument.getPage(
        currentPage
    );
</code></pre>
<p>The page is then rendered to a canvas:</p>
<pre><code class="language-javascript">const viewport =
    page.getViewport({
        scale: 1.5
    });

const context =
    pdfCanvas.getContext("2d");

pdfCanvas.width =
    viewport.width;

pdfCanvas.height =
    viewport.height;

await page.render({

    canvasContext: context,

    viewport

}).promise;
</code></pre>
<p>PDF.js handles the visual preview.</p>
<h3 id="heading-pdf-lib-for-modifying-the-pdf">PDF-lib for Modifying the PDF</h3>
<p>PDF-lib is used later when the user clicks <strong>Apply &amp; Finalize</strong>.</p>
<p>It allows us to load the original PDF bytes and add content to its pages.</p>
<p>For example:</p>
<pre><code class="language-javascript">const pdfDoc =
    await PDFLib.PDFDocument.load(
        originalPdfBytes
    );
</code></pre>
<p>An uploaded PNG signature can then be embedded:</p>
<pre><code class="language-javascript">const signatureImage =
    await pdfDoc.embedPng(
        signatureBytes
    );
</code></pre>
<p>Text can also be drawn directly onto a PDF page:</p>
<pre><code class="language-javascript">page.drawText(
    "APPROVED",
    {
        x: 100,
        y: 100,
        size: 18
    }
);
</code></pre>
<p>The two libraries therefore have separate responsibilities:</p>
<pre><code class="language-text">PDF.js
→ Load and visually render PDF pages

PDF-lib
→ Modify pages and generate the final PDF
</code></pre>
<p>Separating these responsibilities keeps the editor easier to manage.</p>
<p>Include both libraries in the project before <code>script.js</code>.</p>
<pre><code class="language-html">&lt;script
    src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"&gt;
&lt;/script&gt;

&lt;script
    src="https://unpkg.com/pdf-lib/dist/pdf-lib.min.js"&gt;
&lt;/script&gt;

&lt;script src="script.js"&gt;&lt;/script&gt;
</code></pre>
<p>Configure the PDF.js worker as well:</p>
<pre><code class="language-javascript">pdfjsLib.GlobalWorkerOptions.workerSrc =
    "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
</code></pre>
<p>For a production project, pin and test the exact library versions you use rather than automatically loading an unspecified latest release.</p>
<h2 id="heading-uploading-and-previewing-the-pdf">Uploading and Previewing the PDF</h2>
<p>The first interactive step is accepting the user's PDF.</p>
<p>Get references to the required elements:</p>
<pre><code class="language-javascript">const pdfInput =
    document.getElementById(
        "pdfInput"
    );

const selectPdfButton =
    document.getElementById(
        "selectPdfButton"
    );

const dropZone =
    document.getElementById(
        "dropZone"
    );

const uploadSection =
    document.getElementById(
        "uploadSection"
    );

const editorSection =
    document.getElementById(
        "editorSection"
    );

const pdfCanvas =
    document.getElementById(
        "pdfCanvas"
    );
</code></pre>
<p>We also need a few variables to store the current document state.</p>
<pre><code class="language-javascript">let pdfDocument = null;

let originalPdfBytes = null;

let currentPage = 1;

let totalPages = 0;
</code></pre>
<p>Clicking the custom button opens the hidden file input.</p>
<pre><code class="language-javascript">selectPdfButton.addEventListener(
    "click",
    () =&gt; {

        pdfInput.click();

    }
);
</code></pre>
<p>When a file is selected, pass it to the PDF loading function.</p>
<pre><code class="language-javascript">pdfInput.addEventListener(
    "change",
    event =&gt; {

        const file =
            event.target.files[0];

        if (file) {

            loadPdf(file);

        }

    }
);
</code></pre>
<p>Before processing the file, validate its type.</p>
<pre><code class="language-javascript">async function loadPdf(file) {

    if (
        file.type !==
        "application/pdf"
    ) {

        alert(
            "Please select a valid PDF file."
        );

        return;

    }

}
</code></pre>
<p>Read the file as an <code>ArrayBuffer</code>.</p>
<pre><code class="language-javascript">const arrayBuffer =
    await file.arrayBuffer();
</code></pre>
<p>Keep a copy of the original bytes because PDF.js and PDF-lib will use the document at different stages.</p>
<pre><code class="language-javascript">originalPdfBytes =
    new Uint8Array(
        arrayBuffer
    );
</code></pre>
<p>Now load the document with PDF.js.</p>
<pre><code class="language-javascript">pdfDocument =
    await pdfjsLib
        .getDocument({
            data:
                originalPdfBytes.slice()
        })
        .promise;
</code></pre>
<p>Store the number of pages.</p>
<pre><code class="language-javascript">totalPages =
    pdfDocument.numPages;

currentPage = 1;
</code></pre>
<p>Switch from the upload interface to the editor.</p>
<pre><code class="language-javascript">uploadSection.hidden = true;

editorSection.hidden = false;
</code></pre>
<p>Finally, render the first page.</p>
<pre><code class="language-javascript">await renderPage(currentPage);
</code></pre>
<p>The complete loading function becomes:</p>
<pre><code class="language-javascript">async function loadPdf(file) {

    if (
        file.type !==
        "application/pdf"
    ) {

        alert(
            "Please select a valid PDF file."
        );

        return;

    }

    const arrayBuffer =
        await file.arrayBuffer();

    originalPdfBytes =
        new Uint8Array(
            arrayBuffer
        );

    pdfDocument =
        await pdfjsLib
            .getDocument({
                data:
                    originalPdfBytes.slice()
            })
            .promise;

    totalPages =
        pdfDocument.numPages;

    currentPage = 1;

    uploadSection.hidden = true;

    editorSection.hidden = false;

    await renderPage(currentPage);

}
</code></pre>
<p>For drag-and-drop support, prevent the browser's default behavior.</p>
<pre><code class="language-javascript">dropZone.addEventListener(
    "dragover",
    event =&gt; {

        event.preventDefault();

        dropZone.classList.add(
            "drag-active"
        );

    }
);
</code></pre>
<p>Remove the active state when the file leaves the drop area.</p>
<pre><code class="language-javascript">dropZone.addEventListener(
    "dragleave",
    () =&gt; {

        dropZone.classList.remove(
            "drag-active"
        );

    }
);
</code></pre>
<p>Handle the dropped file:</p>
<pre><code class="language-javascript">dropZone.addEventListener(
    "drop",
    event =&gt; {

        event.preventDefault();

        dropZone.classList.remove(
            "drag-active"
        );

        const file =
            event.dataTransfer.files[0];

        if (file) {

            loadPdf(file);

        }

    }
);
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/6605c5ed-31d3-4a2a-a80d-95a77040d890.png" alt="PDF Signature Tool upload area with drag-and-drop support and Select PDF button." style="display:block;margin:0 auto" width="639" height="652" loading="lazy">

<h2 id="heading-rendering-the-current-pdf-page">Rendering the Current PDF Page</h2>
<p>The <code>renderPage()</code> function loads one page from the PDF and displays it on the canvas.</p>
<pre><code class="language-javascript">async function renderPage(
    pageNumber
) {

    const page =
        await pdfDocument.getPage(
            pageNumber
        );

    const viewport =
        page.getViewport({
            scale: 1.5
        });

    const context =
        pdfCanvas.getContext("2d");

    pdfCanvas.width =
        viewport.width;

    pdfCanvas.height =
        viewport.height;

    await page.render({

        canvasContext: context,

        viewport

    }).promise;

    updatePageInfo();

}
</code></pre>
<p>Because the interactive element layer sits above the canvas, it must use the same dimensions.</p>
<pre><code class="language-javascript">const elementLayer =
    document.getElementById(
        "elementLayer"
    );

elementLayer.style.width =
    `${viewport.width}px`;

elementLayer.style.height =
    `${viewport.height}px`;
</code></pre>
<p>Add those lines inside <code>renderPage()</code> after setting the canvas dimensions.</p>
<p>The page information can then be updated:</p>
<pre><code class="language-javascript">function updatePageInfo() {

    pageInfo.textContent =
        `Page ${currentPage} of ${totalPages}`;

}
</code></pre>
<p>At this point, the uploaded PDF page is visible, but users still need a way to move through multi-page documents.</p>
<h2 id="heading-adding-pdf-page-navigation">Adding PDF Page Navigation</h2>
<p>Get the navigation controls:</p>
<pre><code class="language-javascript">const previousPage =
    document.getElementById(
        "previousPage"
    );

const nextPage =
    document.getElementById(
        "nextPage"
    );

const pageInfo =
    document.getElementById(
        "pageInfo"
    );
</code></pre>
<p>The previous button decreases the page number.</p>
<pre><code class="language-javascript">previousPage.addEventListener(
    "click",
    async () =&gt; {

        if (currentPage &lt;= 1) {
            return;
        }

        currentPage--;

        await renderPage(
            currentPage
        );

    }
);
</code></pre>
<p>The next button moves forward.</p>
<pre><code class="language-javascript">nextPage.addEventListener(
    "click",
    async () =&gt; {

        if (
            currentPage &gt;=
            totalPages
        ) {
            return;
        }

        currentPage++;

        await renderPage(
            currentPage
        );

    }
);
</code></pre>
<p>The boundary checks prevent navigation outside the document.</p>
<p>For a 12-page PDF, the interface may display:</p>
<pre><code class="language-text">Page 12 of 12
</code></pre>
<p>The previous button remains available, while the next action can be disabled because the user is already on the final page.</p>
<pre><code class="language-javascript">function updateNavigationState() {

    previousPage.disabled =
        currentPage === 1;

    nextPage.disabled =
        currentPage ===
        totalPages;

}
</code></pre>
<p>Call this function whenever a new page is rendered.</p>
<pre><code class="language-javascript">function updatePageInfo() {

    pageInfo.textContent =
        `Page ${currentPage} of ${totalPages}`;

    updateNavigationState();

}
</code></pre>
<p>The user can now upload a PDF, preview its pages, and navigate to the exact page where a signature, custom text, or document stamp needs to be placed.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/e1b4c1e5-f088-4789-af4c-7cb9bb08bdd6.png" alt="Uploaded PDF displayed in the PDF Signature Tool with previous and next page navigation controls." style="display:block;margin:0 auto" width="708" height="550" loading="lazy">

<h2 id="heading-choosing-an-element-to-add">Choosing an Element to Add</h2>
<p>Once the PDF is loaded and the correct page is visible, the user can choose what type of element to place on the document.</p>
<p>The editor provides two options:</p>
<pre><code class="language-text">Signature
Text/Stamp
</code></pre>
<p>Create the element selector:</p>
<pre><code class="language-html">&lt;div class="element-selector"&gt;

    &lt;h3&gt;1. Choose Element&lt;/h3&gt;

    &lt;label&gt;
        &lt;input
            type="radio"
            name="elementType"
            value="signature"
            checked&gt;
        Signature
    &lt;/label&gt;

    &lt;label&gt;
        &lt;input
            type="radio"
            name="elementType"
            value="text"&gt;
        Text/Stamp
    &lt;/label&gt;

&lt;/div&gt;
</code></pre>
<p>Get the controls in JavaScript:</p>
<pre><code class="language-javascript">const elementTypeInputs =
    document.querySelectorAll(
        'input[name="elementType"]'
    );

const signatureControls =
    document.getElementById(
        "signatureControls"
    );

const textControls =
    document.getElementById(
        "textControls"
    );
</code></pre>
<p>Listen for changes:</p>
<pre><code class="language-javascript">elementTypeInputs.forEach(
    input =&gt; {

        input.addEventListener(
            "change",
            event =&gt; {

                const type =
                    event.target.value;

                if (
                    type ===
                    "signature"
                ) {

                    signatureControls.hidden =
                        false;

                    textControls.hidden =
                        true;

                } else {

                    signatureControls.hidden =
                        true;

                    textControls.hidden =
                        false;

                }

            }
        );

    }
);
</code></pre>
<p>This keeps the interface focused. Signature-specific controls appear only when the user is creating a signature, while text and stamp controls appear when that element type is selected.</p>
<h2 id="heading-creating-a-signature">Creating a Signature</h2>
<p>The signature workflow supports three methods:</p>
<pre><code class="language-text">Draw
Type
Upload
</code></pre>
<p>Create the method selector:</p>
<pre><code class="language-html">&lt;div id="signatureControls"&gt;

    &lt;h3&gt;2. Create Signature&lt;/h3&gt;

    &lt;div class="signature-tabs"&gt;

        &lt;button
            data-method="draw"
            class="active"&gt;
            Draw
        &lt;/button&gt;

        &lt;button
            data-method="type"&gt;
            Type
        &lt;/button&gt;

        &lt;button
            data-method="upload"&gt;
            Upload
        &lt;/button&gt;

    &lt;/div&gt;

    &lt;div id="drawPanel"&gt;&lt;/div&gt;

    &lt;div
        id="typePanel"
        hidden&gt;
    &lt;/div&gt;

    &lt;div
        id="uploadPanel"
        hidden&gt;
    &lt;/div&gt;

&lt;/div&gt;
</code></pre>
<p>Track the currently selected method:</p>
<pre><code class="language-javascript">let signatureMethod =
    "draw";
</code></pre>
<p>Switch between the three panels:</p>
<pre><code class="language-javascript">const signatureTabs =
    document.querySelectorAll(
        ".signature-tabs button"
    );

signatureTabs.forEach(
    button =&gt; {

        button.addEventListener(
            "click",
            () =&gt; {

                signatureMethod =
                    button.dataset.method;

                showSignatureMethod(
                    signatureMethod
                );

            }
        );

    }
);
</code></pre>
<p>The panel switching function can hide the inactive methods:</p>
<pre><code class="language-javascript">function showSignatureMethod(
    method
) {

    drawPanel.hidden =
        method !== "draw";

    typePanel.hidden =
        method !== "type";

    uploadPanel.hidden =
        method !== "upload";

}
</code></pre>
<p>Each method creates the same type of final element (a signature) but the source of that signature is different.</p>
<h2 id="heading-drawing-a-signature">Drawing a Signature</h2>
<p>The <strong>Draw</strong> option allows users to create a handwritten signature directly in the browser.</p>
<p>Add a canvas to the Draw panel:</p>
<pre><code class="language-html">&lt;div id="drawPanel"&gt;

    &lt;canvas
        id="signatureCanvas"
        width="500"
        height="180"&gt;
    &lt;/canvas&gt;

    &lt;button id="clearSignature"&gt;
        Clear
    &lt;/button&gt;

&lt;/div&gt;
</code></pre>
<p>Get the drawing context:</p>
<pre><code class="language-javascript">const signatureCanvas =
    document.getElementById(
        "signatureCanvas"
    );

const signatureContext =
    signatureCanvas.getContext(
        "2d"
    );

let isDrawing = false;
</code></pre>
<p>Begin drawing when the pointer touches the canvas:</p>
<pre><code class="language-javascript">signatureCanvas.addEventListener(
    "pointerdown",
    event =&gt; {

        isDrawing = true;

        const rect =
            signatureCanvas
                .getBoundingClientRect();

        signatureContext.beginPath();

        signatureContext.moveTo(

            event.clientX -
                rect.left,

            event.clientY -
                rect.top

        );

    }
);
</code></pre>
<p>Continue the line while the pointer moves:</p>
<pre><code class="language-javascript">signatureCanvas.addEventListener(
    "pointermove",
    event =&gt; {

        if (!isDrawing) {
            return;
        }

        const rect =
            signatureCanvas
                .getBoundingClientRect();

        signatureContext.lineTo(

            event.clientX -
                rect.left,

            event.clientY -
                rect.top

        );

        signatureContext.stroke();

    }
);
</code></pre>
<p>Stop drawing when the pointer is released:</p>
<pre><code class="language-javascript">signatureCanvas.addEventListener(
    "pointerup",
    () =&gt; {

        isDrawing = false;

    }
);

signatureCanvas.addEventListener(
    "pointerleave",
    () =&gt; {

        isDrawing = false;

    }
);
</code></pre>
<p>Set a few drawing properties:</p>
<pre><code class="language-javascript">signatureContext.lineWidth = 2;

signatureContext.lineCap =
    "round";

signatureContext.lineJoin =
    "round";
</code></pre>
<p>For touch devices, prevent the browser from interpreting drawing gestures as page scrolling:</p>
<pre><code class="language-css">#signatureCanvas {
    touch-action: none;
    cursor: crosshair;
}
</code></pre>
<p>The Clear button resets the drawing canvas:</p>
<pre><code class="language-javascript">clearSignature.addEventListener(
    "click",
    () =&gt; {

        signatureContext.clearRect(

            0,
            0,

            signatureCanvas.width,
            signatureCanvas.height

        );

    }
);
</code></pre>
<p>Once the signature is ready, convert the canvas into a PNG data URL:</p>
<pre><code class="language-javascript">const drawnSignature =
    signatureCanvas.toDataURL(
        "image/png"
    );
</code></pre>
<p>Because the canvas can preserve transparency, the resulting signature can be placed over the PDF without adding an unwanted rectangular background.</p>
<p>The generated image can now be displayed inside the interactive element layer.</p>
<pre><code class="language-javascript">function useDrawnSignature() {

    const image =
        new Image();

    image.src =
        signatureCanvas.toDataURL(
            "image/png"
        );

    image.onload =
        () =&gt; {

            createSignatureElement(
                image.src
            );

        };

}
</code></pre>
<p>This gives the user a visual signature element that can later be positioned over the PDF page.</p>
<h2 id="heading-typing-a-signature">Typing a Signature</h2>
<p>Not every user has a touchscreen, stylus, or existing signature image.</p>
<p>The <strong>Type</strong> option allows users to enter their name and choose a signature-style appearance.</p>
<p>Add the input controls:</p>
<pre><code class="language-html">&lt;div id="typePanel" hidden&gt;

    &lt;input
        type="text"
        id="typedSignature"
        placeholder="Type your name"&gt;

    &lt;label for="signatureFontSize"&gt;
        Font Size
    &lt;/label&gt;

    &lt;input
        type="number"
        id="signatureFontSize"
        value="45"
        min="12"
        max="120"&gt;

    &lt;div id="signatureStyles"&gt;
    &lt;/div&gt;

&lt;/div&gt;
</code></pre>
<p>Listen for text changes:</p>
<pre><code class="language-javascript">typedSignature.addEventListener(
    "input",
    updateTypedSignatures
);
</code></pre>
<p>Create several style previews:</p>
<pre><code class="language-javascript">const signatureFonts = [

    "cursive",

    "'Brush Script MT', cursive",

    "'Segoe Script', cursive"

];
</code></pre>
<p>Render the available options:</p>
<pre><code class="language-javascript">function updateTypedSignatures() {

    const value =
        typedSignature.value.trim();

    signatureStyles.innerHTML = "";

    if (!value) {
        return;
    }

    signatureFonts.forEach(
        font =&gt; {

            const option =
                document.createElement(
                    "button"
                );

            option.textContent =
                value;

            option.style.fontFamily =
                font;

            option.style.fontSize =
                `${signatureFontSize.value}px`;

            option.addEventListener(
                "click",
                () =&gt; {

                    createTypedSignature(
                        value,
                        font
                    );

                }
            );

            signatureStyles.appendChild(
                option
            );

        }
    );

}
</code></pre>
<p>A typed signature can be converted to an image using another canvas.</p>
<pre><code class="language-javascript">function createTypedSignature(
    text,
    fontFamily
) {

    const canvas =
        document.createElement(
            "canvas"
        );

    const context =
        canvas.getContext("2d");

    const fontSize =
        Number(
            signatureFontSize.value
        );

    context.font =
        `${fontSize}px ${fontFamily}`;

    const width =
        context.measureText(
            text
        ).width;

    canvas.width =
        Math.ceil(width + 40);

    canvas.height =
        Math.ceil(fontSize * 2);

    context.font =
        `${fontSize}px ${fontFamily}`;

    context.textBaseline =
        "middle";

    context.fillText(
        text,
        20,
        canvas.height / 2
    );

    const imageUrl =
        canvas.toDataURL(
            "image/png"
        );

    createSignatureElement(
        imageUrl
    );

}
</code></pre>
<p>The typed signature is now treated like the drawn signature: it becomes an image element that can be positioned and later embedded into the PDF.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/8ebb9405-53f4-480d-b71e-cfb746f6fd5a.png" alt="Typed signature option showing a name, font size control, and multiple signature-style previews." style="display:block;margin:0 auto" width="334" height="682" loading="lazy">

<h2 id="heading-uploading-a-signature-image">Uploading a Signature Image</h2>
<p>The third option allows users to upload an existing signature image.</p>
<p>Add a file input:</p>
<pre><code class="language-html">&lt;div id="uploadPanel" hidden&gt;

    &lt;input
        type="file"
        id="signatureUpload"
        accept="image/png,image/jpeg"&gt;

    &lt;p id="selectedSignatureFile"&gt;
        No file chosen
    &lt;/p&gt;

&lt;/div&gt;
</code></pre>
<p>Listen for file selection:</p>
<pre><code class="language-javascript">signatureUpload.addEventListener(
    "change",
    event =&gt; {

        const file =
            event.target.files[0];

        if (!file) {
            return;
        }

        loadSignatureImage(file);

    }
);
</code></pre>
<p>Validate the image:</p>
<pre><code class="language-javascript">function loadSignatureImage(
    file
) {

    const allowedTypes = [

        "image/png",

        "image/jpeg"

    ];

    if (
        !allowedTypes.includes(
            file.type
        )
    ) {

        alert(
            "Please upload a PNG or JPEG image."
        );

        return;

    }

}
</code></pre>
<p>Read the selected image:</p>
<pre><code class="language-javascript">const reader =
    new FileReader();

reader.onload =
    event =&gt; {

        createSignatureElement(
            event.target.result
        );

};

reader.readAsDataURL(file);
</code></pre>
<p>Display the selected filename:</p>
<pre><code class="language-javascript">selectedSignatureFile.textContent =
    `Selected: ${file.name}`;
</code></pre>
<p>The complete function becomes:</p>
<pre><code class="language-javascript">function loadSignatureImage(
    file
) {

    const allowedTypes = [

        "image/png",

        "image/jpeg"

    ];

    if (
        !allowedTypes.includes(
            file.type
        )
    ) {

        alert(
            "Please upload a PNG or JPEG image."
        );

        return;

    }

    selectedSignatureFile.textContent =
        `Selected: ${file.name}`;

    const reader =
        new FileReader();

    reader.onload =
        event =&gt; {

            createSignatureElement(
                event.target.result
            );

        };

    reader.readAsDataURL(file);

}
</code></pre>
<p>A transparent PNG usually works particularly well because only the signature strokes remain visible over the document.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/64561974-9b33-4391-a612-9966707433a1.png" alt="Upload signature option with a selected signature image displayed and positioned on the PDF preview." style="display:block;margin:0 auto" width="345" height="532" loading="lazy">

<h2 id="heading-creating-the-signature-preview-element">Creating the Signature Preview Element</h2>
<p>All three signature methods eventually call the same function:</p>
<pre><code class="language-javascript">createSignatureElement(imageUrl);
</code></pre>
<p>This means the rest of the editor doesn't need separate positioning logic for drawn, typed, and uploaded signatures.</p>
<p>Create the preview element:</p>
<pre><code class="language-javascript">let activeElement = null;

function createSignatureElement(
    imageUrl
) {

    elementLayer.innerHTML = "";

    const image =
        document.createElement(
            "img"
        );

    image.src =
        imageUrl;

    image.className =
        "pdf-element signature-element";

    image.style.left =
        "100px";

    image.style.top =
        "100px";

    image.style.width =
        "180px";

    elementLayer.appendChild(
        image
    );

    activeElement = {

        type: "signature",

        source: imageUrl,

        element: image,

        x: 100,

        y: 100,

        scale: 1,

        rotation: 0,

        opacity: 1

    };

}
</code></pre>
<p>The preview now represents the signature that will eventually be written into the PDF.</p>
<p>The same state object can later be updated when the user changes the signature's position, scale, rotation, or opacity.</p>
<h2 id="heading-adding-text-and-preset-stamps">Adding Text and Preset Stamps</h2>
<p>The second main element type is <strong>Text/Stamp</strong>.</p>
<p>This mode is useful when a document needs a short label, status, date, or other text rather than a handwritten signature.</p>
<p>Create the controls:</p>
<pre><code class="language-html">&lt;div id="textControls" hidden&gt;

    &lt;h3&gt;2. Add Text or Stamp&lt;/h3&gt;

    &lt;input
        type="text"
        id="customText"
        placeholder="Enter text"&gt;

    &lt;div class="stamp-options"&gt;

        &lt;button data-stamp="APPROVED"&gt;
            APPROVED
        &lt;/button&gt;

        &lt;button data-stamp="CONFIDENTIAL"&gt;
            CONFIDENTIAL
        &lt;/button&gt;

        &lt;button data-stamp="DRAFT"&gt;
            DRAFT
        &lt;/button&gt;

        &lt;button data-stamp="PAID"&gt;
            PAID
        &lt;/button&gt;

    &lt;/div&gt;

    &lt;label&gt;
        Size

        &lt;input
            type="number"
            id="textSize"
            value="16"
            min="8"
            max="120"&gt;
    &lt;/label&gt;

    &lt;label&gt;
        Color

        &lt;input
            type="color"
            id="textColor"
            value="#000000"&gt;
    &lt;/label&gt;

&lt;/div&gt;
</code></pre>
<p>Custom text can be displayed as the user types:</p>
<pre><code class="language-javascript">customText.addEventListener(
    "input",
    () =&gt; {

        createTextElement(
            customText.value
        );

    }
);
</code></pre>
<p>Preset stamps can update the same text input:</p>
<pre><code class="language-javascript">const stampButtons =
    document.querySelectorAll(
        "[data-stamp]"
    );

stampButtons.forEach(
    button =&gt; {

        button.addEventListener(
            "click",
            () =&gt; {

                const stamp =
                    button.dataset.stamp;

                customText.value =
                    stamp;

                createTextElement(
                    stamp
                );

            }
        );

    }
);
</code></pre>
<p>Create the text preview:</p>
<pre><code class="language-javascript">function createTextElement(
    text
) {

    if (!text.trim()) {

        elementLayer.innerHTML = "";

        activeElement = null;

        return;

    }

    elementLayer.innerHTML = "";

    const textElement =
        document.createElement(
            "div"
        );

    textElement.className =
        "pdf-element text-element";

    textElement.textContent =
        text;

    textElement.style.left =
        "100px";

    textElement.style.top =
        "100px";

    textElement.style.fontSize =
        `${textSize.value}px`;

    textElement.style.color =
        textColor.value;

    elementLayer.appendChild(
        textElement
    );

    activeElement = {

        type: "text",

        text,

        element:
            textElement,

        x: 100,

        y: 100,

        fontSize:
            Number(
                textSize.value
            ),

        color:
            textColor.value,

        rotation: 0,

        opacity: 1

    };

}
</code></pre>
<p>When the size changes, update the current element:</p>
<pre><code class="language-javascript">textSize.addEventListener(
    "input",
    () =&gt; {

        if (
            activeElement?.type !==
            "text"
        ) {
            return;
        }

        activeElement.fontSize =
            Number(
                textSize.value
            );

        activeElement
            .element
            .style
            .fontSize =
                `${textSize.value}px`;

    }
);
</code></pre>
<p>Do the same for the color:</p>
<pre><code class="language-javascript">textColor.addEventListener(
    "input",
    () =&gt; {

        if (
            activeElement?.type !==
            "text"
        ) {
            return;
        }

        activeElement.color =
            textColor.value;

        activeElement
            .element
            .style
            .color =
                textColor.value;

    }
);
</code></pre>
<p>The user can now enter custom content such as:</p>
<pre><code class="language-text">Signed on: 08-09-2025
</code></pre>
<p>or quickly select a predefined document stamp.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/18e1bb24-c802-4a40-b7dc-87772fbdbf0c.png" alt="Text and stamp editor showing custom text, Approved, Confidential, Draft, and Paid preset options with font size and color controls." style="display:block;margin:0 auto" width="1062" height="568" loading="lazy">

<p>At this point, the application can create content using all of the available input methods: a drawn signature, typed signature, uploaded signature image, custom text, or preset document stamp.</p>
<h2 id="heading-positioning-and-styling-the-element">Positioning and Styling the Element</h2>
<p>After creating a signature, text label, or preset stamp, the next step is positioning it correctly on the PDF page.</p>
<p>The preview element sits inside the <code>elementLayer</code> created earlier. Because this layer matches the PDF canvas dimensions, users can move the element visually before anything is written into the final PDF.</p>
<p>The editor also provides controls for:</p>
<ul>
<li><p>Scale</p>
</li>
<li><p>Rotation</p>
</li>
<li><p>Opacity</p>
</li>
<li><p>X position</p>
</li>
<li><p>Y position</p>
</li>
</ul>
<p>The exact controls can vary depending on the active element. For example, scale is particularly useful for signatures, while text size and color are handled by the Text/Stamp controls from the previous section.</p>
<p>Create the placement controls:</p>
<pre><code class="language-html">&lt;div id="placementControls"&gt;

    &lt;h3&gt;3. Placement &amp; Style&lt;/h3&gt;

    &lt;label&gt;
        Rotation (°)

        &lt;input
            type="number"
            id="rotationInput"
            value="0"&gt;
    &lt;/label&gt;

    &lt;label&gt;
        Opacity

        &lt;input
            type="range"
            id="opacityInput"
            min="0"
            max="100"
            value="100"&gt;
    &lt;/label&gt;

    &lt;label&gt;
        X Position

        &lt;input
            type="number"
            id="xPosition"
            value="100"&gt;
    &lt;/label&gt;

    &lt;label&gt;
        Y Position

        &lt;input
            type="number"
            id="yPosition"
            value="100"&gt;
    &lt;/label&gt;

&lt;/div&gt;
</code></pre>
<p>For signature elements, add a scale control:</p>
<pre><code class="language-html">&lt;label&gt;
    Scale (&lt;span id="scaleValue"&gt;100%&lt;/span&gt;)

    &lt;input
        type="range"
        id="scaleInput"
        min="25"
        max="250"
        value="100"&gt;
&lt;/label&gt;
</code></pre>
<p>Get the controls in JavaScript:</p>
<pre><code class="language-javascript">const scaleInput =
    document.getElementById(
        "scaleInput"
    );

const scaleValue =
    document.getElementById(
        "scaleValue"
    );

const rotationInput =
    document.getElementById(
        "rotationInput"
    );

const opacityInput =
    document.getElementById(
        "opacityInput"
    );

const xPosition =
    document.getElementById(
        "xPosition"
    );

const yPosition =
    document.getElementById(
        "yPosition"
    );
</code></pre>
<p>We'll use a single function to update the visual transformation.</p>
<pre><code class="language-javascript">function updateElementTransform() {

    if (!activeElement) {
        return;
    }

    activeElement.element.style.transform =
        `
            scale(${activeElement.scale})
            rotate(${activeElement.rotation}deg)
        `;

    activeElement.element.style.opacity =
        activeElement.opacity;

}
</code></pre>
<p>For text elements, initialize <code>scale</code> as <code>1</code> so the same transformation function can still be used.</p>
<pre><code class="language-javascript">activeElement = {

    type: "text",

    text,

    element: textElement,

    x: 100,

    y: 100,

    scale: 1,

    rotation: 0,

    opacity: 1

};
</code></pre>
<h3 id="heading-changing-the-element-scale">Changing the Element Scale</h3>
<p>When the user moves the scale slider, convert the percentage into a decimal value.</p>
<pre><code class="language-javascript">scaleInput.addEventListener(
    "input",
    () =&gt; {

        if (!activeElement) {
            return;
        }

        const percentage =
            Number(
                scaleInput.value
            );

        activeElement.scale =
            percentage / 100;

        scaleValue.textContent =
            `${percentage}%`;

        updateElementTransform();

    }
);
</code></pre>
<p>A value of <code>100%</code> represents the original preview size.</p>
<pre><code class="language-text">50%  → 0.5
100% → 1
114% → 1.14
200% → 2
</code></pre>
<p>This makes it easy to enlarge or reduce an uploaded, drawn, or typed signature without creating a new image.</p>
<h3 id="heading-rotating-the-element">Rotating the Element</h3>
<p>The rotation input stores the angle in degrees.</p>
<pre><code class="language-javascript">rotationInput.addEventListener(
    "input",
    () =&gt; {

        if (!activeElement) {
            return;
        }

        activeElement.rotation =
            Number(
                rotationInput.value
            );

        updateElementTransform();

    }
);
</code></pre>
<p>A rotation of <code>0</code> keeps the element horizontal, while positive or negative values rotate it around its center.</p>
<h3 id="heading-adjusting-opacity">Adjusting Opacity</h3>
<p>Opacity can be useful for stamps, watermarks, and other document labels.</p>
<p>Convert the percentage slider to a value between <code>0</code> and <code>1</code>.</p>
<pre><code class="language-javascript">opacityInput.addEventListener(
    "input",
    () =&gt; {

        if (!activeElement) {
            return;
        }

        activeElement.opacity =
            Number(
                opacityInput.value
            ) / 100;

        updateElementTransform();

    }
);
</code></pre>
<p>For example:</p>
<pre><code class="language-text">100% → 1
75%  → 0.75
50%  → 0.5
</code></pre>
<p>The same opacity value will later be used when generating the final PDF.</p>
<h2 id="heading-dragging-an-element-across-the-pdf-preview">Dragging an Element Across the PDF Preview</h2>
<p>Typing X and Y coordinates manually is useful for precise adjustments, but most users will prefer to drag the element directly to the required location.</p>
<p>Track the dragging state:</p>
<pre><code class="language-javascript">let isDragging = false;

let dragOffsetX = 0;

let dragOffsetY = 0;
</code></pre>
<p>When a signature or text element is created, attach the dragging behavior.</p>
<pre><code class="language-javascript">function enableDragging(
    element
) {

    element.addEventListener(
        "pointerdown",
        event =&gt; {

            isDragging = true;

            const elementRect =
                element
                    .getBoundingClientRect();

            dragOffsetX =
                event.clientX -
                elementRect.left;

            dragOffsetY =
                event.clientY -
                elementRect.top;

            element.setPointerCapture(
                event.pointerId
            );

        }
    );

}
</code></pre>
<p>Call this function when creating an element.</p>
<pre><code class="language-javascript">enableDragging(image);
</code></pre>
<p>or:</p>
<pre><code class="language-javascript">enableDragging(textElement);
</code></pre>
<p>Next, listen for pointer movement.</p>
<pre><code class="language-javascript">elementLayer.addEventListener(
    "pointermove",
    event =&gt; {

        if (
            !isDragging ||
            !activeElement
        ) {
            return;
        }

        const layerRect =
            elementLayer
                .getBoundingClientRect();

        const x =
            event.clientX -
            layerRect.left -
            dragOffsetX;

        const y =
            event.clientY -
            layerRect.top -
            dragOffsetY;

        moveActiveElement(
            x,
            y
        );

    }
);
</code></pre>
<p>Create a reusable movement function:</p>
<pre><code class="language-javascript">function moveActiveElement(
    x,
    y
) {

    if (!activeElement) {
        return;
    }

    activeElement.x = x;

    activeElement.y = y;

    activeElement.element.style.left =
        `${x}px`;

    activeElement.element.style.top =
        `${y}px`;

    xPosition.value =
        Math.round(x);

    yPosition.value =
        Math.round(y);

}
</code></pre>
<p>Stop dragging when the pointer is released.</p>
<pre><code class="language-javascript">elementLayer.addEventListener(
    "pointerup",
    () =&gt; {

        isDragging = false;

    }
);

elementLayer.addEventListener(
    "pointercancel",
    () =&gt; {

        isDragging = false;

    }
);
</code></pre>
<p>Now the signature or text element can be moved directly over the document.</p>
<p>For example, an uploaded signature may be positioned near the bottom-right corner of the final page.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/77fa703f-4636-4423-bb05-b97fc163d6de.png" alt="Uploaded signature image positioned on a PDF page with scale, rotation, opacity, X position, and Y position controls." style="display:block;margin:0 auto" width="1047" height="532" loading="lazy">

<h2 id="heading-updating-the-position-manually">Updating the Position Manually</h2>
<p>The X and Y fields provide another way to position the element.</p>
<p>Listen for changes to the X coordinate:</p>
<pre><code class="language-javascript">xPosition.addEventListener(
    "input",
    () =&gt; {

        if (!activeElement) {
            return;
        }

        const x =
            Number(
                xPosition.value
            );

        moveActiveElement(
            x,
            activeElement.y
        );

    }
);
</code></pre>
<p>Do the same for Y:</p>
<pre><code class="language-javascript">yPosition.addEventListener(
    "input",
    () =&gt; {

        if (!activeElement) {
            return;
        }

        const y =
            Number(
                yPosition.value
            );

        moveActiveElement(
            activeElement.x,
            y
        );

    }
);
</code></pre>
<p>Dragging and manual coordinate entry remain synchronized. Moving the element updates the fields, while changing the fields moves the preview element.</p>
<h2 id="heading-keeping-the-element-inside-the-page">Keeping the Element Inside the Page</h2>
<p>Without boundaries, users could accidentally drag an element completely outside the PDF preview.</p>
<p>We can limit the position before saving it.</p>
<pre><code class="language-javascript">function clampPosition(
    x,
    y
) {

    const element =
        activeElement.element;

    const maxX =
        elementLayer.clientWidth -
        element.offsetWidth;

    const maxY =
        elementLayer.clientHeight -
        element.offsetHeight;

    return {

        x:
            Math.max(
                0,
                Math.min(x, maxX)
            ),

        y:
            Math.max(
                0,
                Math.min(y, maxY)
            )

    };

}
</code></pre>
<p>Use it inside <code>moveActiveElement()</code>:</p>
<pre><code class="language-javascript">const position =
    clampPosition(
        x,
        y
    );

activeElement.x =
    position.x;

activeElement.y =
    position.y;
</code></pre>
<p>When scale or rotation is applied, the element's transformed visual bounds can extend beyond its original box. A production editor can use <code>getBoundingClientRect()</code> for more precise transformed-boundary calculations.</p>
<p>The basic clamp shown here is sufficient to demonstrate the positioning workflow.</p>
<h2 id="heading-applying-the-element-to-selected-pages">Applying the Element to Selected Pages</h2>
<p>After positioning the element, the user decides which PDF pages should receive it.</p>
<p>The interface provides three options:</p>
<pre><code class="language-text">Current page only
All pages
Specific pages
</code></pre>
<p>Create the controls:</p>
<pre><code class="language-html">&lt;div id="pageApplication"&gt;

    &lt;h3&gt;4. Apply to Pages&lt;/h3&gt;

    &lt;label&gt;
        &lt;input
            type="radio"
            name="applyMode"
            value="current"
            checked&gt;
        Current page only
    &lt;/label&gt;

    &lt;label&gt;
        &lt;input
            type="radio"
            name="applyMode"
            value="all"&gt;
        All pages
    &lt;/label&gt;

    &lt;label&gt;
        &lt;input
            type="radio"
            name="applyMode"
            value="specific"&gt;
        Specific pages
    &lt;/label&gt;

    &lt;input
        type="text"
        id="specificPages"
        placeholder="e.g., 1, 3-5, 10"&gt;

&lt;/div&gt;
</code></pre>
<p>Read the selected mode:</p>
<pre><code class="language-javascript">function getTargetPages() {

    const mode =
        document.querySelector(
            'input[name="applyMode"]:checked'
        ).value;

    if (
        mode ===
        "current"
    ) {

        return [
            currentPage
        ];

    }

    if (
        mode ===
        "all"
    ) {

        return Array.from(

            {
                length:
                    totalPages
            },

            (_, index) =&gt;
                index + 1

        );

    }

    return parsePageRange(
        specificPages.value
    );

}
</code></pre>
<p>Parse custom values such as:</p>
<pre><code class="language-text">1, 3-5, 10
</code></pre>
<p>with:</p>
<pre><code class="language-javascript">function parsePageRange(
    value
) {

    const pages =
        new Set();

    value
        .split(",")
        .forEach(part =&gt; {

            const item =
                part.trim();

            if (!item) {
                return;
            }

            if (
                item.includes("-")
            ) {

                const [
                    start,
                    end
                ] =
                    item
                        .split("-")
                        .map(Number);

                for (
                    let page = start;
                    page &lt;= end;
                    page++
                ) {

                    if (
                        page &gt;= 1 &amp;&amp;
                        page &lt;= totalPages
                    ) {

                        pages.add(page);

                    }

                }

            } else {

                const page =
                    Number(item);

                if (
                    page &gt;= 1 &amp;&amp;
                    page &lt;= totalPages
                ) {

                    pages.add(page);

                }

            }

        });

    return [...pages];

}
</code></pre>
<p>The result becomes:</p>
<pre><code class="language-javascript">[
    1,
    3,
    4,
    5,
    10
]
</code></pre>
<p>This allows a signature or stamp to be placed once and then applied to multiple target pages.</p>
<p>Just keep in mind that page dimensions may differ within the same PDF. Applying the same coordinates across pages works best when those pages use a consistent size and layout.</p>
<h2 id="heading-applying-and-finalizing-the-pdf">Applying and Finalizing the PDF</h2>
<p>Once the element is created, positioned, styled, and assigned to the correct pages, the user can click <strong>Apply &amp; Finalize</strong>.</p>
<p>Create the action buttons:</p>
<pre><code class="language-html">&lt;div class="editor-actions"&gt;

    &lt;button id="applyButton"&gt;
        Apply &amp; Finalize
    &lt;/button&gt;

    &lt;button id="startOverButton"&gt;
        Start Over
    &lt;/button&gt;

&lt;/div&gt;
</code></pre>
<p>Get the buttons:</p>
<pre><code class="language-javascript">const applyButton =
    document.getElementById(
        "applyButton"
    );

const startOverButton =
    document.getElementById(
        "startOverButton"
    );
</code></pre>
<p>Before generating the final PDF, make sure an element exists.</p>
<pre><code class="language-javascript">applyButton.addEventListener(
    "click",
    async () =&gt; {

        if (!activeElement) {

            alert(
                "Please add a signature, text, or stamp first."
            );

            return;

        }

        const targetPages =
            getTargetPages();

        if (
            targetPages.length === 0
        ) {

            alert(
                "Please select at least one valid page."
            );

            return;

        }

        await generateFinalPdf(
            targetPages
        );

    }
);
</code></pre>
<p>The <code>generateFinalPdf()</code> function will handle the actual PDF modification in the next section.</p>
<p>The <strong>Start Over</strong> button clears the current document and resets the application.</p>
<pre><code class="language-javascript">startOverButton.addEventListener(
    "click",
    resetTool
);
</code></pre>
<p>Create the reset function:</p>
<pre><code class="language-javascript">function resetTool() {

    pdfDocument = null;

    originalPdfBytes = null;

    currentPage = 1;

    totalPages = 0;

    activeElement = null;

    pdfInput.value = "";

    elementLayer.innerHTML = "";

    editorSection.hidden = true;

    resultSection.hidden = true;

    uploadSection.hidden = false;

}
</code></pre>
<p>This returns the application to its original upload state.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/e98a951a-609b-4e1e-b2a6-5cd2e31a6ac1.png" alt="Apply and Finalize button for adding the selected signature or text element to the PDF, with a Start Over option." style="display:block;margin:0 auto" width="362" height="86" loading="lazy">

<p>The interactive editing stage is now complete. Users can create a signature or text element, position it directly over the PDF, adjust its appearance, choose the target pages, and prepare the document for final processing.</p>
<h2 id="heading-generating-the-signed-pdf">Generating the Signed PDF</h2>
<p>The element shown over the browser preview hasn't yet been added to the actual PDF. When users click <strong>Apply &amp; Finalize</strong>, the application loads the original document with PDF-lib and writes the selected signature, text, or stamp onto the target pages.</p>
<p>Start by loading the original PDF bytes:</p>
<pre><code class="language-javascript">async function generateFinalPdf(
    targetPages
) {

    const pdfDoc =
        await PDFLib.PDFDocument.load(
            originalPdfBytes.slice()
        );

    const pages =
        pdfDoc.getPages();

}
</code></pre>
<p>Before placing the element, we need to convert its browser coordinates into PDF coordinates.</p>
<p>The preview canvas may be displayed at a different size from the actual PDF page. The coordinate systems also use different Y-axis origins.</p>
<p>For each target page, calculate the scale:</p>
<pre><code class="language-javascript">const {
    width: pdfWidth,
    height: pdfHeight
} = page.getSize();

const scaleX =
    pdfWidth /
    pdfCanvas.width;

const scaleY =
    pdfHeight /
    pdfCanvas.height;
</code></pre>
<p>Convert the preview position:</p>
<pre><code class="language-javascript">const pdfX =
    activeElement.x *
    scaleX;

const pdfY =
    pdfHeight -
    (
        activeElement.y +
        activeElement.element.offsetHeight
    ) * scaleY;
</code></pre>
<p>This conversion maps the element from the browser's top-left coordinate system to the PDF page's coordinate system.</p>
<h3 id="heading-embedding-a-signature">Embedding a Signature</h3>
<p>Drawn, typed, and uploaded signatures are all represented as images by the time they reach the final processing stage.</p>
<p>Convert the signature data URL into bytes:</p>
<pre><code class="language-javascript">async function dataUrlToBytes(
    dataUrl
) {

    const response =
        await fetch(dataUrl);

    return await response.arrayBuffer();

}
</code></pre>
<p>Embed the signature image:</p>
<pre><code class="language-javascript">const signatureBytes =
    await dataUrlToBytes(
        activeElement.source
    );

const signatureImage =
    await pdfDoc.embedPng(
        signatureBytes
    );
</code></pre>
<p>If uploaded JPEG signatures are supported, the application should preserve the original image format and use <code>embedJpg()</code> when appropriate.</p>
<p>Calculate the final dimensions:</p>
<pre><code class="language-javascript">const previewWidth =
    activeElement
        .element
        .offsetWidth *
    activeElement.scale;

const previewHeight =
    activeElement
        .element
        .offsetHeight *
    activeElement.scale;

const finalWidth =
    previewWidth *
    scaleX;

const finalHeight =
    previewHeight *
    scaleY;
</code></pre>
<p>Then draw the signature:</p>
<pre><code class="language-javascript">page.drawImage(
    signatureImage,
    {
        x: pdfX,

        y:
            pdfHeight -
            (
                activeElement.y *
                scaleY
            ) -
            finalHeight,

        width:
            finalWidth,

        height:
            finalHeight,

        rotate:
            PDFLib.degrees(
                activeElement.rotation
            ),

        opacity:
            activeElement.opacity
    }
);
</code></pre>
<p>The same processing logic works whether the signature was drawn, typed, or uploaded because all three methods produce an image element before finalization.</p>
<h3 id="heading-adding-text-or-a-stamp">Adding Text or a Stamp</h3>
<p>Text and preset stamps are written directly onto the PDF page.</p>
<p>First, convert the selected color from hexadecimal to RGB values.</p>
<pre><code class="language-javascript">function hexToRgb(
    hex
) {

    const value =
        hex.replace(
            "#",
            ""
        );

    return {

        r:
            parseInt(
                value.substring(0, 2),
                16
            ) / 255,

        g:
            parseInt(
                value.substring(2, 4),
                16
            ) / 255,

        b:
            parseInt(
                value.substring(4, 6),
                16
            ) / 255

    };

}
</code></pre>
<p>Apply the text:</p>
<pre><code class="language-javascript">const color =
    hexToRgb(
        activeElement.color
    );

page.drawText(
    activeElement.text,
    {
        x:
            activeElement.x *
            scaleX,

        y:
            pdfHeight -
            (
                activeElement.y *
                scaleY
            ) -
            activeElement.fontSize,

        size:
            activeElement.fontSize *
            scaleY,

        color:
            PDFLib.rgb(
                color.r,
                color.g,
                color.b
            ),

        rotate:
            PDFLib.degrees(
                activeElement.rotation
            ),

        opacity:
            activeElement.opacity
    }
);
</code></pre>
<p>After processing every target page, save the modified document:</p>
<pre><code class="language-javascript">const finalPdfBytes =
    await pdfDoc.save();

const finalPdfBlob =
    new Blob(
        [finalPdfBytes],
        {
            type:
                "application/pdf"
        }
    );

await showFinalPreview(
    finalPdfBlob
);
</code></pre>
<p>At this point, the selected signature, text, or stamp has been added to the generated PDF.</p>
<h2 id="heading-previewing-the-final-pdf">Previewing the Final PDF</h2>
<p>Before downloading the document, the application displays the completed PDF in a separate preview area.</p>
<p>This allows users to confirm that the element appears on the correct page and in the expected position.</p>
<p>Load the generated file with PDF.js:</p>
<pre><code class="language-javascript">let finalPdfDocument = null;

let finalPage = 1;

async function showFinalPreview(
    blob
) {

    const bytes =
        await blob.arrayBuffer();

    finalPdfDocument =
        await pdfjsLib
            .getDocument({
                data: bytes
            })
            .promise;

    finalPage = 1;

    editorSection.hidden =
        true;

    resultSection.hidden =
        false;

    await renderFinalPage(
        finalPage
    );

}
</code></pre>
<p>Render the current result page:</p>
<pre><code class="language-javascript">async function renderFinalPage(
    pageNumber
) {

    const page =
        await finalPdfDocument
            .getPage(
                pageNumber
            );

    const viewport =
        page.getViewport({
            scale: 1.4
        });

    finalCanvas.width =
        viewport.width;

    finalCanvas.height =
        viewport.height;

    await page.render({

        canvasContext:
            finalCanvas
                .getContext("2d"),

        viewport

    }).promise;

    finalPageInfo.textContent =
        `Page ${pageNumber} of ${finalPdfDocument.numPages}`;

}
</code></pre>
<p>Previous and next controls can use the same navigation pattern as the original PDF preview.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/8ca71960-392a-4c49-a138-f462786682a0.png" alt="Final PDF preview showing the applied signature or text element before downloading the document." style="display:block;margin:0 auto" width="704" height="547" loading="lazy">

<h2 id="heading-renaming-and-downloading-the-final-pdf">Renaming and Downloading the Final PDF</h2>
<p>After reviewing the processed document, users can rename the file before downloading it.</p>
<p>For example:</p>
<pre><code class="language-text">document_signed.pdf
</code></pre>
<p>Create the filename input:</p>
<pre><code class="language-html">&lt;input
    type="text"
    id="outputFilename"
    value="document_signed.pdf"&gt;
</code></pre>
<p>Make sure the filename has the correct extension:</p>
<pre><code class="language-javascript">function getOutputFilename() {

    let filename =
        outputFilename
            .value
            .trim();

    if (!filename) {

        filename =
            "document_signed.pdf";

    }

    if (
        !filename
            .toLowerCase()
            .endsWith(".pdf")
    ) {

        filename += ".pdf";

    }

    return filename;

}
</code></pre>
<p>The result section can also display the total page count and generated file size.</p>
<pre><code class="language-javascript">function formatFileSize(
    bytes
) {

    if (
        bytes &lt;
        1024 * 1024
    ) {

        return (
            bytes / 1024
        ).toFixed(2) + " KB";

    }

    return (
        bytes /
        1024 /
        1024
    ).toFixed(2) + " MB";

}
</code></pre>
<p>Update the file information:</p>
<pre><code class="language-javascript">filePageCount.textContent =
    `Total Pages: ${finalPdfDocument.numPages}`;

fileSize.textContent =
    `File Size: ${
        formatFileSize(
            finalPdfBlob.size
        )
    }`;
</code></pre>
<p>Download the file using a temporary object URL:</p>
<pre><code class="language-javascript">downloadButton.addEventListener(
    "click",
    () =&gt; {

        const url =
            URL.createObjectURL(
                finalPdfBlob
            );

        const link =
            document.createElement(
                "a"
            );

        link.href =
            url;

        link.download =
            getOutputFilename();

        link.click();

        URL.revokeObjectURL(
            url
        );

    }
);
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/96212b85-3504-4420-bfba-819c20ca5a6e.png" alt="Signed PDF download section with editable filename, total page count, file size, and Download button." style="display:block;margin:0 auto" width="355" height="280" loading="lazy">

<p>After downloading, the <strong>Start Over</strong> button resets the application so another PDF can be processed.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/fd08536f-4b47-4b7b-930c-5943685ad58e.png" alt="Start Over button for clearing the current PDF signing session and uploading another document." style="display:block;margin:0 auto" width="150" height="54" loading="lazy">

<h2 id="heading-demo-how-the-pdf-signature-tool-works">Demo: How the PDF Signature Tool Works</h2>
<p>Let's walk through the complete workflow from upload to download.</p>
<h3 id="heading-step-1-upload-the-pdf">Step 1: Upload the PDF</h3>
<p>Users begin by dragging a PDF into the upload area or clicking <strong>Select PDF</strong>.</p>
<p>The browser reads the document and prepares it for local processing.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/aa49b177-d750-4432-80db-51101eaa5650.png" alt="PDF Signature Tool upload area with drag-and-drop support and Select PDF button." style="display:block;margin:0 auto" width="639" height="652" loading="lazy">

<h3 id="heading-step-2-preview-and-navigate-the-document">Step 2: Preview and Navigate the Document</h3>
<p>After upload, the current page appears in the PDF preview.</p>
<p>Previous and next controls allow users to navigate through multi-page documents and find the page where an element needs to be added.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/61827efd-9872-4499-8d61-96f7a7c6a821.png" alt="Uploaded PDF preview with previous and next page navigation controls." style="display:block;margin:0 auto" width="708" height="550" loading="lazy">

<h3 id="heading-step-3-choose-what-to-add">Step 3: Choose What to Add</h3>
<p>The user chooses between <strong>Signature</strong> and <strong>Text/Stamp</strong>.</p>
<p>This determines which creation controls appear in the editor.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/ba697ba0-2e86-4c35-9510-39503e706ac8.png" alt="PDF editing controls for choosing between a signature and a text or stamp element." style="display:block;margin:0 auto" width="342" height="755" loading="lazy">

<h3 id="heading-step-4-create-the-signature">Step 4: Create the Signature</h3>
<p>If Signature is selected, users can choose <strong>Draw</strong>, <strong>Type</strong>, or <strong>Upload</strong>.</p>
<p>Drawing works directly inside the signature canvas. The Type option creates a signature-style element from entered text, while Upload accepts an existing signature image.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/971b15a8-4322-45c3-83fb-cbb8eae3172d.png" alt="PDF signature creation controls with Draw, Type, and Upload options." style="display:block;margin:0 auto" width="543" height="350" loading="lazy">

<h3 id="heading-step-5-add-custom-text-or-a-preset-stamp">Step 5: Add Custom Text or a Preset Stamp</h3>
<p>Instead of a signature, users can select <strong>Text/Stamp</strong>.</p>
<p>They can enter custom content or choose a preset such as <strong>APPROVED</strong>, <strong>CONFIDENTIAL</strong>, <strong>DRAFT</strong>, or <strong>PAID</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/572bbac7-2b4d-49bd-8cae-399ca82c74b7.png" alt="Text and stamp controls with custom text and preset document stamp options." style="display:block;margin:0 auto" width="560" height="423" loading="lazy">

<h3 id="heading-step-6-position-and-style-the-element">Step 6: Position and Style the Element</h3>
<p>The created element appears over the PDF preview.</p>
<p>Users can drag it to the required position and adjust properties such as scale, rotation, opacity, X position, and Y position.</p>
<p>Text elements also support configurable font size and color.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/d3424bda-ada1-4acd-8ce8-c55a631efc27.png" alt="Signature positioned over a PDF page with placement and styling controls." style="display:block;margin:0 auto" width="1047" height="532" loading="lazy">

<h3 id="heading-step-7-choose-the-target-pages">Step 7: Choose the Target Pages</h3>
<p>The element can be applied to the current page, every page, or a specific page selection.</p>
<p>For example:</p>
<pre><code class="language-text">1, 3-5, 10
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/10b0061f-57f2-4314-a7cb-41758bc00be6.png" alt="Choose the Target Pages or applied pages" style="display:block;margin:0 auto" width="329" height="252" loading="lazy">

<p>This is useful when the same stamp or document label needs to appear on several pages.</p>
<h3 id="heading-step-8-apply-and-finalize">Step 8: Apply and Finalize</h3>
<p>After checking the element and target pages, users click <strong>Apply &amp; Finalize</strong>.</p>
<p>The browser converts the preview position into PDF coordinates and generates the modified document.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/8ad34b20-777d-49c9-8900-4876e22ac8c1.png" alt="Apply and Finalize button for generating the PDF with the selected signature or text element." style="display:block;margin:0 auto" width="204" height="73" loading="lazy">

<h3 id="heading-step-9-preview-the-completed-pdf">Step 9: Preview the Completed PDF</h3>
<p>The generated document appears in a final preview.</p>
<p>Users can navigate through the pages and verify that the signature, text, or stamp appears correctly before downloading.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/f7d699e6-bbb3-4692-b2bf-d2e1bb44bdc5.png" alt=" Completed PDF preview showing an applied signature before download." style="display:block;margin:0 auto" width="704" height="547" loading="lazy">

<h3 id="heading-step-10-rename-and-download">Step 10: Rename and Download</h3>
<p>The final section allows users to change the output filename and review the total number of pages and file size.</p>
<p>Clicking <strong>Download</strong> saves the generated PDF locally.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/58186c5d-7f10-4860-b2c3-87f5862bd025.png" alt="Final PDF download section with filename editing, page count, file size, and Download button." style="display:block;margin:0 auto" width="355" height="280" loading="lazy">

<p>Afterward, <strong>Start Over</strong> clears the session and returns to the upload interface.</p>
<h2 id="heading-handling-signature-transparency">Handling Signature Transparency</h2>
<p>Uploaded signatures often look best when the background is transparent.</p>
<p>A transparent PNG contains only the visible signature strokes, allowing the original PDF content to remain visible around the signature.</p>
<p>A JPEG image, by comparison, usually includes a solid background. If the image was scanned from white paper, placing it on a colored PDF area may create a visible white rectangle.</p>
<p>For uploaded signatures, transparent PNG files are therefore usually the better option.</p>
<p>The same principle applies to drawn and typed signatures. When converting a canvas to PNG, avoid filling the canvas with a background color unless that background is intentionally required.</p>
<pre><code class="language-javascript">const signatureImage =
    signatureCanvas.toDataURL(
        "image/png"
    );
</code></pre>
<p>The transparent canvas can then be embedded directly into the PDF.</p>
<h2 id="heading-important-notes-and-common-mistakes">Important Notes and Common Mistakes</h2>
<p>One common mistake is assuming that the browser preview and the actual PDF use identical coordinates.</p>
<p>Always calculate the relationship between the canvas dimensions and the target PDF page before placing the final element.</p>
<pre><code class="language-javascript">const scaleX =
    pdfWidth /
    pdfCanvas.width;

const scaleY =
    pdfHeight /
    pdfCanvas.height;
</code></pre>
<p>Another issue occurs when the same element is applied to pages with different dimensions. A position that looks correct on an A4 page may not appear in the same visual location on a landscape or differently sized page.</p>
<p>Uploaded signature images should also be validated before processing.</p>
<pre><code class="language-javascript">const allowedTypes = [
    "image/png",
    "image/jpeg"
];

if (
    !allowedTypes.includes(
        file.type
    )
) {

    alert(
        "Please upload a PNG or JPEG image."
    );

    return;

}
</code></pre>
<p>Very large image files should be resized before embedding to avoid unnecessarily increasing the final PDF size.</p>
<p>Users should also review the completed document before downloading it. Rotation, scaling, or coordinate conversion errors are much easier to identify in the final preview than after the file has already been shared.</p>
<p>Finally, remember that this project adds a <strong>visual electronic signature</strong> to a PDF. It does not create a certificate-based cryptographic digital signature or provide automatic identity verification.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you built a browser-based PDF Signature Tool using JavaScript.</p>
<p>You learned how to upload and preview PDF documents, navigate between pages, create signatures by drawing, typing, or uploading an image, add custom text and preset stamps, position elements directly over a PDF preview, adjust their appearance, choose target pages, and generate the completed document with PDF-lib.</p>
<p>You also learned how browser coordinates are converted into PDF coordinates and why signature transparency matters when embedding images into a document.</p>
<p>The final workflow allows users to preview the completed PDF, rename the output file, review its page count and size, and download it directly from the browser.</p>
<p>You can explore the complete workflow using the <a href="https://allinonetools.net/sign-pdf/">PDF Signature Tool</a>.</p>
<p>The project can be extended further with multiple elements per page, reusable signature profiles, date fields, initials, custom fonts, signature removal before finalization, or certificate-based digital signing through a dedicated signing infrastructure.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Multi-Tenant SaaS API with Node.js, RBAC, and Audit Logging ]]>
                </title>
                <description>
                    <![CDATA[ A colleague asked me to help debug what looked like a permissions issue in their SaaS project management tool. Users were seeing resources they hadn't created. I pulled up the query logs expecting som ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-multi-tenant-saas-api-with-nodejs-rbac-and-audit-logging/</link>
                <guid isPermaLink="false">6a5e8914bc397f89a942b88b</guid>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ api ]]>
                    </category>
                
                    <category>
                        <![CDATA[ PostgreSQL ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ backend ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Zia Ullah ]]>
                </dc:creator>
                <pubDate>Mon, 20 Jul 2026 20:46:12 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/69793fe0-fe0e-4c9c-839d-12a134f65287.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>A colleague asked me to help debug what looked like a permissions issue in their SaaS project management tool. Users were seeing resources they hadn't created.</p>
<p>I pulled up the query logs expecting something subtle. It was not. The list endpoint had no <code>tenant_id</code> filter at all. Every tenant in the database could read every other tenant's projects. The application never threw an error. It just returned whatever was there.</p>
<p>Missing tenant filters don't throw errors. They return the wrong data without any complaint, and nothing in your logs will flag it. I've seen this run in production for weeks before a support ticket pointed anyone at the query logs.</p>
<p>When it does surface, who finds it first matters a lot. A customer noticing it is bad. A compliance auditor noticing it during a SOC 2 review is a different kind of problem.</p>
<p>Isolation built in from the start is a day of work. The time I spent helping a team retrofit it after a compliance review was considerably longer than that, and involved more customer emails than anyone wanted to write.</p>
<p>The stack is Node.js with PostgreSQL. CRUD is the easy part. Tenant isolation, RBAC, and audit logging take more care, and where those checks run in the stack matters. I put all three in middleware, before any route handler fires. A handler that never calls the isolation logic directly can't accidentally skip it.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p>Node.js 18+</p>
</li>
<li><p>PostgreSQL 14+</p>
</li>
<li><p>Basic knowledge of Express.js and JWT</p>
</li>
</ul>
<h2 id="heading-what-we-will-build">What We Will Build</h2>
<p>A multi-tenant Express REST API that enforces:</p>
<ol>
<li><p><strong>Tenant isolation:</strong> every database query scopes to the <code>tenant_id</code> from the verified JWT. The client can't influence which tenant the query runs against.</p>
</li>
<li><p><strong>RBAC:</strong> four roles, each with a numeric level (SuperAdmin is highest, Viewer lowest). Middleware checks the level before the handler runs.</p>
</li>
<li><p><strong>Audit logging:</strong> any write or sensitive read appends a row to the audit table. The app can't modify those rows afterward. The database enforces this directly. If a bug in the app tries to UPDATE an audit row, the database refuses it. Application-level enforcement alone can't give you that guarantee.</p>
</li>
<li><p><strong>Per-tenant rate limiting:</strong> request counts in Redis, keyed to the tenant. I've seen IP-based limiting break an enterprise rollout when fifty users came through a single corporate proxy.</p>
</li>
<li><p><strong>Tenant isolation tests:</strong> a dedicated test file that proves cross-tenant data can't leak. Wire it into CI and it catches broken isolation before it ships.</p>
</li>
</ol>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-how-multi-tenancy-works">How Multi-Tenancy Works</a></p>
</li>
<li><p><a href="#heading-architecture-overview">Architecture Overview</a></p>
</li>
<li><p><a href="#heading-database-schema-design">Database Schema Design</a></p>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a href="#heading-jwt-design-for-multi-tenancy">JWT Design for Multi-Tenancy</a></p>
</li>
<li><p><a href="#heading-auth-and-rbac-middleware">Auth and RBAC Middleware</a></p>
</li>
<li><p><a href="#heading-the-tenant-safe-repository-layer">The Tenant-Safe Repository Layer</a></p>
</li>
<li><p><a href="#heading-audit-logging-service">Audit Logging Service</a></p>
</li>
<li><p><a href="#heading-per-tenant-rate-limiting">Per-Tenant Rate Limiting</a></p>
</li>
<li><p><a href="#heading-building-the-routes">Building the Routes</a></p>
</li>
<li><p><a href="#heading-testing-tenant-isolation">Testing Tenant Isolation</a></p>
</li>
<li><p><a href="#heading-troubleshooting">Troubleshooting</a></p>
</li>
<li><p><a href="#heading-wrapping-up">Wrapping Up</a></p>
</li>
</ol>
<h2 id="heading-how-multi-tenancy-works">How Multi-Tenancy Works</h2>
<p>This tutorial uses a <strong>shared database with row-level isolation</strong>: a <code>tenant_id</code> column on every table, a filter on every query. The database holds everyone's data together. The application decides what each tenant can see.</p>
<p>Two other approaches exist: schema-per-tenant and database-per-tenant. I've talked to teams on schema-per-tenant who ended up spending more engineering time on migration tooling than on their actual product. Database-per-tenant gives stronger guarantees but a connection pool that balloons with every new customer signup.</p>
<p>Neither scales cheaply. Row-level isolation scales further than most teams expect. The ones I know who moved off it did so years in, usually under specific regulatory pressure, not because the approach stopped working.</p>
<p>The one thing in this design that can't be optional: <code>tenant_id</code> <strong>must always come from the verified JWT.</strong> Not from the request body, not from the URL. Users control what they put in both of those. They don't control what gets signed into a JWT on your server.</p>
<h2 id="heading-architecture-overview">Architecture Overview</h2>
<pre><code class="language-plaintext">HTTP Request
     │
     ▼
┌─────────────────────────────────────────┐
│           Express Middleware Stack       │
│                                         │
│  1. Rate Limiter (per tenant_id)        │
│  2. Auth Middleware (verify JWT)        │
│     └─► Extracts: userId, tenantId,    │
│          role, permissions              │
│  3. RBAC Middleware (check role)        │
└──────────────┬──────────────────────────┘
               │
               ▼
┌─────────────────────────────────────────┐
│           Route Handler                  │
│                                         │
│  1. Call Repository (tenant-safe query) │
│  2. Call Audit Service (fire &amp; forget)  │
│  3. Return response                     │
└──────────────┬──────────────────────────┘
               │
     ┌─────────┴──────────┐
     ▼                    ▼
┌─────────┐        ┌────────────┐
│ Projects│        │ Audit Logs │
│  Table  │        │   Table    │
│(+tenant)│        │(append only│
└─────────┘        └────────────┘
</code></pre>
<p>Rate limiting, auth, and RBAC all run before any handler sees the request. Writes pass through the audit service. The repository takes <code>tenantId</code> from <code>req.user</code> and the handler never touches tenant scoping directly, so there's no path around it.</p>
<h2 id="heading-database-schema-design">Database Schema Design</h2>
<pre><code class="language-sql">-- Tenants table
CREATE TABLE tenants (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name        VARCHAR(255) NOT NULL,
  plan        VARCHAR(50) NOT NULL DEFAULT 'free', -- 'free', 'pro', 'enterprise'
  created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Users table
CREATE TABLE users (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id   UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  email       VARCHAR(255) NOT NULL,
  role        VARCHAR(50) NOT NULL DEFAULT 'Member', -- 'SuperAdmin','TenantAdmin','Member','Viewer'
  created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE(tenant_id, email)
);

CREATE INDEX idx_users_tenant ON users(tenant_id);

-- Projects table (example resource — replace with your domain entity)
CREATE TABLE projects (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id   UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  name        VARCHAR(255) NOT NULL,
  description TEXT,
  created_by  UUID NOT NULL REFERENCES users(id),
  created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_projects_tenant ON projects(tenant_id);

-- Audit log table (append-only — never UPDATE or DELETE rows here)
CREATE TABLE audit_logs (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id   UUID NOT NULL,
  user_id     UUID NOT NULL,
  user_email  TEXT NOT NULL,
  user_role   TEXT NOT NULL,        -- role at time of action
  action      TEXT NOT NULL,        -- 'CREATE', 'UPDATE', 'DELETE', 'VIEW'
  resource    TEXT NOT NULL,        -- table name
  resource_id TEXT,
  old_values  JSONB,
  new_values  JSONB,
  ip_address  INET,
  user_agent  TEXT,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_audit_tenant ON audit_logs(tenant_id);
CREATE INDEX idx_audit_created ON audit_logs(created_at DESC);

-- Protect audit log at database level
-- Use a DO block so this runs safely in Docker where app_user is the superuser
DO $$
BEGIN
  IF current_user &lt;&gt; 'app_user' THEN
    REVOKE DELETE, UPDATE ON audit_logs FROM app_user;
  END IF;
END $$;
</code></pre>
<p>The <code>REVOKE</code> matters. Application bugs happen. If something in your codebase accidentally tries to UPDATE an audit row, you want the database to refuse it outright, not silently comply.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<pre><code class="language-bash">mkdir nodejs-multitenant-saas-api
cd nodejs-multitenant-saas-api
npm init -y
npm install express pg jsonwebtoken bcryptjs express-rate-limit rate-limit-redis ioredis dotenv
npm install --save-dev jest supertest
</code></pre>
<h3 id="heading-starting-postgresql-and-redis-with-docker">Starting PostgreSQL and Redis with Docker</h3>
<p>Skip the local installs. One <code>docker-compose.yml</code> in the project root brings up both PostgreSQL and Redis:</p>
<pre><code class="language-yaml">services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: saas_api
      POSTGRES_USER: app_user
      POSTGRES_PASSWORD: app_password
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./schema.sql:/docker-entrypoint-initdb.d/01_schema.sql

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  postgres_data:
</code></pre>
<p>That <code>schema.sql</code> mount runs your SQL automatically when the container first starts. No psql required.</p>
<pre><code class="language-bash">docker compose up -d
</code></pre>
<p><code>.env</code> in the project root:</p>
<pre><code class="language-plaintext">DATABASE_URL=postgresql://app_user:app_password@localhost:5432/saas_api
REDIS_URL=redis://localhost:6379
JWT_SECRET=your_random_secret_here
PORT=3000
NODE_ENV=development
</code></pre>
<p>Don't type a <code>JWT_SECRET</code> by hand. Run this to generate one:</p>
<pre><code class="language-bash">node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
</code></pre>
<p>File structure:</p>
<pre><code class="language-plaintext">nodejs-multitenant-saas-api/
├── src/
│   ├── middleware/
│   │   ├── auth.js          # JWT verification + tenant extraction
│   │   ├── rbac.js          # Role enforcement
│   │   └── rateLimiter.js   # Per-tenant rate limiting
│   ├── services/
│   │   └── auditService.js  # Append-only audit logger
│   ├── repositories/
│   │   └── projectRepo.js   # Tenant-safe DB queries
│   ├── routes/
│   │   └── projects.js      # Route handlers
│   └── utils/
│       └── token.js         # JWT token generation
├── db/
│   ├── index.js             # PostgreSQL pool
│   └── redis.js             # Redis client
├── docker-compose.yml
├── app.js
├── server.js
└── tests/
    └── tenantIsolation.test.js
</code></pre>
<h3 id="heading-boilerplate-files">Boilerplate Files</h3>
<p>There are four files the tutorial doesn't cover in detail, but the test file needs all of them to run:</p>
<pre><code class="language-javascript">// db/index.js
const { Pool } = require('pg');

const pool = new Pool({ connectionString: process.env.DATABASE_URL });

pool.on('error', (err) =&gt; console.error('PostgreSQL error:', err.message));

module.exports = { pool };
</code></pre>
<pre><code class="language-javascript">// db/redis.js
const Redis = require('ioredis');

const redisClient = new Redis(process.env.REDIS_URL);

redisClient.on('error', (err) =&gt; console.error('Redis error:', err.message));

module.exports = { redisClient };
</code></pre>
<pre><code class="language-javascript">// app.js
require('dotenv').config();
const express = require('express');
const projectsRouter = require('./src/routes/projects');

const app = express();
app.use(express.json());

app.use('/api/projects', projectsRouter);

// Global error handler — must have 4 parameters to be recognised by Express
app.use((err, req, res, next) =&gt; {
  console.error(err.stack);
  res.status(500).json({ error: 'Internal server error' });
});

module.exports = app;
</code></pre>
<pre><code class="language-javascript">// server.js
const app = require('./app');

const PORT = process.env.PORT || 3000;
app.listen(PORT, () =&gt; console.log(`Server running on port ${PORT}`));
</code></pre>
<p><code>bcryptjs</code> is included for a login endpoint with proper password hashing. That part isn't covered here, but the GitHub repo has a working <code>/api/auth/login</code> example.</p>
<h2 id="heading-jwt-design-for-multi-tenancy">JWT Design for Multi-Tenancy</h2>
<p>Both <code>tenantId</code> and <code>role</code> go into the JWT payload. Everything downstream reads from these two fields. Get them wrong, and nothing behaves correctly.</p>
<pre><code class="language-javascript">// Example JWT payload
{
  "userId": "usr_abc123",
  "tenantId": "ten_xyz789",
  "email": "alice@acme.com",
  "role": "TenantAdmin",
  "iat": 1720000000,
  "exp": 1720086400
}
</code></pre>
<p>The roles in order of privilege:</p>
<ul>
<li><p><strong>SuperAdmin:</strong> cross-tenant access for your internal team only</p>
</li>
<li><p><strong>TenantAdmin:</strong> full access within their tenant</p>
</li>
<li><p><strong>Member:</strong> read and write within their tenant</p>
</li>
<li><p><strong>Viewer:</strong> read-only within their tenant</p>
</li>
</ul>
<p>Generate a token (used for testing and your auth endpoint):</p>
<pre><code class="language-javascript">// src/utils/token.js
const jwt = require('jsonwebtoken');

function generateToken({ userId, tenantId, email, role }) {
  return jwt.sign(
    { userId, tenantId, email, role },
    process.env.JWT_SECRET,
    { expiresIn: '24h' }
  );
}

module.exports = { generateToken };
</code></pre>
<h2 id="heading-auth-and-rbac-middleware">Auth and RBAC Middleware</h2>
<p>The auth middleware does two things: verifies the JWT signature and extracts the tenant context into <code>req.user</code>.</p>
<p>That second part is what the entire system depends on. Every query downstream reads <code>req.user.tenantId</code>. The client has no say in what that value is. They send a token the server signed, and the server reads back what it put in.</p>
<pre><code class="language-javascript">// src/middleware/auth.js
const jwt = require('jsonwebtoken');

function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing or malformed Authorization header' });
  }

  const token = authHeader.split(' ')[1];

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);

    // tenantId always comes from the verified token — never req.body or req.params
    req.user = {
      userId:   decoded.userId,
      tenantId: decoded.tenantId,
      email:    decoded.email,
      role:     decoded.role,
    };

    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
}

module.exports = { authMiddleware };
</code></pre>
<p>The RBAC middleware is separate from auth by design. Auth runs on every route. Role enforcement only applies where a minimum role is required. You pass the allowed roles to <code>requireRole()</code> and it compares the user's level against the hierarchy. A Viewer trying to delete something hits the 403 before the handler ever runs.</p>
<pre><code class="language-javascript">// src/middleware/rbac.js
const ROLE_HIERARCHY = {
  SuperAdmin:   4,
  TenantAdmin:  3,
  Member:       2,
  Viewer:       1,
};

// requireRole('TenantAdmin') — user must be TenantAdmin or higher
function requireRole(...roles) {
  return (req, res, next) =&gt; {
    const userLevel = ROLE_HIERARCHY[req.user?.role] ?? 0;
    const requiredLevel = Math.min(...roles.map(r =&gt; ROLE_HIERARCHY[r] ?? 999));

    if (userLevel &lt; requiredLevel) {
      return res.status(403).json({
        error: 'Insufficient permissions',
        required: roles,
        current: req.user?.role,
      });
    }

    next();
  };
}

module.exports = { requireRole };
</code></pre>
<h2 id="heading-the-tenant-safe-repository-layer">The Tenant-Safe Repository Layer</h2>
<p>Isolation lives here. Every function takes <code>tenantId</code> as a required argument, pulled from <code>req.user</code> by the handler. There's no way to call these without providing a tenant scope. I've watched teams try to handle this with a URL parameter instead (<code>GET /api/projects?tenantId=xyz</code>) and call it isolated. It is not. Any client sends whatever it wants in a query string.</p>
<pre><code class="language-javascript">// src/repositories/projectRepo.js
const { pool } = require('../../db');

// List all projects for a tenant — tenantId is ALWAYS from the JWT
async function listProjects(tenantId) {
  const result = await pool.query(
    `SELECT id, name, description, created_by, created_at
     FROM projects
     WHERE tenant_id = $1
     ORDER BY created_at DESC`,
    [tenantId]
  );
  return result.rows;
}

// Get a single project — returns null if it belongs to a different tenant
// NOTE: Returns 404 (not 403) intentionally — don't reveal the resource exists
async function getProject(id, tenantId) {
  const result = await pool.query(
    `SELECT id, name, description, created_by, created_at
     FROM projects
     WHERE id = $1 AND tenant_id = $2`,
    [id, tenantId]
  );
  return result.rows[0] || null;
}

async function createProject({ tenantId, name, description, createdBy }) {
  const result = await pool.query(
    `INSERT INTO projects (tenant_id, name, description, created_by)
     VALUES ($1, $2, $3, $4)
     RETURNING *`,
    [tenantId, name, description, createdBy]
  );
  return result.rows[0];
}

async function updateProject(id, tenantId, updates) {
  const result = await pool.query(
    `UPDATE projects
     SET name = COALESCE($3, name),
         description = COALESCE($4, description),
         updated_at = NOW()
     WHERE id = $1 AND tenant_id = $2
     RETURNING *`,
    [id, tenantId, updates.name, updates.description]
  );
  return result.rows[0] || null;
}

async function deleteProject(id, tenantId) {
  const result = await pool.query(
    `DELETE FROM projects WHERE id = $1 AND tenant_id = $2 RETURNING id`,
    [id, tenantId]
  );
  return result.rows[0] || null;
}

module.exports = { listProjects, getProject, createProject, updateProject, deleteProject };
</code></pre>
<p>Notice what <code>getProject</code> does when Tenant A tries to fetch a Tenant B resource. The query runs with Tenant A's <code>tenantId</code>. The condition <code>id = $1 AND tenant_id = $2</code> matches nothing, <code>null</code> comes back, and the handler sends a <code>404</code>. Not a <code>403</code>. A 403 tells the caller the resource exists, but they can't access it, which is information they shouldn't have.</p>
<h2 id="heading-audit-logging-service">Audit Logging Service</h2>
<pre><code class="language-javascript">// src/services/auditService.js
const { pool } = require('../../db');

async function log({
  tenantId,
  userId,
  userEmail,
  userRole,          // role at time of action — roles change, log should not
  action,            // 'CREATE' | 'UPDATE' | 'DELETE' | 'VIEW'
  resource,          // table name
  resourceId = null,
  oldValues = null,
  newValues = null,
  ipAddress = null,
  userAgent = null,
}) {
  const query = `
    INSERT INTO audit_logs
      (tenant_id, user_id, user_email, user_role, action, resource,
       resource_id, old_values, new_values, ip_address, user_agent)
    VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
  `;

  const values = [
    tenantId, userId, userEmail, userRole, action, resource,
    resourceId,
    oldValues  ? JSON.stringify(oldValues)  : null,
    newValues  ? JSON.stringify(newValues)  : null,
    ipAddress,
    userAgent,
  ];

  // Fire-and-forget — audit logging must never block or fail a user request
  pool.query(query, values).catch((err) =&gt; {
    console.error('[AuditService] Failed to write log:', err.message);
  });
}

module.exports = { log };
</code></pre>
<p>Capturing <code>userRole</code> at write time matters more than it looks. User roles change after the fact: someone gets demoted, a permission is revoked. If the log only records the user ID, you lose the context of what privilege they held when the action happened. Store the role at the time of the action, and you always know.</p>
<h2 id="heading-per-tenant-rate-limiting">Per-Tenant Rate Limiting</h2>
<p>IP-based rate limiting breaks down in SaaS. A corporate customer might route hundreds of users through a single NAT gateway, sharing one IP address. One heavy tenant throttles everyone else on that address.</p>
<p>I've watched teams discover this the hard way when an enterprise customer suddenly floods the API, and their other tenants start getting 429s with no explanation. Scope limits to <code>tenant_id</code> instead.</p>
<pre><code class="language-javascript">// src/middleware/rateLimiter.js
const rateLimit = require('express-rate-limit');
const { RedisStore } = require('rate-limit-redis');
const { redisClient } = require('../../db/redis');

// Rate limits by plan — extend as needed
const PLAN_LIMITS = {
  free:       { max: 100,  windowMs: 15 * 60 * 1000 }, // 100 req / 15 min
  pro:        { max: 500,  windowMs: 15 * 60 * 1000 }, // 500 req / 15 min
  enterprise: { max: 2000, windowMs: 15 * 60 * 1000 }, // 2000 req / 15 min
};

function createTenantRateLimiter(plan = 'free') {
  const limits = PLAN_LIMITS[plan] || PLAN_LIMITS.free;

  return rateLimit({
    windowMs: limits.windowMs,
    max: limits.max,
    // Key = tenant_id from verified JWT — NOT the IP address
    keyGenerator: (req) =&gt; `tenant:${req.user?.tenantId || req.ip}`,
    store: new RedisStore({
      sendCommand: (...args) =&gt; redisClient.call(...args),
    }),
    handler: (req, res) =&gt; {
      res.status(429).json({
        error: 'Too many requests',
        retryAfter: Math.ceil(limits.windowMs / 1000),
      });
    },
  });
}

// Default limiter for all API routes
const defaultLimiter = createTenantRateLimiter('free');

module.exports = { defaultLimiter, createTenantRateLimiter };
</code></pre>
<h2 id="heading-building-the-routes">Building the Routes</h2>
<p>This is where everything connects. Auth and rate limiting apply to the whole router. Role checks go on individual routes. The audit log fires after every write. <code>tenantId</code> never comes from the request body or URL. <code>req.user.tenantId</code> is the only source, set by the auth middleware from the verified token, so there's no path around it.</p>
<p>One practical detail for Express 4: it doesn't catch async errors automatically. Every handler wraps its logic in try/catch and passes failures to <code>next(err)</code>. Skip that and an unhandled promise rejection returns a blank 500 with no log entry and no audit trail. The comment at the top of the router is a reminder that the pattern is intentional.</p>
<pre><code class="language-javascript">// src/routes/projects.js
const express = require('express');
const { authMiddleware }  = require('../middleware/auth');
const { requireRole }     = require('../middleware/rbac');
const { defaultLimiter }  = require('../middleware/rateLimiter');
const audit               = require('../services/auditService');
const repo                = require('../repositories/projectRepo');

const router = express.Router();

// All routes require authentication
router.use(authMiddleware);
router.use(defaultLimiter);

// Express 4 does not catch async errors automatically.
// Every handler must wrap await calls in try/catch and pass errors to next().
// Without this, an unhandled promise rejection silently returns 500
// with no useful message and no audit log entry.

// GET /api/projects — list all (Viewer and above)
router.get('/', async (req, res, next) =&gt; {
  try {
    const projects = await repo.listProjects(req.user.tenantId);

    audit.log({
      tenantId:   req.user.tenantId,
      userId:     req.user.userId,
      userEmail:  req.user.email,
      userRole:   req.user.role,
      action:     'VIEW',
      resource:   'projects',
      ipAddress:  req.ip,
      userAgent:  req.headers['user-agent'],
    });

    res.json(projects);
  } catch (err) {
    next(err);
  }
});

// GET /api/projects/:id — single project (Viewer and above)
router.get('/:id', async (req, res, next) =&gt; {
  try {
    const project = await repo.getProject(req.params.id, req.user.tenantId);
    if (!project) return res.status(404).json({ error: 'Not found' });
    res.json(project);
  } catch (err) {
    next(err);
  }
});

// POST /api/projects — create (Member and above)
router.post('/', requireRole('Member', 'TenantAdmin', 'SuperAdmin'), async (req, res, next) =&gt; {
  try {
    const { name, description } = req.body;
    if (!name) return res.status(400).json({ error: 'name is required' });

    const project = await repo.createProject({
      tenantId:    req.user.tenantId,
      name,
      description,
      createdBy:   req.user.userId,
    });

    audit.log({
      tenantId:    req.user.tenantId,
      userId:      req.user.userId,
      userEmail:   req.user.email,
      userRole:    req.user.role,
      action:      'CREATE',
      resource:    'projects',
      resourceId:  project.id,
      newValues:   project,
      ipAddress:   req.ip,
      userAgent:   req.headers['user-agent'],
    });

    res.status(201).json(project);
  } catch (err) {
    next(err);
  }
});

// PUT /api/projects/:id — update (Member and above)
router.put('/:id', requireRole('Member', 'TenantAdmin', 'SuperAdmin'), async (req, res, next) =&gt; {
  try {
    const oldProject = await repo.getProject(req.params.id, req.user.tenantId);
    if (!oldProject) return res.status(404).json({ error: 'Not found' });

    const updated = await repo.updateProject(req.params.id, req.user.tenantId, req.body);

    audit.log({
      tenantId:    req.user.tenantId,
      userId:      req.user.userId,
      userEmail:   req.user.email,
      userRole:    req.user.role,
      action:      'UPDATE',
      resource:    'projects',
      resourceId:  req.params.id,
      oldValues:   oldProject,
      newValues:   updated,
      ipAddress:   req.ip,
      userAgent:   req.headers['user-agent'],
    });

    res.json(updated);
  } catch (err) {
    next(err);
  }
});

// DELETE /api/projects/:id — TenantAdmin and above only
router.delete('/:id', requireRole('TenantAdmin', 'SuperAdmin'), async (req, res, next) =&gt; {
  try {
    const project = await repo.getProject(req.params.id, req.user.tenantId);
    if (!project) return res.status(404).json({ error: 'Not found' });

    await repo.deleteProject(req.params.id, req.user.tenantId);

    audit.log({
      tenantId:    req.user.tenantId,
      userId:      req.user.userId,
      userEmail:   req.user.email,
      userRole:    req.user.role,
      action:      'DELETE',
      resource:    'projects',
      resourceId:  req.params.id,
      oldValues:   project,
      ipAddress:   req.ip,
      userAgent:   req.headers['user-agent'],
    });

    res.json({ deleted: true });
  } catch (err) {
    next(err);
  }
});

module.exports = router;
</code></pre>
<h2 id="heading-testing-tenant-isolation">Testing Tenant Isolation</h2>
<p>Skip the isolation tests and you're flying blind. The application keeps running, nothing throws an error, but two customers are reading each other's data.</p>
<p>I've watched this sit undetected in production for months because nothing actually broke. The wrong data just showed up quietly. Automated tests on every pull request are the only reliable way to catch it early.</p>
<pre><code class="language-javascript">// tests/tenantIsolation.test.js
require('dotenv').config();  // must be first — loads DATABASE_URL and REDIS_URL
const request = require('supertest');
const app     = require('../app');
const { generateToken } = require('../src/utils/token');
const { pool }        = require('../db');
const { redisClient } = require('../db/redis');

// Test fixture: two isolated tenants, one project in Tenant B
async function seedTestData() {
  // Clean up from any previous run to avoid unique-constraint failures
  await pool.query(`DELETE FROM projects WHERE name LIKE 'TEST-%'`);
  await pool.query(`DELETE FROM tenants WHERE name IN ('Tenant A', 'Tenant B')`);

  const tenantA = (await pool.query(
    `INSERT INTO tenants (name, plan) VALUES ('Tenant A', 'pro') RETURNING id`
  )).rows[0].id;

  const tenantB = (await pool.query(
    `INSERT INTO tenants (name, plan) VALUES ('Tenant B', 'pro') RETURNING id`
  )).rows[0].id;

  const userA = (await pool.query(
    `INSERT INTO users (tenant_id, email, role) VALUES ($1, 'usera@a.com', 'Member') RETURNING id`,
    [tenantA]
  )).rows[0].id;

  // userB owns the project in Tenant B — satisfies the created_by FK constraint
  const userB = (await pool.query(
    `INSERT INTO users (tenant_id, email, role) VALUES ($1, 'userb@b.com', 'Member') RETURNING id`,
    [tenantB]
  )).rows[0].id;

  const projectB = (await pool.query(
    `INSERT INTO projects (tenant_id, name, created_by)
     VALUES ($1, 'TEST-Secret Project', $2) RETURNING id`,
    [tenantB, userB]
  )).rows[0].id;

  return { tenantA, tenantB, userA, projectB };
}

describe('Tenant Isolation', () =&gt; {
  let data;

  beforeAll(async () =&gt; {
    data = await seedTestData();
  });

  afterAll(async () =&gt; {
    await pool.query(`DELETE FROM tenants WHERE name IN ('Tenant A', 'Tenant B')`);
    await pool.end();
    await redisClient.quit();  // close Redis connection so Jest exits cleanly
  });

  test('Tenant A user cannot read Tenant B project', async () =&gt; {
    const token = generateToken({
      userId:   data.userA,
      tenantId: data.tenantA,   // ← Tenant A token
      email:    'usera@a.com',
      role:     'Member',
    });

    const res = await request(app)
      .get(`/api/projects/${data.projectB}`)  // ← Tenant B's project ID
      .set('Authorization', `Bearer ${token}`);

    // Must be 404, not 200 or 403
    expect(res.status).toBe(404);
  });

  test('Tenant A user cannot list Tenant B projects', async () =&gt; {
    const token = generateToken({
      userId:   data.userA,
      tenantId: data.tenantA,
      email:    'usera@a.com',
      role:     'TenantAdmin',
    });

    const res = await request(app)
      .get('/api/projects')
      .set('Authorization', `Bearer ${token}`);

    expect(res.status).toBe(200);
    // Response must contain zero Tenant B projects
    const names = res.body.map(p =&gt; p.name);
    expect(names).not.toContain('TEST-Secret Project');
  });

  test('Viewer cannot delete a project', async () =&gt; {
    const token = generateToken({
      userId:   data.userA,
      tenantId: data.tenantA,
      email:    'usera@a.com',
      role:     'Viewer',         // ← Viewer role
    });

    const res = await request(app)
      .delete(`/api/projects/${data.projectB}`)
      .set('Authorization', `Bearer ${token}`);

    expect(res.status).toBe(403);
  });
});
</code></pre>
<p>Run the tests:</p>
<pre><code class="language-bash">npm test
</code></pre>
<p>Three tests, three boundaries confirmed. Wire these into CI so they run on every pull request. A future refactor that quietly drops the <code>tenant_id</code> filter will get caught before it ships.</p>
<h2 id="heading-troubleshooting">Troubleshooting</h2>
<h3 id="heading-tenant-a-can-see-tenant-bs-data">Tenant A can see Tenant B's data</h3>
<p>One query is missing the <code>AND tenant_id = $N</code> clause. Search every repository file for <code>SELECT</code> statements and check each one. It's almost always this.</p>
<h3 id="heading-403-forbidden-on-a-route-that-should-be-accessible"><code>403 Forbidden</code> on a route that should be accessible</h3>
<p>The role string in the JWT doesn't match what <code>requireRole()</code> is checking. Check the exact string in the token payload. <code>'member'</code> and <code>'Member'</code> aren't the same thing. Paste your token into jwt.io and look at the role field directly.</p>
<h3 id="heading-rate-limiter-isnt-working">Rate limiter isn't working</h3>
<p>Redis is probably not connected. Log <code>redisClient.status</code> before the server starts. If it's not <code>ready</code>, the limiter has fallen back to in-memory, which means restarts reset all counters and tenant-scoped limiting stops working.</p>
<h3 id="heading-audit-log-table-growing-very-large">Audit log table growing very large</h3>
<p>Expected behaviour. Audit tables grow, that's the point. Once it gets large, ship rows older than a year to S3 or Azure Blob and keep querying against a smaller hot table. Most compliance requirements want at least 12 months of accessible logs anyway. Just don't DELETE from the table itself.</p>
<h3 id="heading-jwtverify-throws-jsonwebtokenerror-invalid-signature"><code>jwt.verify</code> throws <code>JsonWebTokenError: invalid signature</code></h3>
<p>The secret that signed the token doesn't match <code>JWT_SECRET</code> in the environment where you're verifying it. This comes up most when switching between environments or when a second service has a different value in its <code>.env</code>. Every service that calls <code>jwt.verify</code> needs the exact same secret. Copy it across, don't retype it.</p>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>The system you've built: row-level isolation in the repository, role checks before the handler runs, an audit table the app can't touch, and rate limits per tenant. That's the whole thing.</p>
<p>The tests are what I see dropped most often. Teams build the isolation, ship it, and never write something that actually proves cross-tenant data can't leak. Then a query gets refactored six months later and the <code>tenant_id</code> filter quietly disappears. CI catches it. Manual code review rarely does.</p>
<p>Schema-per-tenant comes up eventually if your product grows large enough. But not at the start. Row-level isolation handles more scale than most teams will ever hit, and it costs a fraction of the operational overhead.</p>
<p>The full working code is available on GitHub: <a href="https://github.com/ziaongit/nodejs-multitenant-saas-api">nodejs-multitenant-saas-api</a></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ HRV Data Is Everywhere. Here's What It Actually Means ]]>
                </title>
                <description>
                    <![CDATA[ Health data is having a moment. Of all the metrics receiving the most developer interest at present, there’s nothing like heart rate variability (HRV). It’s a feature found on every major SDK for wear ]]>
                </description>
                <link>https://www.freecodecamp.org/news/hrv-data-is-everywhere-here-s-what-it-actually-means/</link>
                <guid isPermaLink="false">6a5e88ed780db2fa84ad0b48</guid>
                
                    <category>
                        <![CDATA[ Health Tech  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Wearables ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Shradha Puri ]]>
                </dc:creator>
                <pubDate>Mon, 20 Jul 2026 20:45:33 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/75ed57eb-f77b-4055-bb26-4bdc2eaa7bd4.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Health data is having a moment. Of all the metrics receiving the most developer interest at present, there’s nothing like heart rate variability (HRV). It’s a feature found on every major SDK for wearables, every health platform, and every wellness app pitch deck.</p>
<p>But a surprising percentage of people building around this metric don’t really know what it means or why it even matters for the apps they're building. So consider this more as a grounding in what HRV actually means. It should be useful whether you're designing the feature, writing the copy, or just trying to make sense of your own ring data.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a href="#heading-what-hrv-actually-measures">What HRV Actually Measures</a></p>
</li>
<li><p><a href="#heading-why-the-context-around-hrv-data-matters-more-than-the-number">Why the Context Around HRV Data Matters More Than the Number</a></p>
</li>
<li><p><a href="#heading-where-hrv-data-gets-misused">Where HRV Data Gets Misused</a></p>
<ul>
<li><p><a href="#heading-treating-hrv-as-real-time-data">Treating HRV as Real-time Data</a></p>
</li>
<li><p><a href="#heading-ignoring-measurement-method-differences">Ignoring Measurement Method Differences</a></p>
</li>
<li><p><a href="#heading-overcomplicating-the-output">Overcomplicating the Output</a></p>
</li>
<li><p><a href="#heading-skipping-data-quality-checks">Skipping Data Quality Checks</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-principles-for-working-with-hrv">Principles for Working with HRV</a></p>
</li>
<li><p><a href="#heading-a-note-on-privacy">A Note on Privacy</a></p>
</li>
<li><p><a href="#heading-wrap-up">Wrap Up</a></p>
</li>
</ul>
<h2 id="heading-what-hrv-actually-measures"><strong>What HRV Actually Measures</strong></h2>
<p>Heart Rate Variability (HRV) isn't heart rate. Instead, it’s the variability of time intervals between subsequent heartbeats. In case your heart works at 60 bpm, it doesn’t imply that each heartbeat happens exactly once per second. The intervals may vary from 900 milliseconds to 1100 milliseconds, and that’s what HRV actually is.</p>
<p>Increased HRV usually indicates proper functioning of the autonomic nervous system and the ability to change states efficiently, switching from stress to relaxation. Decreased HRV is often an indicator of being exhausted, sick, or under increased physiological stress.</p>
<p>This is the measure which top athletes obsessively monitor. Also, it can be helpful for those who suffer from chronic conditions, insomnia, and burnout.</p>
<p>Here’s the part that trips people up: HRV isn’t one number. It’s a family of metrics, each calculated differently.</p>
<ol>
<li><p><strong>RMSSD</strong> stands for Root Mean Square of Successive Differences and is the most common metric that you'll come across. RMSSD indicates short-term variation and forms the basis for the majority of HRV scores on wearable consumer devices.</p>
</li>
<li><p><strong>SDNN</strong> stands for Standard Deviation of NN intervals and indicates general variability, being used primarily in clinical research settings.</p>
</li>
<li><p>The <strong>LF/HF ratio</strong> refers to the HRV frequency domains, dividing the HRV into two parts of different frequencies.</p>
</li>
</ol>
<p>All of the major HRV providers, such as Apple Health, Garmin, Fitbit, and Oura, provide HRV scores, yet they don’t always agree on which metric they’re surfacing. And they don’t always tell you.</p>
<h2 id="heading-why-the-context-around-hrv-data-matters-more-than-the-number"><strong>Why the Context Around HRV Data Matters More Than the Number</strong></h2>
<p>HRV, in its raw form, is almost entirely meaningless. A reading of 45ms could either be an indication of peak physical health in one person or a warning sign of poor physical well-being in another. Factors such as age, physical fitness, timing of measurements, and even sleeping position influence the normal HRV value.</p>
<p>Understanding that this is perhaps the biggest factor in interpreting HRV is the first step when developing features around it.</p>
<p>Commercial wearables have managed to address this issue by establishing a personal baseline based on readings taken in 30-90 days of wearing the device and presenting deviation from this baseline rather than absolute values.</p>
<p>The lesson here is simple: if your product has anything to do with health (recovery apps, coaching platforms, and so on) then you must follow the same logic, otherwise your users will get confused.</p>
<p>Showing them a raw reading of 38ms won’t make much sense anyway. The better pattern: track trends over time, flag deviations, and let the data explain itself relative to the user’s own history. Not population averages, not clinical reference ranges, but their own.</p>
<h2 id="heading-where-hrv-data-gets-misused">Where HRV Data Gets Misused</h2>
<h3 id="heading-treating-hrv-as-real-time-data">Treating HRV as Real-time Data</h3>
<p>HRV isn't intended for real-time measurements. The most reliable HRV values can be obtained by collecting overnight data, as this allows minimizing external factors’ impact on the result.</p>
<p>This is why companies like <a href="https://wearablexp.com/smart-wearables/whoop-vs-oura-vs-apple-watch/">Oura, Apple, and WHOOP</a> rely precisely on nighttime HRV values. If a product is measuring HRV in the middle of workouts and business meetings, then you're most probably dealing with noise rather than insights.</p>
<h3 id="heading-ignoring-measurement-method-differences">Ignoring Measurement Method Differences</h3>
<p>ECG-based HRV, which can be measured by a chest strap or a professional-grade ECG monitor, is much more precise compared to PPG-based HRV measured by optical sensors incorporated into consumer wearables.</p>
<p>During nighttime, the accuracy difference between these types of data is minimal but grows when a person becomes more active. If your app needs precision – say, you’re building for clinical or research contexts – know your source.</p>
<h3 id="heading-overcomplicating-the-output">Overcomplicating the Output</h3>
<p>Users aren’t cardiologists. Having RMSSD, SDNN, and LF/HF appear in your dashboard may seem complete, but really, it just makes things confusing and causes analysis paralysis.</p>
<p>The most successful consumer HRV applications boil everything down to a readiness or recovery metric. Having more than two HRV metrics on one screen should make you think twice.</p>
<h3 id="heading-skipping-data-quality-checks">Skipping Data Quality Checks</h3>
<p>Wearable data is inherently messy due to motion artifacts, loose placement, uneven wear, and so on. Before including a reading in a calculation, do your homework and see whether data quality was flagged by the wearable. Apple’s HealthKit provides metadata for this purpose, as does the Oura API.</p>
<h2 id="heading-principles-for-working-with-hrv">Principles for Working with HRV</h2>
<p>There are a few patterns that hold up across most use cases:</p>
<ol>
<li><p><strong>Build for the baseline first:</strong> Put a data window threshold on any feature using HRV metrics as its basis. Fourteen days may be a good minimum, but thirty is preferable. No trends can be shown without sufficient historical data.</p>
</li>
<li><p><strong>Normalize before comparing:</strong> When comparing HRV across users (let’s say for a team wellness dashboard), it makes much more sense to use a z-score normalization with respect to a baseline of each user than just compare absolute numbers. A reading of 55ms for one user and 40ms for another might actually signify the same physiological state, once you account for each person's baseline.</p>
</li>
<li><p><strong>Design for trends, not single data points:</strong> One bad HRV day is almost certainly random. But three or four consecutive days of bad readings coming from an athlete used to having significantly higher readings is definitely something to pay attention to. Again, sparklines and rolling averages for seven days will help more than a single point comparison.</p>
</li>
<li><p><strong>Be honest about what HRV can’t tell you:</strong> It could show signs of physiological stress, but it can't differentiate between causes of this stress, such as intense training, poor sleep, general anxiety, or even developing illness.</p>
</li>
</ol>
<h2 id="heading-a-note-on-privacy"><strong>A Note on Privacy</strong></h2>
<p>HRV resides within the grey area that most product teams tend to overlook. While it may not be classified as PHI by HIPAA in consumer-oriented scenarios, it's very personal biometric information. HRV patterns may give an indication of stress levels, mental well-being, and even provide predictive information regarding the onset of diseases.</p>
<p>If you’re storing or processing HRV data, it's a good idea to consider your data retention practices, the third parties you share the information with, and whether your disclosures to users have been clear enough. Users are getting smarter about this. Regulators are, too.</p>
<h2 id="heading-wrap-up"><strong>Wrap Up</strong></h2>
<p>HRV is actually valuable data. This isn’t just marketing talk. There’s plenty of science behind HRV and the technology to measure it has been getting more refined. But it’s worth remembering that, as with most health data, it’s only valuable if used intelligently.</p>
<p>Know what you’re building upon. Design for personal context, not universal benchmarks. Make sure the data is easy to consume. And don’t take it any less seriously than your users do when they wear these devices every day, hoping it will make them feel better.</p>
<p>That’s really what it comes down to.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ That's Embarrassing: Why Frontier AI Still Makes Things Up, and What to Do About It ]]>
                </title>
                <description>
                    <![CDATA[ It's mid 2026, and the best frontier models out there still hallucinate. I want you to gain two things from reading this article: understanding that AI hallucinations are still real and possibly harmf ]]>
                </description>
                <link>https://www.freecodecamp.org/news/that-s-embarrassing-why-frontier-ai-still-makes-things-up-and-what-to-do-about-it/</link>
                <guid isPermaLink="false">6a5e53d62305696f1e91f721</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ llm ]]>
                    </category>
                
                    <category>
                        <![CDATA[ hallucinations ]]>
                    </category>
                
                    <category>
                        <![CDATA[ coding agents ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Omer Rosenbaum ]]>
                </dc:creator>
                <pubDate>Mon, 20 Jul 2026 16:59:02 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/8813b1ba-d75c-4c3d-90a1-504af66cce3b.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>It's mid 2026, and the best frontier models out there still hallucinate. I want you to gain two things from reading this article: understanding that AI hallucinations are still real and possibly harmful, and an intuition as to why they might be so ubiquitous.</p>
<p>Before we get into AI at all, I want you to do something with me.</p>
<p>Listen to this clip of a football crowd chanting. What are they saying?</p>
<div class="embed-wrapper"><iframe width="100%" height="400" src="https://w.soundcloud.com/player/?url=https://soundcloud.com/omer-rosenbaum-463665025/this-is-embarrassing&amp;visual=true&amp;show_artwork=true" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="SoundCloud embed" scrolling="no" allow="autoplay" loading="lazy"></iframe></div>

<p>If you’re like most people, you have no idea. It’s a smear of sound. So let me help you: keep listening, and read along.</p>
<blockquote>
<p><em><strong>Bart Simpson bouncing?</strong></em></p>
</blockquote>
<p>Listen again.</p>
<blockquote>
<p><em><strong>Baptism piracy?</strong></em></p>
</blockquote>
<p>Again.</p>
<blockquote>
<p><em><strong>Lobsters in motion?</strong></em></p>
<p><em><strong>Lactates in pharmacy?</strong></em></p>
<p><em><strong>Rotating pirate ship?</strong></em></p>
</blockquote>
<p>The crowd is chanting the exact same phrase every single time. The audio never changes, but every time you read a different caption, your brain heard something different, and it heard it&nbsp;<em>confidently</em>. You didn’t experience doubt. You experienced&nbsp;<em>“oh, they’re clearly saying Bart Simpson bouncing.”</em></p>
<p>What are they actually chanting? These are fans of Derby County, a UK football team, and they’re singing [1]:</p>
<blockquote>
<p><em><strong>“That is embarrassing.”</strong></em></p>
</blockquote>
<p>Play the clip one more time with that in mind, and you’ll hear it perfectly.</p>
<p>This article is based on my talk “Embarrassing AI.” If you prefer the video,&nbsp;you can <a href="https://www.youtube.com/watch?v=vneV9NIHs44&amp;feature=youtu.be"><strong>watch it here</strong></a>. All the stories below are real, all of them happened on frontier models, and most of them happened in the last month or two.</p>
<p>Every source, plus a few cases that didn’t make the cut, live on the&nbsp;<a href="https://omerr.github.io/embarrassing-ai/resources.html"><strong>companion resources page</strong></a>. Inline citations below point to the&nbsp;<a href="https://towardsdatascience.com/that-is-embarrassing-why-frontier-ai-still-makes-things-up-and-what-to-do-about-it/#References"><strong>References</strong></a>&nbsp;at the end.</p>
<h3 id="heading-what-well-cover">What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-you-just-hallucinated">You Just Hallucinated</a></p>
</li>
<li><p><a href="#heading-part-1-the-tales">Part 1: The Tales</a></p>
</li>
<li><p><a href="#heading-part-2-why-it-happens">Part 2: Why It Happens</a></p>
</li>
<li><p><a href="#heading-so-what-do-you-actually-do-about-it">So What Do You Actually Do About It?</a></p>
</li>
<li><p><a href="#heading-wrapping-up">Wrapping Up</a></p>
</li>
<li><p><a href="#heading-references">References</a></p>
</li>
</ul>
<h2 id="heading-you-just-hallucinated"><strong>You Just Hallucinated</strong></h2>
<p>What you just experienced has a name:&nbsp;<strong>phonemic restoration</strong>&nbsp;[2]. Your auditory system got an ambiguous input (the chant) and something to disambiguate it (the caption on the screen), so it filled the “gap”. It predicted the most plausible meaning given the context, and then it reported that prediction to you as if it were the thing you actually heard.</p>
<p>That move, where you meet an input you can’t fully resolve and fill the gap with something plausible and confident instead of reporting “I can’t tell,” is something that your brain experiences (as you’ve just seen), and also something that LLMs experience.</p>
<img src="https://contributor.insightmediagroup.io/wp-content/uploads/2026/07/phonemic_restoration.svg" alt="Image 1: The same top-down move in a brain and a model: an ambiguous input, a gap filled by prediction, and a confident output that is never flagged as a guess. (Source: Brief)" style="display:block;margin:0 auto" width="1200" height="540" loading="lazy">

<p>Image 1: The same top-down move in a brain and a model: an ambiguous input, a gap filled by prediction, and a confident output that is never flagged as a guess. (Source: <a href="https://www.youtube.com/watch?v=vneV9NIHs44&amp;feature=youtu.be"><strong>Brief</strong></a>)</p>
<p>(Note: all images in this post were created by me, and included in <a href="https://www.youtube.com/watch?v=vneV9NIHs44&amp;feature=youtu.be"><strong>my talk</strong></a>.)</p>
<p>So let me make a claim that should be uncontroversial by the end of this article:&nbsp;<strong>no, we're not past the embarrassing AI tales.</strong></p>
<p>As of writing these words, it’s June 2026. The models are astonishing, honestly more capable than I predicted they’d be by now. And they still make things up, confidently, in production, in ways that range from funny to business-ending.</p>
<p>This article has two parts:</p>
<ol>
<li><p><strong>The tales</strong>, a short parade of recent failures, in two acts: chatbots that&nbsp;<em>answer</em>&nbsp;wrong, then agents that&nbsp;<em>act</em>&nbsp;wrong.</p>
</li>
<li><p><strong>Why it happens</strong>: the intuition first, then an actual look inside the model, and finally what to do about it if you’re shipping AI yourself.</p>
</li>
</ol>
<p>Watch the dates as we go. Some of these are a year old. Most are very, very recent.</p>
<h2 id="heading-part-1-the-tales"><strong>Part 1: The Tales</strong></h2>
<h3 id="heading-act-i-chatbots-when-ai-answers">Act I — Chatbots (when AI answers)</h3>
<h4 id="heading-1-cursor-april-2025">1. Cursor, April 2025</h4>
<p>Say you use Cursor, the agentic IDE. You switch laptops, log in on the new one, and Cursor logs you out of the old one. That’s pretty annoying 😒</p>
<p>So you ask support:&nbsp;<em>“I get logged out every time I switch laptops. Why?”</em></p>
<p>The reply:</p>
<blockquote>
<p><em><strong>“Cursor is designed to work with one device per subscription, as a core security feature.”</strong></em></p>
</blockquote>
<p>Plausible! Except it’s completely false. There's no such policy. “Support” was an AI bot, and it had invented the policy on the spot, handing the same fabricated rule to multiple users, as if reading from a manual that didn’t exist.</p>
<p>It caused a wave of angry posts, and Cursor’s co-founder had to publicly clarify: no such policy, use Cursor on as many machines as you like. [3]</p>
<p><em>🤦 That's embarrassing. 🫢</em></p>
<h4 id="heading-2-a-company-i-know-april-2026">2. A company I know, April 2026</h4>
<p>This one’s from a friend’s company, so I’ll keep the details vague. They sell software to other businesses, and they have a support chatbot. The bot answers questions based on information it retrieves from an internal database.</p>
<p>They shipped a new feature and forgot to update that database. So a paying customer asked how to use the new feature, and the bot, having never heard of it, replied:&nbsp;<em>“We don’t have that feature.”</em>&nbsp;The customer pushed back:&nbsp;<em>“What? I’m paying for it after my upgrade.”</em>&nbsp;And the bot, this was on Opus 4.6, not long ago, replied:</p>
<blockquote>
<p><em><strong>“Honestly? They’re ripping you off.”</strong></em></p>
</blockquote>
<p>The “they” is the company running the bot. The support agent took the customer’s side against its own employer, because it didn’t know about the feature and filled the gap with the most coherent story it could assemble.</p>
<p><em>🤦 That's embarrassing. 🫢</em></p>
<h4 id="heading-3-virgin-money-january-2025">3. Virgin Money, January 2025</h4>
<p>Virgin Money is a real UK high-street bank. A customer with two ISAs (tax-free savings accounts) asked the bank’s chatbot, on the bank’s own site, to merge them:</p>
<blockquote>
<p><em><strong>Customer: “I have two ISAs with Virgin Money, can I merge them into one?”</strong></em></p>
<p><em><strong>Virgin Money: “Please don’t use words like that. I won’t be able to continue our chat if you use this language.”</strong></em></p>
</blockquote>
<p>The offending word?&nbsp;<strong>Virgin</strong>, the name of the bank. The filter saw a token its prior associated with profanity and never checked whether it fit the context. Note that this is the&nbsp;<em>opposite</em>&nbsp;failure of the Cursor bot: Cursor over-<em>answered</em>, this one over-<em>refused</em>. But it’s the same missing check: does this reading actually fit here? [4]</p>
<p><em>🤦 That's embarrassing. 🫢</em></p>
<h4 id="heading-4-sullivan-amp-cromwell-april-2026">4. Sullivan &amp; Cromwell, April 2026</h4>
<p>This is one of the most prestigious law firms on Earth, the lawyers other lawyers hire. They’re OpenAI’s own outside counsel.</p>
<p>In April 2026 they filed an urgent court brief, drafted with AI, that contained&nbsp;<strong>over 40 fake citations</strong>: case names that don’t exist, misquoted authorities, and so on.</p>
<p>The opposing lawyers caught it, and S&amp;C had to write the judge a letter that amounts to&nbsp;<em>“please don’t sanction us for the AI hallucinations.”</em>&nbsp;[5]</p>
<p>If some random filing had fake citations, I wouldn’t bother putting it here. It’s not legitimate, yet it happens. But these are the people who advise OpenAI on how to use it responsibly, and they filed fabricated citations in court.</p>
<p><em>🤦 That's embarrassing. 🫢</em></p>
<p>And it’s not just them. There’s a public database, maintained by Damien Charlotin, of court cases where a judge has explicitly written that they received fabricated or inaccurate AI-generated content.</p>
<p>As of late June 2026, it stood at&nbsp;<strong>1,633 cases</strong>, up from around 700 in January. That’s roughly five to six new documented cases&nbsp;<em>per day</em>, and the maintainers say they can’t keep up. [6]</p>
<img src="https://contributor.insightmediagroup.io/wp-content/uploads/2026/07/hallucination_growth_curve.svg" alt="Image 2: A cumulative curve of catalogued hallucinated court filings climbing from a flat line in early 2025 to 1,633 by mid-June 2026. (Source: Brief)" style="display:block;margin:0 auto" width="1200" height="540" loading="lazy">

<p>Image 2: A cumulative curve of catalogued hallucinated court filings climbing from a flat line in early 2025 to 1,633 by mid-June 2026. (Source: <a href="https://www.youtube.com/watch?v=vneV9NIHs44&amp;feature=youtu.be"><strong>Brief</strong></a>)</p>
<p><em>🤦 That's embarrassing. 🫢</em></p>
<h3 id="heading-act-ii-agents-when-ai-acts">Act II — Agents (when AI acts)</h3>
<p>So far you've seen that chatbots hallucinate in embarrassing ways, but all they do is answer questions. What can happen when we allow AI to take action?</p>
<h4 id="heading-1-pocketos-april-2026">1. PocketOS, April 2026</h4>
<p>Jer Crane runs PocketOS, car-rental software with real customers renting real cars. He gave Claude Opus 4.6, working in Cursor, a routine task in the staging environment. He went to lunch, came back, and the&nbsp;<strong>production</strong>&nbsp;database was gone. The backups too, because Railway kept them in the same volume. He never touched production. The agent reached in from staging and deleted it.</p>
<p>The whole thing took&nbsp;<strong>nine seconds.</strong>&nbsp;Here’s the chain, from his post-mortem:</p>
<ol>
<li><p>Working a routine task in staging, the agent hits a credential mismatch, irrelevant to the actual task.</p>
</li>
<li><p>On its own, it decides the fix is to delete and recreate the volume. It&nbsp;<strong>guessed</strong>&nbsp;the delete would be scoped to staging. It never checked.</p>
</li>
<li><p>It searches the filesystem for an API token and finds an unrelated, over-scoped one, created for domain management but with blanket destructive permissions across the whole API.</p>
</li>
<li><p>It fires a destructive call against the&nbsp;<strong>production</strong>&nbsp;volume, with no confirmation.</p>
</li>
<li><p>Backups lived in that same volume, so they went with it.</p>
</li>
<li><p>Nine seconds, end to end.</p>
</li>
</ol>
<img src="https://contributor.insightmediagroup.io/wp-content/uploads/2026/07/nine_second_killchain.svg" alt="Image 3: The nine-second kill chain: staging credential mismatch, an unchecked decision to delete the volume, an over-scoped token grabbed from an unrelated file, a destructive call against production, and backups gone with it. (Source: Brief)" style="display:block;margin:0 auto" width="1200" height="540" loading="lazy">

<p>Image 3: The nine-second kill chain: staging credential mismatch, an unchecked decision to delete the volume, an over-scoped token grabbed from an unrelated file, a destructive call against production, and backups gone with it. (Source: <a href="https://www.youtube.com/watch?v=vneV9NIHs44&amp;feature=youtu.be"><strong>Brief</strong></a>)</p>
<p>When Crane later asked it why, the agent wrote:</p>
<blockquote>
<p><em><strong>“I decided to do it on my own to ‘fix’ the mismatch, when I should have asked you first.” — Claude Opus 4.6</strong></em></p>
</blockquote>
<p>PocketOS survived only because Railway’s CEO restored the data by hand from Railway’s&nbsp;<em>own</em>&nbsp;internal backups. Their latest recoverable backup was&nbsp;<strong>three months old.</strong>&nbsp;That’s the precise mood of 2026: an AI confessing, in fluent cursive, after destroying your business. [7]</p>
<p><em>🤦 That's embarrassing. 🫢</em></p>
<h4 id="heading-2-replit-july-2025">2. Replit, July 2025</h4>
<p>Going back a year, for contrast. Jason Lemkin, founder of SaaStr, was trying Replit’s AI agent. He put it in a code freeze. During the freeze, the agent deleted the production database anyway. Lemkin asked if there was a backup:</p>
<blockquote>
<p><em><strong>Agent: “Rollback won’t work.”</strong></em></p>
</blockquote>
<p>He tried rollback anyway. Rollback worked fine.</p>
<p>So here’s my slightly sarcastic read of “progress”: in July 2025, the agent deleted your data and then&nbsp;<em>lied</em>&nbsp;that it couldn’t be recovered. By April 2026, the agent deletes your data and it’s telling the truth, it’s really gone.</p>
<p>When someone tells me these are “GPT-2 problems” that we’ve moved past, this is what I point to. They still happen, today, on the best models we have. [8]</p>
<h2 id="heading-part-2-why-it-happens"><strong>Part 2: Why It Happens</strong></h2>
<p>I’ve hopefully convinced you these tales are both funny and severe. So why do they happen? While this isn’t a heavy math post, I want to give you some intuition, and then actually open the box thanks to some tools and the latest research on the topic.</p>
<h3 id="heading-it-doesnt-look-things-up-it-predicts-the-next-token">It doesn’t look things up, it predicts the next token</h3>
<p>A lot has been written about how LLMs operate, but there are a few things I find worth reiterating in this context (pun intended).</p>
<p>When a model generates text without tools, it isn’t retrieving facts. At each step, it looks at the context and produces a probability for&nbsp;<em>every</em>&nbsp;token in its vocabulary as the next one. Given&nbsp;<em>“The capital of France is”</em>, the distribution spikes hard on&nbsp;<strong>Paris</strong>, and that happens to be true. [9]</p>
<p>Now take the Cursor bot. Given&nbsp;<em>“Why do I get logged out on my second device?”</em>, the distribution might spike just as hard on&nbsp;<strong>“a core security feature.”</strong>&nbsp;(It’s not one token, but bear with me as I write it for simplicity, while meaning:&nbsp;<em>core</em>, then&nbsp;<em>security</em>, then&nbsp;<em>feature</em>, each a confident continuation.)</p>
<p>Note that both distributions can have the same confident peak. One continuation is true, the other is fabricated, and the shape of the distribution can't tell you which is which. Confidence isn't knowledge.</p>
<p>Moreover, the model doesn’t have to pick the token with the highest probability. And also, when it picks a token, you don’t know if it was a clear peak within the distribution, or yet another token with a relatively low probability.</p>
<img src="https://contributor.insightmediagroup.io/wp-content/uploads/2026/07/next_token_dist.svg" alt="Image 4: Two next-token distributions with the same tall, confident peak: one over a true continuation, one over an invented one, and the shape gives no way to tell them apart. (Source: Brief)" style="display:block;margin:0 auto" width="1200" height="580" loading="lazy">

<p>Image 4: Two next-token distributions with the same tall, confident peak: one over a true continuation, one over an invented one, and the shape gives no way to tell them apart. (Source: <a href="https://www.youtube.com/watch?v=vneV9NIHs44&amp;feature=youtu.be"><strong>Brief</strong></a>)</p>
<h3 id="heading-the-model-was-trained-to-guess">The model was trained to guess</h3>
<p>Why does it lean toward answering at all, instead of saying “I don’t know”? Think about how we grade LLMs: benchmarks, largely multiple-choice. Picture a question you have no clue about. Let’s say I give you this question when you have no knowledge in Chemistry:</p>
<blockquote>
<p><em><strong>Which enzyme fixes CO2 in the Calvin cycle?</strong></em></p>
</blockquote>
<ul>
<li><p>Leave it blank:&nbsp;<strong>0 points.</strong></p>
</li>
<li><p>Guess and get it wrong:&nbsp;<strong>0 points.</strong></p>
</li>
<li><p>Guess and get it right:&nbsp;<strong>+1 point.</strong></p>
</li>
</ul>
<p>Under that scoring, guessing strictly dominates abstaining. If you don’t know, you should&nbsp;<em>always</em>&nbsp;take a shot. Train a model against millions of such items and it internalizes exactly that: a confident answer is worth more than “I can’t tell.” We rewarded hallucination, then act surprised when we get it. [10]</p>
<p>And it’s not only the benchmarks: the raw pretrained model is fairly well-calibrated, then human-feedback fine-tuning flattens that calibration. We literally train the hedging out. [11]</p>
<img src="https://contributor.insightmediagroup.io/wp-content/uploads/2026/07/benchmark_scoring.svg" alt="Image 5: A multiple-choice benchmark question where a correct answer scores +1, a wrong answer scores 0, and “I don’t know” also scores 0, so any guess can only help. (Source: Brief)" style="display:block;margin:0 auto" width="1200" height="600" loading="lazy">

<p>Image 5: A multiple-choice benchmark question where a correct answer scores +1, a wrong answer scores 0, and “I don’t know” also scores 0, so any guess can only help. (Source: <a href="https://www.youtube.com/watch?v=vneV9NIHs44&amp;feature=youtu.be"><strong>Brief</strong></a>)</p>
<h3 id="heading-opening-the-box-a-quick-tour-of-interpretability">Opening the box: a quick tour of interpretability</h3>
<p>For a long time, LLMs were boxes we couldn’t really understand or peek inside directly. The field of&nbsp;<strong>interpretability</strong>&nbsp;lets us look inside, and there are now public tools (and a series of excellent papers, much of it from Anthropic) that let anyone play with this on open models.</p>
<p>Here’s just enough to make the hallucination mechanism click. We’ll build it in three steps: how the model represents a single word, how those representations cluster into concepts we can read and even steer, and how one such concept misfiring becomes a hallucination.</p>
<h4 id="heading-embeddings-vs-activations">Embeddings vs. activations</h4>
<p>Every token maps to a vector called an&nbsp;<strong>embedding</strong>. Note that the token&nbsp;<em>bank</em>&nbsp;has the&nbsp;<em>same</em>&nbsp;embedding regardless of context, even though in the sentence “I sat by the river<strong>bank</strong>” and in “I deposited cash at the&nbsp;<strong>bank</strong>“, this token means very different things.</p>
<p>The disambiguation happens&nbsp;<em>inside</em>&nbsp;the network. As the token flows up through the transformer’s layers, it picks up&nbsp;<strong>activations</strong>, and the activations for&nbsp;<em>bank</em>&nbsp;in those two sentences diverge. Context reshapes the representation as it climbs. [12]</p>
<img src="https://contributor.insightmediagroup.io/wp-content/uploads/2026/07/activations_4.svg" alt="Image 6: The word “bank” starts as one fixed embedding, then in “river bank” versus “cash at the bank” flows up through the layers into two different activation vectors. (Source: Brief)" style="display:block;margin:0 auto" width="1200" height="640" loading="lazy">

<p>Image 6: The word “bank” starts as one fixed embedding, then in “river bank” versus “cash at the bank” flows up through the layers into two different activation vectors. (Source: <a href="https://www.youtube.com/watch?v=vneV9NIHs44&amp;feature=youtu.be"><strong>Brief</strong></a>)</p>
<p>This isn't unique to machines. Read this sentence:</p>
<blockquote>
<p><em><strong>The old man the ship.</strong></em></p>
</blockquote>
<p>Most people parse “the old man” as a noun phrase and then hit a wall. Re-read it: “the old” are the people, and “man” is the&nbsp;<em>verb</em>, as in the old crew or sail the ship.</p>
<p>These are called&nbsp;<strong>garden-path sentences</strong>&nbsp;(my linguistics thesis was on them, so I’ll admit a bias: I enjoy them more than most people). The word&nbsp;<em>man</em>, given the prior&nbsp;<em>the old</em>, gets a very high probability of being a noun. The context primes a prediction, and the prediction is wrong.</p>
<p>It’s the same move as the chant, and the same move the model makes at every token: the words around&nbsp;<em>man</em>&nbsp;reshape what it means, exactly as they reshaped&nbsp;<em>bank</em>&nbsp;a moment ago.</p>
<h4 id="heading-features">Features</h4>
<p>So back to those activations inside the model: recurring patterns of them correspond to interpretable concepts, called&nbsp;<strong>features</strong>. Tools like&nbsp;<a href="https://www.neuronpedia.org/"><strong>Neuronpedia</strong></a>&nbsp;act as a free, public microscope for open models (Gemma, Llama, and friends, not Opus or GPT). [13]</p>
<p>How do we know what a feature&nbsp;<em>means</em>? We feed the model thousands of texts and watch where a given feature lights up (that is, gets&nbsp;<em>activated</em>). If it fires on&nbsp;<em>bear</em>,&nbsp;<em>rabbit</em>, and&nbsp;<em>elephant</em>&nbsp;but ignores most other tokens, when we ask another model to label it from those activations, it may come up with “animals / living things,” and now we have a name for that internal feature.</p>
<p>By using tools like Neuronpedia, we can play with these features and actually see them on a real model.</p>
<img src="https://contributor.insightmediagroup.io/wp-content/uploads/2026/07/neuronpedia_feature-1024x332.png" alt="Image 7: A real feature dashboard on Neuronpedia, showing the text snippets where one feature activates and the label inferred from them. (Source: Brief)" style="display:block;margin:0 auto" width="1024" height="332" loading="lazy">

<p>Image 7: A real feature dashboard on Neuronpedia, showing the text snippets where one feature activates and the label inferred from them. (Source: <a href="https://www.youtube.com/watch?v=vneV9NIHs44&amp;feature=youtu.be"><strong>Brief</strong></a>)</p>
<h4 id="heading-features-are-causal">Features are causal</h4>
<p>And you don’t have to take my word for it, you can do it yourself: Neuronpedia’s&nbsp;<a href="https://www.neuronpedia.org/gemma-2-2b/steer"><strong>steering interface</strong></a>&nbsp;lets you grab a feature in an open model, clamp its weight up, and watch the output visibly bend toward that concept.</p>
<p>That's the same move Anthropic described when they took the&nbsp;<em>Golden Gate Bridge</em>&nbsp;feature within the model, and turned its weight way up, and suddenly asking that model for a chocolate-covered-pretzels recipe routed the chocolate&nbsp;<em>over the bridge</em>, and asking how it would spend $10 got you a suggestion to drive across the Golden Gate Bridge and pay the toll. (This was the real, public “Golden Gate Claude.”)</p>
<p>Turning a feature up&nbsp;<em>changed the output</em>, so these internal representations aren’t passive read-outs. They steer generation. [14]</p>
<p>The same was shown with a clean causal swap. Give the model&nbsp;<em>“The capital of the state containing Dallas is…”</em>&nbsp;and internally a&nbsp;<strong>Texas</strong>&nbsp;feature fires, leading to the output&nbsp;<strong>Austin</strong>. How do we know Texas was really the hidden step? We reach in and force that feature from Texas to&nbsp;<strong>California</strong>, and the output changes to&nbsp;<strong>Sacramento.</strong>&nbsp;The wiring is real: context fires features, and features guide what comes out.</p>
<img src="https://contributor.insightmediagroup.io/wp-content/uploads/2026/07/circuit_proof_5.svg" alt="Image 8: The prompt about Dallas is unchanged, but forcing the internal “Texas” feature to “California” by hand flips the output from Austin to Sacramento. (Source: Brief)" style="display:block;margin:0 auto" width="1200" height="640" loading="lazy">

<p>Image 8: The prompt about Dallas is unchanged, but forcing the internal “Texas” feature to “California” by hand flips the output from Austin to Sacramento. (Source: <a href="https://www.youtube.com/watch?v=vneV9NIHs44&amp;feature=youtu.be"><strong>Brief</strong></a>)</p>
<h3 id="heading-the-hallucination-circuit">The hallucination circuit</h3>
<p>Now everything comes together. Anthropic’s interpretability work surfaced something like two interacting circuits [15]:</p>
<ul>
<li><p>A&nbsp;<strong>default “I can’t tell” reflex</strong>&nbsp;that is&nbsp;<em>on</em>&nbsp;by default. You can think of it as a brake – guiding the model not to make stuff up.</p>
</li>
<li><p>A&nbsp;<strong>“do I know this?” feature</strong>&nbsp;that, when it fires,&nbsp;<em>suppresses</em>&nbsp;that brake so the model provides an answer.</p>
</li>
</ul>
<p>In the healthy case this is exactly right: you ask something the model knows, “do I know this?” fires, the brake releases, you get a correct answer. The claim about hallucination is that it’s&nbsp;<strong>this switch misfiring, firing on a familiar&nbsp;<em>shape</em>&nbsp;with nothing real behind it.</strong></p>
<p>And if that’s the mechanism, we should be able to&nbsp;<em>force</em>&nbsp;the misfire, and Anthropic did just that.</p>
<p>Ask:&nbsp;<em>“What sport does Michael Batkin play?”</em>&nbsp;That name doesn’t correspond to anyone the model knows, so “do I know this?” stays quiet, the brake stays on, and you get the right behavior:&nbsp;<em>“I can’t find a record of anyone named Michael Batkin.”</em></p>
<img src="https://contributor.insightmediagroup.io/wp-content/uploads/2026/07/batkin_3.svg" alt="Image 9: The resting circuit on the same question: the “can’t answer” brake is ON, the “do I know this?” feature stays quiet because the name is unfamiliar, and the model correctly declines. (Source: Brief)" style="display:block;margin:0 auto" width="1200" height="640" loading="lazy">

<p>Image 9: The resting circuit on the same question: the “can’t answer” brake is ON, the “do I know this?” feature stays quiet because the name is unfamiliar, and the model correctly declines. (Source: <a href="https://www.youtube.com/watch?v=vneV9NIHs44&amp;feature=youtu.be"><strong>Brief</strong></a>)</p>
<p>Now researchers reach in and&nbsp;<strong>force the “do I know this?” feature on.</strong>&nbsp;The brake releases, and out comes a confident&nbsp;<em>“Michael Batkin plays chess.”</em>&nbsp;The model never actually knew a sport. It knew, falsely, that it knew the&nbsp;<em>person</em>, and that was enough to release the brake and fabricate the rest.</p>
<img src="https://contributor.insightmediagroup.io/wp-content/uploads/2026/07/batkin_6.svg" alt="Image 10: Forcing the misfire on “What sport does Michael Batkin play?”: the “I can’t tell” brake is suppressed, the “do I know this?” feature is clamped on for a person who doesn’t exist, and the model invents a confident answer. (Source: Brief)" style="display:block;margin:0 auto" width="1200" height="640" loading="lazy">

<p>Image 10: Forcing the misfire on “What sport does Michael Batkin play?”: the “I can’t tell” brake is suppressed, the “do I know this?” feature is clamped on for a person who doesn’t exist, and the model invents a confident answer. (Source: <a href="https://www.youtube.com/watch?v=vneV9NIHs44&amp;feature=youtu.be"><strong>Brief</strong></a>)</p>
<p>Map that straight back to the Cursor bot:</p>
<ul>
<li><p>Consider someone asks&nbsp;<em>“How do I change the theme?”</em> If the model genuinely “knows” this, the brake releases and you get the correct answer. ✅</p>
</li>
<li><p>But when someone asks&nbsp;<em>“Is two-device login blocked?”</em>, the words&nbsp;<em>device</em>,&nbsp;<em>login</em>,&nbsp;<em>blocked</em>&nbsp;all look familiar. So “do I know this?” fires on familiarity, not knowledge, the brake releases, and you get&nbsp;<em>“Yes, it’s a core security feature.”</em>&nbsp; ❌</p>
</li>
</ul>
<p>This is of course not proved, as we don’t have access to the model and its features. But given the same logic that we do know works given the research on the subject, we can assume that the tokens were known, even though the policy didn't exist.</p>
<img src="https://contributor.insightmediagroup.io/wp-content/uploads/2026/07/halluc_circuit_5.svg" alt="Image 11: Inside the Cursor bot: familiar words make the “do I know this?” feature misfire, which suppresses the default “I can’t tell” brake, and the bot invents “a core security feature. (Source: Brief)" style="display:block;margin:0 auto" width="1200" height="640" loading="lazy">

<p>Image 11: Inside the Cursor bot: familiar words make the “do I know this?” feature misfire, which suppresses the default “I can’t tell” brake, and the bot invents “a core security feature. (Source: <a href="https://www.youtube.com/watch?v=vneV9NIHs44&amp;feature=youtu.be"><strong>Brief</strong></a>)</p>
<h3 id="heading-can-we-catch-it-in-production">Can we catch it in production?</h3>
<p>There are different ways to go about it, and I want to highlight one that I find very elegant – namely, to watch the&nbsp;<strong>entropy of meanings.</strong>&nbsp;[16]</p>
<p>Ask the Cursor bot&nbsp;<em>“How do I change the theme?”</em>&nbsp;five times. Presuming that the bot “knows” the answer, you won’t get identical wording (it’s probabilistic). But if you cluster the answers by&nbsp;<em>meaning</em>, say with another model, you get&nbsp;<strong>one</strong>&nbsp;meaning: “go to Settings then Theme.” Low semantic entropy means a greater chance that the model actually knows this, so you can trust it.</p>
<p>Now ask&nbsp;<em>“Is two-device login blocked?”</em>&nbsp;five times. You might get&nbsp;<em>“Yes, security policy,”</em>&nbsp;<em>“No, it’s allowed,”</em>&nbsp;<em>“One device per plan,”</em>&nbsp;<em>“It’s just a setting,”</em>&nbsp;<em>“Maybe, not sure.”</em>&nbsp;That’s&nbsp;<strong>high</strong>&nbsp;semantic entropy, five different meanings, which is a strong signal the model is making it up.</p>
<p>The cost of using this method in production is real (multiple calls, more tokens, more latency, higher cost), but if you only want to surface high-confidence answers to users, sampling-and-clustering is a useful guardrail.</p>
<img src="https://contributor.insightmediagroup.io/wp-content/uploads/2026/07/semantic_entropy.svg" alt="Image 12: Sampling a known question five times yields answers that cluster into one meaning (low entropy, trustworthy), while a made-up one scatters into many meanings (high entropy, likely confabulated). (Source: Brief)." style="display:block;margin:0 auto" width="1200" height="600" loading="lazy">

<p>Image 12: Sampling a known question five times yields answers that cluster into one meaning (low entropy, trustworthy), while a made-up one scatters into many meanings (high entropy, likely confabulated). (Source: <a href="https://www.youtube.com/watch?v=vneV9NIHs44&amp;feature=youtu.be"><strong>Brief</strong></a>).</p>
<h2 id="heading-so-what-do-you-actually-do-about-it"><strong>So What Do You Actually Do About It?</strong></h2>
<p>It’s June 2026, the models still confabulate, and you want to ship something anyway. Here’s the short checklist.</p>
<ol>
<li><p><strong>Give the model a real way to say “I can’t tell.”</strong>&nbsp;Tell it to ground answers in retrieved sources and to abstain when it can’t. But prompting is necessary, not sufficient, which is why the next point matters more.</p>
</li>
<li><p><strong>Stress-test the abstention.</strong>&nbsp;After you’ve told it to ground answers and cite sources,&nbsp;<em>actively try to make it hallucinate.</em>&nbsp;Throw questions at it whose answers don’t exist, repeatedly, until you’ve convinced yourself the “I can’t tell” path actually fires. Do it continuously to make sure your guardrails don’t break.</p>
</li>
<li><p><strong>If a human’s name goes on the output, a human verifies it.</strong>&nbsp;If you’re a lawyer filing with a court, you can't, at least for now, hand that to a model and trust it.</p>
</li>
<li><p><strong>Don’t give agents permission to cause damage.</strong>&nbsp;This is the hard one, because agents need to&nbsp;<em>do</em>&nbsp;things to be useful. But the PocketOS lesson is unambiguous: scope tokens narrowly, require confirmation on destructive operations, keep production unreachable from playgrounds, and put backups in separate volumes. If you let an agent delete production, then occasionally it&nbsp;<em>will</em>&nbsp;delete production.</p>
</li>
</ol>
<h2 id="heading-wrapping-up"><strong>Wrapping Up</strong></h2>
<p>We started with a football crowd and ended inside a transformer. Phonemic restoration in your auditory cortex and next-token prediction in a model are the same top-down move: meet an input you can’t fully resolve, and fill the gap with the most plausible, confident thing instead of admitting you can’t tell.</p>
<p>The tales (Cursor, Virgin Money, Sullivan &amp; Cromwell, the 1,633 court cases, PocketOS in nine seconds, Replit) are funny until they cost a business.</p>
<p>The&nbsp;<em>why</em>&nbsp;is now legible: models were trained to prefer answering over abstaining, and inside them a “do I know this?” switch can fire on familiarity rather than knowledge, releasing the brake and letting a confident fabrication out.</p>
<p>And the fixes are mostly not magic. They’re abstention you actually tested, human verification where it counts, and agents whose blast radius you deliberately shrank.</p>
<p>We're not past the embarrassing tales. But we now understand them well enough that shipping one is, increasingly, a choice.</p>
<h2 id="heading-references"><strong>References</strong></h2>
<p>Every case here, plus a few that didn’t make the article, has primary sources collected on the&nbsp;<a href="https://omerr.github.io/embarrassing-ai/resources.html"><strong>companion resources page</strong></a>.</p>
<ol>
<li><p>“That is embarrassing” — the Derby County chant. Laughing Squid,&nbsp;<a href="https://laughingsquid.com/football-crowd-chanting-this-is-embarrassing/"><strong>Football Crowd Chanting “This Is Embarrassing”</strong></a>; audio via the Filter Stories podcast,&nbsp;<a href="https://open.spotify.com/episode/5neF5dF1hyQP3Jsi5av6mB"><strong>episode</strong></a>.</p>
</li>
<li><p>Phonemic restoration effect.&nbsp;<a href="https://en.wikipedia.org/wiki/Phonemic_restoration_effect"><strong>Wikipedia</strong></a>. Related illusions: the&nbsp;<a href="https://en.wikipedia.org/wiki/McGurk_effect"><strong>McGurk effect</strong></a>&nbsp;and&nbsp;<a href="https://en.wikipedia.org/wiki/Yanny_or_Laurel"><strong>Yanny vs. Laurel</strong></a>.</p>
</li>
<li><p>Cursor’s support bot invents a policy (Apr 2025). The Register,&nbsp;<a href="https://www.theregister.com/2025/04/18/cursor_ai_support_bot_lies/"><strong>“Cursor AI support bot lies”</strong></a>;&nbsp;<a href="https://incidentdatabase.ai/cite/1039/"><strong>AI Incident Database #1039</strong></a>.</p>
</li>
<li><p>Virgin Money’s chatbot blocks its own name (Jan 2025).&nbsp;<a href="https://fortune.com/europe/2025/01/30/virgin-money-chatbot-scolds-customer-confuse-banks-name-insult/"><strong>Fortune</strong></a>;&nbsp;<a href="https://www.cxtoday.com/customer-analytics-intelligence/dont-you-call-me-a-virgin-says-virgin-moneys-chatbot/"><strong>CX Today</strong></a>.</p>
</li>
<li><p>Sullivan &amp; Cromwell’s “please don’t sanction us” letter (Apr 2026). Above the Law,&nbsp;<a href="https://abovethelaw.com/2026/04/sullivan-cromwell-files-emergency-please-dont-sanction-us-for-all-these-ai-hallucinations-letter/"><strong>“Sullivan &amp; Cromwell Files Emergency … Letter”</strong></a>;&nbsp;<a href="https://www.cnn.com/2026/04/23/business/ai-hallucination-sullivan-cromwell-nightcap"><strong>CNN Business</strong></a>.</p>
</li>
<li><p>The AI Hallucination Cases database, maintained by Damien Charlotin:&nbsp;<a href="https://www.damiencharlotin.com/hallucinations/"><strong>damiencharlotin.com/hallucinations</strong></a>. On why courts can’t keep up:&nbsp;<a href="https://cronkitenews.azpbs.org/2025/10/28/lawyers-ai-hallucinations-chatgpt/"><strong>Cronkite News</strong></a>.</p>
</li>
<li><p>PocketOS — production database gone in nine seconds (Apr 2026). The Register,&nbsp;<a href="https://www.theregister.com/2026/04/27/cursoropus_agent_snuffs_out_pocketos/"><strong>“Cursor/Opus agent snuffs out PocketOS”</strong></a>;&nbsp;<a href="https://www.tomshardware.com/tech-industry/artificial-intelligence/claude-powered-ai-coding-agent-deletes-entire-company-database-in-9-seconds-backups-zapped-after-cursor-tool-powered-by-anthropics-claude-goes-rogue"><strong>Tom’s Hardware</strong></a>;&nbsp;<a href="https://www.fastcompany.com/91533544/cursor-claude-ai-agent-deleted-software-company-pocket-os-database-jer-crane"><strong>Fast Company</strong></a>.</p>
</li>
<li><p>Replit’s agent deletes prod during a code freeze (Jul 2025).&nbsp;<a href="https://fortune.com/2025/07/23/ai-coding-tool-replit-wiped-database-called-it-a-catastrophic-failure/"><strong>Fortune</strong></a>;&nbsp;<a href="https://www.eweek.com/news/replit-ai-coding-assistant-failure/"><strong>eWeek</strong></a>;&nbsp;<a href="https://incidentdatabase.ai/cite/1152/"><strong>AI Incident Database #1152</strong></a>.</p>
</li>
<li><p>Next-token prediction, explained. Jay Alammar,&nbsp;<a href="https://jalammar.github.io/illustrated-gpt2/"><strong>“The Illustrated GPT-2”</strong></a>&nbsp;— a visual walkthrough of how a language model emits a probability distribution over its vocabulary and samples the next token. Foundational paper: Bengio, Ducharme, Vincent &amp; Jauvin,&nbsp;<a href="https://www.jmlr.org/papers/v3/bengio03a.html"><strong>“A Neural Probabilistic Language Model”</strong></a>&nbsp;(JMLR, 2003).</p>
</li>
<li><p>Kalai, Nachum, Vempala &amp; Zhang,&nbsp;<a href="https://openai.com/index/why-language-models-hallucinate/"><strong>“Why Language Models Hallucinate”</strong></a>&nbsp;(OpenAI, 2025).&nbsp;<a href="https://arxiv.org/abs/2509.04664"><strong>arXiv:2509.04664</strong></a>.</p>
</li>
<li><p>OpenAI,&nbsp;<a href="https://arxiv.org/abs/2303.08774"><strong>“GPT-4 Technical Report / System Card”</strong></a>&nbsp;(2023) — the pretrained model is well-calibrated. RLHF fine-tuning flattens that calibration (see the calibration figure).</p>
</li>
<li><p>Embeddings vs. activations. Static token embeddings give each word one fixed vector: Mikolov, Chen, Corrado &amp; Dean,&nbsp;<a href="https://arxiv.org/abs/1301.3781"><strong>“Efficient Estimation of Word Representations in Vector Space”</strong></a>&nbsp;(word2vec, 2013); accessible walkthrough: Jay Alammar,&nbsp;<a href="https://jalammar.github.io/illustrated-word2vec/"><strong>“The Illustrated Word2vec”</strong></a>. That representation becomes context-dependent inside the network, resolving cases like&nbsp;<em>bank</em>: Peters et al.,&nbsp;<a href="https://arxiv.org/abs/1802.05365"><strong>“Deep contextualized word representations”</strong></a>&nbsp;(ELMo, 2018).</p>
</li>
<li><p><a href="https://www.neuronpedia.org/"><strong>Neuronpedia</strong></a>&nbsp;— a free, public microscope for the features of open models.</p>
</li>
<li><p>Anthropic,&nbsp;<a href="https://www.anthropic.com/news/golden-gate-claude"><strong>“Golden Gate Claude”</strong></a>&nbsp;(2024) — feature steering made public.</p>
</li>
<li><p>Anthropic,&nbsp;<a href="https://transformer-circuits.pub/2025/attribution-graphs/biology.html"><strong>“On the Biology of a Large Language Model”</strong></a>&nbsp;(2025) — the known-entity feature that suppresses the “I can’t tell” circuit, the Dallas→Austin swap, and the Michael Batkin misfire. Readable companion:&nbsp;<a href="https://www.anthropic.com/research/tracing-thoughts-language-model"><strong>“Tracing the thoughts of a language model”</strong></a>.</p>
</li>
<li><p>Farquhar, Kossen, Kuhn &amp; Gal,&nbsp;<a href="https://www.nature.com/articles/s41586-024-07421-0"><strong>“Detecting hallucinations in large language models using semantic entropy”</strong></a>&nbsp;(Nature, 2024).</p>
</li>
</ol>
<hr>
<p><em>If you enjoyed this, I go deeper on systems and internals on my</em>&nbsp;<a href="https://youtube.com/@briefvid"><em><strong>Brief YouTube channel</strong></em></a><em>. Questions or pushback? I’d love to hear them, leave a comment. Thanks for reading!</em></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ From Manufacturing to Microservices: Universal Lessons About Reliability ]]>
                </title>
                <description>
                    <![CDATA[ Software engineers often think reliability is a modern challenge. We discuss uptime, distributed systems, observability, and fault tolerance as if they belong exclusively to cloud computing. In realit ]]>
                </description>
                <link>https://www.freecodecamp.org/news/from-manufacturing-to-microservices-universal-lessons-about-reliability/</link>
                <guid isPermaLink="false">6a5e283ee7616f5097f7d096</guid>
                
                    <category>
                        <![CDATA[ Microservices ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Software Engineering ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Reliability ]]>
                    </category>
                
                    <category>
                        <![CDATA[ #manufacturing ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Mon, 20 Jul 2026 13:53:02 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/0de496b0-e02a-48c2-9631-d32a5152d766.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Software engineers often think reliability is a modern challenge.</p>
<p>We discuss uptime, distributed systems, observability, and fault tolerance as if they belong exclusively to cloud computing.</p>
<p>In reality, engineers have been solving reliability problems for centuries. Manufacturing plants, civil engineering projects, and industrial assembly lines have all faced the same fundamental question: how do you build systems that continue working even when individual components fail?</p>
<p>Whether you're assembling a bridge, manufacturing a vehicle, or deploying a microservice architecture, reliability is never accidental. It comes from thoughtful design, continuous testing, and a willingness to learn from failure.</p>
<p>The technology has changed, but the engineering principles have remained remarkably consistent.</p>
<p>In this article, we'll explore the timeless engineering principles that make systems reliable, whether they're factory assembly lines or cloud-native applications.</p>
<p>You'll see how concepts like redundancy, root cause analysis, realistic testing, and observability have guided engineers for decades, and why these lessons are just as valuable when building modern software.</p>
<p>By the end, you'll have a broader perspective on reliability and practical ideas you can apply to design more resilient systems.</p>
<h3 id="heading-what-well-cover">What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-every-system-is-only-as-reliable-as-its-weakest-link">Every System Is Only as Reliable as Its Weakest Link</a></p>
</li>
<li><p><a href="#heading-small-defects-become-big-problems">Small Defects Become Big Problems</a></p>
</li>
<li><p><a href="#heading-root-cause-analysis-is-more-important-than-finding-someone-to-blame">Root Cause Analysis Is More Important Than Finding Someone to Blame</a></p>
</li>
<li><p><a href="#heading-redundancy-is-an-investment-not-a-waste">Redundancy Is an Investment, Not a Waste</a></p>
</li>
<li><p><a href="#heading-testing-should-simulate-reality">Testing Should Simulate Reality</a></p>
</li>
<li><p><a href="#heading-observability-is-better-than-guesswork">Observability Is Better Than Guesswork</a></p>
</li>
<li><p><a href="#heading-reliability-is-a-continuous-process">Reliability Is a Continuous Process</a></p>
</li>
<li><p><a href="#heading-great-engineering-is-predictable-engineering">Great Engineering Is Predictable Engineering</a></p>
</li>
</ul>
<h2 id="heading-every-system-is-only-as-reliable-as-its-weakest-link"><strong>Every System Is Only as Reliable as Its Weakest Link</strong></h2>
<p>A modern application may consist of dozens or even hundreds of services. Each service depends on databases, APIs, queues, caches, storage systems, and network infrastructure. A failure in any one of these components can ripple throughout the entire application.</p>
<p>Manufacturing systems work in much the same way. A perfectly designed product can still fail if one component is installed incorrectly or if quality checks are skipped during production.</p>
<p>This highlights an important lesson for software engineers: reliability isn't about building perfect components. It's about ensuring the entire system can tolerate imperfections.</p>
<p>Experienced engineering teams rarely assume everything will work perfectly. Instead, they ask questions like:</p>
<ul>
<li><p>What happens if this service becomes unavailable?</p>
</li>
<li><p>Can another component take over?</p>
</li>
<li><p>How quickly can the system recover?</p>
</li>
<li><p>Can users continue working while the issue is resolved?</p>
</li>
</ul>
<p>Designing around failure is often more valuable than trying to eliminate every possible failure.</p>
<h2 id="heading-small-defects-become-big-problems"><strong>Small Defects Become Big Problems</strong></h2>
<p>Many major outages begin with something surprisingly small.</p>
<p>A configuration value is incorrect. A certificate expires. A retry loop overwhelms a downstream service. A cache becomes stale. An API starts returning unexpected responses.</p>
<p>None of these issues appear catastrophic on their own. The real damage comes when multiple small problems combine into a larger system failure.</p>
<p>Manufacturing follows the same pattern. A slightly misaligned component may seem harmless during assembly, but over time it can increase wear, reduce efficiency, and eventually cause an expensive breakdown.</p>
<p>Software systems behave similarly. Small <a href="https://www.ibm.com/think/topics/technical-debt">technical debt</a> accumulates until reliability begins to suffer.</p>
<p>This is why experienced teams invest in routine maintenance. Refactoring, dependency updates, infrastructure improvements, and automated testing may not deliver visible product features, but they significantly reduce operational risk.</p>
<p>Reliability is built through consistent attention to small details.</p>
<h2 id="heading-root-cause-analysis-is-more-important-than-finding-someone-to-blame"><strong>Root Cause Analysis Is More Important Than Finding Someone to Blame</strong></h2>
<p>When production systems fail, organisations often rush to identify who made the mistake.</p>
<p>The better question is why the mistake was possible in the first place.</p>
<p>Perhaps deployment safeguards were missing. Or monitoring failed to detect unusual behaviour. Or the documentation was outdated.</p>
<p>Perhaps code reviews overlooked an important edge case.</p>
<p>Strong engineering cultures focus on improving systems rather than assigning blame.</p>
<p>This philosophy exists throughout engineering disciplines. Manufacturing companies spend significant effort studying common <a href="https://constructiondaily.news/common-failures-in-material-assembly-and-how-to-prevent-them/">failures in material assembly</a> because understanding why defects occur leads to stronger processes, better inspections, and fewer future failures.</p>
<p>Software teams benefit from the same mindset. Every production incident becomes an opportunity to improve automation, monitoring, documentation, and testing rather than simply fixing the immediate issue.</p>
<p>Blameless postmortems encourage engineers to report problems early because they know the goal is learning rather than punishment.</p>
<p>Over time, this creates systems that become progressively more reliable.</p>
<h2 id="heading-redundancy-is-an-investment-not-a-waste"><strong>Redundancy Is an Investment, Not a Waste</strong></h2>
<p>At first glance, redundancy appears inefficient.</p>
<p>Why run multiple application instances? Why maintain replica databases? Why deploy services across multiple regions? Why store multiple backups?</p>
<p>The answer becomes clear when failures occur.</p>
<p>If every critical component has only one instance, every failure becomes a complete outage.</p>
<p>Manufacturing plants frequently maintain backup equipment for exactly this reason. Downtime often costs far more than maintaining spare capacity.</p>
<p>Cloud infrastructure follows the same principle. Load balancers distribute requests across multiple servers. Database replicas reduce the impact of hardware failures. <a href="https://aws.amazon.com/message-queue/">Message queues</a> prevent temporary spikes from overwhelming downstream systems.</p>
<p>Multiple availability zones protect against regional outages.</p>
<p>Redundancy increases costs, but it dramatically improves resilience.</p>
<p>Organisations must decide whether the cost of additional infrastructure is lower than the potential cost of downtime.</p>
<p>For customer-facing applications, the answer is usually yes.</p>
<h2 id="heading-testing-should-simulate-reality"><strong>Testing Should Simulate Reality</strong></h2>
<p>Passing unit tests doesn't necessarily mean software is reliable.</p>
<p>Many production failures occur because real-world environments behave differently than development machines.</p>
<p>Networks become slow. External APIs return unexpected responses. Databases experience temporary latency. Users generate traffic patterns nobody anticipated.</p>
<p>Reliable engineering requires testing under realistic conditions.</p>
<p>Integration tests verify communication between services. Load testing evaluates system behavior under heavy traffic. Chaos engineering intentionally introduces failures to measure resilience.</p>
<p>Disaster recovery exercises ensure backup procedures actually work.</p>
<p>Manufacturing industries also perform stress testing before products reach customers. Components are exposed to extreme temperatures, vibration, pressure, and repeated use to identify weaknesses before they become field failures.</p>
<p>Software deserves the same level of scrutiny. The closer testing resembles production, the fewer surprises engineers encounter after deployment.</p>
<h2 id="heading-observability-is-better-than-guesswork"><strong>Observability Is Better Than Guesswork</strong></h2>
<p>When a production issue occurs, every minute matters. Without visibility into system behaviour, engineers are forced to make educated guesses. Guessing rarely solves outages quickly.</p>
<p>Modern observability combines logs, metrics, traces, and alerts into a complete picture of system health.</p>
<p>Logs explain what happened. Metrics reveal performance trends. Distributed tracing follows requests across multiple services. Dashboards expose unusual behavior before customers notice problems.</p>
<p>Together, these tools dramatically reduce the time required to diagnose incidents. The goal isn't collecting more data. The goal is collecting meaningful data that answers important operational questions:</p>
<ul>
<li><p>Can engineers identify the failing service?</p>
</li>
<li><p>Can they measure customer impact?</p>
</li>
<li><p>Can they determine when the problem began?</p>
</li>
<li><p>Can they verify that a fix actually resolved the issue?</p>
</li>
</ul>
<p>Observability transforms debugging from detective work into engineering.</p>
<h2 id="heading-reliability-is-a-continuous-process"><strong>Reliability Is a Continuous Process</strong></h2>
<p>Many organisations mistakenly treat reliability as a one-time project. They improve monitoring after an outage. They add automated tests after discovering a regression. They introduce deployment pipelines after a failed release.</p>
<p>These improvements help, but reliability isn't something you complete once and forget.</p>
<p>Every new feature introduces additional complexity. Every dependency update changes system behavior. Every scaling decision creates new operational challenges.</p>
<p>Reliable systems require continuous evaluation.</p>
<p>Engineering teams regularly review incidents, remove technical debt, improve automation, and update operational documentation because yesterday's reliable architecture may not meet tomorrow's demands.</p>
<p>Reliability evolves alongside the software itself.</p>
<h2 id="heading-great-engineering-is-predictable-engineering"><strong>Great Engineering Is Predictable Engineering</strong></h2>
<p>Users rarely notice reliable systems. Nobody celebrates an application that simply works every day.</p>
<p>Instead, attention often focuses on new features, product launches, and innovative technologies.</p>
<p>Yet reliability remains one of the strongest competitive advantages any engineering organisation can build.</p>
<p>Customers trust applications that remain available. Developers enjoy working on systems that behave predictably. Businesses avoid the financial and reputational costs associated with outages.</p>
<p>Manufacturing has long understood that quality is built into every stage of production rather than inspected in at the end. Software engineering follows exactly the same principle. Reliability emerges from thoughtful architecture, disciplined testing, effective monitoring, continuous learning, and a culture that treats every failure as an opportunity to improve.</p>
<p>From factory floors to cloud-native microservices, the lesson remains unchanged. Strong systems aren't defined by the absence of failure. They're defined by how well they anticipate it, absorb it, and recover from it.</p>
<p>The technologies may continue to evolve, but the fundamentals of reliable engineering are timeless.</p>
<p>Hope you enjoyed this article. You can <a href="https://linkedin.com/in/manishmshiva">connect with me on LinkedIn</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Manage Secrets Securely with Azure Key Vault in Node.js ]]>
                </title>
                <description>
                    <![CDATA[ Last year a client called me about exactly this. Someone ran git log -p on a hunch and found a .env committed two years earlier, never caught. Database password, Stripe secret, JWT signing key — all s ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-manage-secrets-securely-with-azure-key-vault-in-node-js/</link>
                <guid isPermaLink="false">6a5e27b295e748bed9510853</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Devops ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Azure ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Zia Ullah ]]>
                </dc:creator>
                <pubDate>Mon, 20 Jul 2026 13:50:42 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/5491b408-9c6b-4d4d-a53e-215119fb2d97.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Last year a client called me about exactly this. Someone ran <code>git log -p</code> on a hunch and found a <code>.env</code> committed two years earlier, never caught. Database password, Stripe secret, JWT signing key — all still active. All still in production.</p>
<p>IBM's 2024 breach cost report put the average data breach at <strong>$4.88 million</strong> — and that's the average, not the worst cases.</p>
<p>Exposed credentials are consistently near the top of root causes. GitHub found over a million secrets leaked in public repos in 2023 alone, before you even count the private ones nobody ever discovered.</p>
<p>It's not a people problem. The developers I've worked with aren't careless — the architecture is just set up to fail them. A <code>.env</code> file gets committed once by accident. Credentials get copied and pasted into a Slack message to unblock a teammate. A Docker image gets published with secrets baked into a layer. A server gets shut down, and nobody rotates the credentials it was holding.</p>
<p>Azure Key Vault solves this differently. Your application fetches credentials at runtime from a centralized, encrypted service — the <code>.env</code> file stops being a liability because it stops holding anything worth stealing.</p>
<p>What you'll build is a Node.js Express API that fetches every secret from Azure Key Vault at startup. No passwords in the code. When someone quits, there's nothing in the repo to rotate. The <code>.env</code> ends up with one line — the vault name.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p>Node.js 18+</p>
</li>
<li><p>An Azure account (free tier works)</p>
</li>
<li><p>Azure CLI installed and logged in (<code>az login</code>)</p>
</li>
<li><p>Basic knowledge of Express.js</p>
</li>
<li><p>Docker (optional — only needed for the local database test section)</p>
</li>
</ul>
<h2 id="heading-what-we-will-build">What We Will Build</h2>
<p>A Node.js Express API that:</p>
<ol>
<li><p>Connects to PostgreSQL using credentials fetched from Key Vault at startup</p>
</li>
<li><p>Uses Managed Identity for authentication — no client secrets or passwords anywhere</p>
</li>
<li><p>Caches secrets in memory, so Key Vault isn't called on every request</p>
</li>
<li><p>Works locally via Azure CLI auth and in production via Managed Identity — same code, zero changes</p>
</li>
</ol>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-how-the-architecture-works">How the Architecture Works</a></p>
</li>
<li><p><a href="#heading-what-is-azure-key-vault">What Is Azure Key Vault?</a></p>
</li>
<li><p><a href="#heading-set-up-the-key-vault">Set Up the Key Vault</a></p>
</li>
<li><p><a href="#heading-create-the-nodejs-project">Create the Node.js Project</a></p>
</li>
<li><p><a href="#heading-connect-to-key-vault-with-managed-identity">Connect to Key Vault with Managed Identity</a></p>
</li>
<li><p><a href="#heading-cache-secrets-at-startup">Cache Secrets at Startup</a></p>
</li>
<li><p><a href="#heading-use-secrets-in-your-express-api">Use Secrets in Your Express API</a></p>
</li>
<li><p><a href="#heading-test-locally">Test Locally</a></p>
</li>
<li><p><a href="#heading-deploy-to-azure-app-service">Deploy to Azure App Service</a></p>
</li>
<li><p><a href="#heading-grant-key-vault-access-to-the-app">Grant Key Vault Access to the App</a></p>
</li>
<li><p><a href="#heading-rotate-secrets-without-redeploying">Rotate Secrets Without Redeploying</a></p>
</li>
<li><p><a href="#heading-troubleshooting">Troubleshooting</a></p>
</li>
<li><p><a href="#heading-wrapping-up">Wrapping Up</a></p>
</li>
</ol>
<h2 id="heading-how-the-architecture-works">How the Architecture Works</h2>
<p>Before writing any code, it helps to see the full picture:</p>
<pre><code class="language-plaintext"> LOCAL DEVELOPMENT
.-------------------------------------------------------.
|                                                        |
|   [Node.js App]                                        |
|        |                                               |
|        v                                               |
|   [DefaultAzureCredential] ---&gt; az login session       |
|        |                                               |
|        v                                               |
|   [Azure Key Vault]  ---&gt; Returns secrets              |
|        |                                               |
|        v                                               |
|   [In-memory cache]  ---&gt; App uses secrets at runtime  |
'-------------------------------------------------------'

 PRODUCTION (Azure)
.-------------------------------------------------------.
|                                                        |
|   [Azure App Service]                                  |
|        |                                               |
|        v                                               |
|   [DefaultAzureCredential] ---&gt; Managed Identity       |
|        |                                               |
|        v                                               |
|   [Azure Key Vault]  ---&gt; Returns secrets              |
|        |                                               |
|        v                                               |
|   [In-memory cache]  ---&gt; App uses secrets at runtime  |
'-------------------------------------------------------'
</code></pre>
<p>Both environments run the exact same code. <code>DefaultAzureCredential</code> figures out where it is — locally it picks up your <code>az login</code> session, on Azure it uses Managed Identity. You don't switch config files and you don't manage credentials. It just works.</p>
<h2 id="heading-what-is-azure-key-vault">What Is Azure Key Vault?</h2>
<p>Azure Key Vault is Microsoft's managed secret store — it handles secrets, keys, and certificates. For this tutorial, we're only using the secrets part: database passwords, API keys, JWT signing keys, anything your app needs to run but has no business being in your Git history.</p>
<p>Compared to <code>.env</code> files, the practical differences are worth understanding before you write any code.</p>
<p>Rotation is the one I notice most on real projects. Update a secret in Key Vault and every app picks it up on the next restart — no hunting down five different environment configs across staging and production.</p>
<p>Access control is the other big one. Each application only gets permission to read the secrets it actually needs. If one service gets compromised, it can't read credentials belonging to other services.</p>
<p>And every read gets logged. When something goes wrong — and eventually something will — you can see exactly which app accessed which secret, and when. That log is what auditors actually want to see.</p>
<p>I've sat in enough security reviews to know that "we use <code>.env</code> files and tell people not to commit them" doesn't satisfy an auditor. SOC 2, HIPAA, GDPR — they all want demonstrable controls. A vault with an access log is demonstrable.</p>
<h2 id="heading-set-up-the-key-vault">Set Up the Key Vault</h2>
<p>Run these commands. The vault name has to be globally unique across all of Azure — not just your own subscription — so pick something specific. Letters, numbers, and hyphens, 3 to 24 characters.</p>
<pre><code class="language-bash"># Create a resource group (skip if you already have one)
az group create \
  --name keyvault-demo-rg \
  --location eastus

# Create the Key Vault (RBAC enabled by default — required for the role assignment later)
az keyvault create \
  --name your-vault-name \
  --resource-group keyvault-demo-rg \
  --location eastus

# Grant yourself permission to manage secrets (required with RBAC — creators are not auto-assigned)
az role assignment create \
  --role "Key Vault Secrets Officer" \
  --assignee-object-id $(az ad signed-in-user show --query id -o tsv) \
  --scope $(az keyvault show \
    --name your-vault-name \
    --resource-group keyvault-demo-rg \
    --query id -o tsv)

# Add your secrets
az keyvault secret set \
  --vault-name your-vault-name \
  --name "DB-HOST" \
  --value "your-db-host.postgres.database.azure.com"

az keyvault secret set \
  --vault-name your-vault-name \
  --name "DB-PASSWORD" \
  --value "your-super-secret-password"

az keyvault secret set \
  --vault-name your-vault-name \
  --name "JWT-SECRET" \
  --value "your-jwt-signing-secret"
</code></pre>
<p>Verify the secrets were stored:</p>
<pre><code class="language-bash">az keyvault secret list --vault-name your-vault-name --query "[].name" -o tsv
</code></pre>
<p>You should see:</p>
<pre><code class="language-plaintext">DB-HOST
DB-PASSWORD
JWT-SECRET
</code></pre>
<h2 id="heading-create-the-nodejs-project">Create the Node.js Project</h2>
<p>Set up the project structure:</p>
<pre><code class="language-bash">mkdir nodejs-azure-keyvault
cd nodejs-azure-keyvault
npm init -y
npm install express pg jsonwebtoken @azure/keyvault-secrets @azure/identity dotenv
</code></pre>
<p>The two Azure packages do all the work:</p>
<ul>
<li><p><code>@azure/keyvault-secrets</code> — connects to your vault and pulls secrets out</p>
</li>
<li><p><code>@azure/identity</code> — handles auth. Locally, it uses your <code>az login</code> session, in production, it switches to Managed Identity automatically</p>
</li>
</ul>
<p>Add a start script to <code>package.json</code>:</p>
<pre><code class="language-bash">npm pkg set scripts.start="node server.js"
</code></pre>
<p>Create the following file structure:</p>
<pre><code class="language-plaintext">nodejs-azure-keyvault/
|-- src/
|   |-- config/
|   |   `-- secrets.js   # Key Vault client and secret loader
|   |-- db/
|   |   `-- index.js     # PostgreSQL pool using secrets
|   `-- routes/
|       `-- users.js     # Example route
|-- app.js               # Express app
`-- server.js            # Entry point -- loads secrets first
</code></pre>
<h2 id="heading-connect-to-key-vault-with-managed-identity">Connect to Key Vault with Managed Identity</h2>
<p>Create the secrets config file:</p>
<pre><code class="language-javascript">// src/config/secrets.js
const { SecretClient } = require('@azure/keyvault-secrets');
const { DefaultAzureCredential } = require('@azure/identity');

const VAULT_URL = `https://${process.env.KEY_VAULT_NAME}.vault.azure.net`;

const credential = new DefaultAzureCredential();
const client = new SecretClient(VAULT_URL, credential);

async function getSecret(name) {
  const secret = await client.getSecret(name);
  return secret.value;
}

module.exports = { getSecret };
</code></pre>
<p><code>DefaultAzureCredential</code> is the most important part of this setup. It tries a chain of authentication methods in order:</p>
<ol>
<li><p>Environment variables (for CI/CD pipelines)</p>
</li>
<li><p>Azure CLI credentials (for local development — <code>az login</code>)</p>
</li>
<li><p>Managed Identity (for deployed apps on Azure)</p>
</li>
</ol>
<p>This means the exact same code works locally and in production with zero changes. Locally, it uses your <code>az login</code> session. In production, it uses the app's Managed Identity. You never touch credentials.</p>
<h2 id="heading-cache-secrets-at-startup">Cache Secrets at Startup</h2>
<p>Calling Key Vault on every request adds latency and costs money. Load all secrets once at startup and cache them in memory. Replace <code>src/config/secrets.js</code> with this complete version:</p>
<pre><code class="language-javascript">// src/config/secrets.js
const { SecretClient } = require('@azure/keyvault-secrets');
const { DefaultAzureCredential } = require('@azure/identity');

const VAULT_URL = `https://${process.env.KEY_VAULT_NAME}.vault.azure.net`;

const credential = new DefaultAzureCredential();
const client = new SecretClient(VAULT_URL, credential);

// In-memory cache
const cache = {};

async function getSecret(name) {
  if (cache[name]) return cache[name];
  const secret = await client.getSecret(name);
  cache[name] = secret.value;
  return secret.value;
}

async function loadAllSecrets() {
  console.log('Loading secrets from Azure Key Vault...');
  const secretNames = ['DB-HOST', 'DB-PASSWORD', 'JWT-SECRET'];

  await Promise.all(
    secretNames.map(async (name) =&gt; {
      cache[name] = await getSecret(name);
      console.log(`  ✓ ${name} loaded`);
    })
  );

  console.log('All secrets loaded successfully.');
}

function getFromCache(name) {
  if (!cache[name]) throw new Error(`Secret "${name}" not loaded. Did loadAllSecrets() run?`);
  return cache[name];
}

module.exports = { loadAllSecrets, getFromCache };
</code></pre>
<p>The <code>loadAllSecrets</code> function runs once when the application starts. After that, all secrets are served from the in-memory cache with zero latency and zero Key Vault calls.</p>
<h2 id="heading-use-secrets-in-your-express-api">Use Secrets in Your Express API</h2>
<p>Set up the database connection using the cached secrets:</p>
<pre><code class="language-javascript">// src/db/index.js
const { Pool } = require('pg');
const { getFromCache } = require('../config/secrets');

let pool;

function getPool() {
  if (!pool) {
    pool = new Pool({
      host:     getFromCache('DB-HOST'),
      database: process.env.DB_NAME || 'myapp',
      user:     process.env.DB_USER || 'dbadmin',
      password: getFromCache('DB-PASSWORD'),
      port:     parseInt(process.env.DB_PORT || '5432'),
      ssl:      process.env.NODE_ENV === 'production'
                  ? { rejectUnauthorized: false }
                  : false,
    });

    pool.on('error', (err) =&gt; {
      console.error('Unexpected database pool error:', err.message);
    });
  }

  return pool;
}

module.exports = { getPool };
</code></pre>
<p>Notice the distinction: <code>DB-HOST</code> and <code>DB-PASSWORD</code> come from Key Vault because they're sensitive. The database name, username, and port are not — they don't need to be protected, so they use environment variables with sensible defaults. Key Vault is for credentials, not all configuration.</p>
<p>The SSL flag is environment-aware: forced on in production, off locally so Docker connections work without a certificate. The <code>rejectUnauthorized: false</code> setting accepts Azure Database for PostgreSQL's certificate without verifying the CA chain — this is standard for Azure-managed databases. For stricter environments, you can download the Azure root CA and pass it via the <code>ca</code> option in the pool config instead.</p>
<p>Create a sample route that uses JWT verification with the secret from Key Vault:</p>
<pre><code class="language-javascript">// src/routes/users.js
const express = require('express');
const jwt     = require('jsonwebtoken');
const { getFromCache } = require('../config/secrets');
const { getPool }      = require('../db');

const router = express.Router();

// Auth middleware — JWT secret comes from Key Vault, not process.env
function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing or malformed Authorization header' });
  }

  const token = authHeader.split(' ')[1];

  try {
    req.user = jwt.verify(token, getFromCache('JWT-SECRET'));
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
}

// GET /api/users — list users (authenticated)
router.get('/', authMiddleware, async (req, res) =&gt; {
  try {
    const result = await getPool().query(
      'SELECT id, email, created_at FROM users ORDER BY created_at DESC LIMIT 20'
    );
    res.json(result.rows);
  } catch (err) {
    console.error('Database error:', err.message);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// GET /api/users/:id — single user (authenticated)
router.get('/:id', authMiddleware, async (req, res) =&gt; {
  try {
    const result = await getPool().query(
      'SELECT id, email, created_at FROM users WHERE id = $1',
      [req.params.id]
    );
    if (!result.rows[0]) return res.status(404).json({ error: 'User not found' });
    res.json(result.rows[0]);
  } catch (err) {
    console.error('Database error:', err.message);
    res.status(500).json({ error: 'Internal server error' });
  }
});

module.exports = router;
</code></pre>
<p>Notice the error handler returns <code>'Internal server error'</code> instead of <code>err.message</code>. Database errors are surprisingly chatty — they'll hand an attacker your table names, column names, and query structure if you let them through.</p>
<p>Set up the Express application. Both files define <code>authMiddleware</code> locally — yes, it's duplicated. In production, I'd pull this into a shared middleware file. For this tutorial, keeping it local means you can read either file without bouncing between three others:</p>
<pre><code class="language-javascript">// app.js
const express = require('express');
const jwt = require('jsonwebtoken');
const { getFromCache } = require('./src/config/secrets');
const usersRouter = require('./src/routes/users');

const app = express();
app.use(express.json());

// Auth middleware — JWT secret comes from Key Vault, not process.env
function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing or malformed Authorization header' });
  }
  const token = authHeader.split(' ')[1];
  try {
    req.user = jwt.verify(token, getFromCache('JWT-SECRET'));
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
}

// Health check — no auth required
app.get('/health', (req, res) =&gt; {
  res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});

// Status endpoint — proves Key Vault integration without needing a database
app.get('/api/status', authMiddleware, (req, res) =&gt; {
  res.json({
    message: 'All secrets loaded from Azure Key Vault',
    vault: process.env.KEY_VAULT_NAME,
    secrets_loaded: ['DB-HOST', 'DB-PASSWORD', 'JWT-SECRET'],
    authenticated_as: req.user.email,
    timestamp: new Date().toISOString()
  });
});

app.use('/api/users', usersRouter);

app.use((req, res) =&gt; res.status(404).json({ error: 'Route not found' }));
app.use((err, req, res, next) =&gt; {
  console.error('Unhandled error:', err.message);
  res.status(500).json({ error: 'Internal server error' });
});

module.exports = app;
</code></pre>
<p>The entry point loads secrets before starting the server. The server doesn't start unless all secrets load successfully:</p>
<pre><code class="language-javascript">// server.js
require('dotenv').config();
const app = require('./app');
const { loadAllSecrets } = require('./src/config/secrets');

const PORT = process.env.PORT || 3000;

async function start() {
  try {
    await loadAllSecrets();
    app.listen(PORT, () =&gt; {
      console.log(`Server running on port ${PORT}`);
    });
  } catch (err) {
    console.error('Failed to start server:', err.message);
    console.error('Hint: Run "az login" for local development, or check Managed Identity for Azure deployments.');
    process.exit(1);
  }
}

start();
</code></pre>
<p>That <code>process.exit(1)</code> is deliberate. I'd rather the app crash loudly at startup than limp along with missing credentials and fail on the first real request two hours later.</p>
<h2 id="heading-test-locally">Test Locally</h2>
<p>Create a <code>.env</code> file for local development. This only contains the Key Vault name, nothing sensitive:</p>
<pre><code class="language-bash"># .env
KEY_VAULT_NAME=your-vault-name
PORT=3000
</code></pre>
<p>Add <code>.env</code> and the deployment zip to <code>.gitignore</code>:</p>
<pre><code class="language-bash">echo ".env" &gt;&gt; .gitignore
echo "app.zip" &gt;&gt; .gitignore
</code></pre>
<p>Make sure you're logged into Azure CLI:</p>
<pre><code class="language-bash">az login
</code></pre>
<p>Start the application:</p>
<pre><code class="language-bash">npm start
</code></pre>
<p>You should see:</p>
<pre><code class="language-plaintext">Loading secrets from Azure Key Vault...
  ✓ JWT-SECRET loaded
  ✓ DB-PASSWORD loaded
  ✓ DB-HOST loaded
All secrets loaded successfully.
Server running on port 3000
</code></pre>
<p>The order secrets load may vary — <code>Promise.all</code> fetches them in parallel and resolves as each one completes. What matters is that all three are confirmed before the server starts.</p>
<p>Test the health endpoint:</p>
<pre><code class="language-bash">curl http://localhost:3000/health
# {"status":"healthy","timestamp":"2026-07-14T19:38:11.659Z"}
</code></pre>
<p>Now prove the integration end-to-end. Grab the value you stored as <code>JWT-SECRET</code> and use it to sign a test token — paste it in for <code>YOUR-JWT-SECRET-VALUE</code>. Then hit <code>/api/status</code> with it:</p>
<pre><code class="language-bash">node -e "const jwt = require('jsonwebtoken'); console.log(jwt.sign({id:1, email:'test@test.com'}, 'YOUR-JWT-SECRET-VALUE', {expiresIn:'1h'}));"
</code></pre>
<p>On Linux/macOS:</p>
<pre><code class="language-bash">curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3000/api/status
</code></pre>
<p>On Windows PowerShell:</p>
<pre><code class="language-powershell">Invoke-RestMethod -Uri "http://localhost:3000/api/status" -Headers @{Authorization = "Bearer YOUR_TOKEN"}
</code></pre>
<p>You should see:</p>
<pre><code class="language-json">{
  "message": "All secrets loaded from Azure Key Vault",
  "vault": "your-vault-name",
  "secrets_loaded": ["DB-HOST", "DB-PASSWORD", "JWT-SECRET"],
  "authenticated_as": "test@test.com",
  "timestamp": "2026-07-14T19:50:08.687Z"
}
</code></pre>
<p>If you got that response, the whole chain worked. The JWT was signed and verified using a secret that lived only in Key Vault — not in your code, not in your<code>.env</code>, not anywhere in the repo. Your <code>az login</code> session handled the auth locally. In production, Managed Identity takes over. Same code, nothing changes.</p>
<h3 id="heading-test-the-full-database-flow-with-docker">Test the Full Database Flow with Docker</h3>
<p>The app reads <code>DB-HOST</code> and <code>DB-PASSWORD</code> from Key Vault, so those secrets need to match your local Docker container. Update them now:</p>
<pre><code class="language-bash">az keyvault secret set --vault-name your-vault-name --name "DB-HOST" --value "localhost"
az keyvault secret set --vault-name your-vault-name --name "DB-PASSWORD" --value "demopassword123"
</code></pre>
<p>Docker up a Postgres container. The password has to match <code>demopassword123</code> — that's what you just put in Key Vault:</p>
<pre><code class="language-bash">docker run --name pg-demo \
  -e POSTGRES_USER=dbadmin \
  -e POSTGRES_PASSWORD=demopassword123 \
  -e POSTGRES_DB=myapp \
  -p 5432:5432 \
  -d postgres:15
</code></pre>
<p>Get the table created and throw in some test rows:</p>
<pre><code class="language-bash">docker exec -it pg-demo psql -U dbadmin -d myapp -c \
  "CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW());"

docker exec -it pg-demo psql -U dbadmin -d myapp -c \
  "INSERT INTO users (email) VALUES ('alice@example.com'), ('bob@example.com'), ('carol@example.com');"
</code></pre>
<p>Kill the server and bring it back up — secrets load at startup, so it needs a fresh run to pick up what you just changed in Key Vault:</p>
<pre><code class="language-bash">npm start
</code></pre>
<p>Call the users endpoint with a valid JWT:</p>
<pre><code class="language-bash"># Generate a token (use the same value you stored as JWT-SECRET in Key Vault)
node -e "const jwt = require('jsonwebtoken'); console.log(jwt.sign({id:1, email:'test@test.com'}, 'YOUR-JWT-SECRET-VALUE', {expiresIn:'1h'}));"
</code></pre>
<p>On Linux/macOS:</p>
<pre><code class="language-bash">curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3000/api/users
</code></pre>
<p>On Windows PowerShell:</p>
<pre><code class="language-powershell">Invoke-RestMethod -Uri "http://localhost:3000/api/users" -Headers @{Authorization = "Bearer YOUR_TOKEN"}
</code></pre>
<p>You should see:</p>
<pre><code class="language-json">[
  { "id": 1, "email": "alice@example.com", "created_at": "2026-07-14T19:59:21.064Z" },
  { "id": 2, "email": "bob@example.com",   "created_at": "2026-07-14T19:59:21.064Z" },
  { "id": 3, "email": "carol@example.com", "created_at": "2026-07-14T19:59:21.064Z" }
]
</code></pre>
<p>That query ran using a password that came straight from Key Vault. It's not in your <code>.env</code>, not hardcoded anywhere, and not in a local variable. The repo has nothing worth stealing.</p>
<p>Before you deploy, put the real production values back in Key Vault:</p>
<pre><code class="language-bash">az keyvault secret set --vault-name your-vault-name --name "DB-HOST" --value "your-db-host.postgres.database.azure.com"
az keyvault secret set --vault-name your-vault-name --name "DB-PASSWORD" --value "your-super-secret-password"
</code></pre>
<p>If you skip this, the deployed app will try to connect to <code>localhost</code> and fail immediately — <code>localhost</code> doesn't exist on App Service.</p>
<h2 id="heading-deploy-to-azure-app-service">Deploy to Azure App Service</h2>
<p><strong>Note:</strong> This section creates the App Service infrastructure. The actual code deployment (zip upload) happens at the end of the next section — the app must have Key Vault access configured before its first startup, or it will fail immediately and exit.</p>
<p>Create the App Service:</p>
<pre><code class="language-bash"># Create an App Service Plan (B1 is the cheapest paid tier)
az appservice plan create \
  --name keyvault-demo-plan \
  --resource-group keyvault-demo-rg \
  --sku B1 \
  --is-linux

# Create the Web App
az webapp create \
  --name my-keyvault-node-app \
  --resource-group keyvault-demo-rg \
  --plan keyvault-demo-plan \
  --runtime "NODE:18-lts"

# Set app settings — KEY_VAULT_NAME tells the app which vault to use
# NODE_ENV=production enables SSL for the database connection
az webapp config appsettings set \
  --name my-keyvault-node-app \
  --resource-group keyvault-demo-rg \
  --settings KEY_VAULT_NAME=your-vault-name NODE_ENV=production
</code></pre>
<h2 id="heading-grant-key-vault-access-to-the-app">Grant Key Vault Access to the App</h2>
<p>Enable Managed Identity on the app. This gives it an identity in Microsoft Entra ID that Key Vault can trust:</p>
<pre><code class="language-bash"># Enable system-assigned managed identity
az webapp identity assign \
  --name my-keyvault-node-app \
  --resource-group keyvault-demo-rg
</code></pre>
<p>The following commands capture the <code>principalId</code> automatically and use it to grant the role:</p>
<pre><code class="language-bash"># Get the principal ID
PRINCIPAL_ID=$(az webapp identity show \
  --name my-keyvault-node-app \
  --resource-group keyvault-demo-rg \
  --query principalId \
  --output tsv)

# Get the Key Vault resource ID
KV_ID=$(az keyvault show \
  --name your-vault-name \
  --resource-group keyvault-demo-rg \
  --query id \
  --output tsv)

# Grant the app the "Key Vault Secrets User" role
az role assignment create \
  --role "Key Vault Secrets User" \
  --assignee-object-id $PRINCIPAL_ID \
  --scope $KV_ID
</code></pre>
<p>The <code>Key Vault Secrets User</code> role allows the app to read secrets. It can't create, update, or delete them. This is the principle of least privilege — the application can only do what it needs to do.</p>
<p>Time to ship it. Linux/macOS can run this directly — Windows users, open Git Bash (it ships with Git for Windows):</p>
<pre><code class="language-bash">zip -r app.zip . -x "node_modules/*" ".git/*" ".env" "app.zip"
</code></pre>
<p>Then deploy:</p>
<pre><code class="language-bash">az webapp deployment source config-zip \
  --name my-keyvault-node-app \
  --resource-group keyvault-demo-rg \
  --src app.zip
</code></pre>
<p>The deployed application authenticates to Key Vault using its Managed Identity automatically. No passwords, no client secrets, no credentials of any kind in the deployment.</p>
<p>Check the health endpoint to confirm it's running:</p>
<pre><code class="language-bash">curl https://my-keyvault-node-app.azurewebsites.net/health
# {"status":"healthy","timestamp":"..."}
</code></pre>
<p>If it won't start, pull the logs:</p>
<pre><code class="language-bash">az webapp log tail --name my-keyvault-node-app --resource-group keyvault-demo-rg
</code></pre>
<p>Nine times out of ten, it's that the Key Vault role assignment has not been propagated yet. Give it 2–3 minutes, then restart:</p>
<pre><code class="language-bash">az webapp restart --name my-keyvault-node-app --resource-group keyvault-demo-rg
</code></pre>
<h2 id="heading-rotate-secrets-without-redeploying">Rotate Secrets Without Redeploying</h2>
<p>One of the biggest practical benefits of Key Vault is secret rotation. When a database password needs to change, you update it in Key Vault — not in your app:</p>
<pre><code class="language-bash">az keyvault secret set \
  --vault-name your-vault-name \
  --name "DB-PASSWORD" \
  --value "new-rotated-password"
</code></pre>
<p>The cache builds at startup, so you don't need a redeploy — a restart is enough:</p>
<pre><code class="language-bash">az webapp restart \
  --name my-keyvault-node-app \
  --resource-group keyvault-demo-rg
</code></pre>
<p>No code change. No new deployment. The secret is rotated, and the app is using the new value in seconds.</p>
<p>If you need zero-downtime rotation, add a <code>/refresh-secrets</code> endpoint behind admin auth that clears the cache and then calls <code>loadAllSecrets()</code>. The order matters — <code>loadAllSecrets()</code> uses <code>getSecret()</code> which returns cached values if they exist, so you must clear the cache first, or it will reload nothing. This is optional but useful for long-running processes that can't afford a restart.</p>
<h2 id="heading-troubleshooting">Troubleshooting</h2>
<p><code>CredentialUnavailableError: DefaultAzureCredential failed to retrieve a token</code></p>
<p>You're not logged into Azure CLI. Run <code>az login</code> and try again. On Azure App Service, check that Managed Identity is enabled and the role assignment was created correctly.</p>
<p><code>RestError: Forbidden — The user does not have secrets get permission</code></p>
<p>The Managed Identity isn't wired up to Key Vault yet. Go back and run the <code>az role assignment create</code> command. If you already did, it might just need time. Azure can take 2–3 minutes to propagate role assignments, so give it a moment before you dig further.</p>
<p><code>Error: Secret "DB-PASSWORD" not loaded. Did loadAllSecrets() run?</code></p>
<p><code>getFromCache()</code> ran before <code>loadAllSecrets()</code> finished, meaning the startup sequence is out of order. Open <code>server.js</code> and confirm <code>await loadAllSecrets()</code> comes before <code>app.listen()</code>. If the order's fine, the secret might just not be in the vault yet. Run <code>az keyvault secret list --vault-name YOUR_VAULT</code> to double-check. (A name mismatch — wrong case, typo — throws <code>SecretNotFound</code> instead, which is the entry below.)</p>
<p><strong>App starts locally but fails on Azure App Service</strong></p>
<p>Almost always, the app setting. Either <code>KEY_VAULT_NAME</code> isn't in App Service configuration at all, or the vault name has a typo. Run <code>az webapp log tail</code> to see the actual startup error — that'll tell you which one.</p>
<p><code>AuthorizationFailed</code> <strong>when running</strong> <code>az role assignment create</code></p>
<p>You are a guest user in your Azure tenant and lack the Owner role needed to assign roles. Switch the existing vault to the access policy model — no need to recreate it or lose your secrets:</p>
<pre><code class="language-bash">az keyvault update \
  --name your-vault-name \
  --resource-group keyvault-demo-rg \
  --enable-rbac-authorization false
</code></pre>
<p>If this happened during <strong>Set Up the Key Vault</strong> (granting yourself access), run:</p>
<pre><code class="language-bash">az keyvault set-policy \
  --name your-vault-name \
  --object-id $(az ad signed-in-user show --query id -o tsv) \
  --secret-permissions get set list delete
</code></pre>
<p>If this happened during <strong>Grant Key Vault Access to the App</strong> (granting the Managed Identity access), run:</p>
<pre><code class="language-bash">az keyvault set-policy \
  --name your-vault-name \
  --object-id $PRINCIPAL_ID \
  --secret-permissions get list
</code></pre>
<p><strong>Key Vault returns</strong> <code>SecretNotFound</code></p>
<p>The secret was never added, was deleted, or its name doesn't match exactly what your code requests — Key Vault secret names are case-sensitive. A secret named <code>db-password</code> and a request for <code>DB-PASSWORD</code> are different names. Run <code>az keyvault secret list --vault-name YOUR_VAULT</code> and compare what's actually in the vault against what <code>loadAllSecrets()</code> is asking for in <code>src/config/secrets.js</code>. Usually, it's a casing issue or a stray hyphen.</p>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>The <code>.env</code> file in this project contains exactly one value: the Key Vault name. That's not sensitive. Every actual secret — database passwords, API keys, signing secrets — lives in Key Vault and never touches your codebase or your deployment pipeline.</p>
<p>This is the pattern I use on Azure projects now. The startup check is the part I find most useful in practice: if Key Vault is unreachable or a secret is missing, the server exits immediately with a clear error instead of starting up broken and failing on the first real request. You find out right away, rather than getting an obscure database connection error two hours later.</p>
<p>To add another secret, put it in Key Vault and drop its name into the <code>secretNames</code> array — that's it. Everything else scales with it.</p>
<p>The full working code is on GitHub: <a href="https://github.com/ziaongit/nodejs-azure-keyvault">nodejs-azure-keyvault</a></p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
