<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Naimul Karim</title>
    <description>The latest articles on DEV Community by Naimul Karim (@naimulkarim).</description>
    <link>https://dev.to/naimulkarim</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3139981%2Fdfe579cc-ba31-417d-a087-76f5b7e61559.jpg</url>
      <title>DEV Community: Naimul Karim</title>
      <link>https://dev.to/naimulkarim</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/naimulkarim"/>
    <language>en</language>
    <item>
      <title>Build Smarter AI Agents: Use Deterministic Code Before Calling an LLM</title>
      <dc:creator>Naimul Karim</dc:creator>
      <pubDate>Fri, 13 Nov 2026 01:41:02 +0000</pubDate>
      <link>https://dev.to/naimulkarim/build-smarter-ai-agents-use-deterministic-code-before-calling-an-llm-3ipc</link>
      <guid>https://dev.to/naimulkarim/build-smarter-ai-agents-use-deterministic-code-before-calling-an-llm-3ipc</guid>
      <description>&lt;p&gt;There is a tempting assumption when building AI-powered applications: if an LLM can make a decision, why not ask it to make that decision every time?&lt;/p&gt;

&lt;p&gt;It sounds reasonable. The model is already part of the application, it can interpret context, and it can handle a wide range of situations.&lt;/p&gt;

&lt;p&gt;But there is a catch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not every problem needs reasoning. Some problems just need code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If an AI agent repeatedly solves the same problem, it may be time to turn that solution into a deterministic function. Doing so can reduce inference costs, improve response times, and make the system more predictable.&lt;/p&gt;

&lt;p&gt;This is a small architectural decision with potentially significant benefits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: asking an LLM to solve the same thing repeatedly
&lt;/h2&gt;

&lt;p&gt;Imagine an AI agent that investigates failed CI/CD pipelines.&lt;/p&gt;

&lt;p&gt;Whenever a build fails, the agent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reads the pipeline logs.&lt;/li&gt;
&lt;li&gt;Interprets the error messages.&lt;/li&gt;
&lt;li&gt;Investigates the likely cause.&lt;/li&gt;
&lt;li&gt;Recommends what to do next.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a useful application of an LLM. Build logs can be long, unfamiliar, and sometimes ambiguous. The agent may need to connect several clues before identifying the root cause.&lt;/p&gt;

&lt;p&gt;Now imagine that the same known error appears repeatedly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error NU1301: Unable to load the service index for source.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After investigating the issue, the agent determines that the package feed is temporarily unavailable and recommends retrying package restore.&lt;/p&gt;

&lt;p&gt;The next time the same failure occurs, should the application ask the LLM to investigate everything again?&lt;/p&gt;

&lt;p&gt;What about the hundredth time?&lt;/p&gt;

&lt;p&gt;Probably not. Once the pattern is understood, we can handle it with ordinary application code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn a known decision into deterministic code
&lt;/h2&gt;

&lt;p&gt;For example, we could introduce a small C# function that recognizes known build errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nc"&gt;AnalyzeBuildFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"NU1301"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"NuGet feed may be unavailable. Check connectivity and retry package restore."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"CS0246"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"A type or namespace could not be found. Check using directives and project references."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"401 Unauthorized"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"Authentication failed. Check the credentials or token used by the pipeline."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"Unknown failure. Further investigation is required."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a simplified example; a production implementation should account for log structure, error context, false matches, and the possibility that one error message has multiple causes.&lt;/p&gt;

&lt;p&gt;The important idea is that known patterns can be handled without an LLM call.&lt;/p&gt;

&lt;p&gt;Instead of sending every failure to a model, the application can first check whether it recognizes the issue. If it does, it follows a tested path. If it does not, it can escalate the problem to the LLM.&lt;/p&gt;

&lt;p&gt;That gives us a useful division of responsibility:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Code handles known patterns and explicit rules.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The LLM handles unfamiliar situations, ambiguity, and reasoning.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What do we mean by deterministic code?
&lt;/h2&gt;

&lt;p&gt;Deterministic code produces the same result for the same input, assuming the same relevant state and execution environment.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="nc"&gt;CalculateTotal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;subtotal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;taxRate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;taxRate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Given the same values, this function returns the same result. There is no need for an LLM to interpret the request or generate a calculation.&lt;/p&gt;

&lt;p&gt;Compare that with asking an LLM to calculate an invoice total. The model may produce the correct answer, but it introduces unnecessary uncertainty into a task that should be handled by precise, testable business logic.&lt;/p&gt;

&lt;p&gt;This distinction matters:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use deterministic code when...&lt;/th&gt;
&lt;th&gt;Use an LLM when...&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The rules are known and explicit&lt;/td&gt;
&lt;td&gt;The rules need to be inferred from context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The result must be precise and repeatable&lt;/td&gt;
&lt;td&gt;The input is ambiguous or unstructured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The task involves calculations or validation&lt;/td&gt;
&lt;td&gt;The task involves interpretation or summarization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The same decision is made frequently&lt;/td&gt;
&lt;td&gt;The situation is unfamiliar or requires flexible reasoning&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An LLM is not a replacement for every function in an application. It is a tool for the parts of a problem where language understanding and flexible reasoning add value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two principles for designing efficient AI systems
&lt;/h2&gt;

&lt;p&gt;There are two practical principles worth keeping in mind.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. If you can reliably express a decision as rules, use code
&lt;/h3&gt;

&lt;p&gt;Consider a small business invoicing application.&lt;/p&gt;

&lt;p&gt;Some features might include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Calculating invoice totals.&lt;/li&gt;
&lt;li&gt;Applying a predefined discount.&lt;/li&gt;
&lt;li&gt;Validating required customer information.&lt;/li&gt;
&lt;li&gt;Checking whether an invoice is overdue.&lt;/li&gt;
&lt;li&gt;Updating an invoice's payment status.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are ordinary business rules. They belong in application code, supported by unit tests.&lt;/p&gt;

&lt;p&gt;Other features may benefit from an LLM:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understanding a request such as, "Show me the invoices that are still outstanding from last month."&lt;/li&gt;
&lt;li&gt;Summarizing a customer's message.&lt;/li&gt;
&lt;li&gt;Turning an informal instruction into a structured draft invoice.&lt;/li&gt;
&lt;li&gt;Explaining an unfamiliar error in plain language.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not to make the application less intelligent. It is to use the right tool for each responsibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. If an LLM keeps solving the same problem, consider turning the solution into code
&lt;/h3&gt;

&lt;p&gt;Suppose an agent repeatedly investigates API failures and discovers that a particular class of failures is caused by expired access tokens.&lt;/p&gt;

&lt;p&gt;Initially, the LLM may be useful because it needs to examine logs and understand the situation.&lt;/p&gt;

&lt;p&gt;But once the pattern is confirmed, checking token expiration is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nc"&gt;IsTokenExpired&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;expiresAtUtc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;expiresAtUtc&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application can now perform this check directly.&lt;/p&gt;

&lt;p&gt;The same idea applies to repeated decisions in many domains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Recognizing known CI/CD failures.&lt;/li&gt;
&lt;li&gt;Validating API request structures.&lt;/li&gt;
&lt;li&gt;Applying business rules.&lt;/li&gt;
&lt;li&gt;Checking authorization conditions.&lt;/li&gt;
&lt;li&gt;Detecting predefined data-quality issues.&lt;/li&gt;
&lt;li&gt;Routing requests based on known categories.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A repeated LLM decision is an opportunity to ask: &lt;strong&gt;Can we capture what we have learned as a reliable software rule?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A hybrid architecture: deterministic code plus LLM reasoning
&lt;/h2&gt;

&lt;p&gt;A practical AI agent does not need to choose between traditional code and an LLM. It can use both.&lt;/p&gt;

&lt;p&gt;Consider a simplified workflow for our CI failure investigation agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 CI Pipeline Failure
                          |
                          v
                Read and collect logs
                          |
                          v
                 Check known patterns
                          |
                +---------+---------+
                |                   |
             Recognized          Unknown
                |                   |
                v                   v
          Deterministic         Send context
             handler             to LLM
                |                   |
                v                   v
          Apply known fix       Investigate
          or recommendation    and reason
                |                   |
                +---------+---------+
                          |
                          v
                  Validate the result
                          |
                          v
                 Report the outcome
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deterministic path handles situations the system already understands. The LLM path handles cases that need further investigation.&lt;/p&gt;

&lt;p&gt;There is an important engineering detail here: &lt;strong&gt;recognizing a known pattern is not the same as proving its cause.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For example, an error code may have multiple possible causes. A rule-based handler should only take an automated action when the evidence and risk level justify it. Otherwise, it should provide a safe recommendation or escalate to the LLM or a human.&lt;/p&gt;

&lt;p&gt;This is why validation, tests, observability, and clear fallback behavior remain essential.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI-assisted development makes this approach easier
&lt;/h2&gt;

&lt;p&gt;There is another interesting part of this idea: AI coding tools can help us create the deterministic code that reduces future LLM usage.&lt;/p&gt;

&lt;p&gt;For example, a developer can ask an AI coding assistant to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Review recurring agent decisions or known failure cases.&lt;/li&gt;
&lt;li&gt;Identify decisions that can be expressed as explicit rules.&lt;/li&gt;
&lt;li&gt;Propose a small function or service to handle them.&lt;/li&gt;
&lt;li&gt;Generate unit tests, including edge cases.&lt;/li&gt;
&lt;li&gt;Refactor the agent to call the new function before invoking the LLM.&lt;/li&gt;
&lt;li&gt;Help compare behavior before and after the change.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The generated implementation still needs to be reviewed and tested. An AI-generated function is not automatically correct just because it is deterministic.&lt;/p&gt;

&lt;p&gt;But AI can reduce the effort required to turn a discovered pattern into maintainable, tested software.&lt;/p&gt;

&lt;p&gt;This creates an interesting feedback loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The LLM investigates an unfamiliar problem.&lt;/li&gt;
&lt;li&gt;Engineers verify the diagnosis.&lt;/li&gt;
&lt;li&gt;A recurring pattern is identified.&lt;/li&gt;
&lt;li&gt;The pattern is implemented as deterministic code.&lt;/li&gt;
&lt;li&gt;Future occurrences use the code path.&lt;/li&gt;
&lt;li&gt;The LLM is reserved for genuinely new or ambiguous cases.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Over time, the system can handle a larger share of familiar situations without inference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does this mean future AI agents will use fewer LLM calls?
&lt;/h2&gt;

&lt;p&gt;Potentially, yes.&lt;/p&gt;

&lt;p&gt;A more capable agent does not necessarily need to make more model calls. In a well-designed system, repeated reasoning can gradually become reusable software.&lt;/p&gt;

&lt;p&gt;That does not mean every LLM interaction should be replaced with a rule. Real-world inputs change, edge cases appear, and some decisions remain inherently ambiguous. The system still needs a way to handle cases that do not match known patterns.&lt;/p&gt;

&lt;p&gt;The objective is not to eliminate LLM calls. It is to make each call worthwhile.&lt;/p&gt;

&lt;p&gt;A useful way to think about the architecture is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use deterministic code for precision, repeatability, and known business logic.&lt;/li&gt;
&lt;li&gt;Use LLMs for interpretation, ambiguity, and unfamiliar problems.&lt;/li&gt;
&lt;li&gt;Use tests and validation to make sure both paths behave safely.&lt;/li&gt;
&lt;li&gt;Keep a fallback path for cases the deterministic logic cannot confidently handle.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;One of the biggest opportunities in AI-assisted development may not be adding AI to more parts of an application. It may be using AI to build better software around the places where AI is genuinely needed.&lt;/p&gt;

&lt;p&gt;When an LLM solves a problem once, it provides an answer.&lt;/p&gt;

&lt;p&gt;When we understand that solution, verify it, and turn it into tested code, we create something reusable.&lt;/p&gt;

&lt;p&gt;That code can run repeatedly, predictably, and without another inference call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The best AI architecture may not be the one that calls an LLM for everything. It may be the one that knows exactly when not to.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And perhaps one of the most valuable uses of AI is helping us write the code that makes future AI calls unnecessary.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>softwareengineering</category>
      <category>architecture</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Build a Simple E-commerce MVP with AI: A Step-by-Step Guide</title>
      <dc:creator>Naimul Karim</dc:creator>
      <pubDate>Fri, 13 Nov 2026 01:05:13 +0000</pubDate>
      <link>https://dev.to/naimulkarim/build-a-simple-e-commerce-mvp-with-ai-a-step-by-step-guide-1a79</link>
      <guid>https://dev.to/naimulkarim/build-a-simple-e-commerce-mvp-with-ai-a-step-by-step-guide-1a79</guid>
      <description>&lt;p&gt;If you've ever typed "build me an e-commerce app" into an AI tool, you know how it goes. You get something that runs, but it's not what you pictured. The login works differently than you wanted, the checkout has steps you never asked for, and half the buttons look like they came from different apps.&lt;/p&gt;

&lt;p&gt;The problem isn't the AI. It's that you left it hundreds of decisions to guess at.&lt;/p&gt;

&lt;p&gt;What worked for me was changing the order of things. I stopped asking for code first. Instead I ask Claude to help me write five short documents, answer its questions, and only then hand everything to Claude Code to build. The code ends up being the easy part.&lt;/p&gt;

&lt;p&gt;This post walks through the whole process with the exact prompts I use. The stack here is Angular on the front end and ASP.NET Core on the back end, but the method works with any stack. I'll use a made-up store called QuickCart as the example. Swap in your own idea and it works the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The example: QuickCart
&lt;/h2&gt;

&lt;p&gt;Nothing fancy, on purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two roles: a customer and a store owner&lt;/li&gt;
&lt;li&gt;The owner manages products. Customers browse, search, add to cart and check out&lt;/li&gt;
&lt;li&gt;Payment is cash on delivery only, so no payment gateway to set up&lt;/li&gt;
&lt;li&gt;Orders go Placed, Confirmed, Shipped, Delivered, and the customer can follow along&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The plan
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Write a PRD (what we're building, and what we're not)&lt;/li&gt;
&lt;li&gt;Write a screen flow (every screen and journey)&lt;/li&gt;
&lt;li&gt;Lock the tech stack to exact versions&lt;/li&gt;
&lt;li&gt;Write a design system, and preview it as an HTML mockup&lt;/li&gt;
&lt;li&gt;Design the database schema and API&lt;/li&gt;
&lt;li&gt;Make an implementation plan and a &lt;code&gt;CLAUDE.md&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set up your accounts and local environment (this part is on you)&lt;/li&gt;
&lt;li&gt;Build with Claude Code&lt;/li&gt;
&lt;li&gt;Have separate review agents check the work&lt;/li&gt;
&lt;li&gt;Write a UAT plan and deploy&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;p&gt;You'll need a Claude account and &lt;a href="https://docs.claude.com/en/docs/claude-code/overview" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; installed. On your machine, install the .NET SDK, Node.js (for the Angular CLI), and PostgreSQL (or have a hosted dev database ready). Make a project folder with a &lt;code&gt;docs/&lt;/code&gt; directory inside. Every document Claude writes goes there as a &lt;code&gt;.md&lt;/code&gt; file, because Claude Code reads Markdown without any fuss.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trick I use in every prompt
&lt;/h2&gt;

&lt;p&gt;Every prompt below ends with some version of this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before writing anything, ask me every question you need answered.
For each question, propose a default answer so I can approve quickly.
Do not assume anything.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the single most useful habit I've picked up. Claude comes back with a numbered list of questions, each with a sensible default. You say "defaults for everything except number 3" and you're done in a minute. Here's what that looks like for the PRD:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Q1. Can guests check out without an account?
    Default: No. Login (email) is required to place an order.
Q2. Can customers cancel an order?
    Default: Yes, only while the status is Placed.
Q3. Is there one delivery fee or many?
    Default: One flat fee, set by the owner.
Q4. Which currency and tax handling?
    Default: Single currency, prices tax-inclusive.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this step, those answers still get decided. The AI just decides them quietly, and you find out later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: The PRD
&lt;/h2&gt;

&lt;p&gt;The PRD says what you're building. Just as important, it says what you're not building. I always list non-goals, because otherwise the AI helpfully adds reviews, coupons and a wishlist you never wanted.&lt;/p&gt;

&lt;p&gt;Prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I want to build "QuickCart", a simple online store web app.

Roles: Customer and Store Owner (admin).
- The owner manages products (name, description, price, image,
  category, active/inactive).
- Customers browse, search, add to cart, check out and track orders.
- Payment: Cash on Delivery only.
- Order statuses: Placed -&amp;gt; Confirmed -&amp;gt; Shipped -&amp;gt; Delivered,
  or Cancelled.

Out of scope: stock tracking, online payments, reviews, coupons,
refunds, multi-vendor.

Prepare a PRD/BRD. Do not include the tech stack or database.
Before writing anything, ask me every question you need answered,
with a default answer for each. Do not assume anything.

Deliverable: prd.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result should look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;# QuickCart: Product Requirements Document
&lt;/span&gt;Version: 1.0 | Date: &amp;lt;date&amp;gt; | Owner: &amp;lt;you&amp;gt; | Status: Draft

&lt;span class="gu"&gt;## 1. Executive Summary
&lt;/span&gt;QuickCart is a mobile-first web store where customers order
everyday items and pay cash on delivery. One owner manages the
catalogue and fulfils orders.

&lt;span class="gu"&gt;## 3. Scope
### 3.1 In scope
&lt;/span&gt;&lt;span class="k"&gt;- &lt;/span&gt;Product catalogue management (owner)
&lt;span class="k"&gt;- &lt;/span&gt;Browse, search, cart, checkout (customer)
&lt;span class="k"&gt;- &lt;/span&gt;Order lifecycle and customer-visible tracking
&lt;span class="gu"&gt;### 3.2 Out of scope (v1)
&lt;/span&gt;&lt;span class="k"&gt;- &lt;/span&gt;Stock tracking, online payments, reviews, coupons, refunds

&lt;span class="gu"&gt;## 5. Functional Requirements
&lt;/span&gt;| ID    | Requirement                                   | Priority |
|-------|-----------------------------------------------|----------|
| FR-01 | Customer can register and log in with email   | Must     |
| FR-02 | Customer can add/remove/update cart items     | Must     |
| FR-03 | Customer can cancel an order while Placed     | Should   |
| FR-04 | Owner can mark a product inactive (hidden)    | Must     |

&lt;span class="gu"&gt;## 6. Acceptance Criteria
&lt;/span&gt;FR-03: Given an order in status Placed, when the customer taps
Cancel and confirms, then the status becomes Cancelled.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it properly. Changing a requirement now takes ten seconds. Changing it after the app is built takes an afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: The screen flow
&lt;/h2&gt;

&lt;p&gt;The PRD tells you what the app does. The screen flow tells you what people actually see: every screen, every empty state, every "what happens if the network drops" moment.&lt;/p&gt;

&lt;p&gt;I make Claude interview me again here, this time in four areas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Using prd.md, create a Screen Flow document: a blueprint of every
screen and end-to-end user journey in the MVP.

Do NOT write the document yet. First interview me, grouped into:
1. Navigation architecture and transitions (primary/secondary nav,
   deep links, entry and exit points)
2. State management: empty, error and loading states
3. Page data dependencies (what populates each page, what happens
   if data is slow or missing)
4. Edge cases (session timeout, back button, cancel/exit modals)

For every question propose a default based on standard UX best
practice. The final document must be so complete that a first-time
user can navigate without support.

Deliverable: screen-flow.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A good result gives every screen an ID and spells out its states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## 1. Global Rules
&lt;/span&gt;&lt;span class="k"&gt;- &lt;/span&gt;Customer: bottom tab bar (Home, Cart, Orders, Account)
&lt;span class="k"&gt;- &lt;/span&gt;Owner: sidebar (Dashboard, Orders, Products, Account)
&lt;span class="k"&gt;- &lt;/span&gt;Loading: skeletons for lists, inline spinner on buttons
&lt;span class="k"&gt;- &lt;/span&gt;Network error: inline retry banner, cached data tagged
  "may be outdated"
&lt;span class="k"&gt;- &lt;/span&gt;Empty state: illustration + one line + a relevant action
&lt;span class="k"&gt;- &lt;/span&gt;Back button during checkout: Confirm Exit modal

&lt;span class="gu"&gt;## CUST-04: Cart
&lt;/span&gt;Purpose: Review items and proceed to checkout.
Entry points: Cart tab, "Added to cart" toast action.
Data shown: items, quantity steppers, line totals, order total.
States:
  &lt;span class="k"&gt;- &lt;/span&gt;Empty: "Your cart is empty" + [Browse products]
  &lt;span class="k"&gt;- &lt;/span&gt;Loading: skeleton rows
  &lt;span class="k"&gt;- &lt;/span&gt;Error: retry banner, cart kept locally
Exit points: [Checkout] -&amp;gt; CUST-05, [Continue shopping] -&amp;gt; CUST-01
Notes: Inactive products show "No longer available" and block checkout.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those IDs (CUST-04 and so on) turn out to be handy. You can say "fix CUST-04" later and nobody has to guess which screen you mean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: The tech stack
&lt;/h2&gt;

&lt;p&gt;This one gets handed to Claude Code, so there's no room for "latest" or &lt;code&gt;^1.2.0&lt;/code&gt;. Every dependency gets an exact version. It keeps builds reproducible, and it stops the AI from writing code for an API that changed two releases ago.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create a tech stack document that locks every dependency to an
exact version number. It will be an input document for Claude Code.

Frontend: Angular (standalone components, TypeScript strict mode)
Backend: ASP.NET Core Web API on the current .NET LTS release
Database: PostgreSQL, accessed through Entity Framework Core
Auth: ASP.NET Core Identity with JWT bearer tokens
Hosting: Azure (App Service + Static Web Apps), with AWS as an
alternative

Rules:
- Pin the .NET SDK with global.json and Node/npm in package.json
  "engines".
- Do not recommend any library less than 1 year old with fewer
  than 10,000 GitHub stars, unless it is the official or
  first-party package for a chosen platform.
- Maximise compatibility with Claude Code.

Before finalising, ask me questions with default answers.

Deliverable: tech-stack.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I added that age and stars rule on purpose. Popular, well-documented libraries are also the ones the AI knows best, so you get fewer weird bugs. Here's the shape of the output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## 0. Version-check rule
&lt;/span&gt;Before scaffolding, verify every version: &lt;span class="sb"&gt;`npm view &amp;lt;package&amp;gt; version`&lt;/span&gt;
for npm packages, &lt;span class="sb"&gt;`dotnet package search &amp;lt;name&amp;gt; --exact-match`&lt;/span&gt; (or
nuget.org) for NuGet packages. If anything differs, STOP and report
it. Never silently substitute a version. Use exact versions in
package.json (no ^ or ~) and exact &amp;lt;PackageReference&amp;gt; versions in
.csproj files.

&lt;span class="gu"&gt;## 1. Runtime
&lt;/span&gt;| Component | Locked version | Notes                              |
|-----------|----------------|------------------------------------|
| .NET SDK  | &amp;lt;exact&amp;gt;        | LTS; pinned in global.json         |
| Node.js   | &amp;lt;exact&amp;gt;        | LTS; pinned in package.json engines|

&lt;span class="gu"&gt;## 2. Frontend
&lt;/span&gt;| Package          | Locked version | Notes                        |
|------------------|----------------|------------------------------|
| @angular/core    | &amp;lt;exact&amp;gt;        | Standalone components        |
| @angular/cli     | &amp;lt;exact&amp;gt;        | Same major as core           |
| typescript       | &amp;lt;exact&amp;gt;        | strict mode on               |
| rxjs             | &amp;lt;exact&amp;gt;        | Matches Angular peer range   |
| tailwindcss      | &amp;lt;exact&amp;gt;        | Design tokens as CSS variables |

&lt;span class="gu"&gt;## 3. Backend (NuGet)
&lt;/span&gt;| Package                                  | Locked version | Notes          |
|------------------------------------------|----------------|----------------|
| Microsoft.EntityFrameworkCore            | &amp;lt;exact&amp;gt;        | Matches .NET   |
| Npgsql.EntityFrameworkCore.PostgreSQL    | &amp;lt;exact&amp;gt;        | PostgreSQL     |
| Microsoft.AspNetCore.Identity.EntityFrameworkCore | &amp;lt;exact&amp;gt; | Users/roles   |
| Microsoft.AspNetCore.Authentication.JwtBearer | &amp;lt;exact&amp;gt;   | JWT auth       |
| FluentValidation.AspNetCore              | &amp;lt;exact&amp;gt;        | Request checks |
| xunit                                    | &amp;lt;exact&amp;gt;        | Backend tests  |

&lt;span class="gu"&gt;## 4. Testing
&lt;/span&gt;| @playwright/test | &amp;lt;exact&amp;gt; | End-to-end tests |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I left the versions as &lt;code&gt;&amp;lt;exact&amp;gt;&lt;/code&gt; because they change all the time. Claude will fill in real ones. Check them yourself with the commands in the document before building.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: The design system
&lt;/h2&gt;

&lt;p&gt;Skip this and every page ends up slightly different: a different blue here, a different corner radius there. The design system is one document that answers every "how should this look" question in advance, so Claude Code never has to improvise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I have the PRD, Screen Flow and Tech Stack documents. Create a
Design System and Content Guidelines document for Claude Code to
build every Angular page and component from. Claude Code must not
guess how anything should look or behave.

Cover Light AND Dark mode:
- Visual identity: color palette (primary, neutral, success,
  warning, error), tone, direction for a trustworthy store UI
- Typography: heading/body fonts, sizes, weights, line heights
- Information density: comfortable / balanced / dense (recommend one)
- Layout: grid, spacing scale, breakpoints, navigation behaviour
- Border radius, borders, shadows, elevation as design tokens
- Every component with anatomy, dimensions, typography, spacing,
  colors, states (default, hover, active, focus, disabled, loading),
  accessibility and responsive behaviour: buttons, inputs, selects,
  cards, tables, badges, modals, toasts, tabs, pagination, empty,
  error and loading states, order-status timeline
- Charts: only where they genuinely help; say where a plain number
  or table is clearer
- Loading and feedback rules: when to use skeletons vs spinners
- Explicit DO and DO NOT rules
- How tokens map to the styling approach in the tech stack

Principles: clarity over decoration, familiarity over novelty,
speed over animation.

Review all previous documents and ask me questions first, with
defaults. Do not assume anything.

Deliverable: design-system.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The detail level is the point. Something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Color tokens (CSS variables, never inline hex)
&lt;/span&gt;| Token               | Light     | Dark      | Usage             |
|---------------------|-----------|-----------|-------------------|
| --color-bg          | #FFFFFF   | #0A0A0A   | Page background   |
| --color-surface     | #FAFAFA   | #141414   | Cards, panels     |
| --color-border      | #E5E5E5   | #2A2A2A   | Dividers, borders |
| --color-text        | #171717   | #F5F5F5   | Body text         |
| --color-accent      | #059669   | #10B981   | Primary actions   |

&lt;span class="gu"&gt;## Primary Button
&lt;/span&gt;Height 40px | Radius 8px | Padding-x 16px | Font 14px / 500
Icon 16px | Background --color-accent | Text white
Hover: --color-accent-hover | Active: --color-accent-active
Focus: 2px ring, 2px offset | Disabled: 50% opacity, no pointer
Loading: spinner replaces icon, label stays, width unchanged

&lt;span class="gu"&gt;## DO NOT
&lt;/span&gt;&lt;span class="k"&gt;- &lt;/span&gt;Introduce colors or spacing values that are not tokens
&lt;span class="k"&gt;- &lt;/span&gt;Use color alone to show status (always add a label or icon)
&lt;span class="k"&gt;- &lt;/span&gt;Create one-off component styles for a single page
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  See it before you build it
&lt;/h3&gt;

&lt;p&gt;Once the document exists, I ask for a throwaway mockup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create a single-page HTML/CSS/JavaScript file that shows how my app
will look. Use the colors and iconography from design-system.md.
Show every component: all button types, dropdowns, headings, inputs,
cards, badges, tables, modals, toasts. Include a light/dark toggle.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open it in a browser. If something looks off, fix the document now. It's far cheaper than fixing finished pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: The database schema and API
&lt;/h2&gt;

&lt;p&gt;This is the step where mistakes hurt most, because the data model touches everything. The aim is that Claude Code follows a contract and never invents tables or endpoints on the fly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Design the complete backend for this app (ASP.NET Core Web API +
Entity Framework Core + PostgreSQL). Cover:
- All tables, fields, relationships, foreign keys, constraints,
  indexes, and the matching EF Core entities
- Authentication (ASP.NET Core Identity + JWT) and authorization
  (roles and policies, and which endpoints each role may call)
- API endpoints: route, method, request/response shape, role
  required, and which tables each one touches
- Business rules that must be enforced on the server (for example,
  a customer may only cancel their own order while it is Placed)
- Background jobs and database-level logic, if any
- Data lifecycle: soft delete, and permanent removal after 30 days
- Audit/history requirements

Rules:
- Cross-check against the PRD, Screen Flow, Tech Stack and Design
  System. List contradictions and gaps first.
- Separate what belongs in the database from what is only a
  PRD requirement. Do not copy product concepts into tables
  without validating them.
- Evaluate storage for a startup: PostgreSQL vs file/object storage
  vs a document store. Do not add infrastructure without a clear
  reason.
- Use EF Core migrations as the source of schema changes.

Claude Code must not have to guess the schema or the API. Ask me
every question first, with defaults.

Deliverable: backend-design.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two of those rules do a lot of work. The cross-check catches contradictions between your earlier documents. And the "separate PRD from database" rule stops Claude from turning every sentence in the PRD into a table. For a startup, the answer to the storage question is almost always plain PostgreSQL plus file or blob storage for product images. No extra database needed.&lt;/p&gt;

&lt;p&gt;An excerpt of the output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Storage decision
&lt;/span&gt;PostgreSQL for all relational data. Product images in blob storage
(Azure Blob Storage or S3), with only the image URL kept in the
database. No document database: no requirement needs it.

&lt;span class="gu"&gt;## Table: orders
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;               &lt;span class="kt"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer_id&lt;/span&gt;      &lt;span class="kt"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="nf"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;           &lt;span class="kt"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="s1"&gt;'placed'&lt;/span&gt;
                   &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'placed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'confirmed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'shipped'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                     &lt;span class="s1"&gt;'delivered'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'cancelled'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;total_amount&lt;/span&gt;     &lt;span class="kt"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_amount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;delivery_address&lt;/span&gt; &lt;span class="kt"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;placed_at&lt;/span&gt;        &lt;span class="kt"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nb"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;deleted_at&lt;/span&gt;       &lt;span class="kt"&gt;timestamptz&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;index&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;placed_at&lt;/span&gt; &lt;span class="k"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Authorization: orders
&lt;/span&gt;&lt;span class="k"&gt;- &lt;/span&gt;Customer: GET /api/orders returns only rows where
  customer_id = the user id in the JWT
&lt;span class="k"&gt;- &lt;/span&gt;Customer: POST /api/orders/{id}/cancel allowed only for own
  orders with status 'placed'
&lt;span class="k"&gt;- &lt;/span&gt;Owner (role "Owner"): GET and PATCH on all orders
&lt;span class="k"&gt;- &lt;/span&gt;Enforced in the API with authorization policies and an
  ownership check in the service layer. Never trust an id
  sent by the client.

&lt;span class="gu"&gt;## Design note: PRD vs database
&lt;/span&gt;The PRD says "order tracking". In the database this is a history
table (order_status_history) so the timeline can show timestamps;
a single status column cannot do that.

&lt;span class="gu"&gt;## Data lifecycle
&lt;/span&gt;Soft delete uses deleted_at plus an EF Core global query filter.
A background service (IHostedService) hard-deletes rows older than
30 days once a day.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: The implementation plan
&lt;/h2&gt;

&lt;p&gt;Now you ask for a phased build plan. The most important line in this prompt is the one about what Claude Code should do when it hits something the documents don't cover. It should stop and ask you.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Using all five documents, create a sequenced, step-by-step
implementation plan that Claude Code will follow to build the app
from initialization to deployment, in phases.

Rules:
- If Claude Code reaches a decision not covered by the documents,
  it must stop and ask me. It must never guess.
- Include environment setup (.NET SDK, Node version, local
  PostgreSQL, secrets handling, local run and test commands).
- Each phase ends with a testable checkpoint.
- Backend and frontend work for the same feature belong in the
  same phase.

Deliverables: implementation-plan.md and CLAUDE.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; is the file Claude Code reads automatically at the start of every session. Keep it short and have it point at everything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;# CLAUDE.md
&lt;/span&gt;Read before doing anything: docs/prd.md, docs/screen-flow.md,
docs/tech-stack.md, docs/design-system.md, docs/backend-design.md,
docs/implementation-plan.md

&lt;span class="gu"&gt;## Rules
&lt;/span&gt;&lt;span class="k"&gt;- &lt;/span&gt;Follow implementation-plan.md phase by phase.
&lt;span class="k"&gt;- &lt;/span&gt;Use only the exact versions in tech-stack.md.
&lt;span class="k"&gt;- &lt;/span&gt;Use only tokens and components from design-system.md.
&lt;span class="k"&gt;- &lt;/span&gt;All authorization is enforced in the API, never only in Angular.
&lt;span class="k"&gt;- &lt;/span&gt;If anything is not covered by the docs: STOP and ask me.
&lt;span class="k"&gt;- &lt;/span&gt;Never commit secrets. Use dotnet user-secrets or environment
  variables. appsettings.Development.json is git-ignored.

&lt;span class="gu"&gt;## Layout
&lt;/span&gt;/api (ASP.NET Core)   /web (Angular)   /docs

&lt;span class="gu"&gt;## Phases
&lt;/span&gt;1. Scaffold + tooling   2. Auth + roles   3. Product catalogue
4. Cart + checkout     5. Orders + tracking   6. Polish + deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 7: The part you have to do yourself
&lt;/h2&gt;

&lt;p&gt;Claude can't set up your machine or your accounts. Before building:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install the .NET SDK, Node.js and PostgreSQL, and create an empty database for development&lt;/li&gt;
&lt;li&gt;Put the database connection string and a long random JWT signing key into &lt;code&gt;dotnet user-secrets&lt;/code&gt; (or environment variables), not into source files&lt;/li&gt;
&lt;li&gt;Make sure &lt;code&gt;appsettings.Development.json&lt;/code&gt; and any &lt;code&gt;.env&lt;/code&gt; files are in &lt;code&gt;.gitignore&lt;/code&gt; before your first commit&lt;/li&gt;
&lt;li&gt;Never paste secrets into a chat&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A word of caution: if you ever give Claude Code access to a production database, keep an eye on what it runs. For anything real, use a separate development database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Build it
&lt;/h2&gt;

&lt;p&gt;Open the project in Claude Code and tell it your real-world constraints. Constraints are what keep an MVP small. Mine were a time limit and keeping login as simple as possible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Implement the plan in CLAUDE.md, with these adjustments:
1. Use email and password login with ASP.NET Core Identity and JWT
   (no social or phone login).
2. Build the minimum version of each phase so we can finish in
   about 1 to 1.5 hours.
3. Read the database connection string and JWT key from user-secrets
   or environment variables. Use my local PostgreSQL, no Docker.
4. Work phase by phase. After each phase, tell me what to test and
   which commands to run (dotnet run, ng serve).
5. If a decision is not covered by the documents, stop and ask me.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pay close attention to the first phase. If Claude Code follows your documents there, it will keep following them. If it drifts, fix the document and not just the code, otherwise the same mistake comes back in the next phase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9: Get a second opinion
&lt;/h2&gt;

&lt;p&gt;The agent that wrote the code is a bad judge of it. So I ask Claude Code to spin up separate subagents, each with one narrow job and a fresh context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Spawn four subagents to review the app independently:
1. UI/UX auditor: compare every Angular page to design-system.md
   and screen-flow.md (tokens, states, accessibility, responsive).
2. Code review auditor: structure of both the API and the Angular
   app, duplication, error handling, dead code, type safety,
   async/await and RxJS subscription leaks.
3. SQA: functional gaps against prd.md acceptance criteria, missing
   unit and end-to-end tests, edge cases.
4. Security reviewer: authorization policy gaps, insecure direct
   object access (can user A read user B's order?), JWT settings,
   CORS, input validation, exposed secrets, SQL injection and XSS.

Each agent reports findings by severity (critical, high, medium,
low) with file paths. Then give me one consolidated fix plan
ordered by priority. Do not change code yet.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the plan before anything gets changed. For an MVP, fixing the critical and high items is usually enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 10: Test it and ship it
&lt;/h2&gt;

&lt;p&gt;First, a UAT (user acceptance testing) plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write a User Acceptance Test (UAT) plan for this app. For each
role (Customer, Store Owner), list test cases with: ID, scenario,
preconditions, steps, expected result, and a pass/fail column.
Cover the happy path, validation errors, empty states, permission
checks and cancelling an order. Base it on prd.md and
screen-flow.md.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then actually run it. Do it on your phone as well as your laptop, because mobile is where layout bugs hide.&lt;/p&gt;

&lt;p&gt;Then the deployment guide:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write a step-by-step guide to deploy this Angular + ASP.NET Core +
PostgreSQL app to Azure (App Service for the API, Static Web Apps
for Angular, Azure Database for PostgreSQL) and to AWS (for example
Elastic Beanstalk or App Runner for the API, S3 + CloudFront for
Angular, RDS for PostgreSQL). Include: connecting the Git
repository, CI/CD with GitHub Actions, environment variables and
secrets, running EF Core migrations safely, CORS settings, custom
domain and HTTPS, and a post-deploy smoke test checklist.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things people forget every time: updating the CORS settings so the deployed Angular URL can call the API, and running the EF Core migrations against the production database. If everything works locally but the deployed site shows network errors, check those first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell my past self
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Let the AI interview you. Approving defaults is fast, and it surfaces decisions you didn't know you were making.&lt;/li&gt;
&lt;li&gt;Keep one topic per document. A PRD isn't a schema, and a schema isn't a design system.&lt;/li&gt;
&lt;li&gt;Pin versions and write down non-goals. Both shrink what the AI can guess at.&lt;/li&gt;
&lt;li&gt;Tell it when to stop and ask. Silent guesses cause most of the rework.&lt;/li&gt;
&lt;li&gt;Enforce authorization in the API, not just the UI. Ask a reviewer agent to try to break it.&lt;/li&gt;
&lt;li&gt;When something's wrong, fix the document too, not only the code.&lt;/li&gt;
&lt;li&gt;Keep the scope small. Cut features before you cut quality.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;AI-assisted development isn't really about typing less. It's about making your decisions early, in writing, so the build stops being a guessing game. Take the prompts above, swap QuickCart for your own idea, and read each document before you move to the next.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>dotnet</category>
      <category>angular</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A Practical Guide to CLAUDE.md, Permissions, Plan Mode, Skills, and Sub-Agents</title>
      <dc:creator>Naimul Karim</dc:creator>
      <pubDate>Thu, 05 Nov 2026 02:16:08 +0000</pubDate>
      <link>https://dev.to/naimulkarim/a-practical-guide-to-claudemd-permissions-plan-mode-skills-and-sub-agents-463p</link>
      <guid>https://dev.to/naimulkarim/a-practical-guide-to-claudemd-permissions-plan-mode-skills-and-sub-agents-463p</guid>
      <description>&lt;p&gt;If your Claude Code routine is "type a feature request, hit enter, hope for the best," you're leaving most of the tool on the table. Typing &lt;code&gt;build this feature&lt;/code&gt; works, but it's the shallowest way to use an agent that can read your repo, run your tests, and check its own work.&lt;/p&gt;

&lt;p&gt;The difference between "I use AI to code" and "I work well with an AI agent" comes down to a handful of features: &lt;strong&gt;CLAUDE.md, permissions, plan mode, skills, and sub-agents.&lt;/strong&gt; The trick is not to switch them all on at once. Each one earns its place when a real problem shows up. This post walks through them in the order you'd naturally need them.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, what makes Claude Code different from chatting with Claude?
&lt;/h2&gt;

&lt;p&gt;In a normal chat, you paste code in and paste answers out. Claude Code lives inside your project. It can read files, search the codebase, edit code, run terminal commands, use git, and run your tests to check whether its own change worked.&lt;/p&gt;

&lt;p&gt;So instead of copying a service class into a chat window and explaining the surrounding system, you can say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Invoices can be marked as paid twice. Find out why and fix it."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and it will go hunting through the invoice and payment code on its own, change what needs changing, and run the tests.&lt;/p&gt;

&lt;p&gt;Under the hood it follows a simple loop: &lt;strong&gt;gather context → take action → verify&lt;/strong&gt;. Watch the tool calls scroll by (Read, Grep, Glob, Edit, Bash) and you'll know exactly what it's investigating. And if it's heading the wrong way, hit &lt;code&gt;Esc&lt;/code&gt; and correct it mid-flight rather than waiting for the whole thing to finish.&lt;/p&gt;

&lt;p&gt;A quick privacy note: "runs locally" doesn't mean nothing ever leaves your machine. Relevant file contents still get sent to the model so it can reason about them. If it's your employer's code, check the data policy of the account you're using.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Give it a memory with CLAUDE.md (or AGENTS.md)
&lt;/h2&gt;

&lt;p&gt;Close your session, come back tomorrow, and Claude has forgotten how you run the tests, that you chose Entity Framework Core, and that you already corrected it twice about the same thing. The fix is a markdown file at the root of your project that gets loaded at the start of every session.&lt;/p&gt;

&lt;p&gt;Claude Code reads &lt;code&gt;CLAUDE.md&lt;/code&gt;, and it now falls back to &lt;code&gt;AGENTS.md&lt;/code&gt; if there isn't one, which is nice because that's the convention many other coding tools already use.&lt;/p&gt;

&lt;p&gt;A few things worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's an attention budget.&lt;/strong&gt; Everything in that file competes for space in the context window. Keep it lean. Under 200 lines is the usual advice, and 30 to 40 is even better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be specific, not inspirational.&lt;/strong&gt; "Write clean code" tells Claude nothing. "Controllers live in &lt;code&gt;src/Api/Controllers&lt;/code&gt;" and "run &lt;code&gt;dotnet test&lt;/code&gt; before finishing" do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only include what Claude can't discover itself.&lt;/strong&gt; It can see you use EF Core from the &lt;code&gt;.csproj&lt;/code&gt;. It can't know that money is always stored as integer cents, or that every endpoint needs an authorization policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There are layers.&lt;/strong&gt; Personal rules in your home &lt;code&gt;.claude&lt;/code&gt; folder, shared project rules in the repo root, and a local file for your own sandbox URLs that stays out of git. Big monorepos can have nested files that only load for the area you're working in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start with &lt;code&gt;/init&lt;/code&gt;&lt;/strong&gt;, then actually read what it generates and trim it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep it honest.&lt;/strong&gt; A stale file that points to folders you moved is worse than no file at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more concept that's easy to underestimate: &lt;strong&gt;context rot.&lt;/strong&gt; Output quality can start slipping long before the window is full. A model with a 200k window might already show degradation around 50k tokens. So when you move to something unrelated, run &lt;code&gt;/clear&lt;/code&gt;. Use &lt;code&gt;/compact&lt;/code&gt; only when you're continuing the same work but want a smaller footprint. And &lt;code&gt;/context&lt;/code&gt; shows you where your space is going.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Control what it can do with permissions
&lt;/h2&gt;

&lt;p&gt;Here's the catch with CLAUDE.md: it's &lt;em&gt;guidance&lt;/em&gt;. If you write "never read the production settings file," Claude will probably obey, but nothing technically stops it. For hard boundaries you need permissions, which the tool itself enforces.&lt;/p&gt;

&lt;p&gt;There are three rule types, checked in the order &lt;strong&gt;deny → ask → allow&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Bash(dotnet build)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(dotnet test)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git status)"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ask"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Bash(git push:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(dotnet ef database update:*)"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Read(./**/appsettings.Production.json)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read(./**/secrets.json)"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save that as &lt;code&gt;.claude/settings.json&lt;/code&gt; and you've got a sensible baseline: routine builds and tests run without nagging you, pushes and database updates always need your approval, and production config is off limits.&lt;/p&gt;

&lt;p&gt;Then &lt;strong&gt;test it.&lt;/strong&gt; Drop a dummy &lt;code&gt;appsettings.Production.json&lt;/code&gt; into the project, ask Claude to read it, and confirm the request gets refused. Claude should tell you the read was blocked instead of hunting for a workaround. Don't assume a rule works just because you wrote it.&lt;/p&gt;

&lt;p&gt;You'll also meet different modes along the way (manual, auto-edit, plan, auto), which you can cycle through with &lt;code&gt;Shift+Tab&lt;/code&gt;. Manual is great while you're learning what Claude does. Once you trust the setup, a more automatic mode removes the boring prompts. The goal isn't zero prompts. It's zero &lt;em&gt;pointless&lt;/em&gt; prompts, while keeping a human in the loop for things that matter.&lt;/p&gt;

&lt;p&gt;One habit worth building: put real connection strings into user secrets or environment variables yourself. Never paste them into the chat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Use plan mode when the decision is expensive
&lt;/h2&gt;

&lt;p&gt;Early features like a new endpoint or a UI tweak are easy to review and easy to undo. If you don't like the result, you change it. A database schema is a different animal. Migrations, relationships, and how something like invoice status is represented are costly to rework once code is built on top of them.&lt;/p&gt;

&lt;p&gt;That's what plan mode is for. Claude can read, search and research, but it can't touch your source until you approve a plan. Imagine asking for a PostgreSQL + Entity Framework Core setup with only customers, invoices and line items, and explicitly ruling out recurring billing, tax rules and multi-currency for now. Claude would spin up an exploring sub-agent, ask a few clarifying questions (should payment status be stored or derived from payments? where do line items live?), and come back with a plan.&lt;/p&gt;

&lt;p&gt;The lesson that matters most: &lt;strong&gt;read the plan.&lt;/strong&gt; Approving without reading gains you nothing. A weak plan says "add database, add invoices, update API." A good one names tables, relationships, files to change, the migration approach, and how it'll verify the result. You can even open it in an editor with &lt;code&gt;Ctrl+G&lt;/code&gt; and edit it directly.&lt;/p&gt;

&lt;p&gt;Catching "you're over-engineering this" in the plan takes seconds. Catching it after the entities, migrations and API changes exist takes a lot longer.&lt;/p&gt;

&lt;p&gt;Two other pointers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't use plan mode for everything.&lt;/strong&gt; Changing a heading doesn't need a ceremony.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After a big decision, update CLAUDE.md&lt;/strong&gt; with only the conventions future sessions need to remember, such as the migration command and where entity configurations live.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: Turn repeated processes into skills
&lt;/h2&gt;

&lt;p&gt;By the time you've added a handful of API endpoints, you notice you keep saying the same things: look at how existing controllers are structured, reuse the validation and error-handling patterns, write tests alongside, verify when done. Pasting that checklist into every prompt gets old.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;skill&lt;/strong&gt; is a folder with a &lt;code&gt;SKILL.md&lt;/code&gt; that Claude loads only when relevant. The rule of thumb is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Project fact&lt;/strong&gt; ("run tests with &lt;code&gt;dotnet test&lt;/code&gt;", "use Entity Framework Core for data access") → &lt;code&gt;CLAUDE.md&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeatable process&lt;/strong&gt; ("inspect, reuse, build, verify") → a skill&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-off task&lt;/strong&gt; → just ask
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;add-endpoint&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Add or modify a REST API endpoint following the project's controller, validation, error-handling and testing conventions. Use when creating a new endpoint or significantly changing an existing one.&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

Add or update the following endpoint: $ARGUMENTS
&lt;span class="p"&gt;
1.&lt;/span&gt; Inspect an existing, similar endpoint and its tests first.
&lt;span class="p"&gt;2.&lt;/span&gt; Reuse the existing request/response models, validators and error format.
&lt;span class="p"&gt;3.&lt;/span&gt; Keep business logic in the service layer, not the controller.
&lt;span class="p"&gt;4.&lt;/span&gt; Add unit and integration tests, run &lt;span class="sb"&gt;`dotnet test`&lt;/span&gt;, then summarize.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;description is the most important line.&lt;/strong&gt; Claude uses it to decide whether the skill applies. A vague one means it won't trigger when you expect it to.&lt;/p&gt;

&lt;p&gt;After that, something like &lt;code&gt;/add-endpoint GET /invoices/{id}/pdf&lt;/code&gt; gives you a result that matches your existing code, and &lt;code&gt;/add-endpoint POST /customers&lt;/code&gt; follows right after with the same process. Same workflow, different feature.&lt;/p&gt;

&lt;p&gt;A few more skill tips:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Skills load in layers (metadata first, then the main file, then extra reference files only when needed), which keeps your context light.&lt;/li&gt;
&lt;li&gt;For anything with side effects, like deploy or commit, you can disable automatic invocation so only you can trigger it.&lt;/li&gt;
&lt;li&gt;Don't hoard them. Every skill adds metadata Claude has to consider.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read third-party skills and plugins before installing.&lt;/strong&gt; They can contain scripts and tool access.&lt;/li&gt;
&lt;li&gt;Don't try to write the perfect skill on day one. Use it, notice where Claude still slips, then sharpen it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Library maintainers and vendors can ship their own skills and plugins too. A skill is one reusable process, while a plugin is a package that can bundle skills plus other capabilities such as MCP servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Hand off investigations to sub-agents
&lt;/h2&gt;

&lt;p&gt;Eventually the codebase is big enough that, before building something like an audit log, Claude would need to understand your entities, how the current user flows through a request, background jobs, error handling and the test setup. If it researches all of that in your main conversation, every file read and search result piles up in the same context you need for the actual build.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;sub-agent&lt;/strong&gt; is a separate Claude instance with its own context window. It takes a focused task, explores, and reports back only the useful findings. That's the entire value: &lt;strong&gt;context isolation.&lt;/strong&gt; It's not a smarter Claude, and it doesn't gain expertise from a fancy job title. "Senior staff elite security architect" is just decoration. The useful question is: &lt;em&gt;what work can happen separately that my main conversation doesn't need to carry?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For the audit-log idea, you could ask for five independent investigations, with an explicit "don't implement anything yet":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The entity model and &lt;code&gt;DbContext&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;How authentication and the current user identity are resolved&lt;/li&gt;
&lt;li&gt;Existing background jobs&lt;/li&gt;
&lt;li&gt;The logging and error-handling middleware&lt;/li&gt;
&lt;li&gt;The test fixtures and conventions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these depend on each other, so they can run in parallel and come back as one combined summary. That's usually faster than a single agent working through them one after another, and your main context only receives the conclusions.&lt;/p&gt;

&lt;p&gt;Claude Code also has built-in agent types such as Explore (read-only investigation), Plan, and a general-purpose one. You don't have to call anything special. Just tell Claude what to delegate.&lt;/p&gt;

&lt;p&gt;Afterward, scope the first version tightly: record who changed what and when for create, update and delete on invoices, and nothing else yet, so no retention policies, search UI or export. And remember that &lt;strong&gt;identity must come from the server.&lt;/strong&gt; The user ID in an audit entry should be taken from the authenticated principal, never from a value the client sends. That's exactly the sort of thing to verify in the plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Spawn a sub-agent to review the code
&lt;/h2&gt;

&lt;p&gt;Sub-agents aren't only for research. One of the most useful jobs you can give one is reviewing what the main session just wrote.&lt;/p&gt;

&lt;p&gt;Why not just ask the same session to "check your work"? Because it's carrying the whole conversation: every assumption, every shortcut, every reason it chose approach A over B. It tends to agree with its own decisions. A fresh sub-agent starts with a clean context and sees the diff the way a new teammate would, with no attachment to how it got there.&lt;/p&gt;

&lt;p&gt;You can do it with a plain prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Spawn a sub-agent to review the changes on this branch against &lt;code&gt;main&lt;/code&gt;. It should check correctness and edge cases, security (authorization, input validation, secrets in logs), missing tests, and whether the code follows the conventions in CLAUDE.md. It must not modify any files. Return findings grouped by severity, with file and line references."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you do this often, define a reusable reviewer in &lt;code&gt;.claude/agents/code-reviewer.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;code-reviewer&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Reviews code changes for bugs, security issues, missing tests and convention violations. Use after finishing a feature or before opening a pull request.&lt;/span&gt;
&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Read, Grep, Glob, Bash&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

You are a careful code reviewer. Review the current branch's diff against main.

Focus on:
&lt;span class="p"&gt;-&lt;/span&gt; Logic errors and unhandled edge cases
&lt;span class="p"&gt;-&lt;/span&gt; Authorization gaps and unvalidated input
&lt;span class="p"&gt;-&lt;/span&gt; N+1 queries and missing transactions in data access
&lt;span class="p"&gt;-&lt;/span&gt; Missing or weak tests
&lt;span class="p"&gt;-&lt;/span&gt; Violations of the conventions in CLAUDE.md

Do not edit files. Report findings by severity (high, medium, low) with file and line,
a one-sentence explanation of the risk, and a suggested fix.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;tools&lt;/code&gt; line. A reviewer shouldn't be able to edit anything, so you leave out Edit and Write. That's a small guardrail that keeps the agent doing exactly one job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Triage before you fix
&lt;/h3&gt;

&lt;p&gt;The findings come back, and the temptation is to say "fix everything." Don't. For each one, decide whether it's real, whether it's relevant, and whether it's worth fixing now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verify first.&lt;/strong&gt; Ask Claude to confirm the finding against your actual code and explain the risk in the context of this app, without changing anything. Reviewers can be wrong, or flag a problem that doesn't apply to your setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix now&lt;/strong&gt; what is real and cheap, especially anything touching auth, data access or money.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write down for later&lt;/strong&gt; what is real but out of scope. A low-severity finding whose proper fix needs a whole new subsystem shouldn't hijack the current task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never silently ignore&lt;/strong&gt; a finding. "Not now" is fine. "Never looked at it" isn't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A reviewer sub-agent doesn't replace your own review, and it isn't a substitute for dedicated security tooling on sensitive areas like authentication or payments. Think of it as a second pair of eyes that happens to be cheap and fast. You're still the one who decides.&lt;/p&gt;

&lt;h2&gt;
  
  
  The progression that matters
&lt;/h2&gt;

&lt;p&gt;Looking back, the arc is worth remembering:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start in an empty folder and give Claude a small, bounded task.&lt;/li&gt;
&lt;li&gt;Add project context (&lt;code&gt;CLAUDE.md&lt;/code&gt;) once you're tired of repeating yourself.&lt;/li&gt;
&lt;li&gt;Add permissions when approval prompts get annoying or secrets need protecting.&lt;/li&gt;
&lt;li&gt;Use plan mode when decisions become expensive to reverse.&lt;/li&gt;
&lt;li&gt;Write skills when you spot a repeating process.&lt;/li&gt;
&lt;li&gt;Use sub-agents when investigation starts crowding your main context.&lt;/li&gt;
&lt;li&gt;Use a separate reviewer sub-agent before you trust what was just written.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Don't switch everything on at once.&lt;/strong&gt; Let the problem you have create the reason to use the feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key take aways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Give Claude good context, keep tasks small, and review what it does.&lt;/li&gt;
&lt;li&gt;Test your guardrails instead of assuming they work.&lt;/li&gt;
&lt;li&gt;The cheapest place to fix a bad decision is the plan.&lt;/li&gt;
&lt;li&gt;Have a fresh sub-agent review what the main session wrote, then triage the findings instead of auto-fixing them.&lt;/li&gt;
&lt;li&gt;Keep your project instructions current, and delete stale ones.&lt;/li&gt;
&lt;li&gt;Give it more freedom gradually, as you learn how it behaves on &lt;em&gt;your&lt;/em&gt; projects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Beyond these sit parallel agents on isolated git worktrees, external tools through MCP, and hooks that automate checks. Those are next on my list.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building Modern Scalable Applications: Understanding Docker, Kubernetes, Redis, Kafka &amp; Prometheus</title>
      <dc:creator>Naimul Karim</dc:creator>
      <pubDate>Thu, 05 Nov 2026 01:57:35 +0000</pubDate>
      <link>https://dev.to/naimulkarim/building-modern-scalable-applications-understanding-docker-kubernetes-redis-kafka-prometheus-1p0o</link>
      <guid>https://dev.to/naimulkarim/building-modern-scalable-applications-understanding-docker-kubernetes-redis-kafka-prometheus-1p0o</guid>
      <description>&lt;p&gt;A customer opens an online store, browses a product, and clicks &lt;strong&gt;Place Order&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;From the customer's perspective, it is a simple interaction. Behind the scenes, however, several components may work together: containers run the application, orchestration platforms manage those containers, databases store information, caches improve response times, queues handle background work, and monitoring tools help engineers understand what is happening in production.&lt;/p&gt;

&lt;p&gt;The interesting part is not memorizing the names of these technologies. It is understanding &lt;strong&gt;which problem each one solves, how they connect, and what trade-offs they introduce&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Let's follow one order through a typical online application and explore the technologies along the way.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The architecture in this article is an illustrative example, not a required stack. A real application should use only the components that solve problems it actually has.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. Docker: Package the application consistently
&lt;/h2&gt;

&lt;p&gt;A familiar problem for developers is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It works on my machine, but it doesn't work in production.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An application may run with one runtime version on a developer's laptop, while the production server has another. A dependency may be missing, or a library version may be different.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.docker.com/" rel="noopener noreferrer"&gt;Docker&lt;/a&gt; helps reduce this environment mismatch by packaging an application and its required runtime dependencies into an image.&lt;/p&gt;

&lt;p&gt;A useful way to think about the two main terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Image:&lt;/strong&gt; the packaged template.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container:&lt;/strong&gt; a running instance created from that image.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same image can be used in development, testing, staging, and production, helping those environments behave more consistently.&lt;/p&gt;

&lt;p&gt;There is an important qualification: a container image does not make every machine identical. CPU architecture and operating-system compatibility still matter. For example, an image built for one processor architecture may not run natively on a machine with another architecture. Multi-platform image builds can address this when needed.&lt;/p&gt;

&lt;p&gt;Docker solves the packaging problem. It does not, by itself, manage a fleet of containers.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. ECS and Kubernetes: Run and manage containers
&lt;/h2&gt;

&lt;p&gt;Once an application is containerized, we need a way to run it, scale it, recover from failures, and manage its deployment.&lt;/p&gt;

&lt;p&gt;Two common options are &lt;a href="https://aws.amazon.com/ecs/" rel="noopener noreferrer"&gt;Amazon ECS&lt;/a&gt; and &lt;a href="https://kubernetes.io/" rel="noopener noreferrer"&gt;Kubernetes&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Amazon ECS
&lt;/h3&gt;

&lt;p&gt;If an organization is already using AWS, Amazon Elastic Container Service (ECS) can be a practical choice. It is an AWS-managed container orchestration service, and AWS handles much of the underlying orchestration infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kubernetes
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://kubernetes.io/" rel="noopener noreferrer"&gt;Kubernetes&lt;/a&gt; is an open-source container orchestration platform. It can run across different cloud providers and in on-premises environments, offering flexibility and control.&lt;/p&gt;

&lt;p&gt;That flexibility comes with operational responsibility. A managed Kubernetes service such as Amazon EKS or Google Kubernetes Engine manages important parts of the control plane, but teams still need to understand and manage areas such as worker nodes, networking, add-ons, workloads, and cluster configuration.&lt;/p&gt;

&lt;p&gt;So which one is better?&lt;/p&gt;

&lt;p&gt;Neither is universally better. The right choice depends on the team's needs and operational capacity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A smaller team operating primarily on AWS may prefer ECS for its simpler operational model.&lt;/li&gt;
&lt;li&gt;A team that needs Kubernetes portability, its ecosystem, or more control over orchestration may choose Kubernetes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key question is not which tool is more popular. It is how much complexity the team is prepared to operate.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Terraform: Define infrastructure as code
&lt;/h2&gt;

&lt;p&gt;Containers need infrastructure: compute resources, networks, access rules, databases, and other cloud resources.&lt;/p&gt;

&lt;p&gt;One approach is to create these resources manually through a cloud console. The problem appears when the same setup must be repeated for development, staging, and production.&lt;/p&gt;

&lt;p&gt;Over time, environments can become different. Someone changes a security rule in staging but forgets to make the corresponding change in production. A bug then appears in production but cannot be reproduced in staging.&lt;/p&gt;

&lt;p&gt;This is known as &lt;strong&gt;configuration drift&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.hashicorp.com/terraform" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt; addresses this by allowing infrastructure to be described as code.&lt;/p&gt;

&lt;p&gt;Instead of relying on a sequence of manual console operations, teams define resources in configuration files and keep those files in version control.&lt;/p&gt;

&lt;p&gt;That brings several benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Infrastructure changes can be reviewed through pull requests.&lt;/li&gt;
&lt;li&gt;Environments can be created more consistently.&lt;/li&gt;
&lt;li&gt;Git history helps explain what changed and when.&lt;/li&gt;
&lt;li&gt;Rebuilding an environment becomes more repeatable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Infrastructure as code does not eliminate the need for careful review. A reviewed configuration can still contain a mistake. It does, however, make infrastructure changes more visible, repeatable, and manageable.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. DNS and load balancing: Route incoming requests
&lt;/h2&gt;

&lt;p&gt;Now imagine the application is deployed and a customer opens &lt;code&gt;shop.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The browser needs to find the server responsible for that domain. This is where &lt;strong&gt;DNS (Domain Name System)&lt;/strong&gt; comes in: it resolves a domain name to the information needed to reach the service, commonly an IP address.&lt;/p&gt;

&lt;p&gt;The request then reaches a &lt;strong&gt;load balancer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A production application may run across multiple backend instances rather than on one server. The load balancer distributes incoming requests among those instances.&lt;/p&gt;

&lt;p&gt;It can also perform health checks. If an instance stops responding or fails its health check, the load balancer can stop sending new traffic to it and route requests to healthy instances instead.&lt;/p&gt;

&lt;p&gt;This helps improve availability and prevents one server from becoming the only point handling all incoming traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. PostgreSQL: Store the source-of-truth data
&lt;/h2&gt;

&lt;p&gt;The backend receives the order request, validates it, and saves the order.&lt;/p&gt;

&lt;p&gt;A relational database such as &lt;a href="https://www.postgresql.org/" rel="noopener noreferrer"&gt;PostgreSQL&lt;/a&gt; can store the application's core business data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Customers&lt;/li&gt;
&lt;li&gt;Products&lt;/li&gt;
&lt;li&gt;Orders&lt;/li&gt;
&lt;li&gt;Payments and order status&lt;/li&gt;
&lt;li&gt;Other related records&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For many applications, the relational database is the authoritative source of truth.&lt;/p&gt;

&lt;p&gt;But as traffic grows, repeatedly running the same database queries can become expensive. A popular product page, for example, might be requested thousands of times.&lt;/p&gt;

&lt;p&gt;That leads to the next component.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Redis: Reduce repeated database work with caching
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://redis.io/" rel="noopener noreferrer"&gt;Redis&lt;/a&gt; is commonly used as an in-memory data store and cache.&lt;/p&gt;

&lt;p&gt;Suppose many customers open the same product page. If every request reads the same product information from PostgreSQL, the database has to repeat work that may not need to be repeated.&lt;/p&gt;

&lt;p&gt;A cache can help:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The application checks Redis for the product data.&lt;/li&gt;
&lt;li&gt;If the data is present, the application returns it from the cache.&lt;/li&gt;
&lt;li&gt;If it is missing, the application reads it from PostgreSQL.&lt;/li&gt;
&lt;li&gt;The application can then place a copy in Redis for later requests.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a common cache-aside pattern.&lt;/p&gt;

&lt;p&gt;Redis does not automatically replace the main database. PostgreSQL can remain the source of truth while Redis stores temporary or reusable copies.&lt;/p&gt;

&lt;h3&gt;
  
  
  The challenge: cache invalidation
&lt;/h3&gt;

&lt;p&gt;What happens if a product's price changes in PostgreSQL while Redis still contains the old price?&lt;/p&gt;

&lt;p&gt;The application could return stale information.&lt;/p&gt;

&lt;p&gt;The cache therefore needs an invalidation or update strategy. Depending on the use case, that might involve deleting the cached entry, updating it after a database change, or allowing it to expire after a defined time.&lt;/p&gt;

&lt;p&gt;Caching can improve performance, but it introduces a consistency problem that needs to be designed deliberately.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. OpenSearch: Handle more advanced search requirements
&lt;/h2&gt;

&lt;p&gt;A customer searches for wireless headphones. They might make a typo, use a slightly different phrase, or expect the most relevant products to appear first.&lt;/p&gt;

&lt;p&gt;A relational database can support text search, but more advanced requirements—such as relevance ranking, typo tolerance, and large-scale search—may call for a dedicated search engine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://opensearch.org/" rel="noopener noreferrer"&gt;OpenSearch&lt;/a&gt; is designed for search and analytics use cases.&lt;/p&gt;

&lt;p&gt;An application can index its product catalog in OpenSearch and use it to handle product-search queries. PostgreSQL can still remain the authoritative source of product data.&lt;/p&gt;

&lt;p&gt;This distinction matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL stores the core business records.&lt;/li&gt;
&lt;li&gt;OpenSearch provides search capabilities over indexed data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The search index is typically a derived representation of the source data, so the application needs a process to keep it reasonably up to date.&lt;/p&gt;

&lt;p&gt;A dedicated search engine is useful when the product's search requirements justify the extra infrastructure. It is not something every application needs from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Amazon S3: Store files outside the relational database
&lt;/h2&gt;

&lt;p&gt;Product images and profile photos are files, not ordinary relational records.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://aws.amazon.com/s3/" rel="noopener noreferrer"&gt;Amazon S3&lt;/a&gt; is an object storage service designed to store files and other unstructured data.&lt;/p&gt;

&lt;p&gt;A typical flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The application uploads an image to S3.&lt;/li&gt;
&lt;li&gt;S3 stores the object.&lt;/li&gt;
&lt;li&gt;The application stores the object's key or reference in PostgreSQL.&lt;/li&gt;
&lt;li&gt;When the image is needed, the application or browser retrieves it from S3, subject to the configured access controls.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This separates responsibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL stores business data and file references.&lt;/li&gt;
&lt;li&gt;S3 stores the actual file objects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The file reference should not be treated as automatically public. Access can be controlled using bucket policies, application authorization, or time-limited pre-signed URLs, depending on the design.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Message queues: Move slow work out of the request
&lt;/h2&gt;

&lt;p&gt;Saving an order may trigger several additional tasks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Send an order confirmation email.&lt;/li&gt;
&lt;li&gt;Generate an invoice.&lt;/li&gt;
&lt;li&gt;Notify the warehouse.&lt;/li&gt;
&lt;li&gt;Record an analytics event.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Should the customer wait for every task to finish before receiving a response?&lt;/p&gt;

&lt;p&gt;Usually, that would make the request slower than necessary.&lt;/p&gt;

&lt;p&gt;A message queue allows the backend to place work onto a queue and return a response without waiting for every background task to complete. Background workers consume the messages and perform the work independently.&lt;/p&gt;

&lt;p&gt;Common options include &lt;a href="https://aws.amazon.com/sqs/" rel="noopener noreferrer"&gt;Amazon SQS&lt;/a&gt; and &lt;a href="https://www.rabbitmq.com/" rel="noopener noreferrer"&gt;RabbitMQ&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Acknowledgements and retries
&lt;/h3&gt;

&lt;p&gt;A worker generally acknowledges a message after successfully processing it. If the worker crashes before acknowledging it, the queue may make the message available for processing again.&lt;/p&gt;

&lt;p&gt;That means a message can sometimes be delivered more than once.&lt;/p&gt;

&lt;p&gt;Workers should therefore be designed to handle duplicate delivery safely. This is called &lt;strong&gt;idempotency&lt;/strong&gt;: processing the same operation more than once should not produce an unintended duplicate effect.&lt;/p&gt;

&lt;p&gt;For example, an email worker could record that a confirmation has already been sent for a particular order and avoid sending it again when the same message is retried.&lt;/p&gt;

&lt;p&gt;Queues improve responsiveness and decouple background work from the main request, but they introduce concerns such as retries, duplicate messages, failure handling, and monitoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Kafka: Distribute events to multiple consumers
&lt;/h2&gt;

&lt;p&gt;A queue is useful when an application needs to hand off tasks to background workers. But sometimes an event needs to be made available to several independent systems.&lt;/p&gt;

&lt;p&gt;For example, when an order is placed, different consumers may need to react:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An analytics service records the purchase.&lt;/li&gt;
&lt;li&gt;An inventory service updates stock.&lt;/li&gt;
&lt;li&gt;A notification service sends a message.&lt;/li&gt;
&lt;li&gt;A reporting pipeline processes the event.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://kafka.apache.org/" rel="noopener noreferrer"&gt;Apache Kafka&lt;/a&gt; is a distributed event-streaming platform that can support this kind of event-driven architecture.&lt;/p&gt;

&lt;p&gt;Rather than thinking of every event as a one-time task handed to a single worker, Kafka lets producers publish events to topics. Multiple consumer applications can read those events independently, according to their own processing needs.&lt;/p&gt;

&lt;p&gt;Kafka is especially useful when event volume, independent consumers, replay, or durable event streams are important requirements.&lt;/p&gt;

&lt;p&gt;It is not automatically a replacement for every queue. A simpler queue may be easier to operate when the requirement is simply to distribute background tasks. Choose based on the messaging problem, not just the technology's popularity.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. CI/CD and Amazon ECR: Deliver code changes reliably
&lt;/h2&gt;

&lt;p&gt;The application is running, but developers continue to fix bugs and add features. How does a code change reach production?&lt;/p&gt;

&lt;p&gt;With a manual deployment process, someone might connect to each server, pull the latest code, and restart the application. If one server is missed, different instances may run different versions.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;CI/CD pipeline&lt;/strong&gt; automates much of this delivery process.&lt;/p&gt;

&lt;p&gt;A typical container-based pipeline might:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start when a developer pushes code.&lt;/li&gt;
&lt;li&gt;Build the application and run automated tests.&lt;/li&gt;
&lt;li&gt;Build a Docker image if the required checks pass.&lt;/li&gt;
&lt;li&gt;Push the image to a container registry.&lt;/li&gt;
&lt;li&gt;Deploy the selected image version to the target environment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On AWS, &lt;a href="https://aws.amazon.com/ecr/" rel="noopener noreferrer"&gt;Amazon Elastic Container Registry (ECR)&lt;/a&gt; can store Docker and other OCI container images.&lt;/p&gt;

&lt;p&gt;Think of ECR as a registry for container images: it stores the packaged application versions that deployment systems can pull.&lt;/p&gt;

&lt;p&gt;CI/CD makes delivery more repeatable, but the quality of the pipeline still depends on the checks, approvals, and deployment safeguards configured by the team.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Helm: Manage Kubernetes application configuration
&lt;/h2&gt;

&lt;p&gt;When deploying to Kubernetes, teams need configuration describing how an application should run.&lt;/p&gt;

&lt;p&gt;Development, staging, and production may share most of the same deployment structure while differing in image tags, replica counts, resource limits, or environment-specific settings.&lt;/p&gt;

&lt;p&gt;Maintaining separate copies of nearly identical configuration files can lead to drift.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://helm.sh/" rel="noopener noreferrer"&gt;Helm&lt;/a&gt; helps manage Kubernetes applications through charts and configurable values.&lt;/p&gt;

&lt;p&gt;A chart defines reusable Kubernetes templates. Different values files can provide environment-specific settings.&lt;/p&gt;

&lt;p&gt;This reduces duplication and makes it easier to keep deployment configuration consistent across environments.&lt;/p&gt;

&lt;p&gt;Helm is a Kubernetes package manager; it does not replace Kubernetes itself. It helps organize and render the configuration that Kubernetes uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  13. Argo CD: Use GitOps to keep deployments aligned
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://argo-cd.readthedocs.io/" rel="noopener noreferrer"&gt;Argo CD&lt;/a&gt; is a declarative GitOps continuous delivery tool for Kubernetes.&lt;/p&gt;

&lt;p&gt;The central idea behind GitOps is that the desired state of the application and cluster configuration is stored in Git.&lt;/p&gt;

&lt;p&gt;Argo CD watches the configured Git repository and compares the desired state there with the state currently running in the Kubernetes cluster.&lt;/p&gt;

&lt;p&gt;If they differ, Argo CD can report the difference and, depending on the configured synchronization policy, apply the desired configuration.&lt;/p&gt;

&lt;p&gt;For example, if someone manually changes a deployment in the cluster, Argo CD can detect that the live state no longer matches the configuration in Git. With automated synchronization and self-healing configured, it can bring the cluster back toward the declared state.&lt;/p&gt;

&lt;p&gt;This gives teams a traceable workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configuration changes are committed to Git.&lt;/li&gt;
&lt;li&gt;Changes can be reviewed.&lt;/li&gt;
&lt;li&gt;The cluster is reconciled against the declared configuration.&lt;/li&gt;
&lt;li&gt;Git history provides a record of changes and supports rollback workflows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitOps does not make every deployment safe automatically. If an incorrect configuration is merged, the system may faithfully deploy that incorrect configuration. Reviews, tests, and rollout safeguards remain important.&lt;/p&gt;

&lt;h2&gt;
  
  
  14. Prometheus and Grafana: Understand application health
&lt;/h2&gt;

&lt;p&gt;The application is live. Then an order endpoint that normally responds in one second starts taking five seconds.&lt;/p&gt;

&lt;p&gt;Where should the team look?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://prometheus.io/" rel="noopener noreferrer"&gt;Prometheus&lt;/a&gt; is a monitoring system that collects and stores metrics. Applications and infrastructure can expose measurements such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request count&lt;/li&gt;
&lt;li&gt;Request latency&lt;/li&gt;
&lt;li&gt;Error rate&lt;/li&gt;
&lt;li&gt;CPU and memory usage&lt;/li&gt;
&lt;li&gt;Queue depth&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prometheus commonly uses a pull-based model: it periodically scrapes metrics from configured targets.&lt;/p&gt;

&lt;p&gt;If a target becomes unavailable, Prometheus can report that it is no longer being scraped successfully. That signal can help teams detect failures.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://grafana.com/" rel="noopener noreferrer"&gt;Grafana&lt;/a&gt; is commonly used to visualize metrics through dashboards. Engineers can inspect trends, identify when a latency spike began, and compare measurements across services.&lt;/p&gt;

&lt;p&gt;Metrics are useful for showing that something is wrong. But a graph showing that an endpoint is slow may not explain which dependency caused the delay.&lt;/p&gt;

&lt;p&gt;That is where tracing becomes useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  15. OpenTelemetry: Instrument and follow requests across services
&lt;/h2&gt;

&lt;p&gt;In a distributed application, one customer request may travel through several components:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The order service receives the request.&lt;/li&gt;
&lt;li&gt;It calls the inventory service.&lt;/li&gt;
&lt;li&gt;The inventory service calls another dependency.&lt;/li&gt;
&lt;li&gt;The order service communicates with a payment service or external API.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the overall request takes five seconds, which step consumed the time?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distributed tracing&lt;/strong&gt; follows a request as it moves across services. A trace is made up of spans, which represent timed operations and their relationships. Looking at those spans helps engineers identify where time was spent.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://opentelemetry.io/" rel="noopener noreferrer"&gt;OpenTelemetry&lt;/a&gt; provides vendor-neutral APIs, SDKs, and instrumentation for generating and collecting telemetry, including traces, metrics, and logs.&lt;/p&gt;

&lt;p&gt;An application needs to be instrumented for useful telemetry to appear. That can involve adding SDK instrumentation to the code or using supported automatic instrumentation.&lt;/p&gt;

&lt;p&gt;The OpenTelemetry Collector can receive, process, and export telemetry to compatible observability backends.&lt;/p&gt;

&lt;p&gt;One important point: installing OpenTelemetry alone does not magically reveal application behavior. The application and its dependencies must produce telemetry, and the collection and export pipeline must be configured.&lt;/p&gt;

&lt;p&gt;Prometheus, Grafana, and OpenTelemetry serve related but different roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prometheus:&lt;/strong&gt; collects and stores metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grafana:&lt;/strong&gt; visualizes metrics and other supported telemetry sources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenTelemetry:&lt;/strong&gt; provides instrumentation and a standard way to generate, collect, and export telemetry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together, these tools help teams move from noticing a performance problem to investigating its cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  16. Bringing the pieces together
&lt;/h2&gt;

&lt;p&gt;Let's revisit the customer's order and group the technologies by the problems they solve.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Technologies&lt;/th&gt;
&lt;th&gt;Main responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Application packaging&lt;/td&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;td&gt;Package the application and its dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container orchestration&lt;/td&gt;
&lt;td&gt;ECS or Kubernetes&lt;/td&gt;
&lt;td&gt;Run and manage containers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure&lt;/td&gt;
&lt;td&gt;Terraform&lt;/td&gt;
&lt;td&gt;Define and provision infrastructure as code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Request routing&lt;/td&gt;
&lt;td&gt;DNS and load balancer&lt;/td&gt;
&lt;td&gt;Resolve the service address and distribute traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary data&lt;/td&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;Store core business records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caching&lt;/td&gt;
&lt;td&gt;Redis&lt;/td&gt;
&lt;td&gt;Reduce repeated reads and improve response times&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search&lt;/td&gt;
&lt;td&gt;OpenSearch&lt;/td&gt;
&lt;td&gt;Support advanced search and relevance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File storage&lt;/td&gt;
&lt;td&gt;Amazon S3&lt;/td&gt;
&lt;td&gt;Store images and other objects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background processing&lt;/td&gt;
&lt;td&gt;SQS or RabbitMQ&lt;/td&gt;
&lt;td&gt;Decouple asynchronous tasks from requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event streaming&lt;/td&gt;
&lt;td&gt;Kafka&lt;/td&gt;
&lt;td&gt;Distribute event streams to independent consumers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delivery&lt;/td&gt;
&lt;td&gt;CI/CD and ECR&lt;/td&gt;
&lt;td&gt;Test, package, store, and deliver application versions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes configuration&lt;/td&gt;
&lt;td&gt;Helm&lt;/td&gt;
&lt;td&gt;Template and manage Kubernetes application configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitOps deployment&lt;/td&gt;
&lt;td&gt;Argo CD&lt;/td&gt;
&lt;td&gt;Reconcile cluster state with configuration in Git&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monitoring and observability&lt;/td&gt;
&lt;td&gt;Prometheus, Grafana, OpenTelemetry&lt;/td&gt;
&lt;td&gt;Collect, visualize, and investigate telemetry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A simplified flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         DEVELOPER WORKFLOW
                                  |
                           Code pushed to Git
                                  |
                              CI/CD
                         Build + test + image
                                  |
                             Amazon ECR
                                  |
                       Helm configuration in Git
                                  |
                               Argo CD
                                  |
                         ECS / Kubernetes
                                  |
                            Docker images
                                  |
CUSTOMER --&amp;gt; DNS --&amp;gt; LOAD BALANCER --&amp;gt; BACKEND APPLICATION
                                      |
                +---------------------+----------------------+
                |                     |                      |
            PostgreSQL              Redis                OpenSearch
          source of truth           cache              product search
                |
          File references
                |
             Amazon S3

       Order created --&amp;gt; Queue --&amp;gt; Background workers
              |
              +------&amp;gt; Kafka events --&amp;gt; Independent consumers

        Prometheus --&amp;gt; Metrics
        Grafana ----&amp;gt; Dashboards
        OpenTelemetry --&amp;gt; Traces, metrics, and logs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a conceptual view. The exact deployment and telemetry architecture will vary by application and cloud environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  17. Every technology solves a problem—and adds responsibility
&lt;/h2&gt;

&lt;p&gt;It is tempting to think that adding more tools automatically makes an application more scalable. In reality, every new component introduces operational and design trade-offs.&lt;/p&gt;

&lt;p&gt;Consider a few examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Redis&lt;/strong&gt; can reduce database load, but cached data can become stale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queues&lt;/strong&gt; keep slow work out of the request path, but messages may be retried or delivered more than once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kafka&lt;/strong&gt; supports durable event streams and multiple consumers, but it adds operational and architectural complexity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes&lt;/strong&gt; provides orchestration flexibility, but teams must manage more of the platform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Argo CD&lt;/strong&gt; can reconcile a cluster to Git, but it can also apply a bad configuration if that configuration is approved and committed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenSearch&lt;/strong&gt; improves search capabilities, but the search index must be maintained alongside the source data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This leads to a more useful way to evaluate technology:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What concrete problem are we trying to solve?&lt;/li&gt;
&lt;li&gt;Is the problem significant enough to justify another component?&lt;/li&gt;
&lt;li&gt;What new failure modes and operational responsibilities will this introduce?&lt;/li&gt;
&lt;li&gt;Can the team support and monitor it?&lt;/li&gt;
&lt;li&gt;Is there a simpler solution that is sufficient?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;A scalable application is not built by collecting as many technologies as possible.&lt;/p&gt;

&lt;p&gt;Docker, Kubernetes, Terraform, PostgreSQL, Redis, Kafka, Prometheus, and the other tools discussed here each address different concerns. Their value comes from how well they fit the application's actual requirements and how thoughtfully they are operated together.&lt;/p&gt;

&lt;p&gt;Start with the simplest architecture that meets the need. Add caching when repeated reads become a bottleneck. Add queues when background work slows down user requests. Introduce a dedicated search engine when search requirements outgrow the database's capabilities. Adopt orchestration and GitOps practices when deployment complexity justifies them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;System design is not about remembering a list of tools. It is about understanding the problem that makes each tool worth introducing—and the trade-offs that come with it.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/" rel="noopener noreferrer"&gt;Docker documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/ecs/" rel="noopener noreferrer"&gt;Amazon ECS documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/" rel="noopener noreferrer"&gt;Kubernetes documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/docs" rel="noopener noreferrer"&gt;Terraform documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/" rel="noopener noreferrer"&gt;PostgreSQL documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://redis.io/docs/" rel="noopener noreferrer"&gt;Redis documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://opensearch.org/docs/" rel="noopener noreferrer"&gt;OpenSearch documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/s3/" rel="noopener noreferrer"&gt;Amazon S3 documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/sqs/" rel="noopener noreferrer"&gt;Amazon SQS documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rabbitmq.com/docs" rel="noopener noreferrer"&gt;RabbitMQ documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kafka.apache.org/documentation/" rel="noopener noreferrer"&gt;Apache Kafka documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/ecr/" rel="noopener noreferrer"&gt;Amazon ECR documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://helm.sh/docs/" rel="noopener noreferrer"&gt;Helm documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://argo-cd.readthedocs.io/" rel="noopener noreferrer"&gt;Argo CD documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://prometheus.io/docs/" rel="noopener noreferrer"&gt;Prometheus documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/docs/" rel="noopener noreferrer"&gt;Grafana documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://opentelemetry.io/docs/" rel="noopener noreferrer"&gt;OpenTelemetry documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>beginners</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>How to Debug Android Chrome Browser Logs from a Windows Laptop</title>
      <dc:creator>Naimul Karim</dc:creator>
      <pubDate>Mon, 17 Aug 2026 23:02:26 +0000</pubDate>
      <link>https://dev.to/naimulkarim/how-to-debug-android-chrome-browser-logs-from-a-windows-laptop-1bib</link>
      <guid>https://dev.to/naimulkarim/how-to-debug-android-chrome-browser-logs-from-a-windows-laptop-1bib</guid>
      <description>&lt;p&gt;Testing a website on a &lt;strong&gt;real Android device&lt;/strong&gt; is often necessary when a problem cannot be reproduced in desktop Chrome or Chrome's device emulation.&lt;/p&gt;

&lt;p&gt;Chrome's &lt;strong&gt;Remote Debugging&lt;/strong&gt; feature allows you to connect an Android phone to a Windows laptop and inspect the mobile Chrome tab using desktop Chrome DevTools.&lt;/p&gt;

&lt;p&gt;With this setup, you can inspect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JavaScript console logs and errors&lt;/li&gt;
&lt;li&gt;Network requests and API responses&lt;/li&gt;
&lt;li&gt;Request and response headers&lt;/li&gt;
&lt;li&gt;HTML and CSS&lt;/li&gt;
&lt;li&gt;Cookies and browser storage&lt;/li&gt;
&lt;li&gt;JavaScript source code&lt;/li&gt;
&lt;li&gt;Breakpoints and debugging&lt;/li&gt;
&lt;li&gt;Authentication and browser-specific issues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This guide covers the complete setup, including troubleshooting the common &lt;code&gt;Pending authentication&lt;/code&gt; and &lt;code&gt;unauthorized&lt;/code&gt; ADB errors.&lt;/p&gt;




&lt;h1&gt;
  
  
  Part 1: Set Up Android Remote Debugging
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Step 1: Enable Developer Options on Android
&lt;/h2&gt;

&lt;p&gt;Android hides &lt;strong&gt;Developer Options&lt;/strong&gt; by default.&lt;/p&gt;

&lt;p&gt;The exact steps can vary by manufacturer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Google Pixel
&lt;/h3&gt;

&lt;p&gt;Go to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Settings → About phone → Build number&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tap &lt;strong&gt;Build number&lt;/strong&gt; seven times.&lt;/p&gt;

&lt;p&gt;You may be asked to enter your phone PIN.&lt;/p&gt;

&lt;p&gt;Android should display a message indicating that Developer Options have been enabled.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other Android Devices
&lt;/h3&gt;

&lt;p&gt;The location can vary between Samsung, OnePlus, Xiaomi, Motorola, and other manufacturers.&lt;/p&gt;

&lt;p&gt;If you cannot find it, search Android Settings for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build number
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2: Enable USB Debugging
&lt;/h2&gt;

&lt;p&gt;On a Google Pixel, go to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Settings → System → Developer options&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Find:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;USB debugging&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Turn it on and confirm the warning when Android asks.&lt;/p&gt;

&lt;p&gt;USB debugging allows the Windows computer to communicate with the Android device through ADB.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Connect the Android Phone to Windows
&lt;/h2&gt;

&lt;p&gt;Connect your Android phone to your Windows laptop using a &lt;strong&gt;USB data cable&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Make sure the cable supports data transfer, not just charging.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep the phone unlocked
&lt;/h3&gt;

&lt;p&gt;This is important because Android may need to display the USB debugging authorization prompt.&lt;/p&gt;

&lt;p&gt;Windows may display a notification such as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Choose what to do with this device&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can close this notification.&lt;/p&gt;

&lt;p&gt;This is a &lt;strong&gt;Windows AutoPlay notification&lt;/strong&gt;, not the Android USB debugging authorization prompt.&lt;/p&gt;

&lt;p&gt;If necessary, open the USB notification on Android and select:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File transfer / Android Auto&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Authorize USB Debugging
&lt;/h2&gt;

&lt;p&gt;After connecting the phone, Android should display:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Allow USB debugging?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The dialog will also display the computer's RSA key fingerprint.&lt;/p&gt;

&lt;p&gt;Select:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always allow from this computer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then tap:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Allow&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This authorizes the Windows computer to communicate with your Android device through ADB.&lt;/p&gt;




&lt;h1&gt;
  
  
  Part 2: Open the Website on Android Chrome
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Step 5: Open Chrome on the Android Device
&lt;/h2&gt;

&lt;p&gt;On your Android phone:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open Chrome.&lt;/li&gt;
&lt;li&gt;Navigate to the website you want to test.&lt;/li&gt;
&lt;li&gt;Leave the website tab open.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The page you want to debug must be running in &lt;strong&gt;Chrome on the Android phone&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  Part 3: Connect Chrome DevTools
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Step 6: Open Chrome Remote Debugging on Windows
&lt;/h2&gt;

&lt;p&gt;On your Windows laptop, open Chrome and navigate to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chrome://inspect/#devices
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure this option is enabled:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Discover USB devices&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your Android device should appear on the page.&lt;/p&gt;

&lt;p&gt;Under the device, Chrome should display the Chrome tabs currently open on your Android phone.&lt;/p&gt;

&lt;p&gt;You should see something similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Android Device
    Chrome
        https://example.com
        inspect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find the website you want to debug and click:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inspect&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Part 4: Debug the Android Website
&lt;/h1&gt;

&lt;p&gt;Chrome DevTools will now open on your Windows laptop.&lt;/p&gt;

&lt;p&gt;Although DevTools is running on Windows, it is connected to the webpage running on the &lt;strong&gt;physical Android device&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Console
&lt;/h2&gt;

&lt;p&gt;Use the &lt;strong&gt;Console&lt;/strong&gt; tab to inspect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;console.log()&lt;/code&gt; output&lt;/li&gt;
&lt;li&gt;JavaScript errors&lt;/li&gt;
&lt;li&gt;Warnings&lt;/li&gt;
&lt;li&gt;Uncaught exceptions&lt;/li&gt;
&lt;li&gt;Runtime errors&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Network
&lt;/h2&gt;

&lt;p&gt;Use the &lt;strong&gt;Network&lt;/strong&gt; tab to inspect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API requests&lt;/li&gt;
&lt;li&gt;HTTP status codes&lt;/li&gt;
&lt;li&gt;Request payloads&lt;/li&gt;
&lt;li&gt;Response bodies&lt;/li&gt;
&lt;li&gt;Request headers&lt;/li&gt;
&lt;li&gt;Response headers&lt;/li&gt;
&lt;li&gt;Request timing&lt;/li&gt;
&lt;li&gt;Failed requests&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Example: Login works on desktop but fails on Android
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Network&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Perform the login on the Android phone.&lt;/li&gt;
&lt;li&gt;Find the login/API request.&lt;/li&gt;
&lt;li&gt;Open the request.&lt;/li&gt;
&lt;li&gt;Inspect:

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Headers&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Payload&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Response&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status Code&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can now see what the &lt;strong&gt;physical Android browser actually sent and received&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  Part 5: Troubleshooting &lt;code&gt;Pending authentication&lt;/code&gt;
&lt;/h1&gt;

&lt;p&gt;Sometimes &lt;code&gt;chrome://inspect/#devices&lt;/code&gt; detects the Android phone but cannot establish the debugging connection.&lt;/p&gt;

&lt;p&gt;You may see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Device information is stale.
Offline
#DEVICE_SERIAL
Pending authentication: please accept debugging session on the device.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the same time, the phone may indicate that USB debugging is connected.&lt;/p&gt;

&lt;p&gt;This usually means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Windows can detect the Android device, but the ADB authorization handshake has not completed successfully.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The easiest way to troubleshoot this is to reset the ADB authorization.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Install Android Platform Tools
&lt;/h2&gt;

&lt;p&gt;Download &lt;strong&gt;SDK Platform-Tools for Windows&lt;/strong&gt; from Google's Android developer website:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.android.com/tools/releases/platform-tools" rel="noopener noreferrer"&gt;https://developer.android.com/tools/releases/platform-tools&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Extract the downloaded ZIP file.&lt;/p&gt;

&lt;p&gt;Inside the extracted &lt;code&gt;platform-tools&lt;/code&gt; directory, you should see files such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;adb.exe
fastboot.exe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;adb.exe&lt;/code&gt; executable is what we will use to check and manage the Android connection.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Open Command Prompt in &lt;code&gt;platform-tools&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Open the extracted &lt;code&gt;platform-tools&lt;/code&gt; directory in Windows File Explorer.&lt;/p&gt;

&lt;p&gt;Click the &lt;strong&gt;File Explorer address bar&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cmd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press &lt;strong&gt;Enter&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Command Prompt should open directly in the &lt;code&gt;platform-tools&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C:\Users\YourName\Downloads\platform-tools&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows you to run ADB commands without adding ADB to the Windows PATH.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Check the ADB Connection
&lt;/h2&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb devices
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If authorization has not completed, you may see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;List of devices attached
DEVICE_SERIAL    unauthorized
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;unauthorized
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Windows can see the Android device.&lt;/li&gt;
&lt;li&gt;ADB can detect the device.&lt;/li&gt;
&lt;li&gt;The Android device has &lt;strong&gt;not authorized this computer&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 4: Reset USB Debugging Authorization
&lt;/h2&gt;

&lt;p&gt;On your Android phone, go to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Settings → System → Developer options&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Select:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Revoke USB debugging authorizations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Confirm the action.&lt;/p&gt;

&lt;p&gt;Then turn:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;USB debugging → Off&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Restart the ADB Server
&lt;/h2&gt;

&lt;p&gt;On Windows, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb kill-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Disconnect the USB cable from the phone.&lt;/p&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Turn &lt;strong&gt;USB debugging&lt;/strong&gt; back on.&lt;/li&gt;
&lt;li&gt;Reconnect the USB cable.&lt;/li&gt;
&lt;li&gt;Keep the phone unlocked.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Start ADB again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb start-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You may see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;* daemon not running; starting now at tcp:5037
* daemon started successfully
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check the device:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb devices
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 6: Accept the Android Authorization Prompt
&lt;/h2&gt;

&lt;p&gt;Look at your Android phone.&lt;/p&gt;

&lt;p&gt;You should now receive:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Allow USB debugging?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Select:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always allow from this computer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then tap:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Allow&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the critical step.&lt;/p&gt;

&lt;p&gt;If the authorization dialog does not appear, keep the phone unlocked and try disconnecting and reconnecting the USB cable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7: Verify ADB Authorization
&lt;/h2&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb devices
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Before authorization
&lt;/h3&gt;

&lt;p&gt;You may see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;List of devices attached
DEVICE_SERIAL    unauthorized
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  After successful authorization
&lt;/h3&gt;

&lt;p&gt;You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;List of devices attached
DEVICE_SERIAL    device
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important change is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;unauthorized
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;device
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This confirms that ADB authorization has succeeded.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 8: Return to Chrome Remote Debugging
&lt;/h2&gt;

&lt;p&gt;On Windows, refresh:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chrome://inspect/#devices
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;Pending authentication&lt;/strong&gt; message should disappear.&lt;/p&gt;

&lt;p&gt;Now:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open Chrome on the Android phone.&lt;/li&gt;
&lt;li&gt;Navigate to the website you want to test.&lt;/li&gt;
&lt;li&gt;Keep the tab open.&lt;/li&gt;
&lt;li&gt;Refresh &lt;code&gt;chrome://inspect/#devices&lt;/code&gt; on Windows.&lt;/li&gt;
&lt;li&gt;Find the Android Chrome tab.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Inspect&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can now use Chrome DevTools normally.&lt;/p&gt;




&lt;h1&gt;
  
  
  Quick Troubleshooting Checklist
&lt;/h1&gt;

&lt;p&gt;If your Android Chrome tab does not appear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Developer Options are enabled.&lt;/li&gt;
&lt;li&gt;[ ] USB debugging is enabled.&lt;/li&gt;
&lt;li&gt;[ ] The Android phone is unlocked.&lt;/li&gt;
&lt;li&gt;[ ] The USB cable supports data transfer.&lt;/li&gt;
&lt;li&gt;[ ] The phone is connected using &lt;strong&gt;File transfer / Android Auto&lt;/strong&gt; if necessary.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Discover USB devices&lt;/strong&gt; is enabled in &lt;code&gt;chrome://inspect/#devices&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;adb devices&lt;/code&gt; detects the phone.&lt;/li&gt;
&lt;li&gt;[ ] The phone is not shown as &lt;code&gt;unauthorized&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] You accepted &lt;strong&gt;Allow USB debugging?&lt;/strong&gt; on the phone.&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;adb devices&lt;/code&gt; reports the device as &lt;code&gt;device&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] The website is open in Chrome on the Android phone.&lt;/li&gt;
&lt;li&gt;[ ] You refreshed &lt;code&gt;chrome://inspect/#devices&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  The Complete Flow
&lt;/h1&gt;

&lt;p&gt;Once everything is working, the debugging flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Android Chrome
      ↓
USB Debugging
      ↓
ADB
      ↓
Windows Laptop
      ↓
chrome://inspect/#devices
      ↓
Chrome DevTools
      ↓
Console / Network / Elements / Application / Sources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important distinction is that the &lt;strong&gt;website runs on the physical Android device&lt;/strong&gt;, while the &lt;strong&gt;DevTools interface runs on your Windows laptop&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Use a Real Android Device?
&lt;/h1&gt;

&lt;p&gt;Chrome's desktop device emulation is useful, but it cannot reproduce every behavior of a physical Android device.&lt;/p&gt;

&lt;p&gt;Remote debugging is especially useful for investigating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mobile-only JavaScript errors&lt;/li&gt;
&lt;li&gt;Authentication problems&lt;/li&gt;
&lt;li&gt;API failures&lt;/li&gt;
&lt;li&gt;Cookie and storage issues&lt;/li&gt;
&lt;li&gt;Browser-specific behavior&lt;/li&gt;
&lt;li&gt;Responsive UI problems&lt;/li&gt;
&lt;li&gt;Touch-related issues&lt;/li&gt;
&lt;li&gt;Network problems&lt;/li&gt;
&lt;li&gt;Mobile Chrome rendering issues&lt;/li&gt;
&lt;li&gt;Problems that cannot be reproduced using desktop Chrome&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When an issue occurs only on a real Android phone, remote debugging gives you direct access to the browser's &lt;strong&gt;Console, Network, Elements, Application, and Sources&lt;/strong&gt; tools while the application is running on the actual device.&lt;/p&gt;




&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Connecting a real Android device to Chrome DevTools is straightforward once ADB authorization is working.&lt;/p&gt;

&lt;p&gt;The normal flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Enable Developer Options
        ↓
Enable USB Debugging
        ↓
Connect Android to Windows
        ↓
Authorize the Computer
        ↓
Open Chrome on Android
        ↓
chrome://inspect/#devices
        ↓
Click Inspect
        ↓
Debug with Chrome DevTools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Chrome shows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pending authentication
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and ADB shows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;unauthorized
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the key is to &lt;strong&gt;reset USB debugging authorization, restart ADB, reconnect the phone, and accept the authorization prompt on Android&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Once &lt;code&gt;adb devices&lt;/code&gt; changes from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;unauthorized
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;device
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chrome Remote Debugging should be able to establish the connection.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tags
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;#android&lt;/code&gt; &lt;code&gt;#chrome&lt;/code&gt; &lt;code&gt;#devtools&lt;/code&gt; &lt;code&gt;#debugging&lt;/code&gt; &lt;code&gt;#webdevelopment&lt;/code&gt; &lt;code&gt;#javascript&lt;/code&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>debugging</category>
      <category>mobile</category>
      <category>tools</category>
    </item>
    <item>
      <title>Stop Prompt Engineering, Start Context Engineering</title>
      <dc:creator>Naimul Karim</dc:creator>
      <pubDate>Mon, 03 Aug 2026 04:32:35 +0000</pubDate>
      <link>https://dev.to/naimulkarim/stop-prompt-engineering-start-context-engineering-2gf5</link>
      <guid>https://dev.to/naimulkarim/stop-prompt-engineering-start-context-engineering-2gf5</guid>
      <description>&lt;p&gt;AI agents get talked about a lot, but most explanations stay abstract. Here's a short, practical breakdown of what actually makes an agent work — plus a simple example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chat vs. Agent
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chat model&lt;/strong&gt; → question → answer (you do the work)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent&lt;/strong&gt; → goal → result (it plans + executes for you)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Chat:   You ask ──&amp;gt; Model answers ──&amp;gt; You act
Agent:  You set goal ──&amp;gt; Agent plans → acts → checks ──&amp;gt; Result delivered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Agent Loop
&lt;/h2&gt;

&lt;p&gt;Every agent — no matter the platform — runs the same loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   ┌─────────┐
   │ OBSERVE │  read context, current state
   └────┬────┘
        ▼
   ┌─────────┐
   │  THINK  │  decide next action
   └────┬────┘
        ▼
   ┌─────────┐
   │   ACT   │  execute, then loop again
   └────┬────┘
        │
        └──────► repeats until task is done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An agent = &lt;strong&gt;LLM (brain) + Loop + Tools + Context&lt;/strong&gt;. The "harness" (Claude Code, Cowork, Codex, etc.) is just the app that runs this loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack, in Layers
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────┐
│ 5. Skills   → reusable SOPs │
├────────────────────────────┤
│ 4. Tools    → via MCP       │
├────────────────────────────┤
│ 3. Memory   → memory.md     │
├────────────────────────────┤
│ 2. Context  → agents.md     │
├────────────────────────────┤
│ 1. Loop     → observe→think→act │
└────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context file (&lt;code&gt;agents.md&lt;/code&gt; / &lt;code&gt;Claude.md&lt;/code&gt;)&lt;/strong&gt; — persistent "system prompt" with your role, business, tools, and preferences. Loaded every session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory (&lt;code&gt;memory.md&lt;/code&gt;)&lt;/strong&gt; — updated whenever the agent is corrected. Compounds over time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools (MCP)&lt;/strong&gt; — Model Context Protocol connects the agent to Gmail, Notion, Calendar, Stripe, etc. without custom integration work per tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills&lt;/strong&gt; — a markdown file documenting a repeatable process once, so it never needs re-explaining. Can be chained or scheduled.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prompt engineering → Context engineering.&lt;/strong&gt; Rich context turns a 2-word prompt into a great result.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Quick Example
&lt;/h2&gt;

&lt;p&gt;A solo creator automating their weekly newsletter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;agents.md&lt;/code&gt; — newsletter's audience, tone, format, connected tools (Notion, Docs, social)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memory.md&lt;/code&gt; — learns preferences over time ("shorter subject lines," etc.)&lt;/li&gt;
&lt;li&gt;Connect Notion + Docs + social via MCP&lt;/li&gt;
&lt;li&gt;One prompt: &lt;em&gt;"Pull this week's notes, draft the issue, write a companion thread."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Package it as a &lt;code&gt;weekly-newsletter&lt;/code&gt; skill → schedule it for every Thursday&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Same shape every time: &lt;strong&gt;context + memory + tools set up once → simple prompts → consistent results.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Starter Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Pick a simple harness, create a project folder&lt;/li&gt;
&lt;li&gt;[ ] Write &lt;code&gt;agents.md&lt;/code&gt; (role, business, tools, preferences)&lt;/li&gt;
&lt;li&gt;[ ] Add a starter &lt;code&gt;memory.md&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Connect 2–3 high-value tools via MCP&lt;/li&gt;
&lt;li&gt;[ ] Run a real task, watch the loop work&lt;/li&gt;
&lt;li&gt;[ ] Turn a repeatable task into a skill&lt;/li&gt;
&lt;li&gt;[ ] Schedule a recurring skill&lt;/li&gt;
&lt;li&gt;[ ] Repeat for another role/project&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't about cleverer prompts — it's front-loading context once so every future ask can stay simple.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
    </item>
    <item>
      <title>AI Agentic Workflow Explained: A Quick Tour of Harness, Tools, Skills, MCP, and Memory</title>
      <dc:creator>Naimul Karim</dc:creator>
      <pubDate>Wed, 22 Jul 2026 04:35:27 +0000</pubDate>
      <link>https://dev.to/naimulkarim/ai-agentic-workflow-explained-a-quick-tour-of-harness-tools-skills-mcp-and-memory-n8g</link>
      <guid>https://dev.to/naimulkarim/ai-agentic-workflow-explained-a-quick-tour-of-harness-tools-skills-mcp-and-memory-n8g</guid>
      <description>&lt;p&gt;AI applications are moving beyond simple chat experiences.&lt;/p&gt;

&lt;p&gt;The next generation of AI systems are &lt;strong&gt;AI agents&lt;/strong&gt; — systems that can understand goals, reason about problems, use external tools, access enterprise data, and complete multi-step workflows.&lt;/p&gt;

&lt;p&gt;However, an AI agent is not just a Large Language Model (LLM) with a prompt.&lt;/p&gt;

&lt;p&gt;A production AI agent is a combination of multiple components working together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI Agent
   |
   +-- Model
   |
   +-- Harness
   |
   +-- Tools
   |
   +-- MCP Integrations
   |
   +-- AGENTS.md
   |
   +-- Skills
   |
   +-- Memory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model provides intelligence and reasoning.&lt;/p&gt;

&lt;p&gt;The surrounding infrastructure provides the ability to take action reliably.&lt;/p&gt;




&lt;h1&gt;
  
  
  Understanding the AI Agent Workflow
&lt;/h1&gt;

&lt;p&gt;A typical AI agent execution flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    User Request
                         |
                         v
                  Agent Harness
                         |
        +----------------+----------------+
        |                                 |
        v                                 v
 Load Always-On Context          Understand Task
        |                                 |
        |                                 v
        |                         Select Relevant Skills
        |                                 |
        |                                 v
        +------------------------ Load Skill Context
                         |
                         v
                 Retrieve Memory
                         |
                         v
              Discover Tools via MCP
                         |
                         v
                  Agent Reasoning
                         |
                         v
                  Execute Actions
                         |
                         v
              Observe Tool Results
                         |
                         v
              Update Context/Memory
                         |
                         v
                  Task Completed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the core of this workflow is a continuous loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reason → Act → Observe → Repeat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model decides what should happen next.&lt;/p&gt;

&lt;p&gt;The agent infrastructure makes those decisions actionable.&lt;/p&gt;




&lt;h1&gt;
  
  
  1. Agent Harness: The Runtime Behind the Agent
&lt;/h1&gt;

&lt;p&gt;The &lt;strong&gt;AI Agent Harness&lt;/strong&gt; is the execution layer that surrounds the model.&lt;/p&gt;

&lt;p&gt;It manages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Task execution&lt;/li&gt;
&lt;li&gt;Context assembly&lt;/li&gt;
&lt;li&gt;Tool access&lt;/li&gt;
&lt;li&gt;Skill loading&lt;/li&gt;
&lt;li&gt;Memory retrieval&lt;/li&gt;
&lt;li&gt;Security policies&lt;/li&gt;
&lt;li&gt;Observability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful analogy:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The model is the brain. The harness is the environment that allows the brain to interact with the world.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;User request:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Find customers who have not logged in for 90 days and send them a reminder email."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The model reasons:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I need customer activity data and an email capability."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The harness handles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Loading relevant capabilities&lt;/li&gt;
&lt;li&gt;Accessing customer data&lt;/li&gt;
&lt;li&gt;Generating email content&lt;/li&gt;
&lt;li&gt;Applying approval rules&lt;/li&gt;
&lt;li&gt;Sending the communication&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The harness connects reasoning with execution.&lt;/p&gt;




&lt;h1&gt;
  
  
  2. Tools: Giving Agents the Ability to Act
&lt;/h1&gt;

&lt;p&gt;Tools provide agents with the ability to interact with external systems.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;APIs&lt;/li&gt;
&lt;li&gt;Databases&lt;/li&gt;
&lt;li&gt;Search engines&lt;/li&gt;
&lt;li&gt;Code execution environments&lt;/li&gt;
&lt;li&gt;File systems&lt;/li&gt;
&lt;li&gt;Business applications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without tools, an LLM can only provide recommendations.&lt;/p&gt;

&lt;p&gt;With tools, an agent can perform actions.&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;p&gt;The model decides:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"I need customer information.
I will query the database."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The harness executes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;last_login&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'90 days'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is returned to the model.&lt;/p&gt;

&lt;p&gt;This creates the agent loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reason → Tool Call → Result → Reason Again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  3. Model Context Protocol (MCP): Standardizing Tool Connections
&lt;/h1&gt;

&lt;p&gt;As agents become more capable, they need access to many external systems.&lt;/p&gt;

&lt;p&gt;Managing custom integrations for every application does not scale.&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt; becomes important.&lt;/p&gt;

&lt;p&gt;MCP provides a standard way for AI applications to connect with external tools and data sources.&lt;/p&gt;

&lt;p&gt;Without MCP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent

 ├── Custom Database Connector
 ├── Custom File Connector
 ├── Custom API Connector
 └── Custom Search Connector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With MCP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent

       |
  MCP Client

       |
 -------------------------
 |          |             |
Database   Files        APIs
MCP        MCP          MCP
Server     Server       Server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An MCP server can expose capabilities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;search_customers()

get_customer_orders()

update_customer_record()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MCP separates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agent reasoning&lt;/li&gt;
&lt;li&gt;Tool implementation&lt;/li&gt;
&lt;li&gt;Enterprise system access&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  4. AGENTS.md: The Agent's Always-Loaded Instructions
&lt;/h1&gt;

&lt;p&gt;As agent systems grow, context management becomes critical.&lt;/p&gt;

&lt;p&gt;Not every instruction should be loaded for every task.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; provides the agent with its always-available operating instructions.&lt;/p&gt;

&lt;p&gt;It defines the baseline rules for how an agent should work.&lt;/p&gt;

&lt;p&gt;Typical contents include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Project conventions&lt;/li&gt;
&lt;li&gt;Coding standards&lt;/li&gt;
&lt;li&gt;Security rules&lt;/li&gt;
&lt;li&gt;Repository guidelines&lt;/li&gt;
&lt;li&gt;Environment instructions&lt;/li&gt;
&lt;li&gt;Common workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/

├── AGENTS.md
├── src/
├── tests/
└── skills/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example &lt;code&gt;AGENTS.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Agent Instructions&lt;/span&gt;

&lt;span class="gu"&gt;## Project Rules&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Follow coding standards
&lt;span class="p"&gt;-&lt;/span&gt; Run tests before committing changes
&lt;span class="p"&gt;-&lt;/span&gt; Never expose secrets
&lt;span class="p"&gt;-&lt;/span&gt; Update documentation when required

&lt;span class="gu"&gt;## Development Workflow&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; Understand the requirement
&lt;span class="p"&gt;2.&lt;/span&gt; Inspect existing code
&lt;span class="p"&gt;3.&lt;/span&gt; Implement changes
&lt;span class="p"&gt;4.&lt;/span&gt; Validate results
&lt;span class="p"&gt;5.&lt;/span&gt; Provide a summary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;AGENTS.md&lt;/code&gt; is always included in the context, it should contain only essential instructions.&lt;/p&gt;

&lt;p&gt;A large &lt;code&gt;AGENTS.md&lt;/code&gt; increases context usage and can reduce agent effectiveness.&lt;/p&gt;




&lt;h1&gt;
  
  
  5. Skills: Dynamic Agent Capabilities
&lt;/h1&gt;

&lt;p&gt;Skills provide specialized knowledge and workflows that are loaded only when relevant.&lt;/p&gt;

&lt;p&gt;Unlike &lt;code&gt;AGENTS.md&lt;/code&gt;, skills are not always included in the context.&lt;/p&gt;

&lt;p&gt;They are selected dynamically based on the task.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database migration skill&lt;/li&gt;
&lt;li&gt;Security review skill&lt;/li&gt;
&lt;li&gt;Frontend development skill&lt;/li&gt;
&lt;li&gt;Documentation skill&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;skills/

├── database-migration/
│   └── SKILL.md
│
├── security-review/
│   └── SKILL.md
│
└── api-development/
    └── SKILL.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example &lt;code&gt;SKILL.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Database Migration Skill&lt;/span&gt;

&lt;span class="gu"&gt;## Purpose&lt;/span&gt;

Safely modify database schemas.

&lt;span class="gu"&gt;## Workflow&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; Analyze schema changes
&lt;span class="p"&gt;2.&lt;/span&gt; Create migration scripts
&lt;span class="p"&gt;3.&lt;/span&gt; Validate compatibility
&lt;span class="p"&gt;4.&lt;/span&gt; Run migration tests
&lt;span class="p"&gt;5.&lt;/span&gt; Review rollback strategy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The harness loads this skill only when a database migration task appears.&lt;/p&gt;




&lt;h1&gt;
  
  
  AGENTS.md vs Skills: Why Both Matter
&lt;/h1&gt;

&lt;p&gt;A common mistake is putting every instruction into &lt;code&gt;AGENTS.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AGENTS.md

- Coding rules
- Database procedures
- Deployment workflows
- Security reviews
- Testing strategies
- Documentation rules
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Over time, this file becomes too large.&lt;/p&gt;

&lt;p&gt;The agent receives unnecessary information for every task.&lt;/p&gt;

&lt;p&gt;A better approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AGENTS.md

Always loaded:
- Core rules
- Project conventions
- Security requirements


Skills

Loaded when needed:
- Database workflows
- Deployment procedures
- Security analysis
- API patterns
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal is not maximum information.&lt;/p&gt;

&lt;p&gt;The goal is the &lt;strong&gt;right information at the right time&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  6. Memory: Giving Agents Continuity
&lt;/h1&gt;

&lt;p&gt;LLMs are stateless by default.&lt;/p&gt;

&lt;p&gt;Without memory, every interaction starts from zero.&lt;/p&gt;

&lt;p&gt;Agent memory usually has multiple layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Short-Term Memory
&lt;/h2&gt;

&lt;p&gt;Current conversation context.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User:
"My order arrived damaged."

Agent remembers:

- Order details
- Previous messages
- Current issue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Working Memory
&lt;/h2&gt;

&lt;p&gt;Temporary information needed during execution.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Research Task:

Files analyzed:

- sales_report.csv
- customer_feedback.json
- product_notes.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Long-Term Memory
&lt;/h2&gt;

&lt;p&gt;Information retained across sessions.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user_preferences"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"communication"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"English"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Memory allows agents to become more personalized and effective.&lt;/p&gt;




&lt;h1&gt;
  
  
  Complete AI Agent Workflow Example
&lt;/h1&gt;

&lt;p&gt;Consider a software engineering agent.&lt;/p&gt;

&lt;p&gt;User request:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Fix the failing payment API tests."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 1: Load Agent Instructions
&lt;/h2&gt;

&lt;p&gt;The harness loads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AGENTS.md

- Coding standards
- Repository rules
- Security policies
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2: Load Relevant Skills
&lt;/h2&gt;

&lt;p&gt;The harness identifies the task and loads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;software-engineering-skill/

├── SKILL.md
├── coding-guidelines.md
└── testing-workflow.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3: Retrieve Memory
&lt;/h2&gt;

&lt;p&gt;The agent recalls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Previous changes:

- Payment API migration completed
- Database schema updated
- Authentication tests modified
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 4: Discover Tools Through MCP
&lt;/h2&gt;

&lt;p&gt;The agent accesses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MCP Servers:

- Git repository
- CI/CD system
- Test runner
- Issue tracker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 5: Execute the Task
&lt;/h2&gt;

&lt;p&gt;Actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Analyze failing tests
2. Inspect source code
3. Modify implementation
4. Run tests
5. Review results
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 6: Verify and Complete
&lt;/h2&gt;

&lt;p&gt;The harness validates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tests pass&lt;/li&gt;
&lt;li&gt;Policies are followed&lt;/li&gt;
&lt;li&gt;No restricted actions occurred&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent delivers the result.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Agentic Workflows Matter
&lt;/h1&gt;

&lt;p&gt;Building AI agents is no longer only about choosing a powerful model.&lt;/p&gt;

&lt;p&gt;Reliable agents require:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Harness&lt;/td&gt;
&lt;td&gt;Runs and coordinates the agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tools&lt;/td&gt;
&lt;td&gt;Enable external actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP&lt;/td&gt;
&lt;td&gt;Standardizes integrations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AGENTS.md&lt;/td&gt;
&lt;td&gt;Provides always-on instructions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skills&lt;/td&gt;
&lt;td&gt;Provide task-specific expertise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;Maintains context over time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The model provides intelligence.&lt;/p&gt;

&lt;p&gt;The workflow around the model provides capability.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;The future of AI applications will be built around agentic workflows.&lt;/p&gt;

&lt;p&gt;Successful AI agents will combine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reasoning models&lt;/li&gt;
&lt;li&gt;Execution harnesses&lt;/li&gt;
&lt;li&gt;Tool ecosystems&lt;/li&gt;
&lt;li&gt;MCP-based integrations&lt;/li&gt;
&lt;li&gt;Dynamic skills&lt;/li&gt;
&lt;li&gt;Context-aware memory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest shift is moving from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How do we prompt the model?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How do we design the complete system around the model?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the foundation of modern AI agent engineering.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>agents</category>
      <category>ai</category>
      <category>architecture</category>
      <category>llm</category>
    </item>
    <item>
      <title>What is an AI Agent Harness?</title>
      <dc:creator>Naimul Karim</dc:creator>
      <pubDate>Wed, 22 Jul 2026 04:20:31 +0000</pubDate>
      <link>https://dev.to/naimulkarim/what-is-an-ai-agent-harness-370m</link>
      <guid>https://dev.to/naimulkarim/what-is-an-ai-agent-harness-370m</guid>
      <description>&lt;p&gt;Large Language Models (LLMs) are incredibly good at understanding language, reasoning through problems, and generating responses. However, on their own, they &lt;strong&gt;cannot execute code, call APIs, access files, remember previous work, or complete multi-step workflows&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To move beyond answering prompts and actually perform tasks, an LLM needs an &lt;strong&gt;AI Agent Harness&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;AI Agent Harness&lt;/strong&gt; is the software infrastructure that surrounds a Large Language Model (LLM) and enables it to interact with the outside world. It connects the model to tools, memory, execution environments, and safety controls, allowing the model's decisions to be translated into meaningful actions.&lt;/p&gt;

&lt;p&gt;Simply put:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI Agent = Model + Harness
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Think of the &lt;strong&gt;model&lt;/strong&gt; as the brain that reasons and decides what should happen next. The &lt;strong&gt;harness&lt;/strong&gt; is everything around it that enables those decisions to be executed safely and reliably.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Does an AI Agent Harness Provide?
&lt;/h2&gt;

&lt;p&gt;A typical AI agent harness consists of four key capabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools
&lt;/h2&gt;

&lt;p&gt;The harness connects the model to external systems, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;APIs&lt;/li&gt;
&lt;li&gt;Databases&lt;/li&gt;
&lt;li&gt;Search engines&lt;/li&gt;
&lt;li&gt;Code execution environments&lt;/li&gt;
&lt;li&gt;Business applications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tools allow the agent to perform actions instead of simply generating text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory
&lt;/h2&gt;

&lt;p&gt;Models have limited context windows and do not remember previous interactions by default.&lt;/p&gt;

&lt;p&gt;The harness manages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Conversation history&lt;/li&gt;
&lt;li&gt;User preferences&lt;/li&gt;
&lt;li&gt;Previous workflow state&lt;/li&gt;
&lt;li&gt;Long-term task context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This enables agents to continue work across multiple interactions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workspace
&lt;/h2&gt;

&lt;p&gt;Agents often need an environment where they can perform tasks.&lt;/p&gt;

&lt;p&gt;A harness provides access to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Files&lt;/li&gt;
&lt;li&gt;Documents&lt;/li&gt;
&lt;li&gt;Data&lt;/li&gt;
&lt;li&gt;Execution environments&lt;/li&gt;
&lt;li&gt;Shared storage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This workspace allows agents to create, modify, and persist information while completing tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guardrails
&lt;/h2&gt;

&lt;p&gt;Enterprise agents need to operate safely.&lt;/p&gt;

&lt;p&gt;The harness enforces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Permissions&lt;/li&gt;
&lt;li&gt;Security policies&lt;/li&gt;
&lt;li&gt;Human approval workflows&lt;/li&gt;
&lt;li&gt;Monitoring&lt;/li&gt;
&lt;li&gt;Governance controls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without these safeguards, an agent could perform actions that should not happen automatically.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Do AI Agents Need Both a Model and a Harness?
&lt;/h1&gt;

&lt;p&gt;Modern AI agents are built around two complementary layers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;Reasons, plans, and decides what to do next&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Harness&lt;/td&gt;
&lt;td&gt;Executes those decisions using tools, memory, and external systems&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The model focuses on &lt;strong&gt;thinking&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The harness focuses on &lt;strong&gt;doing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Together, they form a complete AI agent.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Reason → Act → Observe Loop
&lt;/h1&gt;

&lt;p&gt;Most AI agents operate using a continuous execution cycle known as the &lt;strong&gt;ReAct (Reason + Act)&lt;/strong&gt; pattern.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reason
   |
   v
Act
   |
   v
Observe
   |
   v
Repeat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  1. Reason
&lt;/h2&gt;

&lt;p&gt;The model analyzes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user's request&lt;/li&gt;
&lt;li&gt;Available context&lt;/li&gt;
&lt;li&gt;Previous results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It determines the next action required to move toward the goal.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Act
&lt;/h2&gt;

&lt;p&gt;The harness executes that action by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Calling an API&lt;/li&gt;
&lt;li&gt;Running code&lt;/li&gt;
&lt;li&gt;Querying a database&lt;/li&gt;
&lt;li&gt;Searching external sources&lt;/li&gt;
&lt;li&gt;Updating files&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Observe
&lt;/h2&gt;

&lt;p&gt;The result of that action is returned to the model as new context.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Repeat
&lt;/h2&gt;

&lt;p&gt;The model reasons again using the updated information.&lt;/p&gt;

&lt;p&gt;This cycle continues until the task is completed.&lt;/p&gt;

&lt;p&gt;For example, consider a coding agent fixing a bug:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The model proposes a code change.&lt;/li&gt;
&lt;li&gt;The harness runs the code in an isolated sandbox.&lt;/li&gt;
&lt;li&gt;Test results are returned to the model.&lt;/li&gt;
&lt;li&gt;If tests fail, the model analyzes the errors and tries another approach.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The model focuses on solving the problem, while the harness manages execution and feedback.&lt;/p&gt;




&lt;h1&gt;
  
  
  Model vs Harness vs Agent
&lt;/h1&gt;

&lt;p&gt;These terms are often used interchangeably, but they represent different parts of an AI system.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;What it Does&lt;/th&gt;
&lt;th&gt;Analogy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;Reasons and generates outputs&lt;/td&gt;
&lt;td&gt;Brain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Harness&lt;/td&gt;
&lt;td&gt;Executes actions, manages memory, tools, and policies&lt;/td&gt;
&lt;td&gt;Body and workspace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent&lt;/td&gt;
&lt;td&gt;Complete system combining model and harness&lt;/td&gt;
&lt;td&gt;Worker that can think and act&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The agent is not just the model.&lt;/p&gt;

&lt;p&gt;It is the combination of intelligent reasoning and the infrastructure required to turn decisions into real-world actions.&lt;/p&gt;




&lt;h1&gt;
  
  
  Core Building Blocks of an AI Agent Harness
&lt;/h1&gt;

&lt;p&gt;Most production-ready harnesses include several foundational components.&lt;/p&gt;

&lt;h2&gt;
  
  
  System Prompts
&lt;/h2&gt;

&lt;p&gt;System prompts provide persistent instructions that define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The agent's role&lt;/li&gt;
&lt;li&gt;Objectives&lt;/li&gt;
&lt;li&gt;Rules&lt;/li&gt;
&lt;li&gt;Behavioral constraints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They establish the operating guidelines before the agent begins a task.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool Execution
&lt;/h2&gt;

&lt;p&gt;The model decides which tool should be used.&lt;/p&gt;

&lt;p&gt;The harness is responsible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Executing the tool&lt;/li&gt;
&lt;li&gt;Handling authentication&lt;/li&gt;
&lt;li&gt;Capturing results&lt;/li&gt;
&lt;li&gt;Returning information back to the model&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Execution Environments (Sandboxes)
&lt;/h2&gt;

&lt;p&gt;Running AI-generated code directly on production systems creates security risks.&lt;/p&gt;

&lt;p&gt;Sandboxes provide isolated environments where agents can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Execute code safely&lt;/li&gt;
&lt;li&gt;Experiment&lt;/li&gt;
&lt;li&gt;Run tests&lt;/li&gt;
&lt;li&gt;Avoid unintended system changes&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Filesystem and Storage
&lt;/h2&gt;

&lt;p&gt;Long-running tasks require persistent storage.&lt;/p&gt;

&lt;p&gt;The harness manages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Files&lt;/li&gt;
&lt;li&gt;Intermediate results&lt;/li&gt;
&lt;li&gt;Notes&lt;/li&gt;
&lt;li&gt;Plans&lt;/li&gt;
&lt;li&gt;Generated artifacts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This allows agents to maintain progress across sessions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Memory and Context Management
&lt;/h2&gt;

&lt;p&gt;Models have limited context windows, so the harness manages what information remains available.&lt;/p&gt;

&lt;p&gt;This includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keeping relevant information active&lt;/li&gt;
&lt;li&gt;Summarizing older conversations&lt;/li&gt;
&lt;li&gt;Retrieving previous task history&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Good context management helps agents maintain performance during complex workflows.&lt;/p&gt;




&lt;h2&gt;
  
  
  Feedback and Self-Verification
&lt;/h2&gt;

&lt;p&gt;Reliable agents do not simply generate output and stop.&lt;/p&gt;

&lt;p&gt;The harness can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run tests&lt;/li&gt;
&lt;li&gt;Validate results&lt;/li&gt;
&lt;li&gt;Check outputs&lt;/li&gt;
&lt;li&gt;Ask the model to review its own work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These feedback loops allow agents to detect mistakes and improve their actions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Guardrails and Human-in-the-Loop Controls
&lt;/h2&gt;

&lt;p&gt;Some actions require human approval.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sending external communications&lt;/li&gt;
&lt;li&gt;Making financial transactions&lt;/li&gt;
&lt;li&gt;Deleting important data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The harness can introduce approval checkpoints before executing sensitive operations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Observability and Logging
&lt;/h2&gt;

&lt;p&gt;Production systems need visibility into agent behavior.&lt;/p&gt;

&lt;p&gt;The harness records:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tool calls&lt;/li&gt;
&lt;li&gt;Execution traces&lt;/li&gt;
&lt;li&gt;Latency&lt;/li&gt;
&lt;li&gt;Errors&lt;/li&gt;
&lt;li&gt;Token usage&lt;/li&gt;
&lt;li&gt;Workflow history&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This helps developers debug, evaluate, and improve agent systems.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Harness Design Matters
&lt;/h1&gt;

&lt;p&gt;As LLMs become more capable, the harness becomes an increasingly important factor in agent performance.&lt;/p&gt;

&lt;p&gt;A strong harness improves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Context management&lt;/li&gt;
&lt;li&gt;Tool orchestration&lt;/li&gt;
&lt;li&gt;Memory handling&lt;/li&gt;
&lt;li&gt;Verification&lt;/li&gt;
&lt;li&gt;Safety&lt;/li&gt;
&lt;li&gt;Reliability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In many real-world scenarios, a well-designed harness around a smaller model can outperform a more capable model with poor execution infrastructure.&lt;/p&gt;

&lt;p&gt;The future of AI performance will depend not only on better models but also on better systems built around those models.&lt;/p&gt;




&lt;h1&gt;
  
  
  Harness Engineering: The Next Evolution
&lt;/h1&gt;

&lt;p&gt;AI development has evolved through several stages:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Discipline&lt;/th&gt;
&lt;th&gt;Focus&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prompt Engineering&lt;/td&gt;
&lt;td&gt;Writing better instructions for models&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context Engineering&lt;/td&gt;
&lt;td&gt;Controlling the information provided to models&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Harness Engineering&lt;/td&gt;
&lt;td&gt;Designing the complete runtime environment around models&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Prompt engineering and context engineering are now becoming parts of a broader discipline: &lt;strong&gt;Harness Engineering&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Harness engineering focuses on building the complete ecosystem that allows AI agents to operate reliably in real-world environments.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;A language model provides intelligence, but intelligence alone does not create an AI agent.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;AI Agent Harness&lt;/strong&gt; transforms that intelligence into action by connecting the model to tools, memory, execution environments, and safety mechanisms.&lt;/p&gt;

&lt;p&gt;As AI agents become more autonomous and move into production systems, harnesses will become one of the most important layers of the AI technology stack.&lt;/p&gt;

&lt;p&gt;The future of AI will not be defined only by the models we build. It will also be defined by the quality of the harnesses that allow those models to work effectively.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>infrastructure</category>
      <category>llm</category>
    </item>
    <item>
      <title>How I Fixed Windows 11 Sleep Not Working by Tracking Down a Hidden WebView2 Video Wake Lock</title>
      <dc:creator>Naimul Karim</dc:creator>
      <pubDate>Wed, 22 Jul 2026 02:59:39 +0000</pubDate>
      <link>https://dev.to/naimulkarim/how-i-fixed-windows-11-sleep-not-working-by-tracking-down-a-hidden-webview2-video-wake-lock-aa8</link>
      <guid>https://dev.to/naimulkarim/how-i-fixed-windows-11-sleep-not-working-by-tracking-down-a-hidden-webview2-video-wake-lock-aa8</guid>
      <description>&lt;p&gt;Have you ever configured your Windows laptop to sleep after a few minutes of inactivity, only to find the screen never turns off?&lt;/p&gt;

&lt;p&gt;I recently ran into this issue on my &lt;strong&gt;Dell Inspiron 16 running Windows 11 Pro&lt;/strong&gt;, and after hours of troubleshooting, I discovered the culprit wasn't Windows itself—it was an application using &lt;strong&gt;Microsoft Edge WebView2&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's how I diagnosed and fixed it.&lt;/p&gt;




&lt;h2&gt;
  
  
  🖥️ The Problem
&lt;/h2&gt;

&lt;p&gt;My power settings were configured correctly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Display turns off after &lt;strong&gt;3 minutes&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Sleep after &lt;strong&gt;3 minutes&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Hibernate enabled&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Despite that, the laptop would remain awake indefinitely. The display never turned off, and the system never entered sleep.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔍 Step 1: Check for Power Requests
&lt;/h2&gt;

&lt;p&gt;Windows includes a built-in command that tells you what's preventing your computer from sleeping.&lt;/p&gt;

&lt;p&gt;Open &lt;strong&gt;Command Prompt&lt;/strong&gt; as Administrator and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;powercfg /requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output looked something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DISPLAY:
[PROCESS] msedgewebview2.exe
Video Wake Lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part was &lt;strong&gt;Video Wake Lock&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A Video Wake Lock tells Windows that media playback is active, so Windows intentionally keeps the display awake.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔎 Step 2: Find Which Application Owns WebView2
&lt;/h2&gt;

&lt;p&gt;Many Windows applications use &lt;strong&gt;Microsoft Edge WebView2&lt;/strong&gt;, so simply seeing &lt;code&gt;msedgewebview2.exe&lt;/code&gt; doesn't tell you which application is responsible.&lt;/p&gt;

&lt;p&gt;I used PowerShell to identify the parent process for each WebView2 instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-CimInstance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Win32_Process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"msedgewebview2.exe"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ProcessId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ParentProcessId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Host"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;Expression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-CimInstance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Win32_Process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ProcessId=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ParentProcessId&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Name&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Sort&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Host&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output revealed several applications using WebView2:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Widgets.exe&lt;/li&gt;
&lt;li&gt;Microsoft Teams&lt;/li&gt;
&lt;li&gt;SearchHost.exe&lt;/li&gt;
&lt;li&gt;Visual Studio (&lt;code&gt;devenv.exe&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PCAppStore.exe&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point I still didn't know which one was preventing sleep.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 Step 3: Eliminate Processes One by One
&lt;/h2&gt;

&lt;p&gt;I terminated each host process individually and checked the power requests again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Stop-Process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PID&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After each process was stopped, I reran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;powercfg /requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I terminated &lt;strong&gt;PCAppStore.exe&lt;/strong&gt;, the output immediately changed to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DISPLAY:
None.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🎉 &lt;strong&gt;That was the smoking gun.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PCAppStore.exe&lt;/code&gt; was holding a &lt;strong&gt;Video Wake Lock&lt;/strong&gt; through WebView2, preventing Windows from turning off the display or entering sleep.&lt;/p&gt;




&lt;h2&gt;
  
  
  💻 Step 4: Verify Sleep Support
&lt;/h2&gt;

&lt;p&gt;To ensure there wasn't a hardware limitation, I checked the supported sleep states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;powercfg /a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Standby (S0 Low Power Idle)
Hibernate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means the laptop uses &lt;strong&gt;Modern Standby (S0)&lt;/strong&gt; rather than the traditional &lt;strong&gt;S3 Sleep&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Modern Standby is more sensitive to applications requesting wake locks, making &lt;code&gt;powercfg /requests&lt;/code&gt; an essential troubleshooting tool.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ Step 5: Fix the Root Cause
&lt;/h2&gt;

&lt;p&gt;Once I identified the offending application, the fix was straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Disable the application from Startup&lt;/li&gt;
&lt;li&gt;Close it when not needed&lt;/li&gt;
&lt;li&gt;Uninstall it if you never use it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After preventing &lt;strong&gt;PCAppStore&lt;/strong&gt; from running, my laptop finally behaved as expected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Display turns off after 3 minutes&lt;/li&gt;
&lt;li&gt;✅ Laptop enters sleep automatically&lt;/li&gt;
&lt;li&gt;✅ Wake-up works normally&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔐 Bonus: Require a PIN After Sleep
&lt;/h2&gt;

&lt;p&gt;After fixing sleep, I noticed another issue:&lt;/p&gt;

&lt;p&gt;The laptop woke directly to the desktop instead of asking for my PIN.&lt;/p&gt;

&lt;p&gt;To fix that:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Settings → Accounts → Sign-in options&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Configure:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If you've been away, when should Windows require you to sign in again?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Choose the shortest available delay (or &lt;strong&gt;Immediately&lt;/strong&gt;, depending on your Windows version).&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠 Useful Commands
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Check what's preventing sleep
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;powercfg /requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  View supported sleep states
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;powercfg /a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Generate an energy report
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;powercfg /energy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Generate a Sleep Study report
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;powercfg /sleepstudy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Show devices allowed to wake the PC
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;powercfg /devicequery wake_armed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  💡 Lessons Learned
&lt;/h2&gt;

&lt;p&gt;A few things I learned from this experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start with &lt;code&gt;powercfg /requests&lt;/code&gt; whenever Windows won't sleep.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;msedgewebview2.exe&lt;/code&gt; is often just the &lt;strong&gt;messenger&lt;/strong&gt;—you need to identify the parent application.&lt;/li&gt;
&lt;li&gt;Modern Standby systems are more susceptible to applications holding wake locks.&lt;/li&gt;
&lt;li&gt;Third-party utilities can unintentionally interfere with Windows power management.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Initially, I suspected Windows, Dell firmware, or Modern Standby itself.&lt;/p&gt;

&lt;p&gt;In reality, a background application was keeping the display awake by holding a &lt;strong&gt;WebView2 Video Wake Lock&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If your Windows laptop refuses to sleep despite correct power settings, don't overlook background applications.&lt;/p&gt;

&lt;p&gt;A single process may be the only thing standing between you and a properly sleeping system.&lt;/p&gt;

&lt;p&gt;Hopefully this saves someone else a few hours of troubleshooting.&lt;/p&gt;




&lt;h2&gt;
  
  
  Have you encountered a similar issue?
&lt;/h2&gt;

&lt;p&gt;I'd love to hear what prevented sleep on your machine.&lt;/p&gt;

&lt;p&gt;Share your experience in the comments!&lt;/p&gt;

&lt;h1&gt;
  
  
  Windows #Windows11 #PowerManagement #Troubleshooting #WebView2 #PowerShell #Dell #ModernStandby #DeveloperTips
&lt;/h1&gt;

</description>
      <category>hardware</category>
      <category>microsoft</category>
      <category>software</category>
    </item>
    <item>
      <title>Building a Secure Real-Time Messaging App with .NET 8 and Angular 18</title>
      <dc:creator>Naimul Karim</dc:creator>
      <pubDate>Fri, 22 May 2026 04:42:50 +0000</pubDate>
      <link>https://dev.to/naimulkarim/building-a-secure-real-time-messaging-app-with-net-8-and-angular-18-jdo</link>
      <guid>https://dev.to/naimulkarim/building-a-secure-real-time-messaging-app-with-net-8-and-angular-18-jdo</guid>
      <description>&lt;p&gt;&lt;em&gt;A deep dive into JWT authentication, AES-256-GCM encryption, SignalR, and production security patterns.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Real-time messaging apps are everywhere, but most tutorials gloss over the hard parts — the security. In this post I'll walk you through &lt;strong&gt;SecureChat&lt;/strong&gt;, a production-grade messaging app I built with .NET 8 and Angular 18. By the end you'll understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to encrypt messages at rest using &lt;strong&gt;AES-256-GCM&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;How to build a &lt;strong&gt;JWT + rotating refresh token&lt;/strong&gt; authentication flow&lt;/li&gt;
&lt;li&gt;How to wire up &lt;strong&gt;SignalR&lt;/strong&gt; with a Redis backplane for real-time delivery&lt;/li&gt;
&lt;li&gt;How to implement a &lt;strong&gt;silent token refresh interceptor&lt;/strong&gt; in Angular&lt;/li&gt;
&lt;li&gt;How to layer in &lt;strong&gt;rate limiting, security headers, and an audit trail&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full source is on GitHub: &lt;a href="https://github.com/naimulkarim/SecureChat" rel="noopener noreferrer"&gt;github.com/naimulkarim/SecureChat&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;Before we dive into code, here's the full picture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────┐
│             Angular 18 Frontend             │
│   Auth guard · Chat module · JWT interceptor│
└──────────────────┬──────────────────────────┘
                   │ HTTPS + WSS (TLS 1.3)
┌──────────────────▼──────────────────────────┐
│              .NET 8 Web API                 │
│  Auth API · Messages API · SignalR Hub      │
│  Rate limiting · JWT · Security headers     │
└────┬──────────────────┬───────────────┬─────┘
     │                  │               │
┌────▼────┐     ┌───────▼──────┐  ┌────▼─────┐
│SQL Server│     │    Redis     │  │  Azure   │
│Users/Msgs│     │Cache/Backpl. │  │  Blobs   │
└──────────┘     └──────────────┘  └──────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key design decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Short-lived JWTs (15 min)&lt;/strong&gt; limit the blast radius if a token is stolen&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotating refresh tokens&lt;/strong&gt; mean a stolen refresh token can only be used once&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AES-256-GCM&lt;/strong&gt; encrypts every message before it hits the database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis backplane&lt;/strong&gt; allows the SignalR hub to scale across multiple API instances&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-user SignalR groups&lt;/strong&gt; deliver messages to all of a user's devices simultaneously&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 1 — The .NET 8 Backend
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Setting up &lt;code&gt;Program.cs&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The entry point wires together every security concern. Let me break it down section by section.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JWT Authentication&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAuthentication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JwtBearerDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AuthenticationScheme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddJwtBearer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TokenValidationParameters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;TokenValidationParameters&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ValidateIssuerSigningKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;IssuerSigningKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SymmetricSecurityKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jwtKey&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
            &lt;span class="n"&gt;ValidateIssuer&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ValidIssuer&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Jwt:Issuer"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;ValidateAudience&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ValidAudience&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Jwt:Audience"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;ValidateLifetime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ClockSkew&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Zero&lt;/span&gt;   &lt;span class="c1"&gt;// ← no grace period on expiry&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="c1"&gt;// SignalR WebSocket upgrades can't send headers,&lt;/span&gt;
        &lt;span class="c1"&gt;// so we read the token from the query string instead&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Events&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;JwtBearerEvents&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;OnMessageReceived&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"access_token"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
                    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartsWithSegments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/hubs"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ClockSkew = TimeSpan.Zero&lt;/code&gt; is intentional. The default is 5 minutes, meaning a "15-minute" token actually lives for 20 minutes. That's an unnecessary exposure window.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;OnMessageReceived&lt;/code&gt; event is needed because WebSocket upgrade requests can't carry &lt;code&gt;Authorization&lt;/code&gt; headers — the browser spec doesn't allow it. So the Angular client appends the JWT as a query parameter only for hub connections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate Limiting&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRateLimiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddFixedWindowLimiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lim&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;lim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Window&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;lim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PermitLimit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddFixedWindowLimiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"auth"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lim&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;lim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Window&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;lim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PermitLimit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// max 10 login attempts per 5 min&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Auth endpoints get a stricter limit to slow down brute-force attacks. Message endpoints are generous enough for normal usage but will stop a script flooding the server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security Headers&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-Content-Type-Options"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"nosniff"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-Frame-Options"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="s"&gt;"DENY"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-XSS-Protection"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="s"&gt;"1; mode=block"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Referrer-Policy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="s"&gt;"no-referrer"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Security-Policy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"default-src 'self'; connect-src 'self' wss:"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These headers are cheap to add and block an entire category of attacks — MIME sniffing, clickjacking, and cross-site scripting.&lt;/p&gt;




&lt;h3&gt;
  
  
  Authentication Service — JWT + BCrypt + Refresh Tokens
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;AuthService&lt;/code&gt; handles the full auth lifecycle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Registration&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AuthResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;RegisterAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RegisterRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AnyAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToLower&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;AuthResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Email already registered."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Id&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;Email&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToLower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;DisplayName&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DisplayName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;PasswordHash&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BCrypt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BCrypt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HashPassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workFactor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;CreatedAt&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_audit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"auth.register"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;IssueTokensAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Work factor 12 for BCrypt means ~250ms to hash a password on modern hardware — fast enough for users, slow enough to make brute-forcing impractical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JWT Generation&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;GenerateJwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SymmetricSecurityKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Jwt:Key"&lt;/span&gt;&lt;span class="p"&gt;]!));&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;creds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SigningCredentials&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SecurityAlgorithms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HmacSha256&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;claims&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Claim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ClaimTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NameIdentifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Claim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ClaimTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Claim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ClaimTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DisplayName&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Claim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JwtRegisteredClaimNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Jti&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;JwtSecurityToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="n"&gt;_config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Jwt:Issuer"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;audience&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="n"&gt;_config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Jwt:Audience"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;            &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;signingCredentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;JwtSecurityTokenHandler&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;WriteToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Jti&lt;/code&gt; (JWT ID) claim is a unique identifier for each token. This is what lets you build a token blocklist if you ever need to revoke individual tokens before expiry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rotating Refresh Tokens&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AuthResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;RefreshAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stored&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RefreshTokens&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;refreshToken&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsRevoked&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stored&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExpiresAt&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;AuthResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Invalid or expired refresh token."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsRevoked&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// ← revoke old token immediately&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;IssueTokensAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// ← issue fresh pair&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rotation means every successful refresh invalidates the old token and issues a new one. If an attacker steals a refresh token and uses it, the legitimate user's next refresh will detect the conflict (token already revoked) and you can force a full re-login.&lt;/p&gt;




&lt;h3&gt;
  
  
  Encryption Service — AES-256-GCM
&lt;/h3&gt;

&lt;p&gt;This is the most security-critical piece of the backend.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;Encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;plaintext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Fresh 96-bit nonce for every single message&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;nonce&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AesGcm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NonceByteSizes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxSize&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="n"&gt;RandomNumberGenerator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;plaintextBytes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plaintext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ciphertext&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;plaintextBytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AesGcm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TagByteSizes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxSize&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// 128-bit auth tag&lt;/span&gt;

    &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;aes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AesGcm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;aes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plaintextBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Pack: [nonce 12B][tag 16B][ciphertext nB]&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;combined&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="n"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BlockCopy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;combined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                        &lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BlockCopy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;combined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BlockCopy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;combined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToBase64String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;combined&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why AES-256-GCM specifically?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AES-256&lt;/strong&gt; — the key length (256 bits) is computationally infeasible to brute-force&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GCM mode&lt;/strong&gt; — provides both confidentiality (encryption) and integrity (the 128-bit authentication tag). If anyone tampers with the ciphertext in the database, decryption throws an exception rather than silently returning garbage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fresh nonce per message&lt;/strong&gt; — reusing a nonce with the same key in GCM mode catastrophically breaks security. We use &lt;code&gt;RandomNumberGenerator.Fill&lt;/code&gt; (cryptographically secure) for each encryption call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The nonce and tag are packed together with the ciphertext so decryption is self-contained — no separate columns needed.&lt;/p&gt;




&lt;h3&gt;
  
  
  SignalR Hub
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Authorize&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatHub&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Hub&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;OnConnectedAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetUserId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="c1"&gt;// Each user gets a personal group across all their devices&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddToGroupAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConnectionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;$"user:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_audit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hub.connected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetHttpContext&lt;/span&gt;&lt;span class="p"&gt;()?.&lt;/span&gt;&lt;span class="n"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RemoteIpAddress&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnConnectedAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SendMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SendMessageRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HubException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Invalid message content."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;senderId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetUserId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;saved&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;senderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Fan out to every participant's group (covers multi-device)&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;participantId&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParticipantIds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"user:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;participantId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ReceiveMessage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SendTyping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;conversationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isTyping&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetUserId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;participants&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetParticipantsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conversationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;participantId&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;participants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"user:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;participantId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TypingIndicator"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;conversationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isTyping&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few design decisions worth noting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Personal groups vs connection IDs.&lt;/strong&gt; We add each connection to &lt;code&gt;user:{userId}&lt;/code&gt; instead of tracking individual &lt;code&gt;ConnectionId&lt;/code&gt; values. This means if a user has two browser tabs open, both get the message without any extra code. If you used &lt;code&gt;ConnectionId&lt;/code&gt; you'd need a lookup table mapping users to connections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typing indicators are never persisted.&lt;/strong&gt; They're pure ephemeral SignalR events — no database write, no message history. They disappear the moment the connection drops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;HubException&lt;/code&gt; for client-visible errors.&lt;/strong&gt; Regular C# exceptions become generic 500-style errors on the client. &lt;code&gt;HubException&lt;/code&gt; propagates the message string to the caller, which lets Angular show a human-readable error.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2 — The Angular 18 Frontend
&lt;/h2&gt;

&lt;h3&gt;
  
  
  JWT Interceptor with Silent Refresh
&lt;/h3&gt;

&lt;p&gt;This is the piece most tutorials get wrong. The naive approach — refresh on 401, retry — breaks badly when multiple requests expire simultaneously. Our interceptor queues concurrent requests while a refresh is in flight.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;isRefreshing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;refreshSubject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;BehaviorSubject&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authInterceptor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HttpInterceptorFn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AuthService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;addToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;setHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;addToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;catchError&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HttpErrorResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/auth/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;throwError&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isRefreshing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Another request is already refreshing — wait for it&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;refreshSubject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="nf"&gt;switchMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;addToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;isRefreshing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;refreshSubject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// signal "refreshing in progress"&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refresh&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;switchMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newToken&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;isRefreshing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="nx"&gt;refreshSubject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// unblock queued requests&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;addToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newToken&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="nf"&gt;catchError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refreshErr&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;isRefreshing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;throwError&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;refreshErr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Walk through the flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Request goes out with the current JWT&lt;/li&gt;
&lt;li&gt;Server returns 401 (token expired)&lt;/li&gt;
&lt;li&gt;If no refresh is in progress, start one and set &lt;code&gt;isRefreshing = true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;All other requests that 401 in the meantime subscribe to &lt;code&gt;refreshSubject&lt;/code&gt; and wait&lt;/li&gt;
&lt;li&gt;When refresh succeeds, &lt;code&gt;refreshSubject.next(newToken)&lt;/code&gt; unblocks all queued requests&lt;/li&gt;
&lt;li&gt;Each queued request clones itself with the new token and retries&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Without this queuing, three simultaneous expired requests would trigger three parallel refresh calls, the second and third would fail (token already rotated), and the user would be logged out unexpectedly.&lt;/p&gt;




&lt;h3&gt;
  
  
  SignalR Chat Service with Angular Signals
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;providedIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Reactive state using Angular 18 signals&lt;/span&gt;
  &lt;span class="nx"&gt;messages&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
  &lt;span class="nx"&gt;conversations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Conversation&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
  &lt;span class="nx"&gt;typingUsers&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TypingIndicator&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
  &lt;span class="nx"&gt;connected&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Derived state — no manual subscription management&lt;/span&gt;
  &lt;span class="nx"&gt;unreadCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;conversations&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unreadCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;signalR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HubConnectionBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/hubs/chat`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;accessTokenFactory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;signalR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HttpTransportType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WebSockets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;skipNegotiation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withAutomaticReconnect&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ReceiveMessage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msgs&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;msgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;conversations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;convs&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nx"&gt;convs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
          &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;conversationId&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lastMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;unreadCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unreadCount&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;accessTokenFactory&lt;/code&gt; is a function, not a value. SignalR calls it on every reconnect attempt, so it will always pick up the freshest token (including any that were silently refreshed by the interceptor).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;withAutomaticReconnect([0, 2000, 5000, 10000, 30000])&lt;/code&gt; defines a custom backoff schedule — immediate retry, then 2s, 5s, 10s, 30s. After exhausting this list SignalR stops retrying, which lets you show a "connection lost" UI rather than hammering the server forever.&lt;/p&gt;

&lt;p&gt;Using Angular signals instead of RxJS &lt;code&gt;BehaviorSubject&lt;/code&gt; keeps the state management simple — components read &lt;code&gt;chatService.messages()&lt;/code&gt; directly in their templates and Angular handles change detection automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3 — Security Checklist
&lt;/h2&gt;

&lt;p&gt;Here's every security mechanism in the app and why it's there:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threat&lt;/th&gt;
&lt;th&gt;Mitigation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Password database breach&lt;/td&gt;
&lt;td&gt;BCrypt, work factor 12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token theft&lt;/td&gt;
&lt;td&gt;JWT expiry 15 min, &lt;code&gt;ClockSkew = Zero&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refresh token theft&lt;/td&gt;
&lt;td&gt;Server-side rotation — stolen token is single-use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eavesdropping in transit&lt;/td&gt;
&lt;td&gt;HTTPS + WSS enforced, TLS 1.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database breach (messages)&lt;/td&gt;
&lt;td&gt;AES-256-GCM at rest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GCM nonce reuse&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RandomNumberGenerator.Fill&lt;/code&gt; per message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message tampering&lt;/td&gt;
&lt;td&gt;GCM authentication tag — tampered ciphertext throws&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brute-force login&lt;/td&gt;
&lt;td&gt;Rate limit: 10 attempts / 5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message flooding&lt;/td&gt;
&lt;td&gt;Rate limit: 60 messages / min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clickjacking&lt;/td&gt;
&lt;td&gt;&lt;code&gt;X-Frame-Options: DENY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MIME sniffing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;X-Content-Type-Options: nosniff&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unauthorized hub access&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;[Authorize]&lt;/code&gt; attribute, JWT validated on upgrade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hub conversation access&lt;/td&gt;
&lt;td&gt;Server-side participant check before join/deliver&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XSS&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Content-Security-Policy: default-src 'self'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forensics / incident response&lt;/td&gt;
&lt;td&gt;Append-only audit log on every auth event&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Part 4 — Running It Locally
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone&lt;/span&gt;
git clone https://github.com/naimulkarim/SecureChat.git
&lt;span class="nb"&gt;cd &lt;/span&gt;SecureChat

&lt;span class="c"&gt;# Generate secrets&lt;/span&gt;
openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 32   &lt;span class="c"&gt;# → Encryption:Key&lt;/span&gt;
openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 64   &lt;span class="c"&gt;# → Jwt:Key&lt;/span&gt;

&lt;span class="c"&gt;# Backend&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;SecureChat.API
&lt;span class="c"&gt;# Add secrets to appsettings.Development.json (never commit this file)&lt;/span&gt;
dotnet ef database update
dotnet run

&lt;span class="c"&gt;# Frontend (new terminal)&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../secure-chat-ui
npm &lt;span class="nb"&gt;install
&lt;/span&gt;ng serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Navigate to &lt;code&gt;https://localhost:4200&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;There are a few things I'd add before calling this truly production-ready:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key management&lt;/strong&gt; — move the AES key and JWT secret out of &lt;code&gt;appsettings.json&lt;/code&gt; and into Azure Key Vault (or AWS Secrets Manager). The current setup is fine for local dev but secrets should never live in a config file in a real deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client-side encryption&lt;/strong&gt; — the current model encrypts at the server before writing to the DB. True end-to-end encryption (where even the server can't read messages) requires encrypting on the client with the recipient's public key. This would involve WebCrypto API on the Angular side and a public key exchange flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Message deletion / expiry&lt;/strong&gt; — a GDPR-friendly implementation needs the ability to delete or expire messages, with the encrypted blobs actually removed from storage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Push notifications&lt;/strong&gt; — when the user isn't connected to the hub, they need push notifications. Web Push API + a service worker can deliver notifications even when the tab is closed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The patterns in this post — short-lived JWTs with rotating refresh tokens, AES-GCM encryption at rest, request queuing during token refresh, and per-user SignalR groups — are the foundations of any serious real-time app. None of them are particularly exotic, but getting all the details right (nonce freshness, clock skew, refresh token rotation, concurrent refresh queuing) makes the difference between a demo and something you'd trust with real user data.&lt;/p&gt;

&lt;p&gt;The full source is at &lt;a href="https://github.com/naimulkarim/SecureChat" rel="noopener noreferrer"&gt;github.com/naimulkarim/SecureChat&lt;/a&gt;. PRs and issues welcome.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: &lt;code&gt;dotnet&lt;/code&gt; &lt;code&gt;angular&lt;/code&gt; &lt;code&gt;security&lt;/code&gt; &lt;code&gt;signalr&lt;/code&gt; &lt;code&gt;webdev&lt;/code&gt; &lt;code&gt;csharp&lt;/code&gt; &lt;code&gt;typescript&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>angular</category>
      <category>dotnet</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>🔐 SecretVault Internals: How the Code Is Actually Implemented</title>
      <dc:creator>Naimul Karim</dc:creator>
      <pubDate>Tue, 19 May 2026 02:46:31 +0000</pubDate>
      <link>https://dev.to/naimulkarim/secretvault-internals-how-the-code-is-actually-implemented-2kon</link>
      <guid>https://dev.to/naimulkarim/secretvault-internals-how-the-code-is-actually-implemented-2kon</guid>
      <description>&lt;p&gt;This is the &lt;strong&gt;implementation companion&lt;/strong&gt; to the SecretVault introduction post. Where that post showed you &lt;em&gt;what&lt;/em&gt; SecretVault does, this one shows you &lt;em&gt;how it's built&lt;/em&gt; — every class, every design decision, and why things are structured the way they are.&lt;/p&gt;




&lt;h2&gt;
  
  
  Project Structure at a Glance
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SecretVault/
├── src/
│   ├── SecretVault.Core/                  # Abstractions, caching, chaining
│   │   ├── ISecretManager.cs
│   │   ├── ISecretRotationManager.cs
│   │   ├── Models.cs
│   │   ├── CachedSecretManager.cs
│   │   └── ChainedSecretManager.cs
│   ├── SecretVault.Aws/                   # AWS Secrets Manager
│   │   └── AwsSecretManager.cs
│   ├── SecretVault.Azure/                 # Azure Key Vault
│   │   └── AzureKeyVaultSecretManager.cs
│   ├── SecretVault.HashiCorp/             # HashiCorp Vault KV v2
│   │   └── HashiCorpVaultSecretManager.cs
│   ├── SecretVault.Gcp/                   # GCP Secret Manager
│   │   └── GcpSecretManager.cs
│   └── SecretVault.Extensions.Hosting/   # ASP.NET Core integration
│       ├── SecretVaultServiceExtensions.cs
│       └── SecretVaultHealthCheck.cs
└── tests/
    └── SecretVault.Core.Tests/
        └── CachedAndChainedTests.cs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The golden rule:&lt;/strong&gt; if a class only references &lt;code&gt;ISecretManager&lt;/code&gt; → it goes in &lt;code&gt;Core&lt;/code&gt;. If it references a provider SDK (AWS, Azure, etc.) → it goes in that provider's project.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1 — The Core Abstractions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;ISecretManager&lt;/code&gt; — The Unified Interface
&lt;/h3&gt;

&lt;p&gt;This is the only type your application code ever depends on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;SecretVault.Core&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ISecretManager&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// On-demand runtime fetching&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;GetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;DeleteSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Bulk fetch — useful at startup to warm secrets&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IReadOnlyDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetSecretsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;secretNames&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;string?&lt;/code&gt; and not &lt;code&gt;string&lt;/code&gt;?&lt;/strong&gt; Returning &lt;code&gt;null&lt;/code&gt; instead of throwing when a secret doesn't exist is a deliberate choice — it lets callers decide whether a missing secret is an error or an expected condition without forcing a try/catch everywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;Task&amp;lt;T?&amp;gt; GetSecretAsync&amp;lt;T&amp;gt;&lt;/code&gt;?&lt;/strong&gt; Many secrets are stored as JSON objects (e.g. a database config with host, port, and password together). The generic overload deserializes directly into your type so you don't have to &lt;code&gt;JsonSerializer.Deserialize&lt;/code&gt; manually on every call.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;code&gt;ISecretRotationManager&lt;/code&gt; — Rotation on Top
&lt;/h3&gt;

&lt;p&gt;Rotation is an optional capability — not every use case needs it. So instead of bloating &lt;code&gt;ISecretManager&lt;/code&gt;, we extend it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ISecretRotationManager&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ISecretManager&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SecretRotationResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;RotateSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;newValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;gracePeriod&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IReadOnlyList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SecretVersion&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ListVersionsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetSecretVersionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All four provider implementations (&lt;code&gt;AwsSecretManager&lt;/code&gt;, &lt;code&gt;AzureKeyVaultSecretManager&lt;/code&gt;, etc.) implement &lt;code&gt;ISecretRotationManager&lt;/code&gt; — which means they also satisfy &lt;code&gt;ISecretManager&lt;/code&gt;. You can inject either interface depending on what you need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The return types:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Returned from RotateSecretAsync — tells you what happened&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;SecretRotationResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;SecretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;NewVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;PreviousVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt; &lt;span class="n"&gt;RotatedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;GracePeriodEndsAt&lt;/span&gt;  &lt;span class="c1"&gt;// null if no grace period requested&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// One entry from ListVersionsAsync&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;SecretVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsCurrent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;C# records are used here because these are pure data carriers — immutable, with built-in equality and &lt;code&gt;ToString()&lt;/code&gt;. No behaviour, no setters.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Exception Hierarchy
&lt;/h3&gt;

&lt;p&gt;Provider SDKs throw completely different exceptions — &lt;code&gt;AmazonServiceException&lt;/code&gt;, &lt;code&gt;RequestFailedException&lt;/code&gt;, &lt;code&gt;RpcException&lt;/code&gt;. Your application shouldn't need to know which one it got. We normalize them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Base — all SecretVault errors derive from this&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SecretVaultException&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Secret doesn't exist in the provider&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SecretNotFoundException&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SecretVaultException&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;SecretName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;// carry the name so callers can log it&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// IAM / RBAC permissions failure&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SecretAccessDeniedException&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SecretVaultException&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Network outage or provider down&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SecretProviderUnavailableException&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SecretVaultException&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each provider catches its own SDK exception types and re-throws as the appropriate &lt;code&gt;SecretVaultException&lt;/code&gt;. Your application catches one hierarchy regardless of which provider is running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SecretNotFoundException&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* missing secret */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SecretAccessDeniedException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* permissions issue */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SecretProviderUnavailableException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* outage */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 2 — The AWS Implementation
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AwsSecretManager&lt;/code&gt; implements &lt;code&gt;ISecretRotationManager&lt;/code&gt; (which includes all of &lt;code&gt;ISecretManager&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AwsSecretManager&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ISecretRotationManager&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IAmazonSecretsManager&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AwsSecretManager&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;AwsSecretManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IAmazonSecretsManager&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AwsSecretManager&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;IAmazonSecretsManager&lt;/code&gt; not &lt;code&gt;AmazonSecretsManagerClient&lt;/code&gt;&lt;/strong&gt; — we take the interface, not the concrete class. This makes the provider testable via mocking without hitting real AWS.&lt;/p&gt;

&lt;h3&gt;
  
  
  GetSecretAsync
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogDebug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Fetching secret '{Name}' from AWS Secrets Manager"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSecretValueAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;GetSecretValueRequest&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;SecretId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// AWS can return either a string OR binary — handle both&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SecretString&lt;/span&gt;
            &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToBase64String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SecretBinary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ResourceNotFoundException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SecretNotFoundException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// normalize&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AmazonServiceException&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrorCode&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"AccessDeniedException"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SecretAccessDeniedException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// normalize&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SecretProviderUnavailableException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AWS Secrets Manager"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// normalize&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Exception filters (&lt;code&gt;when&lt;/code&gt;)&lt;/strong&gt; are used to catch only the specific AWS error codes we care about, letting other &lt;code&gt;AmazonServiceException&lt;/code&gt; subtypes fall through to the general handler.&lt;/p&gt;

&lt;h3&gt;
  
  
  SetSecretAsync — Create or Update
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PutSecretValueAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;PutSecretValueRequest&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;SecretId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;SecretString&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ResourceNotFoundException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Secret doesn't exist yet — create it first&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;CreateSecretRequest&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;SecretString&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AWS &lt;code&gt;PutSecretValue&lt;/code&gt; requires the secret to already exist. We catch &lt;code&gt;ResourceNotFoundException&lt;/code&gt; and create it automatically so callers don't have to think about whether a secret is new or existing.&lt;/p&gt;

&lt;h3&gt;
  
  
  RotateSecretAsync
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SecretRotationResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;RotateSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;newValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;gracePeriod&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Read the current version ID before rotating&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DescribeSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DescribeSecretRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;SecretId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;previousVersion&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VersionIdsToStages&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Write the new value, explicitly marking it as current&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PutSecretValueAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;PutSecretValueRequest&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;SecretId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;SecretString&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;VersionStages&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"AWSCURRENT"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Read back the new version ID&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DescribeSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DescribeSecretRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;SecretId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;newVersion&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VersionIdsToStages&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"unknown"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;rotatedAt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SecretRotationResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;SecretName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;NewVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;newVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;PreviousVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;previousVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;RotatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rotatedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;GracePeriodEndsAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;gracePeriod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasValue&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;rotatedAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gracePeriod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;AWSCURRENT&lt;/code&gt; stage label is how AWS tracks which version is active. The old version automatically moves to &lt;code&gt;AWSPREVIOUS&lt;/code&gt; — staying accessible during the grace period before it expires.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3 — CachedSecretManager (The Decorator Pattern)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CachedSecretManager&lt;/code&gt; is a &lt;strong&gt;decorator&lt;/strong&gt; — it wraps any &lt;code&gt;ISecretManager&lt;/code&gt; and adds caching without modifying the wrapped class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your App → CachedSecretManager → AwsSecretManager → AWS API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CachedSecretManager&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ISecretManager&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ISecretManager&lt;/span&gt; &lt;span class="n"&gt;_inner&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// the real provider&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IMemoryCache&lt;/span&gt; &lt;span class="n"&gt;_cache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CachedSecretManager&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;CachedSecretManagerOptions&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;CachedSecretManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ISecretManager&lt;/span&gt; &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;IMemoryCache&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CachedSecretManager&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CachedSecretManagerOptions&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_inner&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_cache&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_options&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CachedSecretManagerOptions&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// defaults to 5 min&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  GetSecretAsync — Cache-Aside Pattern
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cacheKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CacheKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// "secretvault:my-secret"&lt;/span&gt;

    &lt;span class="c1"&gt;// 1. Check cache first&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cacheKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogDebug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Cache hit for secret '{SecretName}'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Cache miss — fetch from the real provider&lt;/span&gt;
    &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogDebug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Cache miss for '{SecretName}', fetching from provider"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_inner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Store in cache (only if we got a value)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cacheKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;MemoryCacheEntryOptions&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;AbsoluteExpirationRelativeToNow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AbsoluteExpiration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;SlidingExpiration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SlidingExpiration&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the classic &lt;strong&gt;cache-aside&lt;/strong&gt; (lazy loading) pattern: populate the cache on demand, not upfront.&lt;/p&gt;

&lt;h3&gt;
  
  
  Write-Through Cache Invalidation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_inner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// write to provider first&lt;/span&gt;
    &lt;span class="n"&gt;_cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;CacheKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;                 &lt;span class="c1"&gt;// then invalidate cache&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why invalidate instead of update?&lt;/strong&gt; Updating the cache after a write seems logical but introduces a race condition — between the write and the cache update, another thread could read the old value. Invalidating forces the next read to fetch fresh from the provider, which is always safe.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bulk Fetch — Partial Cache Hits
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IReadOnlyDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetSecretsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;secretNames&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// secrets not in cache&lt;/span&gt;

    &lt;span class="c1"&gt;// 1. Check cache for each name&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;secretNames&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;CacheKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// cache hit&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
            &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;         &lt;span class="c1"&gt;// needs fetching&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Fetch only the ones that weren't cached&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;fetched&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_inner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSecretsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fetched&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;_cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;CacheKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AbsoluteExpiration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you request 10 secrets and 7 are cached, only 3 API calls go to the provider. The result dictionary combines both.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Options Class
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CachedSecretManagerOptions&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Expires exactly N minutes after caching, regardless of usage&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;AbsoluteExpiration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Resets the timer on every access — null means disabled&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;SlidingExpiration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can combine both: sliding keeps frequently-accessed secrets warm, while absolute prevents them from living in cache indefinitely even if constantly hit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4 — ChainedSecretManager (The Chain of Responsibility Pattern)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChainedSecretManager&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ISecretManager&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IReadOnlyList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ISecretManager&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_managers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;ChainedSecretManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ISecretManager&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;managers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ChainedSecretManager&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_managers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;managers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_managers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"At least one ISecretManager must be provided."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_managers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogDebug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Secret '{Name}' found in {Provider}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetType&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SecretNotFoundException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// Not found here — try next provider&lt;/span&gt;
                &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogDebug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Secret '{Name}' not found in {Provider}, trying next"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetType&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// Provider error — log and try next, don't crash&lt;/span&gt;
                &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogWarning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Error fetching '{Name}' from {Provider}, trying next"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetType&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Exhausted all providers&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SecretNotFoundException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Writes always go to the PRIMARY (first) provider only&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_managers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;SetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key design decisions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SecretNotFoundException&lt;/code&gt; from a provider means "not here, try next" — it's expected and swallowed&lt;/li&gt;
&lt;li&gt;Any other exception (network error, auth failure) is logged as a warning and the chain continues — one broken provider doesn't block fallbacks&lt;/li&gt;
&lt;li&gt;Writes go to &lt;code&gt;_managers[0]&lt;/code&gt; (primary) only — writing to all providers would create sync problems&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 5 — ASP.NET Core Integration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;AddSecretVault&lt;/code&gt; — The DI Extension Method
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IServiceCollection&lt;/span&gt; &lt;span class="nf"&gt;AddSecretVault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;IServiceCollection&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ISecretManager&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CachedSecretManagerOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;?&lt;/span&gt; &lt;span class="n"&gt;configureCaching&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;configureCaching&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMemoryCache&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Register the CachedSecretManager as a concrete singleton&lt;/span&gt;
        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CachedSecretManagerOptions&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nf"&gt;configureCaching&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// apply caller's configuration&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IMemoryCache&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CachedSecretManager&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;();&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CachedSecretManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="c1"&gt;// Map ISecretManager → CachedSecretManager&lt;/span&gt;
        &lt;span class="c1"&gt;// Any class that injects ISecretManager gets the cached version&lt;/span&gt;
        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ISecretManager&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
            &lt;span class="n"&gt;sp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CachedSecretManager&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// No caching — register the raw provider directly&lt;/span&gt;
        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ISecretManager&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key line is &lt;code&gt;services.AddSingleton&amp;lt;ISecretManager&amp;gt;(sp =&amp;gt; sp.GetRequiredService&amp;lt;CachedSecretManager&amp;gt;())&lt;/code&gt;. This creates an alias in the DI container — &lt;code&gt;ISecretManager&lt;/code&gt; resolves to &lt;code&gt;CachedSecretManager&lt;/code&gt;, which itself wraps &lt;code&gt;AwsSecretManager&lt;/code&gt;. Your services inject &lt;code&gt;ISecretManager&lt;/code&gt; and silently get three layers of behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;IConfiguration&lt;/code&gt; Integration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The source tells the config system how to build our provider&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SecretVaultConfigurationSource&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IConfigurationSource&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ISecretManager&lt;/span&gt; &lt;span class="n"&gt;_manager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_secretNames&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IConfigurationProvider&lt;/span&gt; &lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IConfigurationBuilder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SecretVaultConfigurationProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_manager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_secretNames&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// The provider does the actual loading&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SecretVaultConfigurationProvider&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ConfigurationProvider&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Fetch all secrets synchronously at startup&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSecretsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_secretNames&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;GetAwaiter&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;GetResult&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Store in the Data dictionary — this is what IConfiguration reads from&lt;/span&gt;
        &lt;span class="n"&gt;Data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kvp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;kvp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToDictionary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kvp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;kvp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kvp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;kvp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;!);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;.GetAwaiter().GetResult()&lt;/code&gt; instead of &lt;code&gt;await&lt;/code&gt;?&lt;/strong&gt; &lt;code&gt;IConfigurationProvider.Load()&lt;/code&gt; is synchronous — it's called during host startup before the async pipeline is running. This is one of the few legitimate places to block on async code in .NET.&lt;/p&gt;

&lt;h3&gt;
  
  
  Health Check
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SecretVaultHealthCheck&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IHealthCheck&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HealthCheckResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CheckHealthAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;HealthCheckContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"__healthcheck__"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;HealthCheckResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Healthy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Secret provider is reachable."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SecretNotFoundException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Provider is reachable — the probe secret just doesn't exist, which is fine&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;HealthCheckResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Healthy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Secret provider is reachable."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SecretProviderUnavailableException&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;HealthCheckResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Unhealthy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Secret provider is unavailable."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;HealthCheckResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Degraded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unexpected error."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;__healthcheck__&lt;/code&gt; probe secret almost certainly doesn't exist — and that's intentional. &lt;code&gt;SecretNotFoundException&lt;/code&gt; proves the provider is reachable and responding. Only &lt;code&gt;SecretProviderUnavailableException&lt;/code&gt; (network issue, auth failure) marks the check as unhealthy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 6 — Tests
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CachedSecretManagerTests&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ISecretManager&lt;/span&gt; &lt;span class="n"&gt;_inner&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Substitute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;For&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ISecretManager&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IMemoryCache&lt;/span&gt; &lt;span class="n"&gt;_cache&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MemoryCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MemoryCacheOptions&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;CachedSecretManager&lt;/span&gt; &lt;span class="n"&gt;_sut&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;GetSecretAsync_SecondCall_ReturnsCachedValue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_inner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"my-secret"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Returns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"super-secret"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_sut&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"my-secret"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// first call — hits inner&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_sut&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"my-secret"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// second call — from cache&lt;/span&gt;

        &lt;span class="c1"&gt;// Inner was only called ONCE despite two GetSecretAsync calls&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_inner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Received&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;GetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"my-secret"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SetSecretAsync_InvalidatesCache&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_inner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"my-secret"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Returns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"old-value"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"new-value"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_sut&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"my-secret"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;         &lt;span class="c1"&gt;// caches "old-value"&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_sut&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"my-secret"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"new-value"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// invalidates cache&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_sut&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"my-secret"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// re-fetches "new-value"&lt;/span&gt;

        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Should&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Be&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"new-value"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_inner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Received&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;GetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"my-secret"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChainedSecretManagerTests&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;GetSecretAsync_FallsBackToSecondary_WhenPrimaryThrowsNotFound&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_primary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Throws&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SecretNotFoundException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;_fallback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Returns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"from-fallback"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_sut&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Should&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Be&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"from-fallback"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;NSubstitute&lt;/strong&gt; is used for mocking — &lt;code&gt;Substitute.For&amp;lt;ISecretManager&amp;gt;()&lt;/code&gt; creates a fake implementation that returns configured values. &lt;strong&gt;FluentAssertions&lt;/strong&gt; makes assertions read like English. &lt;strong&gt;Real &lt;code&gt;MemoryCache&lt;/code&gt;&lt;/strong&gt; is used instead of a mock because testing with the actual cache catches real caching bugs that a mock would hide.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 7 — GitHub Actions CI/CD
&lt;/h2&gt;

&lt;h3&gt;
  
  
  CI (&lt;code&gt;ci.yml&lt;/code&gt;) — Every push and PR
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;develop&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build-and-test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-dotnet@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;dotnet-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;9.0.x'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dotnet restore SecretVault.sln&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dotnet build SecretVault.sln --configuration Release --no-restore&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dotnet test SecretVault.sln --configuration Release --no-build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Publish (&lt;code&gt;publish.yml&lt;/code&gt;) — Tag a version, ship everything
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v*.*.*'&lt;/span&gt;   &lt;span class="c1"&gt;# v1.0.0, v1.2.3, etc.&lt;/span&gt;

&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Extract version from tag&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;version&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "VERSION=${GITHUB_REF_NAME#v}" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;
    &lt;span class="c1"&gt;# v1.0.0 → 1.0.0 (strips the "v" prefix for NuGet)&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pack all packages&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;dotnet pack src/SecretVault.Core/SecretVault.Core.csproj \&lt;/span&gt;
        &lt;span class="s"&gt;--configuration Release \&lt;/span&gt;
        &lt;span class="s"&gt;/p:Version=${{ steps.version.outputs.VERSION }} \&lt;/span&gt;
        &lt;span class="s"&gt;--output ./nupkgs&lt;/span&gt;
      &lt;span class="s"&gt;# ... repeated for all 6 packages&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish to NuGet&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;dotnet nuget push ./nupkgs/*.nupkg \&lt;/span&gt;
        &lt;span class="s"&gt;--api-key ${{ secrets.NUGET_API_KEY }} \&lt;/span&gt;
        &lt;span class="s"&gt;--source https://api.nuget.org/v3/index.json \&lt;/span&gt;
        &lt;span class="s"&gt;--skip-duplicate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;${GITHUB_REF_NAME#v}&lt;/code&gt; is a bash parameter expansion — it strips the &lt;code&gt;v&lt;/code&gt; prefix from the tag name so &lt;code&gt;v1.0.0&lt;/code&gt; becomes &lt;code&gt;1.0.0&lt;/code&gt;, which is what NuGet expects for version numbers.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--skip-duplicate&lt;/code&gt; prevents the pipeline from failing if a package version was already published (e.g. if you re-run the workflow).&lt;/p&gt;

&lt;p&gt;To release:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git tag v1.0.0
git push origin v1.0.0
&lt;span class="c"&gt;# GitHub Actions handles the rest automatically&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Summary — Design Patterns Used
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Interface segregation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ISecretManager&lt;/code&gt; + &lt;code&gt;ISecretRotationManager&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Rotation is optional — don't force it on basic consumers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Decorator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CachedSecretManager&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Add caching without modifying providers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Chain of responsibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ChainedSecretManager&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fallback across providers without coupling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Factory via DI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AddSecretVault&lt;/code&gt; extension&lt;/td&gt;
&lt;td&gt;Compose the full object graph in one place&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Normalized exceptions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Per-provider catch/rethrow&lt;/td&gt;
&lt;td&gt;Shield callers from SDK-specific error types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cache-aside&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CachedSecretManager.GetSecretAsync&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lazy populate, invalidate on write&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Configuration provider&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SecretVaultConfigurationSource&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Plug into the standard .NET config pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;&lt;a href="https://github.com/YourUsername/SecretVault" rel="noopener noreferrer"&gt;View the full source on GitHub&lt;/a&gt; · &lt;a href="https://www.nuget.org/packages?q=SecretVault" rel="noopener noreferrer"&gt;NuGet Packages&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>architecture</category>
      <category>security</category>
    </item>
    <item>
      <title>I Built an AI-Powered Dead Code Detector for VS Code</title>
      <dc:creator>Naimul Karim</dc:creator>
      <pubDate>Tue, 12 May 2026 03:10:26 +0000</pubDate>
      <link>https://dev.to/naimulkarim/i-built-an-ai-powered-dead-code-detector-for-vs-code-and-it-goes-way-beyond-unused-imports-50dc</link>
      <guid>https://dev.to/naimulkarim/i-built-an-ai-powered-dead-code-detector-for-vs-code-and-it-goes-way-beyond-unused-imports-50dc</guid>
      <description>&lt;p&gt;We've all been there. You're reading through a codebase and you spot it — a feature flag that's been set to &lt;code&gt;false&lt;/code&gt; for two years, a function exported from a module that nothing ever imports, a block of logic sitting underneath a &lt;code&gt;return&lt;/code&gt; statement that will never, ever run.&lt;/p&gt;

&lt;p&gt;ESLint catches unused variables. TypeScript catches type mismatches. But &lt;strong&gt;dead business logic&lt;/strong&gt;? That's a different beast — and that's what I built this extension to hunt.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is "Dead Code" Really?
&lt;/h2&gt;

&lt;p&gt;Most linters think dead code = unused variable. But in production codebases the real offenders are:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Unreachable Logic&lt;/strong&gt;&lt;br&gt;
Code that can never execute — not because of a syntax issue, but because of your business logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Never reached — the return above always fires&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bonus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Applying bonus:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bonus&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;bonus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Unused APIs and Functions&lt;/strong&gt;&lt;br&gt;
Exported functions nobody calls. Classes defined and never instantiated. Entire modules orphaned after a refactor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Exported but nothing in the codebase ever calls this&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OldReportGenerator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;exportToCsv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Obsolete Feature Flags&lt;/strong&gt;&lt;br&gt;
The slowest-burning dead code. A flag gets hardcoded, the ticket gets closed, and the condition guards code that either always runs or never runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FEATURE_DARK_MODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// 🚩 Has been false for 18 months&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;USE_NEW_CHECKOUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// 🚩 Always true — the else branch is dead&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FEATURE_DARK_MODE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;applyDarkTheme&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;//  Never runs&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;USE_NEW_CHECKOUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;showNewCheckout&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;showLegacyCheckout&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;//  Never runs&lt;/span&gt;
  &lt;span class="nf"&gt;trackLegacyCheckoutEvent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Traditional static analysis tools struggle with these because they require understanding &lt;strong&gt;intent&lt;/strong&gt;, not just syntax.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: AI-Powered Analysis
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;Dead Code Detector&lt;/strong&gt; — a VS Code extension that sends your file to Claude (via the Anthropic API) with a carefully engineered prompt, gets back a structured JSON response, and renders the results as both inline diagnostics and a filterable results panel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/naimulkarim/dead-code-detector" rel="noopener noreferrer"&gt;github.com/naimulkarim/dead-code-detector&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's what it looks like in action:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔴 Inline squiggles in the editor (appears in your Problems panel)&lt;/li&gt;
&lt;li&gt;📊 A results panel that opens beside your file with filtering, severity badges, and jump-to-line&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Prompt Engineering
&lt;/h3&gt;

&lt;p&gt;The heart of the extension is the system prompt in &lt;code&gt;src/analyzer.ts&lt;/code&gt;. Getting an LLM to return reliable, parseable structured data requires being very explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SYSTEM_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`You are an expert static analysis tool specializing in dead code detection. 
Analyze the provided source code and identify:

1. **Unreachable Business Logic** — code after unconditional returns, inside impossible 
   conditions (e.g. if (false), if (1 === 2)), catch blocks that swallow everything, etc.

2. **Unused APIs / Functions** — exported or defined functions/classes/variables that appear 
   to never be called or referenced within the file.

3. **Obsolete Feature Flags** — boolean constants or config checks that are hardcoded to 
   true/false, e.g. const FEATURE_X = false used in conditions.

Respond ONLY with a JSON array (no markdown, no preamble). Each item must have:
{
  "category": "unreachable-logic" | "unused-api" | "obsolete-feature-flag" | "dead-code",
  "message": "Short one-line summary (&amp;lt; 80 chars)",
  "explanation": "2-3 sentences explaining why this is dead/obsolete code",
  "suggestion": "What to do — remove, refactor, or clean up",
  "line": &amp;lt;integer line number where the issue starts, or null&amp;gt;,
  "severity": "high" | "medium" | "low"
}

Return [] if no issues found.`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key decisions here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Respond ONLY with a JSON array"&lt;/strong&gt; — no preamble, no markdown fences in the model's thinking. We strip any stray fences client-side as a safety net.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit schema&lt;/strong&gt; — defining every field prevents the model from inventing its own structure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Return [] if no issues found"&lt;/strong&gt; — so a clean file doesn't produce a parse error.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Calling the API
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.anthropic.com/v1/messages&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-api-key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anthropic-version&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2023-06-01&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-sonnet-4-20250514&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userMessage&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Strip possible markdown fences before parsing&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/``&lt;/span&gt;&lt;span class="err"&gt;`
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;endraw&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="s2"&gt;```/g, "").trim();
const results = JSON.parse(clean);
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We cap the file at &lt;strong&gt;12,000 characters&lt;/strong&gt; before sending — enough for most real-world files without burning tokens unnecessarily.&lt;/p&gt;

&lt;h3&gt;
  
  
  Turning Results into VS Code Diagnostics
&lt;/h3&gt;

&lt;p&gt;Once we have the JSON array back, we map each issue to a &lt;code&gt;vscode.Diagnostic&lt;/code&gt; so it shows up inline in the editor and the Problems panel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyDiagnostics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TextDocument&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DeadCodeResult&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;diagnostics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lineAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lineCount&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;diag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Diagnostic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;`[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;] &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DiagnosticSeverity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Warning&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;diag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Dead Code AI&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;diag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;diag&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;diagnosticCollection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;diagnostics&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Results Panel
&lt;/h3&gt;

&lt;p&gt;Results also open in a Webview panel beside your editor. It's filterable by category and sortable by severity, with a &lt;strong&gt;Jump to Line&lt;/strong&gt; button on each card that snaps the cursor to the exact location in your file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The webview posts a message back to the extension host&lt;/span&gt;
&lt;span class="nx"&gt;ResultsPanel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentPanel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webview&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onDidReceiveMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jumpTo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;editor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visibleTextEditors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Selection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revealRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TextEditorRevealType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InCenter&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ Detects &lt;strong&gt;unreachable logic&lt;/strong&gt;, &lt;strong&gt;unused APIs&lt;/strong&gt;, and &lt;strong&gt;obsolete feature flags&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;✅ Works on &lt;strong&gt;JS, TS, JSX, TSX, and Python&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Inline squiggles&lt;/strong&gt; in the editor via VS Code's Diagnostics API&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Filterable results panel&lt;/strong&gt; with severity badges and jump-to-line&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Workspace-wide scan&lt;/strong&gt; (with a confirmation prompt for large repos)&lt;/li&gt;
&lt;li&gt;✅ Configurable severity level (&lt;code&gt;error&lt;/code&gt; / &lt;code&gt;warning&lt;/code&gt; / &lt;code&gt;information&lt;/code&gt; / &lt;code&gt;hint&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;✅ Toggle individual checks on/off in settings&lt;/li&gt;
&lt;li&gt;✅ Works via environment variable (&lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt;) or Settings UI&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Installation &amp;amp; Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node.js&lt;/strong&gt; v18+&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VS Code&lt;/strong&gt; 1.85+&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;Anthropic API key&lt;/strong&gt; — get one at &lt;a href="https://console.anthropic.com" rel="noopener noreferrer"&gt;console.anthropic.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  From GitHub
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone the repo&lt;/span&gt;
git clone https://github.com/naimulkarim/dead-code-detector.git
&lt;span class="nb"&gt;cd &lt;/span&gt;dead-code-detector

&lt;span class="c"&gt;# Install dependencies&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# Open in VS Code&lt;/span&gt;
code &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then press &lt;strong&gt;F5&lt;/strong&gt; — this opens a new Extension Development Host window with the extension loaded.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add Your API Key
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Option A — Settings UI (recommended):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open Settings (&lt;code&gt;Cmd+,&lt;/code&gt; on Mac / &lt;code&gt;Ctrl+,&lt;/code&gt; on Windows)&lt;/li&gt;
&lt;li&gt;Search for &lt;code&gt;Dead Code&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Paste your key into &lt;strong&gt;Dead Code Detector: Anthropic Api Key&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Option B — Environment variable:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-ant-api03-...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Shortcut / How&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Analyze current file&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Cmd+Shift+D&lt;/code&gt; / &lt;code&gt;Ctrl+Shift+D&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analyze current file&lt;/td&gt;
&lt;td&gt;Right-click in editor → &lt;em&gt;Dead Code: Analyze Current File&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analyze entire workspace&lt;/td&gt;
&lt;td&gt;Command Palette → &lt;em&gt;Dead Code: Analyze Entire Workspace&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clear diagnostics&lt;/td&gt;
&lt;td&gt;Command Palette → &lt;em&gt;Dead Code: Clear All Diagnostics&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Test It Immediately
&lt;/h3&gt;

&lt;p&gt;The repo includes &lt;code&gt;sample-test.ts&lt;/code&gt; — a file pre-loaded with intentional dead code of all three types. Open it and hit &lt;code&gt;Cmd+Shift+D&lt;/code&gt; to see the extension in action right away.&lt;/p&gt;




&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;.vscode/settings.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;settings&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"deadCode.anthropicApiKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sk-ant-..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"deadCode.severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"warning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"deadCode.checks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"unreachableLogic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"unusedApis"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"obsoleteFeatureFlags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User triggers Cmd+Shift+D
        ↓
extension.ts
  → reads active document text
  → reads enabled checks from config
        ↓
analyzer.ts
  → builds user prompt with language + filename + code
  → POST /v1/messages → Claude Sonnet
  → strips markdown fences, JSON.parse()
  → returns DeadCodeResult[]
        ↓
extension.ts
  → vscode.DiagnosticCollection.set() → squiggles in editor
        ↓
resultsPanel.ts
  → Webview HTML panel with filter buttons + severity cards
  → postMessage() → jump-to-line on click
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;A few things I want to add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;Quick Fix actions&lt;/strong&gt; — a code action that lets you delete the dead code with one click&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Sidebar tree view&lt;/strong&gt; — persistent panel showing all issues across the workspace&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Git blame integration&lt;/strong&gt; — show who added the dead code and when&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Caching&lt;/strong&gt; — skip re-analyzing files that haven't changed since last scan&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;More languages&lt;/strong&gt; — Go, Ruby, Rust&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PRs and issues are very welcome at &lt;a href="https://github.com/naimulkarim/dead-code-detector" rel="noopener noreferrer"&gt;github.com/naimulkarim/dead-code-detector&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🐙 &lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/naimulkarim/dead-code-detector" rel="noopener noreferrer"&gt;github.com/naimulkarim/dead-code-detector&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🤖 &lt;strong&gt;Anthropic API docs:&lt;/strong&gt; &lt;a href="https://docs.anthropic.com" rel="noopener noreferrer"&gt;docs.anthropic.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🔑 &lt;strong&gt;Get an API key:&lt;/strong&gt; &lt;a href="https://console.anthropic.com" rel="noopener noreferrer"&gt;console.anthropic.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;VS Code Extension API:&lt;/strong&gt; &lt;a href="https://code.visualstudio.com/api" rel="noopener noreferrer"&gt;code.visualstudio.com/api&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>vscode</category>
      <category>typescript</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
