<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[SDLC Corp Software Development]]></title><description><![CDATA[SDLC Corp Software Development]]></description><link>https://sdlccorp-softwaredev.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 07 Sep 2026 11:47:54 GMT</lastBuildDate><atom:link href="https://sdlccorp-softwaredev.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Progressive Image Loading From Scratch]]></title><description><![CDATA[Large images can make a page feel slow even when the rest of the interface loads quickly.


Progressive image loading improves perceived performance by showing a lightweight placeholder first and repl]]></description><link>https://sdlccorp-softwaredev.hashnode.dev/progressive-image-loading-from-scratch</link><guid isPermaLink="true">https://sdlccorp-softwaredev.hashnode.dev/progressive-image-loading-from-scratch</guid><dc:creator><![CDATA[Samcorp]]></dc:creator><pubDate>Thu, 03 Sep 2026 10:39:51 GMT</pubDate><content:encoded><![CDATA[<p>Large images can make a page feel slow even when the rest of the interface loads quickly.</p>
<img src="https://cdn.hashnode.com/uploads/covers/685a6d69d4d0ea366852534c/d384d2ed-ff63-4da9-bcc2-7ae158266073.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Progressive image loading</strong> improves perceived performance by showing a lightweight placeholder first and replacing it with the full-quality image once it has downloaded and decoded.</p>
<p>The typical flow looks like this:</p>
<pre><code class="language-text">Tiny Placeholder
      ↓
Full Image Downloads
      ↓
Image Decodes
      ↓
Smooth Fade-In
</code></pre>
<p>In this tutorial, we will build that behavior using plain HTML, CSS, and JavaScript.</p>
<hr />
<h2>What Is Progressive Image Loading?</h2>
<p>Progressive image loading does not necessarily make the final image file smaller. Instead, it improves the loading experience.</p>
<p>Without progressive loading:</p>
<pre><code class="language-text">Blank Area
    ↓
Wait
    ↓
Large Image Appears
</code></pre>
<p>With progressive loading:</p>
<pre><code class="language-text">Small Blurred Preview
        ↓
Full Image Loads
        ↓
Sharp Image Replaces Preview
</code></pre>
<p>This approach is sometimes called the <strong>blur-up technique</strong>.</p>
<p>It works especially well for:</p>
<ul>
<li><p>Blog images</p>
</li>
<li><p>Product galleries</p>
</li>
<li><p>Portfolio pages</p>
</li>
<li><p>Image-heavy landing pages</p>
</li>
<li><p>Ecommerce cards</p>
</li>
</ul>
<hr />
<h2>Step 1: Create the HTML</h2>
<p>Start with a wrapper containing a low-resolution placeholder and the full image.</p>
<pre><code class="language-html">&lt;div class="progressive-image"&gt;
  &lt;img
    class="placeholder"
    src="images/mountain-small.jpg"
    alt=""
    aria-hidden="true"
  /&gt;

  &lt;img
    class="full-image"
    src="images/mountain-large.jpg"
    alt="Snow-covered mountains at sunset"
    width="1200"
    height="800"
  /&gt;
&lt;/div&gt;
</code></pre>
<p>The first image should be extremely small.</p>
<p>For example:</p>
<pre><code class="language-text">mountain-small.jpg → 20–50 px wide
mountain-large.jpg → 1200 px wide
</code></pre>
<p>The tiny version downloads almost immediately.</p>
<p>The explicit <code>width</code> and <code>height</code> attributes are important because they reserve space before the image finishes loading and help prevent layout shifts.</p>
<hr />
<h2>Step 2: Layer the Images With CSS</h2>
<p>Now place both images in the same area.</p>
<pre><code class="language-css">.progressive-image {
  position: relative;
  overflow: hidden;
  aspect-ratio: 3 / 2;
  background: #eee;
}

.progressive-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.placeholder {
  position: absolute;
  inset: 0;
  filter: blur(18px);
  transform: scale(1.08);
}

.full-image {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 350ms ease;
}
</code></pre>
<p>The placeholder is intentionally blurred.</p>
<p>The slight scaling:</p>
<pre><code class="language-css">transform: scale(1.08);
</code></pre>
<p>helps hide blurry edges created by the CSS filter.</p>
<hr />
<h2>Step 3: Reveal the Full Image When It Loads</h2>
<p>Add a class after the high-resolution image is ready.</p>
<pre><code class="language-javascript">document
  .querySelectorAll(".progressive-image")
  .forEach((wrapper) =&gt; {
    const image = wrapper.querySelector(".full-image");

    image.addEventListener("load", () =&gt; {
      image.classList.add("loaded");
    });
  });
</code></pre>
<p>Then add the following CSS:</p>
<pre><code class="language-css">.full-image.loaded {
  opacity: 1;
}
</code></pre>
<p>Now the loading sequence becomes:</p>
<pre><code class="language-text">Blurred Preview
      ↓
Large Image Loads
      ↓
opacity: 0 → 1
      ↓
Sharp Image
</code></pre>
<hr />
<h2>Step 4: Wait for Image Decoding</h2>
<p>The network request finishing does not always mean the browser has finished decoding the image.</p>
<p>For smoother replacement, use:</p>
<pre><code class="language-javascript">image.decode()
</code></pre>
<p>The <code>decode()</code> method returns a promise that resolves after the image has been decoded and is ready to render.</p>
<p>Update the JavaScript:</p>
<pre><code class="language-javascript">document
  .querySelectorAll(".progressive-image")
  .forEach(async (wrapper) =&gt; {
    const image = wrapper.querySelector(".full-image");

    try {
      if (!image.complete) {
        await new Promise((resolve, reject) =&gt; {
          image.addEventListener(
            "load",
            resolve,
            { once: true }
          );

          image.addEventListener(
            "error",
            reject,
            { once: true }
          );
        });
      }

      await image.decode();

      image.classList.add("loaded");
    } catch {
      wrapper.classList.add("image-error");
    }
  });
</code></pre>
<p>This avoids fading in an image before it is actually ready to paint.</p>
<hr />
<h2>Step 5: Lazy Load Images Below the Fold</h2>
<p>For images that are not visible immediately, let the browser delay the full-resolution request.</p>
<p>Add:</p>
<pre><code class="language-html">&lt;img
  class="full-image"
  src="images/mountain-large.jpg"
  loading="lazy"
  decoding="async"
  alt="Snow-covered mountains at sunset"
  width="1200"
  height="800"
/&gt;
</code></pre>
<p>Native <code>loading="lazy"</code> tells the browser that off-screen images can be deferred until they approach the viewport.</p>
<p>Use it primarily for images <strong>below the fold</strong>.</p>
<hr />
<h2>Do Not Lazy Load Your Hero Image</h2>
<p>If an image is visible immediately and may become your Largest Contentful Paint element, lazy loading can delay it unnecessarily.</p>
<p>For a hero image, use:</p>
<pre><code class="language-html">&lt;img
  src="images/hero.jpg"
  alt="Product dashboard"
  width="1600"
  height="900"
  fetchpriority="high"
/&gt;
</code></pre>
<p><code>fetchpriority="high"</code> tells the browser that the image is relatively important. This hint should be used sparingly because assigning high priority to too many resources can reduce its effectiveness.</p>
<p>A simple rule is:</p>
<pre><code class="language-text">Hero / LCP Image
→ eager + fetchpriority="high"

Below-the-fold Images
→ loading="lazy"
</code></pre>
<hr />
<h2>Step 6: Add Responsive Images</h2>
<p>Progressive loading should not mean sending a 2000-pixel image to every device.</p>
<p>Use <code>srcset</code>:</p>
<pre><code class="language-html">&lt;img
  class="full-image"
  src="images/mountain-1200.jpg"
  srcset="
    images/mountain-480.jpg 480w,
    images/mountain-800.jpg 800w,
    images/mountain-1200.jpg 1200w
  "
  sizes="
    (max-width: 600px) 100vw,
    800px
  "
  loading="lazy"
  decoding="async"
  width="1200"
  height="800"
  alt="Snow-covered mountains at sunset"
/&gt;
</code></pre>
<p>The browser can now choose the most appropriate image based on the display size and device pixel ratio.</p>
<p>Using <code>srcset</code> and <code>sizes</code> helps prevent browsers from downloading unnecessarily large images on smaller devices.</p>
<p>Responsive image handling is also an important part of <a href="https://sdlccorp.com/website-development-company/">performance-focused web development</a>, especially when teams are optimizing Core Web Vitals, mobile layouts, and page-load speed.</p>
<hr />
<h2>Step 7: Remove the Placeholder After Loading</h2>
<p>Once the full image is visible, the placeholder is no longer necessary.</p>
<p>Update the code:</p>
<pre><code class="language-javascript">await image.decode();

image.classList.add("loaded");

image.addEventListener(
  "transitionend",
  () =&gt; {
    const placeholder =
      wrapper.querySelector(".placeholder");

    placeholder?.remove();
  },
  { once: true }
);
</code></pre>
<p>Now the browser can discard the unnecessary placeholder element after the transition finishes.</p>
<hr />
<h2>Complete Progressive Image Loading Example</h2>
<h3>HTML</h3>
<pre><code class="language-html">&lt;div class="progressive-image"&gt;
  &lt;img
    class="placeholder"
    src="images/mountain-small.jpg"
    alt=""
    aria-hidden="true"
  /&gt;

  &lt;img
    class="full-image"
    src="images/mountain-1200.jpg"
    srcset="
      images/mountain-480.jpg 480w,
      images/mountain-800.jpg 800w,
      images/mountain-1200.jpg 1200w
    "
    sizes="
      (max-width: 600px) 100vw,
      800px
    "
    loading="lazy"
    decoding="async"
    width="1200"
    height="800"
    alt="Snow-covered mountains at sunset"
  /&gt;
&lt;/div&gt;
</code></pre>
<h3>CSS</h3>
<pre><code class="language-css">.progressive-image {
  position: relative;
  overflow: hidden;
  aspect-ratio: 3 / 2;
  background: #eee;
}

.progressive-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.placeholder {
  position: absolute;
  inset: 0;
  filter: blur(18px);
  transform: scale(1.08);
}

.full-image {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 350ms ease;
}

.full-image.loaded {
  opacity: 1;
}
</code></pre>
<h3>JavaScript</h3>
<pre><code class="language-javascript">document
  .querySelectorAll(".progressive-image")
  .forEach(async (wrapper) =&gt; {
    const image = wrapper.querySelector(".full-image");

    try {
      if (!image.complete) {
        await new Promise((resolve, reject) =&gt; {
          image.addEventListener(
            "load",
            resolve,
            { once: true }
          );

          image.addEventListener(
            "error",
            reject,
            { once: true }
          );
        });
      }

      await image.decode();

      image.classList.add("loaded");

      image.addEventListener(
        "transitionend",
        () =&gt; {
          wrapper
            .querySelector(".placeholder")
            ?.remove();
        },
        { once: true }
      );
    } catch {
      wrapper.classList.add("image-error");
    }
  });
</code></pre>
<hr />
<h2>What About JavaScript-Based Lazy Loading?</h2>
<p>Older implementations often used <code>IntersectionObserver</code> to decide when to assign an image URL.</p>
<p>That approach still has valid use cases when you need custom loading behavior, but native:</p>
<pre><code class="language-html">loading="lazy"
</code></pre>
<p>is usually simpler for standard image lazy loading.</p>
<p>Intersection Observer remains useful when you need more control over exactly when elements enter or leave the viewport.</p>
<p>Avoid adding custom JavaScript when the browser already provides the behavior you need.</p>
<hr />
<h2>Progressive Loading vs Progressive JPEG</h2>
<p>These two concepts are related but different.</p>
<p><strong>Progressive JPEG</strong> encodes one image so it appears in multiple visual passes while downloading.</p>
<p>The technique used in this tutorial relies on:</p>
<pre><code class="language-text">Tiny Separate Image
        +
Full Resolution Image
        +
CSS Transition
</code></pre>
<p>This gives developers more control over:</p>
<ul>
<li><p>Placeholders</p>
</li>
<li><p>Responsive images</p>
</li>
<li><p>Lazy loading</p>
</li>
<li><p>Image decoding</p>
</li>
<li><p>Transition behavior</p>
</li>
</ul>
<hr />
<h2>Common Progressive Image Loading Mistakes</h2>
<p>Avoid these common issues:</p>
<ul>
<li><p>Lazy loading the main hero or LCP image</p>
</li>
<li><p>Serving the same huge image to every device</p>
</li>
<li><p>Omitting <code>width</code> and <code>height</code></p>
</li>
<li><p>Using a placeholder that is still hundreds of kilobytes</p>
</li>
<li><p>Applying excessive blur effects</p>
</li>
<li><p>Loading the full image twice</p>
</li>
<li><p>Waiting only for <code>load</code> when smooth decoding matters</p>
</li>
<li><p>Forgetting meaningful <code>alt</code> text</p>
</li>
<li><p>Writing complex JavaScript when native lazy loading is enough</p>
</li>
</ul>
<p>The goal is not to make the implementation complicated.</p>
<p>The goal is to improve <strong>perceived loading speed without harming actual performance</strong>.</p>
<p>A strong <a href="https://sdlccorp.com/ui-ux-design-company/">frontend user experience strategy</a> <a href="https://sdlccorp.com/ui-ux-design-company/"></a>should balance visual quality with performance so placeholders, responsive layouts, and loading states improve the experience instead of distracting users.</p>
<hr />
<h2>Recommended Image Loading Strategy</h2>
<p>A practical setup looks like this:</p>
<h3>Above-the-Fold Images</h3>
<pre><code class="language-text">Above the Fold
      ↓
Responsive Image
      ↓
fetchpriority="high"
      ↓
Display Immediately
</code></pre>
<h3>Below-the-Fold Images</h3>
<pre><code class="language-text">Below the Fold
      ↓
Tiny Placeholder
      ↓
loading="lazy"
      ↓
Responsive Full Image
      ↓
decode()
      ↓
Fade In
</code></pre>
<p>This approach prioritizes important visual content while delaying resources that users do not immediately need.</p>
<hr />
<h2>Final Thoughts</h2>
<p>Good <strong>progressive image loading</strong> combines several small techniques rather than relying on a single trick.</p>
<p>Use a lightweight placeholder to give users immediate visual feedback. Reserve image dimensions to prevent layout shifts. Let native lazy loading defer non-critical images, use responsive sources to avoid unnecessary downloads, and wait for decoding before revealing the final image.</p>
<p>The simplest rule is:</p>
<pre><code class="language-text">Load important images early.
Defer unimportant images.
Never download more image data than necessary.
</code></pre>
<p>Following these principles gives users a smoother image-loading experience without introducing a heavy image-loading library.</p>
]]></content:encoded></item><item><title><![CDATA[Structured Output Parsing That Doesn't Break]]></title><description><![CDATA[Getting an LLM to return JSON is easy, Getting it to return JSON that your application can safely depend on is harder.


A production LLM structured output pipeline needs more than a prompt saying:
Re]]></description><link>https://sdlccorp-softwaredev.hashnode.dev/structured-output-parsing-that-doesn-t-break</link><guid isPermaLink="true">https://sdlccorp-softwaredev.hashnode.dev/structured-output-parsing-that-doesn-t-break</guid><dc:creator><![CDATA[Samcorp]]></dc:creator><pubDate>Wed, 02 Sep 2026 09:17:33 GMT</pubDate><content:encoded><![CDATA[<p>Getting an LLM to return JSON is easy, Getting it to return JSON that your application can safely depend on is harder.</p>
<img src="https://cdn.hashnode.com/uploads/covers/685a6d69d4d0ea366852534c/65122e1b-bf64-4ba6-99de-43abfbcfa87b.png" alt="" style="display:block;margin:0 auto" />

<p>A production <strong>LLM structured output</strong> pipeline needs more than a prompt saying:</p>
<pre><code class="language-text">Return valid JSON.
</code></pre>
<p>You need a schema, validation, error handling, retry rules, and a clear strategy for incomplete responses and refusals.</p>
<p>In this tutorial, we'll build a safer structured-output flow using Python, Pydantic, and schema-constrained model output.</p>
<hr />
<h2>Why Plain JSON Prompts Break</h2>
<p>A common approach looks like this:</p>
<pre><code class="language-python">prompt = """
Extract the support ticket.

Return JSON with:
- category
- priority
- summary
"""
</code></pre>
<p>The response might be:</p>
<pre><code class="language-json">{
  "category": "billing",
  "priority": "high",
  "summary": "Customer was charged twice."
}
</code></pre>
<p>Looks fine.</p>
<p>But another request might return:</p>
<pre><code class="language-text">Here is the JSON you requested:

{
  "category": "billing",
  "priority": "urgent",
  "summary": "Customer was charged twice."
}
</code></pre>
<p>Now you have several problems:</p>
<ul>
<li><p>Extra prose before the JSON</p>
</li>
<li><p>An unexpected value such as <code>urgent</code></p>
</li>
<li><p>Missing fields</p>
</li>
<li><p>Wrong data types</p>
</li>
<li><p>Partial JSON after token truncation</p>
</li>
<li><p>Fields your application never expected</p>
</li>
</ul>
<p>The fix is not a more aggressive regex.</p>
<p>The fix is to treat model output like any other <strong>untrusted external input</strong>.</p>
<hr />
<h2>Step 1: Define the Contract First</h2>
<p>Start with the shape your application needs.</p>
<p>Using Pydantic:</p>
<pre><code class="language-python">from typing import Literal
from pydantic import BaseModel


class SupportTicket(BaseModel):
    category: Literal[
        "billing",
        "account",
        "technical",
        "other"
    ]

    priority: Literal[
        "low",
        "medium",
        "high"
    ]

    summary: str
    customer_name: str | None
</code></pre>
<p>Now your application has a clear contract.</p>
<h3>Valid</h3>
<pre><code class="language-json">{
  "category": "billing",
  "priority": "high",
  "summary": "Customer was charged twice.",
  "customer_name": "Alex"
}
</code></pre>
<h3>Invalid</h3>
<pre><code class="language-json">{
  "category": "payments",
  "priority": "urgent"
}
</code></pre>
<p>The schema is now the source of truth—not the prompt.</p>
<hr />
<h2>Step 2: Use Schema-Constrained Output</h2>
<p>Install the SDKs:</p>
<pre><code class="language-bash">pip install openai pydantic
</code></pre>
<p>Then ask the model to return the Pydantic structure directly:</p>
<pre><code class="language-python">from openai import OpenAI

client = OpenAI()

response = client.responses.parse(
    model="gpt-5.6",
    input=[
        {
            "role": "system",
            "content": (
                "Extract the support ticket "
                "information from the message."
            ),
        },
        {
            "role": "user",
            "content": (
                "Alex says their card was charged "
                "twice for the same subscription."
            ),
        },
    ],
    text_format=SupportTicket,
)

ticket = response.output_parsed

print(ticket)
</code></pre>
<p>The current OpenAI SDK can parse Structured Outputs directly into Pydantic models, reducing the need for manual <code>json.loads()</code> plumbing.</p>
<p>Conceptually:</p>
<pre><code class="language-text">User Input
    ↓
LLM
    ↓
JSON Schema
    ↓
Structured Output
    ↓
Typed Application Object
</code></pre>
<hr />
<h2>Step 3: Do Not Confuse JSON With Valid Data</h2>
<p>These are different guarantees:</p>
<pre><code class="language-text">Valid JSON
≠
Correct Schema
≠
Correct Business Data
</code></pre>
<p>For example:</p>
<pre><code class="language-json">{
  "category": "billing",
  "priority": "high",
  "summary": "Refund requested.",
  "customer_name": null
}
</code></pre>
<p>This may perfectly match the schema.</p>
<p>But perhaps your system requires a customer ID before a refund workflow can start.</p>
<p>Schema validation cannot know every business rule.</p>
<p>Use another validation layer:</p>
<pre><code class="language-python">def validate_ticket(ticket: SupportTicket):
    if (
        ticket.category == "billing"
        and not ticket.summary.strip()
    ):
        raise ValueError(
            "Billing tickets require a summary."
        )
</code></pre>
<p>Think in three layers:</p>
<pre><code class="language-text">Syntax validation
      ↓
Schema validation
      ↓
Business validation
</code></pre>
<p>Teams building production-grade LLM workflows can also explore <a href="https://sdlccorp.com/generative-ai-development-services/">custom generative AI development</a> for output validation, guardrails, model integration, and reliable AI application delivery.</p>
<hr />
<h2>Step 4: Handle Nullable Fields Explicitly</h2>
<p>Do not ask the model to invent missing information.</p>
<p>If a customer name may not exist, model that explicitly:</p>
<pre><code class="language-python">customer_name: str | None
</code></pre>
<p>Then:</p>
<pre><code class="language-json">{
  "customer_name": null
}
</code></pre>
<p>is better than:</p>
<pre><code class="language-json">{
  "customer_name": "Unknown User"
}
</code></pre>
<p>unless <code>"Unknown User"</code> has a real meaning in your system.</p>
<p>For current OpenAI Structured Outputs, schema fields are required; optional behavior can be represented using a nullable type.</p>
<p>That creates a useful rule:</p>
<blockquote>
<p>Missing information should be represented as missing—not guessed.</p>
</blockquote>
<hr />
<h2>Step 5: Handle Refusals Separately</h2>
<p>Even when you request structured output, the model may refuse some user inputs.</p>
<p>A refusal should not be treated as malformed JSON.</p>
<p>Current OpenAI responses expose refusals separately because a refusal does not necessarily follow your requested schema.</p>
<p>A simple helper:</p>
<pre><code class="language-python">def find_refusal(response):
    for output in response.output:
        if output.type != "message":
            continue

        for item in output.content:
            if item.type == "refusal":
                return item.refusal

    return None
</code></pre>
<p>Then:</p>
<pre><code class="language-python">refusal = find_refusal(response)

if refusal:
    print("Request refused:", refusal)
else:
    ticket = response.output_parsed
</code></pre>
<p>Do not automatically retry a refusal as though it were a parsing error.</p>
<hr />
<h2>Step 6: Detect Incomplete Responses</h2>
<p>Structured output can still fail if generation is interrupted.</p>
<p>For example:</p>
<pre><code class="language-text">Token limit reached
        ↓
Response stops early
        ↓
Structured object incomplete
</code></pre>
<p>OpenAI's documentation explicitly recommends checking for incomplete responses, including cases where the maximum output-token limit was reached.</p>
<p>Use a guard:</p>
<pre><code class="language-python">if response.status == "incomplete":
    reason = response.incomplete_details.reason

    raise RuntimeError(
        f"Incomplete model response: {reason}"
    )
</code></pre>
<p>Never send a half-generated object deeper into your application.</p>
<hr />
<h2>Step 7: Fail Closed When Parsing Fails</h2>
<p>Avoid this:</p>
<pre><code class="language-python">ticket = response.output_parsed or {}
</code></pre>
<p>It hides the failure.</p>
<p>Later your code may do:</p>
<pre><code class="language-python">ticket["priority"]
</code></pre>
<p>and fail somewhere completely unrelated.</p>
<p>Instead:</p>
<pre><code class="language-python">ticket = response.output_parsed

if ticket is None:
    raise ValueError(
        "Model did not return valid structured data."
    )
</code></pre>
<p>Failing close to the source makes debugging much easier.</p>
<hr />
<h2>Step 8: Add Controlled Retries</h2>
<p>Some failures can be retried.</p>
<p>A simple approach:</p>
<pre><code class="language-python">def parse_ticket(message, attempts=2):
    last_error = None

    for _ in range(attempts):
        try:
            response = client.responses.parse(
                model="gpt-5.6",
                input=[
                    {
                        "role": "system",
                        "content":
                            "Extract support ticket data.",
                    },
                    {
                        "role": "user",
                        "content": message,
                    },
                ],
                text_format=SupportTicket,
            )

            if response.status == "incomplete":
                raise RuntimeError(
                    "Incomplete response"
                )

            refusal = find_refusal(response)

            if refusal:
                raise PermissionError(refusal)

            if response.output_parsed is None:
                raise ValueError(
                    "Structured parsing failed"
                )

            return response.output_parsed

        except PermissionError:
            raise

        except Exception as exc:
            last_error = exc

    raise RuntimeError(
        "Unable to obtain valid structured output"
    ) from last_error
</code></pre>
<p>The important part is not the exact retry count.</p>
<p>It is deciding <strong>which failures deserve a retry</strong>.</p>
<p>Do not endlessly retry:</p>
<pre><code class="language-text">Invalid input
Refusals
Business-rule failures
Permanent configuration errors
</code></pre>
<p>Retries should be bounded.</p>
<hr />
<h2>Step 9: Avoid Regex-Based JSON Extraction</h2>
<p>This pattern is fragile:</p>
<pre><code class="language-python">import re

match = re.search(r"\{.*\}", output, re.DOTALL)
</code></pre>
<p>It may break with:</p>
<ul>
<li><p>Nested objects</p>
</li>
<li><p>Braces inside strings</p>
</li>
<li><p>Multiple JSON objects</p>
</li>
<li><p>Markdown</p>
</li>
<li><p>Truncated responses</p>
</li>
</ul>
<p>If your provider supports structured schema output, use it.</p>
<p>If it supports only JSON mode, parse JSON normally and then validate it.</p>
<p>For example:</p>
<pre><code class="language-python">import json

raw = json.loads(model_output)

ticket = SupportTicket.model_validate(raw)
</code></pre>
<p>Or validate JSON directly:</p>
<pre><code class="language-python">ticket = SupportTicket.model_validate_json(
    model_output
)
</code></pre>
<p>Pydantic provides built-in JSON parsing and validation through <code>model_validate_json()</code>.</p>
<hr />
<h2>Step 10: Keep Schemas Small</h2>
<p>Do not start with a giant structure containing 50 fields.</p>
<p>Instead of:</p>
<pre><code class="language-text">Customer
 ├── Profile
 ├── Addresses
 ├── Orders
 ├── Refunds
 ├── Preferences
 ├── Marketing
 └── Support History
</code></pre>
<p>extract only what the current operation requires.</p>
<p>For example:</p>
<pre><code class="language-python">class RefundIntent(BaseModel):
    order_id: str | None
    reason: str
    requested: bool
</code></pre>
<p>Smaller schemas are easier to:</p>
<ul>
<li><p>Understand</p>
</li>
<li><p>Test</p>
</li>
<li><p>Version</p>
</li>
<li><p>Validate</p>
</li>
<li><p>Monitor</p>
</li>
<li><p>Change safely</p>
</li>
</ul>
<hr />
<h2>Step 11: Version Your Output Contract</h2>
<p>Eventually your schema will change.</p>
<h3>Version 1</h3>
<pre><code class="language-json">{
  "category": "billing",
  "summary": "Duplicate payment"
}
</code></pre>
<h3>Version 2</h3>
<p>Version 2 might add:</p>
<pre><code class="language-json">{
  "category": "billing",
  "summary": "Duplicate payment",
  "priority": "high"
}
</code></pre>
<p>Treat this like an API change.</p>
<p>A practical pattern is:</p>
<pre><code class="language-text">support_ticket_v1
support_ticket_v2
</code></pre>
<p>or include an explicit application-side schema version.</p>
<p>Do not silently change a structure that other services already depend on.</p>
<hr />
<h2>Step 12: Test With Bad Inputs</h2>
<p>Do not test only perfect prompts.</p>
<p>Try cases like:</p>
<pre><code class="language-text">Empty input
Very long input
Missing information
Conflicting information
Multiple entities
Unexpected languages
Prompt injection attempts
Ambiguous dates
Malformed IDs
</code></pre>
<p>Then verify:</p>
<pre><code class="language-text">Did the schema hold?
Did missing values become null?
Was a refusal handled?
Did business validation catch problems?
Did retries stop correctly?
</code></pre>
<p>Structured output should be tested like an API boundary.</p>
<p>For applications that process large volumes of unstructured text, <a href="https://sdlccorp.com/natural-language-processing-services/">NLP-powered automation solutions</a> can help structure, validate, and integrate model-generated data into downstream workflows.</p>
<hr />
<h2>A Production-Safe Parsing Flow</h2>
<p>A reliable <strong>LLM structured output</strong> pipeline looks like this:</p>
<pre><code class="language-text">User Input
    ↓
Schema-Constrained Generation
    ↓
Response Complete?
   / \
 No   Yes
 ↓     ↓
Fail  Refusal?
        / \
      Yes  No
       ↓    ↓
    Handle  Parse
              ↓
       Schema Validation
              ↓
       Business Validation
              ↓
         Trusted Object
</code></pre>
<p>Only the final object should enter the rest of your application.</p>
<hr />
<h2>Common Structured Output Mistakes</h2>
<p>Avoid these patterns:</p>
<ul>
<li><p>Asking for JSON only through prompt wording</p>
</li>
<li><p>Using regex to extract objects</p>
</li>
<li><p>Trusting valid JSON without schema validation</p>
</li>
<li><p>Allowing unknown enum values</p>
</li>
<li><p>Forcing the model to invent missing data</p>
</li>
<li><p>Ignoring truncated responses</p>
</li>
<li><p>Retrying refusals indefinitely</p>
</li>
<li><p>Swallowing parsing errors</p>
</li>
<li><p>Using enormous schemas</p>
</li>
<li><p>Changing output contracts without versioning</p>
</li>
</ul>
<p>The goal is not simply to make the model output JSON.</p>
<p>The goal is to make the <strong>application behavior predictable when the model does something unexpected</strong>.</p>
<hr />
<h2>Final Thoughts</h2>
<p>Reliable structured output comes from treating the LLM like an external service, not a trusted function.</p>
<p>Use a real schema. Validate the response. Represent missing information explicitly. Detect incomplete output and refusals. Add business validation after parsing, and retry only when the failure is actually recoverable.</p>
<p>A strong <strong>LLM structured output</strong> flow therefore looks like:</p>
<pre><code class="language-text">Generate
   ↓
Constrain
   ↓
Validate
   ↓
Handle Failure
   ↓
Use Data
</code></pre>
<p>Once you build that boundary correctly, downstream application code becomes much simpler because it no longer has to guess what shape the model decided to return.</p>
]]></content:encoded></item><item><title><![CDATA[Hybrid Search: Combining BM25 and Embeddings]]></title><description><![CDATA[Keyword search is excellent when users know the exact terms they are looking for. Semantic search is better when the query and the relevant document use different words.


In real applications, we usu]]></description><link>https://sdlccorp-softwaredev.hashnode.dev/hybrid-search-combining-bm25-and-embeddings</link><guid isPermaLink="true">https://sdlccorp-softwaredev.hashnode.dev/hybrid-search-combining-bm25-and-embeddings</guid><dc:creator><![CDATA[Samcorp]]></dc:creator><pubDate>Wed, 02 Sep 2026 08:56:19 GMT</pubDate><content:encoded><![CDATA[<p>Keyword search is excellent when users know the exact terms they are looking for. Semantic search is better when the query and the relevant document use different words.</p>
<img src="https://cdn.hashnode.com/uploads/covers/685a6d69d4d0ea366852534c/889f2edf-afc5-469d-86ca-0e3e83250f0c.png" alt="" style="display:block;margin:0 auto" />

<p>In real applications, we usually need both.</p>
<p><strong>Hybrid search with BM25 and embeddings</strong> combines lexical matching with semantic similarity so exact keywords, product codes, names, and technical terms remain important while conceptually related content can still rank.</p>
<p>In this tutorial, we'll build the core search flow and combine both result sets using <strong>Reciprocal Rank Fusion (RRF)</strong>.</p>
<hr />
<h2>Why Combine BM25 and Embeddings?</h2>
<p>Suppose our documents contain:</p>
<pre><code class="language-text">1. "How to reset your account password"
2. "Recover access when you cannot sign in"
3. "Configure OAuth authentication"
</code></pre>
<p>Now search for:</p>
<pre><code class="language-text">forgot login credentials
</code></pre>
<p>A keyword search may struggle because the document does not necessarily contain the exact words.</p>
<p>An embedding model can understand that:</p>
<pre><code class="language-text">forgot login credentials
        ≈
recover access when you cannot sign in
</code></pre>
<p>But semantic search has another weakness.</p>
<p>Consider:</p>
<pre><code class="language-text">ERR_CONNECTION_RESET
</code></pre>
<p>or:</p>
<pre><code class="language-text">SKU-48219
</code></pre>
<p>Exact lexical matching may be much more useful than semantic similarity.</p>
<p>That gives us:</p>
<table>
<thead>
<tr>
<th>Search Method</th>
<th>Good At</th>
</tr>
</thead>
<tbody><tr>
<td><strong>BM25</strong></td>
<td>Keywords, names, IDs, exact terminology</td>
</tr>
<tr>
<td><strong>Embeddings</strong></td>
<td>Meaning, synonyms, natural-language queries</td>
</tr>
<tr>
<td><strong>Hybrid Search</strong></td>
<td>Combining both signals</td>
</tr>
</tbody></table>
<p>OpenSearch uses BM25 as its default keyword-ranking algorithm and describes hybrid search as combining keyword and semantic retrieval to improve relevance.</p>
<hr />
<h2>The Hybrid Search Architecture</h2>
<p>The basic flow looks like this:</p>
<pre><code class="language-text">                 User Query
                     |
             ┌───────┴───────┐
             ↓               ↓
        BM25 Search     Create Embedding
             ↓               ↓
      Keyword Results   Vector Search
             │               │
             └───────┬───────┘
                     ↓
                Rank Fusion
                     ↓
                Final Results
</code></pre>
<p>Instead of asking one retrieval method to solve every search problem, we let each produce its best candidates.</p>
<p>Then we combine them.</p>
<hr />
<h2>Step 1: Index Text for BM25</h2>
<p>Let's use Elasticsearch for the example.</p>
<p>Create an index containing normal text fields:</p>
<pre><code class="language-json">PUT articles
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text"
      },
      "content": {
        "type": "text"
      }
    }
  }
}
</code></pre>
<p>A normal <code>match</code> query uses lexical relevance:</p>
<pre><code class="language-json">GET articles/_search
{
  "query": {
    "match": {
      "content": "database connection timeout"
    }
  }
}
</code></pre>
<p>BM25 ranks documents based on signals such as term occurrence, document length, and how uncommon terms are across the collection.</p>
<p>This works especially well when exact terminology matters.</p>
<hr />
<h2>Step 2: Add Semantic Representations</h2>
<p>For semantic search, documents need vector representations.</p>
<p>Conceptually:</p>
<pre><code class="language-text">"reset account password"
          ↓
     Embedding Model
          ↓
[0.14, -0.28, 0.67, ...]
</code></pre>
<p>The query goes through the same model:</p>
<pre><code class="language-text">"forgot my password"
          ↓
     Embedding Model
          ↓
[0.12, -0.25, 0.70, ...]
</code></pre>
<p>Vector search then finds nearby representations.</p>
<p>Sentence Transformers describes this process as embedding both the corpus and the query into the same vector space and retrieving entries with high semantic similarity. Cosine similarity is commonly used for this comparison.</p>
<p>Teams building search experiences around embeddings can also explore <a href="https://sdlccorp.com/natural-language-processing-services/">semantic search and NLP s</a>olutions for vector-based retrieval, contextual search, and recommendation systems.</p>
<p>In Elasticsearch, the current <code>semantic_text</code> workflow can manage embedding generation automatically.</p>
<p>A simplified mapping can contain both versions:</p>
<pre><code class="language-json">PUT knowledge-base
{
  "mappings": {
    "properties": {
      "content": {
        "type": "text"
      },
      "content_embedding": {
        "type": "semantic_text"
      }
    }
  }
}
</code></pre>
<p>The important design is:</p>
<pre><code class="language-text">content
→ lexical/BM25 search

content_embedding
→ semantic search
</code></pre>
<p>Elasticsearch currently recommends <code>semantic_text</code> as its simpler managed workflow for semantic retrieval.</p>
<hr />
<h2>Step 3: Run Both Searches</h2>
<p>For a query such as:</p>
<pre><code class="language-text">fix slow database requests
</code></pre>
<p>Run lexical search against the regular field:</p>
<pre><code class="language-json">{
  "standard": {
    "query": {
      "match": {
        "content": "fix slow database requests"
      }
    }
  }
}
</code></pre>
<p>Then run semantic retrieval against the embedding-backed field:</p>
<pre><code class="language-json">{
  "standard": {
    "query": {
      "match": {
        "content_embedding": "fix slow database requests"
      }
    }
  }
}
</code></pre>
<p>The two searches may produce different rankings.</p>
<h3>BM25 Results</h3>
<pre><code class="language-text">1. Fixing slow database queries
2. Database request logging
3. Database timeout configuration
</code></pre>
<h3>Semantic Search Results</h3>
<pre><code class="language-text">1. Improving SQL query performance
2. Fixing slow database queries
3. Optimizing application data access
</code></pre>
<p>Both lists contain useful information.</p>
<p>We now need to merge them.</p>
<hr />
<h2>Step 4: Do Not Simply Add the Scores</h2>
<p>A tempting approach is:</p>
<pre><code class="language-text">final_score =
    bm25_score +
    vector_similarity
</code></pre>
<p>That can be unreliable.</p>
<p>BM25 and vector similarity scores do not necessarily share the same scale. Elasticsearch specifically notes that BM25 scores can be unbounded while vector-related scores may use bounded ranges, making direct score combination difficult without normalization.</p>
<p>Instead, we can combine <strong>rank positions</strong>.</p>
<hr />
<h2>Step 5: Combine Results With RRF</h2>
<p>Reciprocal Rank Fusion assigns a score based on where a document appears in each result list.</p>
<p>Conceptually:</p>
<pre><code class="language-text">RRF score =
1 / (k + BM25 rank)
+
1 / (k + vector rank)
</code></pre>
<p>For example:</p>
<pre><code class="language-text">Document A
BM25 rank:   1
Vector rank: 3

Document B
BM25 rank:   5
Vector rank: 1
</code></pre>
<p>A document that performs well in either or both systems can rise in the final ranking.</p>
<p>RRF is useful because it works on ranking positions rather than requiring BM25 and vector scores to be directly comparable.</p>
<p>Elasticsearch currently recommends RRF for hybrid retrieval.</p>
<hr />
<h2>Step 6: Create the Hybrid Query</h2>
<p>With Elasticsearch's RRF retriever:</p>
<pre><code class="language-json">GET knowledge-base/_search
{
  "retriever": {
    "rrf": {
      "retrievers": [
        {
          "standard": {
            "query": {
              "match": {
                "content": "fix slow database requests"
              }
            }
          }
        },
        {
          "standard": {
            "query": {
              "match": {
                "content_embedding": "fix slow database requests"
              }
            }
          }
        }
      ]
    }
  }
}
</code></pre>
<p>The first branch handles lexical matching.</p>
<p>The second handles semantic retrieval.</p>
<p>RRF merges both into one result list. Elasticsearch's current hybrid-search documentation uses this same pattern with separate text and semantic fields.</p>
<hr />
<h2>Step 7: Understand the Final Ranking</h2>
<p>Suppose our searches return:</p>
<h3>BM25 Results</h3>
<pre><code class="language-text">1. Database request troubleshooting
2. Improving SQL query performance
3. Application latency debugging
</code></pre>
<h3>Embedding Results</h3>
<pre><code class="language-text">1. Improving SQL query performance
2. Optimizing slow backend systems
3. Database request troubleshooting
</code></pre>
<p>The hybrid ranking could become:</p>
<pre><code class="language-text">1. Improving SQL query performance
2. Database request troubleshooting
3. Optimizing slow backend systems
4. Application latency debugging
</code></pre>
<p>The first two documents receive strong signals from both retrieval methods.</p>
<p>That is the main advantage of hybrid retrieval.</p>
<hr />
<h2>Step 8: Add Metadata Filters</h2>
<p>Most real search systems also need filters.</p>
<p>For example:</p>
<pre><code class="language-text">Search:
"authentication problems"

Filters:
category = documentation
language = English
status = published
</code></pre>
<p>Apply structured filters before or alongside retrieval so irrelevant documents do not become candidates simply because their text is similar.</p>
<p>Common filters include:</p>
<ul>
<li><p>Tenant or organization ID</p>
</li>
<li><p>Product</p>
</li>
<li><p>Language</p>
</li>
<li><p>Region</p>
</li>
<li><p>Date</p>
</li>
<li><p>Permissions</p>
</li>
<li><p>Document type</p>
</li>
</ul>
<p>For business applications, authorization filtering is especially important.</p>
<p>Search results should never expose content the current user cannot access.</p>
<hr />
<h2>Step 9: Tune Candidate Sizes</h2>
<p>Vector search normally retrieves the nearest <code>k</code> documents.</p>
<p>Elasticsearch's kNN search also supports a larger candidate pool before selecting the final neighbors.</p>
<p>Approximate kNN is designed for scalable vector retrieval, while exact brute-force vector search becomes expensive on larger collections.</p>
<p>Conceptually:</p>
<pre><code class="language-text">Vector index
    ↓
Retrieve 100 candidates
    ↓
Keep best 20
    ↓
Combine with BM25 candidates
    ↓
RRF
</code></pre>
<p>Increasing the candidate pool can improve recall, but it also increases query work.</p>
<p>Benchmark it rather than choosing large values automatically.</p>
<hr />
<h2>Step 10: Evaluate Search Quality</h2>
<p>Do not decide whether hybrid search works based on only a few manual searches.</p>
<p>Create a small evaluation set containing:</p>
<pre><code class="language-text">Query
Expected relevant documents
</code></pre>
<p>For example:</p>
<table>
<thead>
<tr>
<th>Query</th>
<th>Expected Result</th>
</tr>
</thead>
<tbody><tr>
<td><code>password reset</code></td>
<td>Password recovery guide</td>
</tr>
<tr>
<td><code>can't access account</code></td>
<td>Password recovery guide</td>
</tr>
<tr>
<td><code>ERR_CONNECTION_RESET</code></td>
<td>Exact error troubleshooting</td>
</tr>
<tr>
<td><code>speed up SQL</code></td>
<td>Database optimization guide</td>
</tr>
</tbody></table>
<p>Include multiple query types:</p>
<pre><code class="language-text">Exact keyword queries
+
Natural-language queries
+
Synonyms
+
Product IDs
+
Acronyms
+
Misspellings
</code></pre>
<p>Then compare:</p>
<pre><code class="language-text">BM25 only
vs.
Embeddings only
vs.
Hybrid
</code></pre>
<p>Useful search metrics include:</p>
<ul>
<li><p>Precision@K</p>
</li>
<li><p>Recall@K</p>
</li>
<li><p>Mean Reciprocal Rank</p>
</li>
<li><p>NDCG</p>
</li>
</ul>
<p>The best hybrid configuration depends on your own users and documents.</p>
<hr />
<h2>When Hybrid Search Works Best</h2>
<p><strong>Hybrid search with BM25 and embeddings</strong> is particularly useful for:</p>
<ul>
<li><p>Documentation search</p>
</li>
<li><p>Enterprise knowledge bases</p>
</li>
<li><p>Ecommerce product discovery</p>
</li>
<li><p>RAG retrieval</p>
</li>
<li><p>Support-center search</p>
</li>
<li><p>Developer documentation</p>
</li>
<li><p>Internal company search</p>
</li>
</ul>
<p>Hybrid retrieval is also useful in <a href="https://sdlccorp.com/generative-ai-development-services/">RAG and generative AI applications</a>, where vector databases and retrieval pipelines help ground model responses in relevant enterprise data.</p>
<p>For example, a developer documentation system needs semantic understanding for:</p>
<pre><code class="language-text">"why is my API request being rejected?"
</code></pre>
<p>while still requiring precise matches for:</p>
<pre><code class="language-text">HTTP 429
</code></pre>
<p>Hybrid retrieval handles both cases better than forcing one search method to do everything.</p>
<hr />
<h2>Common Hybrid Search Mistakes</h2>
<h3>Combining Raw Scores Blindly</h3>
<p>BM25 and embedding scores may have very different scales.</p>
<p>Use normalization or rank-based fusion such as RRF.</p>
<h3>Using Different Embedding Models</h3>
<p>Documents and queries must be represented consistently.</p>
<p>For dense-vector kNN search, Elasticsearch notes that query vectors should use the same model and dimensions as the indexed document vectors.</p>
<h3>Ignoring Exact Matches</h3>
<p>Semantic retrieval should not push an exact product ID or error code below loosely related content.</p>
<p>That is precisely why the BM25 branch matters.</p>
<h3>Retrieving Too Many Candidates</h3>
<p>A huge candidate pool can increase latency without significantly improving relevance.</p>
<h3>Skipping Evaluation</h3>
<p>Hybrid search is not automatically better for every dataset.</p>
<p>Test it against real queries.</p>
<hr />
<h2>Final Hybrid Search Flow</h2>
<p>A practical implementation looks like this:</p>
<pre><code class="language-text">User Query
    ↓
┌─────────────────────────┐
│                         │
↓                         ↓
BM25                    Embedding
Search                   Model
↓                         ↓
Lexical Results       Vector Search
│                         │
└────────────┬────────────┘
             ↓
             RRF
             ↓
      Metadata Filters
             ↓
      Final Ranked Results
</code></pre>
<hr />
<h2>Final Thoughts</h2>
<p>Keyword and semantic search solve different problems.</p>
<p>BM25 is strong when words themselves matter. Embeddings are strong when <strong>meaning</strong> matters.</p>
<p>Combining them gives you both:</p>
<pre><code class="language-text">BM25
Exact lexical relevance
        +
Embeddings
Semantic understanding
        ↓
Hybrid Search
</code></pre>
<p>A practical <strong>hybrid search BM25 embeddings</strong> architecture therefore starts with two independent retrieval paths and combines their rankings using a method such as RRF.</p>
<p>Start simple, test against real queries, and tune only after you understand where your current ranking fails.</p>
<p>That usually produces a better search system than relying entirely on either keyword search or embeddings alone.</p>
]]></content:encoded></item><item><title><![CDATA[Implementing Skip Links and Focus Management Properly]]></title><description><![CDATA[Keyboard accessibility is not just about making buttons respond to the Tab key.


Users also need an efficient way to bypass repeated navigation, understand where keyboard focus is, and maintain their]]></description><link>https://sdlccorp-softwaredev.hashnode.dev/implementing-skip-links-and-focus-management-properly</link><guid isPermaLink="true">https://sdlccorp-softwaredev.hashnode.dev/implementing-skip-links-and-focus-management-properly</guid><dc:creator><![CDATA[Samcorp]]></dc:creator><pubDate>Tue, 01 Sep 2026 10:47:00 GMT</pubDate><content:encoded><![CDATA[<p>Keyboard accessibility is not just about making buttons respond to the <code>Tab</code> key.</p>
<img src="https://cdn.hashnode.com/uploads/covers/685a6d69d4d0ea366852534c/065a70ac-a1e3-4d7a-a844-a515d037670e.png" alt="" style="display:block;margin:0 auto" />

<p>Users also need an efficient way to bypass repeated navigation, understand where keyboard focus is, and maintain their position when dialogs, forms, or dynamic content change.</p>
<p>Good <strong>focus management accessibility</strong> makes keyboard navigation predictable instead of forcing users to repeatedly search for their place.</p>
<p>In this tutorial, we'll implement skip links, visible focus states, modal focus handling, form-error focus, and focus behavior for dynamic pages.</p>
<hr />
<h2>Why Focus Management Matters</h2>
<p>Imagine navigating a site without a mouse.</p>
<p>Every page contains:</p>
<pre><code class="language-text">Logo
↓
Navigation Link 1
↓
Navigation Link 2
↓
Navigation Link 3
↓
More Header Controls
↓
Finally, Main Content
</code></pre>
<p>Repeating this on every page becomes frustrating.</p>
<p>WCAG 2.2 Success Criterion <strong>2.4.1 Bypass Blocks</strong> requires a mechanism for bypassing repeated content, while <strong>2.4.3 Focus Order</strong> requires keyboard focus to move in an order that preserves meaning and usability.</p>
<hr />
<h2>Step 1: Add a Skip Link</h2>
<p>Place the skip link near the beginning of the page:</p>
<pre><code class="language-html">&lt;body&gt;
  &lt;a class="skip-link" href="#main-content"&gt;
    Skip to main content
  &lt;/a&gt;

  &lt;header&gt;
    &lt;!-- Navigation --&gt;
  &lt;/header&gt;

  &lt;main id="main-content"&gt;
    &lt;h1&gt;Dashboard&lt;/h1&gt;
  &lt;/main&gt;
&lt;/body&gt;
</code></pre>
<p>When activated, the link moves the user past repeated navigation and directly to the primary content.</p>
<p>This is one of the standard techniques recommended by W3C for bypassing repeated page sections.</p>
<hr />
<h2>Step 2: Hide the Skip Link Until It Receives Focus</h2>
<p>The skip link does not need to stay visually prominent all the time.</p>
<p>Hide it off-screen and reveal it when keyboard focus reaches it:</p>
<pre><code class="language-css">.skip-link {
  position: absolute;
  left: 1rem;
  top: -100px;
  padding: 0.75rem 1rem;
  background: #ffffff;
  color: #111111;
  z-index: 1000;
}

.skip-link:focus {
  top: 1rem;
}
</code></pre>
<p>Now a keyboard user can press <code>Tab</code> and immediately see:</p>
<pre><code class="language-text">[ Skip to main content ]
</code></pre>
<p>Avoid hiding the link with:</p>
<pre><code class="language-css">display: none;
</code></pre>
<p>or:</p>
<pre><code class="language-css">visibility: hidden;
</code></pre>
<p>because that removes it from keyboard navigation.</p>
<hr />
<h2>Step 3: Make the Target Reliably Focusable</h2>
<p>For applications where you explicitly manage focus, you can make the main region programmatically focusable:</p>
<pre><code class="language-html">&lt;main id="main-content" tabindex="-1"&gt;
  &lt;h1&gt;Dashboard&lt;/h1&gt;
&lt;/main&gt;
</code></pre>
<p><code>tabindex="-1"</code> allows JavaScript or fragment navigation to focus the element without placing it in the normal <code>Tab</code> sequence.</p>
<p>This distinction is important:</p>
<pre><code class="language-text">tabindex="0"
→ Included in normal Tab navigation

tabindex="-1"
→ Programmatically focusable

tabindex="1" or higher
→ Avoid
</code></pre>
<p>WAI's keyboard guidance strongly advises against using positive <code>tabindex</code> values to control page focus order.</p>
<p>Instead, keep the DOM structure itself logical.</p>
<hr />
<h2>Step 4: Keep Focus Indicators Visible</h2>
<p>Never remove focus styling without replacing it.</p>
<p>This is problematic:</p>
<pre><code class="language-css">button:focus {
  outline: none;
}
</code></pre>
<p>A keyboard user may no longer know which button is active.</p>
<p>Use a clear focus indicator instead:</p>
<pre><code class="language-css">button:focus-visible,
a:focus-visible,
input:focus-visible {
  outline: 3px solid currentColor;
  outline-offset: 3px;
}
</code></pre>
<p><code>:focus-visible</code> is useful because browsers can show the stronger indicator when keyboard-style focus needs to be communicated.</p>
<p>Accessible focus states should also be considered during interface design, not only during development. Well-planned <a href="https://sdlccorp.com/ui-ux-design-company/">UI/UX design</a> can help teams build keyboard-friendly interfaces, accessible components, and WCAG-aligned user experiences.</p>
<hr />
<h2>Step 5: Manage Focus When Opening a Dialog</h2>
<p>Dynamic interfaces require more deliberate focus management.</p>
<p>Suppose a user activates:</p>
<pre><code class="language-text">[Delete Account]
</code></pre>
<p>and a confirmation dialog opens.</p>
<p>Focus should not remain somewhere behind the dialog.</p>
<p>Using the native <code>&lt;dialog&gt;</code> element:</p>
<pre><code class="language-html">&lt;button id="open-dialog"&gt;
  Delete Account
&lt;/button&gt;

&lt;dialog id="confirm-dialog"&gt;
  &lt;h2&gt;Delete account?&lt;/h2&gt;

  &lt;button id="confirm-delete"&gt;
    Delete
  &lt;/button&gt;

  &lt;button id="close-dialog"&gt;
    Cancel
  &lt;/button&gt;
&lt;/dialog&gt;
</code></pre>
<p>JavaScript:</p>
<pre><code class="language-js">const dialog =
  document.querySelector("#confirm-dialog");

const openButton =
  document.querySelector("#open-dialog");

const closeButton =
  document.querySelector("#close-dialog");

openButton.addEventListener("click", () =&gt; {
  dialog.showModal();
});

closeButton.addEventListener("click", () =&gt; {
  dialog.close();
});

dialog.addEventListener("close", () =&gt; {
  openButton.focus();
});
</code></pre>
<p>The important flow is:</p>
<pre><code class="language-text">Delete Button
     ↓
Dialog Opens
     ↓
Focus Moves Into Dialog
     ↓
User Completes/Closes Dialog
     ↓
Focus Returns to Trigger
</code></pre>
<p>WAI guidance emphasizes maintaining predictable focus and restoring it logically when focused content disappears or closes.</p>
<p>For complex custom dialogs, use a proven accessible component pattern rather than inventing your own focus trap.</p>
<hr />
<h2>Step 6: Handle Focus After Content Is Removed</h2>
<p>Suppose a task list contains:</p>
<pre><code class="language-text">Task A
Task B [Delete]
Task C
</code></pre>
<p>If the user deletes <strong>Task B</strong>, its delete button disappears.</p>
<p>Leaving focus on a removed DOM element can cause the browser to fall back to the document body, making keyboard navigation confusing.</p>
<p>Move focus somewhere logical instead:</p>
<pre><code class="language-js">deleteButton.addEventListener("click", () =&gt; {
  const nextTask =
    task.nextElementSibling ||
    task.previousElementSibling;

  task.remove();

  const nextButton =
    nextTask?.querySelector("button");

  nextButton?.focus();
});
</code></pre>
<p>The exact destination depends on the interface, but the principle remains:</p>
<blockquote>
<p><strong>After an action changes the UI, focus should land somewhere that makes sense.</strong></p>
</blockquote>
<p>WAI specifically identifies removed focused elements as situations where developers need to manage focus.</p>
<hr />
<h2>Step 7: Manage Focus After Form Errors</h2>
<p>Consider a long registration form.</p>
<p>If validation fails, showing red borders alone may not help keyboard or screen-reader users understand what happened.</p>
<p>Create an error summary:</p>
<pre><code class="language-html">&lt;div
  id="error-summary"
  tabindex="-1"
  role="alert"
&gt;
  &lt;h2&gt;There are 2 errors&lt;/h2&gt;

  &lt;ul&gt;
    &lt;li&gt;
      &lt;a href="#email"&gt;
        Enter a valid email address
      &lt;/a&gt;
    &lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;
</code></pre>
<p>Then move focus to it after validation:</p>
<pre><code class="language-js">const errorSummary =
  document.querySelector("#error-summary");

errorSummary.focus();
</code></pre>
<p>The user immediately learns that validation failed and can follow links to individual fields.</p>
<hr />
<h2>Step 8: Manage Focus in Single-Page Applications</h2>
<p>Traditional navigation usually gives users a clear page transition.</p>
<p>Single-page applications can replace content without producing the same browser behavior.</p>
<p>For example:</p>
<pre><code class="language-text">Products
   ↓
User clicks "Account"
   ↓
URL changes
   ↓
New content appears
</code></pre>
<p>If focus remains on the old navigation link, a screen-reader or keyboard user may not immediately know that the page changed.</p>
<p>One approach is to focus the new page heading:</p>
<pre><code class="language-html">&lt;h1 id="page-heading" tabindex="-1"&gt;
  My Account
&lt;/h1&gt;
</code></pre>
<p>After navigation:</p>
<pre><code class="language-js">document
  .querySelector("#page-heading")
  .focus();
</code></pre>
<p>A sensible SPA pattern is therefore:</p>
<pre><code class="language-text">Route Changes
      ↓
Update Page Content
      ↓
Update Document Title
      ↓
Move Focus to Main Heading
</code></pre>
<p>Do not move focus after every small UI update.</p>
<p>Use programmatic focus when it helps users understand a meaningful context change.</p>
<hr />
<h2>Step 9: Keep Focused Elements Visible</h2>
<p>Sticky headers, cookie banners, and fixed toolbars can accidentally cover the element that receives focus.</p>
<p>For example:</p>
<pre><code class="language-text">┌──────────────────────────┐
│     Sticky Header        │
├──────────────────────────┤
│ Hidden Focused Button    │
│                          │
│ Main Content             │
└──────────────────────────┘
</code></pre>
<p>CSS such as this can help with anchored content:</p>
<pre><code class="language-css">html {
  scroll-padding-top: 6rem;
}
</code></pre>
<p>WCAG 2.2 added <strong>2.4.11 Focus Not Obscured (Minimum)</strong> at Level AA, requiring a focused component not to be entirely hidden by author-created content.</p>
<p>Test sticky UI carefully with keyboard navigation.</p>
<hr />
<h2>Step 10: Use Native HTML Before ARIA</h2>
<p>Do not make everything manually focusable.</p>
<p>Instead of:</p>
<pre><code class="language-html">&lt;div
  role="button"
  tabindex="0"
&gt;
  Save
&lt;/div&gt;
</code></pre>
<p>prefer:</p>
<pre><code class="language-html">&lt;button&gt;
  Save
&lt;/button&gt;
</code></pre>
<p>Native controls already provide much of the expected:</p>
<ul>
<li><p>Keyboard behavior</p>
</li>
<li><p>Focus handling</p>
</li>
<li><p>Semantics</p>
</li>
<li><p>Browser support</p>
</li>
<li><p>Assistive-technology integration</p>
</li>
</ul>
<p>Custom ARIA widgets require developers to implement their expected keyboard behavior themselves.</p>
<p>WAI's Authoring Practices explicitly notes this responsibility.</p>
<p>Consistent <a href="https://sdlccorp.com/web-development-company/"></a><a href="https://sdlccorp.com/web-development-company/">keyboard-friendly web development</a> helps teams implement semantic HTML, logical focus order, screen-reader support, and accessible interactive components across the application.</p>
<p>Use ARIA when necessary, not as a replacement for semantic HTML.</p>
<hr />
<h2>A Practical Focus Management Checklist</h2>
<p>Before shipping an interface, test it using only your keyboard:</p>
<pre><code class="language-text">Can I reach every interactive control?
        ↓
Is the focus indicator always visible?
        ↓
Does Tab order make sense?
        ↓
Can I bypass repeated navigation?
        ↓
Does focus enter dialogs correctly?
        ↓
Does focus return when dialogs close?
        ↓
Does dynamic content preserve my position?
        ↓
Are focused elements visible?
</code></pre>
<p>If any answer is <strong>no</strong>, the interface probably needs more focus-management work.</p>
<hr />
<h2>Common Accessibility Mistakes</h2>
<p>Avoid these patterns:</p>
<ul>
<li><p>Removing <code>outline</code> without providing another focus style</p>
</li>
<li><p>Using positive <code>tabindex</code> values to reorder controls</p>
</li>
<li><p>Moving focus unnecessarily</p>
</li>
<li><p>Forgetting to restore focus after closing dialogs</p>
</li>
<li><p>Allowing focus to remain on deleted elements</p>
</li>
<li><p>Hiding skip links with <code>display: none</code></p>
</li>
<li><p>Creating clickable <code>&lt;div&gt;</code> elements instead of buttons</p>
</li>
<li><p>Allowing sticky content to cover focused controls</p>
</li>
<li><p>Testing accessibility only with a mouse</p>
</li>
</ul>
<p>WAI recommends that all interactive functionality remain keyboard operable and that focus movement stay visible and predictable.</p>
<hr />
<h2>Final Thoughts</h2>
<p>Good <strong>focus management accessibility</strong> is mostly about predictability.</p>
<p>Keyboard users should always be able to answer three questions:</p>
<p><strong>Where am I? What can I do here? Where will I go next?</strong></p>
<p>Start with semantic HTML and a correctly implemented skip link.</p>
<p>Preserve the browser's natural focus order wherever possible, provide strong visible focus states, and move focus programmatically only when an interface change genuinely requires it.</p>
<p>Skip links may look like a small accessibility feature, but combined with thoughtful focus management, they make complex websites significantly easier to navigate.</p>
]]></content:encoded></item><item><title><![CDATA[Building a Headless Shopify Storefront With Hydrogen]]></title><description><![CDATA[Shopify's traditional storefront works well for many stores, while businesses with more advanced requirements may need specialized Shopify development and greater control over performance, frontend ar]]></description><link>https://sdlccorp-softwaredev.hashnode.dev/building-a-headless-shopify-storefront-with-hydrogen</link><guid isPermaLink="true">https://sdlccorp-softwaredev.hashnode.dev/building-a-headless-shopify-storefront-with-hydrogen</guid><dc:creator><![CDATA[Samcorp]]></dc:creator><pubDate>Tue, 01 Sep 2026 06:09:56 GMT</pubDate><content:encoded><![CDATA[<img src="https://cdn.hashnode.com/uploads/covers/685a6d69d4d0ea366852534c/9220b1ed-9b47-4704-929e-476d8d83a145.png" alt="" style="display:block;margin:0 auto" />

<p>Shopify's traditional storefront works well for many stores, while businesses with more advanced requirements may need specialized <a href="https://sdlccorp.com/services/shopify/"><strong>Shopify development</strong></a> and greater control over performance, frontend architecture, design, or integrations.</p>
<p>That is where <strong>headless commerce</strong> comes in. Shopify manages products, inventory, customers, carts, and checkout, while your frontend runs separately.</p>
<p>For Shopify, <strong>Hydrogen</strong> is the official React-based framework for building headless storefronts. Current Hydrogen projects are built on React Router and include Shopify-optimized components, utilities, routing, Storefront API access, and deployment support.</p>
<p>In this <strong>Shopify Hydrogen tutorial</strong>, we will create a storefront, connect it to Shopify, fetch product data, understand cart handling, and deploy it with Oxygen.</p>
<hr />
<h2>What Are Hydrogen and Oxygen?</h2>
<p>The basic architecture looks like this:</p>
<pre><code class="language-text">Customer
   ↓
Hydrogen Storefront
   ↓
Shopify Storefront API
   ↓
Products / Collections / Cart / Checkout
</code></pre>
<p><strong>Hydrogen</strong> handles your storefront application.</p>
<p><strong>Shopify Storefront API</strong> exposes commerce data through GraphQL. Shopify's current Storefront API is GraphQL-only and supports products, collections, carts, checkout-related commerce flows, and more.</p>
<p><strong>Oxygen</strong> is Shopify's hosting environment designed for Hydrogen storefronts.</p>
<p>You can technically host Hydrogen elsewhere, but Oxygen gives you a straightforward Shopify-native deployment path.</p>
<hr />
<h2>Step 1: Create a Hydrogen Project</h2>
<p>Make sure you have a recent Node.js environment installed.</p>
<p>Create a starter project with:</p>
<pre><code class="language-bash">npm create @shopify/hydrogen@latest -- --quickstart
</code></pre>
<p>Shopify's current quickstart creates a working storefront using <strong>Mock.shop</strong>, so you can experiment before connecting a real store.</p>
<p>It also generates common routes for:</p>
<ul>
<li><p>Products</p>
</li>
<li><p>Collections</p>
</li>
<li><p>Cart</p>
</li>
<li><p>Accounts</p>
</li>
<li><p>Search</p>
</li>
<li><p>Policies</p>
</li>
<li><p>Blogs</p>
</li>
<li><p>Other storefront pages</p>
</li>
</ul>
<p>Enter the project:</p>
<pre><code class="language-bash">cd hydrogen-quickstart
</code></pre>
<p>Start development:</p>
<pre><code class="language-bash">shopify hydrogen dev
</code></pre>
<p>Then open:</p>
<pre><code class="language-text">http://localhost:3000
</code></pre>
<p>You should see the starter storefront running with sample products.</p>
<hr />
<h2>Step 2: Understand the Project Structure</h2>
<p>A Hydrogen application contains familiar React application files:</p>
<pre><code class="language-text">app/
├── components/
├── routes/
├── styles/
├── root.jsx
├── entry.client.jsx
└── entry.server.jsx
</code></pre>
<p>The <code>routes</code> directory is especially important.</p>
<p>Common storefront URLs include:</p>
<pre><code class="language-text">/
/products/:handle
/collections
/collections/:handle
/cart
/account
/search
</code></pre>
<p>Hydrogen uses route loaders to fetch Shopify data on the server before rendering the page.</p>
<p>Shopify recommends this pattern for Storefront API, Customer Account API, and third-party data fetching.</p>
<hr />
<h2>Step 3: Connect Hydrogen to Your Shopify Store</h2>
<p>The starter initially uses Mock.shop data.</p>
<p>To connect a real Shopify store, run:</p>
<pre><code class="language-bash">npx shopify hydrogen link
</code></pre>
<p>Follow the prompts to log into Shopify and choose or create a Hydrogen storefront.</p>
<p>Next, pull the required environment configuration:</p>
<pre><code class="language-bash">npx shopify hydrogen env pull
</code></pre>
<p>Shopify automatically adds values such as your Storefront API credentials and Customer Account API settings to the local environment.</p>
<p>Restart the development server:</p>
<pre><code class="language-bash">npm run dev
</code></pre>
<p>Your storefront should now display products from your Shopify store.</p>
<hr />
<h2>Step 4: Fetch Products With the Storefront API</h2>
<p>One of Hydrogen's biggest advantages is its built-in Storefront API client.</p>
<p>A basic product route can use a loader like this:</p>
<pre><code class="language-js">import {useLoaderData} from '@shopify/remix-oxygen';

export async function loader({params, context}) {
  const {handle} = params;

  const {product} = await context.storefront.query(
    PRODUCT_QUERY,
    {
      variables: {handle},
    },
  );

  if (!product) {
    throw new Response('Product not found', {
      status: 404,
    });
  }

  return {product};
}

export default function ProductPage() {
  const {product} = useLoaderData();

  return (
    &lt;main&gt;
      &lt;h1&gt;{product.title}&lt;/h1&gt;
      &lt;p&gt;{product.description}&lt;/p&gt;
    &lt;/main&gt;
  );
}
</code></pre>
<p>Now define the GraphQL query:</p>
<pre><code class="language-js">const PRODUCT_QUERY = `#graphql
  query Product($handle: String!) {
    product(handle: $handle) {
      id
      title
      description
      featuredImage {
        url
        altText
        width
        height
      }
    }
  }
`;
</code></pre>
<p>The flow is:</p>
<pre><code class="language-text">/products/example-product
          ↓
Route Loader
          ↓
Storefront API
          ↓
GraphQL Product Query
          ↓
React Component
</code></pre>
<p>Hydrogen's built-in <code>storefront.query()</code> client is designed specifically for this server-side Storefront API pattern.</p>
<hr />
<h2>Step 5: Render Product Information</h2>
<p>You can now expand the page with an image and product information:</p>
<pre><code class="language-js">export default function ProductPage() {
  const {product} = useLoaderData();

  return (
    &lt;main&gt;
      &lt;h1&gt;{product.title}&lt;/h1&gt;

      {product.featuredImage &amp;&amp; (
        &lt;img
          src={product.featuredImage.url}
          alt={
            product.featuredImage.altText ||
            product.title
          }
          width="600"
        /&gt;
      )}

      &lt;p&gt;{product.description}&lt;/p&gt;
    &lt;/main&gt;
  );
}
</code></pre>
<p>In a production storefront, you would normally add:</p>
<ul>
<li><p>Product variants</p>
</li>
<li><p>Pricing</p>
</li>
<li><p>Availability</p>
</li>
<li><p>Quantity controls</p>
</li>
<li><p>Add-to-cart button</p>
</li>
<li><p>Product recommendations</p>
</li>
<li><p>Structured SEO metadata</p>
</li>
</ul>
<p>The important part is that Shopify remains the commerce backend while Hydrogen controls the customer-facing experience.</p>
<hr />
<h2>Step 6: Handle the Shopping Cart</h2>
<p>The Hydrogen starter already includes cart routes and Shopify cart utilities.</p>
<p>A simplified add-to-cart interaction works around a Shopify merchandise variant ID:</p>
<pre><code class="language-jsx">&lt;CartForm
  route="/cart"
  action={CartForm.ACTIONS.LinesAdd}
  inputs={{
    lines: [
      {
        merchandiseId: variant.id,
        quantity: 1,
      },
    ],
  }}
&gt;
  &lt;button type="submit"&gt;
    Add to Cart
  &lt;/button&gt;
&lt;/CartForm&gt;
</code></pre>
<p>Behind the scenes, Hydrogen sends the appropriate cart mutation to Shopify.</p>
<p>Your architecture remains:</p>
<pre><code class="language-text">Product Page
     ↓
Add to Cart
     ↓
Hydrogen Cart Handler
     ↓
Storefront API
     ↓
Shopify Cart
</code></pre>
<p>Shopify's Hydrogen cart utilities support adding, updating, and removing line items while keeping commerce logic connected to the Storefront API.</p>
<hr />
<h2>Step 7: Think About Performance</h2>
<p>Going headless does not automatically make a storefront faster.</p>
<p>You still need to manage:</p>
<ul>
<li><p>GraphQL query size</p>
</li>
<li><p>Image optimization</p>
</li>
<li><p>Caching</p>
</li>
<li><p>JavaScript bundle size</p>
</li>
<li><p>Third-party scripts</p>
</li>
<li><p>Server response times</p>
</li>
</ul>
<p>Hydrogen supports caching Shopify API responses, and Shopify's current tooling provides caching options for non-personalized catalog data such as products, collections, and pages.</p>
<p>For example:</p>
<pre><code class="language-js">const {product} = await storefront.query(
  PRODUCT_QUERY,
  {
    variables: {handle},
    cache: storefront.CacheLong(),
  },
);
</code></pre>
<p>Use longer caching for stable public catalog data and avoid public caching for personalized customer information.</p>
<hr />
<h2>Step 8: Keep SEO in the Architecture</h2>
<p>A headless storefront still needs strong <a href="https://sdlccorp.com/services/ecommerce/ecommerce-seo-services/"><strong>ecommerce SEO</strong></a>, especially because your team becomes responsible for URL structure, metadata, canonicalization, structured data, redirects, and crawlability.</p>
<p>Preserve predictable URLs such as:</p>
<pre><code class="language-text">/products/:handle
/collections/:handle
</code></pre>
<p>Also implement:</p>
<ul>
<li><p>Unique page titles</p>
</li>
<li><p>Meta descriptions</p>
</li>
<li><p>Canonical URLs</p>
</li>
<li><p>Product structured data</p>
</li>
<li><p>XML sitemap</p>
</li>
<li><p>Robots.txt</p>
</li>
<li><p>Redirects from old URLs</p>
</li>
</ul>
<p>This becomes especially important when migrating an existing Shopify theme to Hydrogen.</p>
<p>Shopify recommends maintaining redirects when storefront URL structures change so existing links continue to work.</p>
<hr />
<h2>Step 9: Deploy the Hydrogen Storefront</h2>
<p>Once the storefront works locally, deploy it to Oxygen.</p>
<p>First, make sure the project is linked to your Shopify storefront.</p>
<p>Then run:</p>
<pre><code class="language-bash">npx shopify hydrogen deploy
</code></pre>
<p>Choose the preview environment when prompted.</p>
<p>The Hydrogen CLI builds the application, creates an Oxygen deployment, and returns a preview URL.</p>
<p>Your final architecture becomes:</p>
<pre><code class="language-text">Customer
   ↓
Custom Domain
   ↓
Hydrogen on Oxygen
   ↓
Shopify Storefront API
   ↓
Shopify Commerce Backend
</code></pre>
<hr />
<h2>When Does Hydrogen Make Sense?</h2>
<p>Hydrogen is worth considering when you need more frontend control than a standard Shopify theme provides.</p>
<p>Typical use cases include:</p>
<ul>
<li><p>Highly customized ecommerce experiences</p>
</li>
<li><p>Complex frontend integrations</p>
</li>
<li><p>Content-rich commerce</p>
</li>
<li><p>Multiple backend data sources</p>
</li>
<li><p>Custom product discovery</p>
</li>
<li><p>International storefront experiences</p>
</li>
<li><p>Performance-sensitive ecommerce applications</p>
</li>
</ul>
<p>However, headless also introduces more frontend ownership.</p>
<p>You become responsible for areas such as:</p>
<ul>
<li><p>Routing</p>
</li>
<li><p>SEO implementation</p>
</li>
<li><p>Testing</p>
</li>
<li><p>Monitoring</p>
</li>
<li><p>Accessibility</p>
</li>
<li><p>Frontend architecture</p>
</li>
<li><p>Deployment workflows</p>
</li>
</ul>
<p>Do not choose headless simply because it sounds more modern.</p>
<p>Choose it when the additional flexibility solves a real business or technical requirement.</p>
<hr />
<h2>Common Hydrogen Mistakes</h2>
<p>Avoid these problems when following a <strong>Shopify Hydrogen tutorial</strong> or building a production storefront:</p>
<ul>
<li><p>Fetching more GraphQL fields than the page requires</p>
</li>
<li><p>Ignoring caching</p>
</li>
<li><p>Building custom cart logic when Hydrogen utilities already solve it</p>
</li>
<li><p>Exposing private API credentials to browser code</p>
</li>
<li><p>Forgetting redirects during migration</p>
</li>
<li><p>Ignoring SEO because the storefront is headless</p>
</li>
<li><p>Recreating Shopify functionality unnecessarily</p>
</li>
<li><p>Adding too many client-side dependencies</p>
</li>
</ul>
<p>Keep the architecture simple until the store genuinely requires additional complexity.</p>
<hr />
<h2>Final Thoughts</h2>
<p>Building a headless Shopify storefront with Hydrogen gives developers much more control over the frontend while keeping Shopify responsible for the commerce engine.</p>
<p>The practical workflow is straightforward:</p>
<pre><code class="language-text">Create Hydrogen Project
        ↓
Connect Shopify Store
        ↓
Query Storefront API
        ↓
Build Product Experience
        ↓
Configure Cart
        ↓
Optimize Performance + SEO
        ↓
Deploy to Oxygen
</code></pre>
<p>Hydrogen is most valuable when you need a storefront that goes beyond the limits of a conventional Shopify theme.</p>
<p>Start with Shopify's generated storefront, understand its existing routes and commerce utilities, and customize only the areas that genuinely need a headless architecture.</p>
]]></content:encoded></item><item><title><![CDATA[Practical Feature Flags for Safe Continuous Deployment]]></title><description><![CDATA[Continuous deployment helps teams release software faster, but pushing every new feature to all users at once can be risky.
Feature flags reduce that risk by separating deployment from release. You ca]]></description><link>https://sdlccorp-softwaredev.hashnode.dev/practical-feature-flags-for-safe-continuous-deployment</link><guid isPermaLink="true">https://sdlccorp-softwaredev.hashnode.dev/practical-feature-flags-for-safe-continuous-deployment</guid><dc:creator><![CDATA[Samcorp]]></dc:creator><pubDate>Mon, 31 Aug 2026 09:45:41 GMT</pubDate><content:encoded><![CDATA[<img src="https://cdn.hashnode.com/uploads/covers/685a6d69d4d0ea366852534c/5d741df3-2235-46d8-b1bd-72c954d2ec80.png" alt="" style="display:block;margin:0 auto" />

<p>Continuous deployment helps teams release software faster, but pushing every new feature to all users at once can be risky.</p>
<p>Feature flags reduce that risk by separating <strong>deployment from release</strong>. You can deploy code to production, keep the feature turned off, enable it for a small audience, monitor real-world behavior, and expand the rollout gradually.</p>
<p>That makes <strong>feature flags deployment</strong> a practical way to reduce release risk without slowing down software delivery.</p>
<hr />
<h2>What Is a Feature Flag?</h2>
<p>A feature flag controls whether a feature is enabled or disabled at runtime.</p>
<p>Instead of calling a new feature directly:</p>
<pre><code class="language-javascript">showNewCheckout();
</code></pre>
<p>you can place it behind a feature flag:</p>
<pre><code class="language-javascript">if (newCheckoutEnabled) {
  showNewCheckout();
} else {
  showCurrentCheckout();
}
</code></pre>
<p>The release process then becomes:</p>
<pre><code class="language-text">Deploy Code
    ↓
Feature OFF
    ↓
Enable for Small Group
    ↓
Monitor
    ↓
Increase Rollout
    ↓
Release to Everyone
</code></pre>
<p>This gives teams more control over how and when users receive new functionality.</p>
<hr />
<h2>Step 1: Add a Feature Flag</h2>
<p>For this example, we can use <strong>OpenFeature</strong>, an open standard for feature flagging.</p>
<p>Install the SDK:</p>
<pre><code class="language-bash">npm install @openfeature/server-sdk
</code></pre>
<p>Then evaluate the feature flag in your application:</p>
<pre><code class="language-javascript">const enabled =
  await featureFlags.getBooleanValue(
    "new-checkout",
    false
  );

if (enabled) {
  return renderNewCheckout();
}

return renderCurrentCheckout();
</code></pre>
<p>The <code>false</code> value acts as a safe fallback if the feature flag cannot be evaluated.</p>
<p>This means the application continues using the existing checkout experience instead of exposing an unfinished or potentially risky feature.</p>
<hr />
<h2>Step 2: Deploy Before Releasing</h2>
<p>One of the biggest advantages of feature flags is the ability to deploy code without immediately releasing the feature.</p>
<p>Deploy the new application version while keeping the flag disabled:</p>
<pre><code class="language-text">New Version
    ↓
Deploy to Production
    ↓
Feature Flag = OFF
    ↓
Existing Experience Continues
</code></pre>
<p>The new code is now running in production, but users still see the existing experience.</p>
<p>This approach is especially useful when releasing modern <a href="https://sdlccorp.com/cloud-application-development-services">cloud applications</a>, where teams may want to validate production stability before exposing a new capability to all users.</p>
<hr />
<h2>Step 3: Enable the Feature for Selected Users</h2>
<p>Instead of enabling a feature for everyone at once, start with a controlled group.</p>
<p>For example:</p>
<ul>
<li><p>Internal team members</p>
</li>
<li><p>Beta users</p>
</li>
<li><p>Selected customer accounts</p>
</li>
<li><p>Specific regions</p>
</li>
<li><p>Particular subscription plans</p>
</li>
</ul>
<p>You can also provide evaluation context to the feature flag system:</p>
<pre><code class="language-javascript">const context = {
  targetingKey: user.id,
  region: user.region,
  plan: user.plan,
};
</code></pre>
<p>The flag provider can then use this information to determine which users should receive the new feature.</p>
<p>For example, you could release a feature only to internal employees or premium customers before making it generally available.</p>
<hr />
<h2>Step 4: Use Progressive Rollouts</h2>
<p>Once internal testing is successful, increase exposure gradually.</p>
<p>A rollout could look like this:</p>
<pre><code class="language-text">Internal Users
      ↓
      5%
      ↓
     10%
      ↓
     25%
      ↓
     50%
      ↓
    100%
</code></pre>
<p>A practical rollout plan might be:</p>
<table>
<thead>
<tr>
<th>Stage</th>
<th>Traffic</th>
</tr>
</thead>
<tbody><tr>
<td>Internal Testing</td>
<td>Employees</td>
</tr>
<tr>
<td>Canary</td>
<td>5%</td>
</tr>
<tr>
<td>Early Rollout</td>
<td>10%</td>
</tr>
<tr>
<td>Wider Rollout</td>
<td>25–50%</td>
</tr>
<tr>
<td>General Release</td>
<td>100%</td>
</tr>
</tbody></table>
<p>This approach makes it easier to detect problems before they affect every user.</p>
<p>If an issue appears when only 5% of traffic is using the feature, the impact is much smaller than discovering the same issue after a full release.</p>
<hr />
<h2>Step 5: Monitor the Rollout</h2>
<p>Do not increase the rollout percentage without checking production metrics.</p>
<p>Important metrics may include:</p>
<ul>
<li><p>Error rate</p>
</li>
<li><p>Response time</p>
</li>
<li><p>CPU usage</p>
</li>
<li><p>Memory usage</p>
</li>
<li><p>Database load</p>
</li>
<li><p>Failed transactions</p>
</li>
<li><p>Conversion rate</p>
</li>
<li><p>User engagement</p>
</li>
</ul>
<p>For example:</p>
<pre><code class="language-text">Feature Rollout: 10%

Error Rate
Before: 0.3%
After:  0.4%

Latency
Before: 220 ms
After:  235 ms
</code></pre>
<p>If the system remains healthy, continue increasing the rollout.</p>
<p>If error rates, latency, failed transactions, or other important metrics increase significantly, disable the feature flag and investigate the problem.</p>
<p>This is one of the main benefits of progressive delivery: teams can react without performing another deployment.</p>
<hr />
<h2>Step 6: Use Flags as Kill Switches</h2>
<p>Feature flags can also work as operational kill switches for risky integrations or services.</p>
<p>For example:</p>
<pre><code class="language-javascript">const paymentsEnabled =
  await featureFlags.getBooleanValue(
    "payments-enabled",
    true
  );

if (!paymentsEnabled) {
  return showMaintenanceMessage();
}

return processPayment();
</code></pre>
<p>If the payment service becomes unstable, the feature can be disabled without rebuilding or redeploying the application.</p>
<p>Kill switches can be useful for:</p>
<ul>
<li><p>External APIs</p>
</li>
<li><p>Background jobs</p>
</li>
<li><p>Payment systems</p>
</li>
<li><p>Expensive features</p>
</li>
<li><p>Experimental services</p>
</li>
<li><p>Third-party integrations</p>
</li>
</ul>
<p>This can reduce recovery time during production incidents.</p>
<hr />
<h2>Step 7: Connect Flags to Monitoring</h2>
<p>A good feature flag system should help teams answer questions such as:</p>
<pre><code class="language-text">Which variation did the user receive?

When was the flag changed?

Did errors increase after rollout?

What percentage of users currently has the feature enabled?
</code></pre>
<p>Feature flags become far more valuable when connected to logging, monitoring, and observability tools.</p>
<p>For example, application logs can record which flag variation a user received when an error occurred.</p>
<p>This helps teams determine whether a production issue started after a rollout change.</p>
<p>A useful event might contain information such as:</p>
<pre><code class="language-text">User: 48291
Feature: new-checkout
Variation: enabled
Rollout: 25%
Timestamp: 14:32 UTC
</code></pre>
<p>When rollout data and application telemetry are connected, troubleshooting becomes much faster.</p>
<hr />
<h2>Step 8: Remove Old Flags</h2>
<p>Temporary feature flags should not remain in the application forever.</p>
<p>After a feature reaches 100% rollout and remains stable, remove the flag and the unused code.</p>
<p>A simple cleanup process looks like this:</p>
<pre><code class="language-text">100% Rollout
     ↓
Monitor Stability
     ↓
Remove Old Code
     ↓
Remove Flag
</code></pre>
<p>For example, avoid keeping logic like this permanently:</p>
<pre><code class="language-javascript">if (newCheckout) {
  newCheckoutFlow();
} else {
  oldCheckoutFlow();
}
</code></pre>
<p>Once the old checkout flow is no longer required, remove both the feature flag and the unused code.</p>
<p>Otherwise, large numbers of old flags can make the application difficult to understand and maintain.</p>
<p>Feature flag cleanup should therefore be part of the release process.</p>
<hr />
<h2>Recommended Feature Flag Deployment Flow</h2>
<p>A practical feature flag deployment process looks like this:</p>
<pre><code class="language-text">Build Feature
     ↓
Add Feature Flag
     ↓
Deploy with Flag OFF
     ↓
Enable Internally
     ↓
Roll Out to 5%
     ↓
Monitor
     ↓
25% → 50% → 100%
     ↓
Monitor Stability
     ↓
Remove Temporary Flag
</code></pre>
<p>This approach gives teams several opportunities to stop or reverse a release before it affects the entire user base.</p>
<hr />
<h2>Common Mistakes to Avoid</h2>
<p>Feature flags are useful, but poor implementation can create new problems.</p>
<p>Avoid these common mistakes:</p>
<ul>
<li><p>Enabling a feature for everyone immediately</p>
</li>
<li><p>Using unsafe fallback values</p>
</li>
<li><p>Rolling out without monitoring</p>
</li>
<li><p>Giving too many people permission to change production flags</p>
</li>
<li><p>Keeping temporary flags forever</p>
</li>
<li><p>Creating deeply nested flag logic</p>
</li>
<li><p>Using flags without documenting ownership</p>
</li>
<li><p>Changing rollout percentages without tracking the change</p>
</li>
<li><p>Treating feature flags as a replacement for testing</p>
</li>
</ul>
<p>Feature flags reduce deployment risk, but they should work alongside automated testing, CI/CD controls, monitoring, and observability.</p>
<p>They are an additional release-control mechanism, not a substitute for good engineering practices.</p>
<hr />
<h2>Feature Flags vs Traditional Deployment</h2>
<p>Traditional releases often connect deployment and release together:</p>
<pre><code class="language-text">Deploy New Version
      ↓
Everyone Gets Feature
</code></pre>
<p>Feature flags separate those activities:</p>
<pre><code class="language-text">Deploy New Version
      ↓
Feature Disabled
      ↓
Controlled Release
      ↓
Monitor
      ↓
Increase Exposure
</code></pre>
<p>That separation makes production releases easier to control and reverse.</p>
<p>If something goes wrong, teams may be able to disable the feature immediately rather than rolling back the entire application.</p>
<p>A disciplined <a href="https://sdlccorp.com/software-developer/">software development</a> process should combine feature flags with automated testing, CI/CD controls, monitoring, and clear release ownership.</p>
<hr />
<h2>Final Thoughts</h2>
<p>A strong <strong>feature flags deployment</strong> strategy separates shipping code from releasing features.</p>
<p>Deploy the code first, keep the new feature disabled, enable it for a small audience, monitor real production behavior, and increase exposure gradually.</p>
<p>The most effective feature flag systems include <strong>safe defaults, targeted releases, progressive rollouts, kill switches, monitoring, access controls, and regular cleanup</strong>.</p>
<p>Feature flags do not remove deployment risk completely, but they give engineering teams much better control over how that risk reaches users.</p>
]]></content:encoded></item><item><title><![CDATA[Building a CI/CD Pipeline That Enforces Security Gates]]></title><description><![CDATA[A CI/CD pipeline should not deploy code just because the build succeeds. It should also verify that the application meets basic security requirements before reaching production.
That is where CI/CD se]]></description><link>https://sdlccorp-softwaredev.hashnode.dev/building-a-ci-cd-pipeline-that-enforces-security-gates</link><guid isPermaLink="true">https://sdlccorp-softwaredev.hashnode.dev/building-a-ci-cd-pipeline-that-enforces-security-gates</guid><dc:creator><![CDATA[Samcorp]]></dc:creator><pubDate>Mon, 31 Aug 2026 09:16:15 GMT</pubDate><content:encoded><![CDATA[<img src="https://cdn.hashnode.com/uploads/covers/685a6d69d4d0ea366852534c/4e2d6a65-2590-443e-9cd9-6f6eeebaf3b5.png" alt="" style="display:block;margin:0 auto" />

<p>A CI/CD pipeline should not deploy code just because the build succeeds. It should also verify that the application meets basic security requirements before reaching production.</p>
<p>That is where <strong>CI/CD security gates</strong> help.</p>
<p>A security gate is an automated checkpoint that blocks the pipeline when it detects problems such as vulnerable dependencies, insecure code, failed tests, or unsafe container images.</p>
<p>In this tutorial, we will create a simple GitHub Actions pipeline that adds security checks before deployment.</p>
<hr />
<h2>What Are CI/CD Security Gates?</h2>
<p>A normal CI/CD pipeline may look like this:</p>
<pre><code class="language-text">Code Push
   ↓
Build
   ↓
Test
   ↓
Deploy
</code></pre>
<p>A secure pipeline adds additional checkpoints:</p>
<pre><code class="language-text">Code Push
   ↓
Build &amp; Test
   ↓
Dependency Scan
   ↓
SAST
   ↓
Container Scan
   ↓
Production Approval
   ↓
Deploy
</code></pre>
<p>If an important security check fails, the deployment should stop automatically.</p>
<p>The goal is simple:</p>
<blockquote>
<p><strong>Unsafe code should never reach production by accident.</strong></p>
</blockquote>
<hr />
<h2>Step 1: Create the Base CI Pipeline</h2>
<p>Create the following GitHub Actions workflow file:</p>
<pre><code class="language-text">.github/workflows/secure-pipeline.yml
</code></pre>
<p>Start with a basic build and testing job:</p>
<pre><code class="language-yaml">name: Secure CI/CD Pipeline

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Code
        uses: actions/checkout@v7

      - name: Setup Node.js
        uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm

      - name: Install Dependencies
        run: npm ci

      - name: Run Tests
        run: npm test
</code></pre>
<p>If the tests fail, this job fails and the pipeline should not continue toward deployment.</p>
<p>This gives us our first security-related gate: <strong>code that cannot pass its tests cannot move forward.</strong></p>
<hr />
<h2>Step 2: Add a Dependency Security Gate</h2>
<p>Third-party packages can introduce known vulnerabilities into your application.</p>
<p>For a Node.js project, you can add a dependency audit:</p>
<pre><code class="language-yaml">dependency-audit:
  runs-on: ubuntu-latest

  steps:
    - uses: actions/checkout@v7

    - uses: actions/setup-node@v7
      with:
        node-version: 24

    - run: npm ci

    - name: Check Vulnerable Dependencies
      run: npm audit --audit-level=high
</code></pre>
<p>With this configuration, high-severity dependency vulnerabilities can cause the job to fail.</p>
<p>For pull requests, you can also use GitHub's dependency review:</p>
<pre><code class="language-yaml">dependency-review:
  if: github.event_name == 'pull_request'
  runs-on: ubuntu-latest

  steps:
    - uses: actions/checkout@v7

    - name: Dependency Review
      uses: actions/dependency-review-action@v5
      with:
        fail-on-severity: high
</code></pre>
<p>This helps prevent developers from introducing risky dependencies without noticing them.</p>
<p>Instead of discovering the problem after deployment, the pipeline catches it during development.</p>
<hr />
<h2>Step 3: Add Static Security Testing</h2>
<p>Static Application Security Testing, commonly called <strong>SAST</strong>, analyzes source code for potential security weaknesses.</p>
<p>GitHub CodeQL can be added directly to the workflow:</p>
<pre><code class="language-yaml">sast:
  runs-on: ubuntu-latest

  permissions:
    contents: read
    security-events: write

  steps:
    - uses: actions/checkout@v7

    - name: Initialize CodeQL
      uses: github/codeql-action/init@v4
      with:
        languages: javascript-typescript

    - name: Analyze Code
      uses: github/codeql-action/analyze@v4
</code></pre>
<p>This gives the pipeline another security checkpoint before deployment.</p>
<p>For stronger enforcement, configure repository rules so required security checks must pass before a pull request can merge.</p>
<p>That way, security scanning becomes part of the development workflow rather than an optional report developers can ignore.</p>
<hr />
<h2>Step 4: Scan the Container Image</h2>
<p>If your application runs inside Docker, you should also scan the final container image.</p>
<p>A container may contain vulnerabilities even when the application source code itself looks secure.</p>
<p>Tools such as <strong>Trivy</strong> can inspect operating-system packages and application dependencies inside an image.</p>
<p>For example:</p>
<pre><code class="language-bash">trivy image \
  --exit-code 1 \
  --severity HIGH,CRITICAL \
  my-app:${GITHUB_SHA}
</code></pre>
<p>The important option is:</p>
<pre><code class="language-bash">--exit-code 1
</code></pre>
<p>It tells the scanner to return a failed status when serious vulnerabilities are detected.</p>
<p>Without this setting, your pipeline may successfully detect vulnerabilities but continue deploying anyway.</p>
<p>That is not really a security gate.</p>
<p>It is only a security report.</p>
<p>A real security gate must be able to <strong>stop the pipeline</strong>.</p>
<hr />
<h2>Step 5: Connect Security Gates to Deployment</h2>
<p>Now make the deployment job depend on the security jobs:</p>
<pre><code class="language-yaml">deploy:
  needs:
    - test
    - dependency-audit
    - sast
    - container-scan

  runs-on: ubuntu-latest

  environment:
    name: production

  steps:
    - name: Deploy Application
      run: ./scripts/deploy.sh
</code></pre>
<p>The pipeline now behaves like this:</p>
<pre><code class="language-text">Tests ─────────────┐
Dependencies ──────┤
SAST ──────────────┼──&gt; Production Deployment
Container Scan ────┘
</code></pre>
<p>If one required job fails, the deployment job will not start.</p>
<p>For example:</p>
<pre><code class="language-text">Test Passed
Dependency Scan Passed
SAST Passed
Container Scan Failed
        ↓
Deployment Blocked
</code></pre>
<p>This is the core idea behind effective <strong>CI/CD security gates</strong>.</p>
<p>Deployment should happen only when every required security condition has been satisfied.</p>
<hr />
<h2>Step 6: Add Production Approval</h2>
<p>Automated checks are valuable, but sensitive production deployments may also require human approval.</p>
<p>GitHub environments can be configured with required reviewers.</p>
<p>The release process can then look like this:</p>
<pre><code class="language-text">Security Checks
      ↓
All Passed
      ↓
Manual Approval
      ↓
Production
</code></pre>
<p>This creates another safeguard for important releases.</p>
<p>Automated tools determine whether the software meets technical requirements, while an authorized person can verify whether the release should actually proceed.</p>
<p>This can be especially useful for:</p>
<ul>
<li><p>High-risk production changes</p>
</li>
<li><p>Financial applications</p>
</li>
<li><p>Healthcare systems</p>
</li>
<li><p>Enterprise platforms</p>
</li>
<li><p>Infrastructure changes</p>
</li>
<li><p>Major releases</p>
</li>
</ul>
<p>Human approval should not replace automated security checks. It should complement them.</p>
<hr />
<h2>Step 7: Protect Deployment Credentials</h2>
<p>Security checks are not enough if the deployment credentials themselves are poorly protected.</p>
<p>Avoid storing long-lived cloud credentials inside GitHub secrets whenever possible.</p>
<p>For example, try to avoid permanently storing credentials such as:</p>
<pre><code class="language-text">AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
</code></pre>
<p>When building or deploying modern <a href="https://sdlccorp.com/cloud-application-development-services">cloud applications</a>, use <strong>OpenID Connect (OIDC)</strong> whenever your cloud platform supports it instead of relying on long-lived credentials.A workflow can request temporary credentials using permissions such as:</p>
<pre><code class="language-yaml">permissions:
  contents: read
  id-token: write
</code></pre>
<p>The cloud provider can then issue short-lived credentials specifically for that workflow execution.</p>
<p>Short-lived credentials reduce the potential damage if credentials are exposed or compromised.</p>
<hr />
<h2>Protect the Pipeline Too</h2>
<p>Security gates are only useful when attackers cannot easily modify or bypass them.</p>
<p>Your CI/CD configuration should therefore be treated as security-sensitive code.</p>
<p>Protect workflow files with pull-request reviews and branch protection rules.</p>
<p>You should also keep GitHub Actions permissions as limited as possible.</p>
<p>For example:</p>
<pre><code class="language-yaml">permissions:
  contents: read
</code></pre>
<p>Then grant additional permissions only to the jobs that genuinely need them.</p>
<p>For security-sensitive projects, also consider pinning third-party GitHub Actions to reviewed commit SHAs rather than relying only on movable version tags.</p>
<p>The pipeline itself is part of your application's attack surface.</p>
<hr />
<h2>Recommended CI/CD Security Flow</h2>
<p>A practical secure delivery pipeline can look like this:</p>
<pre><code class="language-text">Developer Push
      ↓
Build &amp; Tests
      ↓
Dependency Scan
      ↓
SAST
      ↓
Container Scan
      ↓
All Gates Passed?
     /        \
   No          Yes
   ↓            ↓
 Block      Approval
                ↓
            Production
</code></pre>
<p>Each stage answers a different security question:</p>
<table>
<thead>
<tr>
<th>Security Gate</th>
<th>What It Checks</th>
</tr>
</thead>
<tbody><tr>
<td>Build and Tests</td>
<td>Does the application work correctly?</td>
</tr>
<tr>
<td>Dependency Scan</td>
<td>Are known vulnerable packages present?</td>
</tr>
<tr>
<td>SAST</td>
<td>Does the source code contain security weaknesses?</td>
</tr>
<tr>
<td>Container Scan</td>
<td>Does the deployment image contain serious vulnerabilities?</td>
</tr>
<tr>
<td>Production Approval</td>
<td>Is the release authorized for production?</td>
</tr>
</tbody></table>
<p>Do not add security tools simply because they are popular.</p>
<p>Every gate should protect against a real risk and have a clear rule defining when the pipeline must stop.</p>
<hr />
<h2>What Makes a Good Security Gate?</h2>
<p>A useful security gate should have three characteristics.</p>
<h3>1. It Checks a Meaningful Risk</h3>
<p>The gate should detect something that could realistically affect your application.</p>
<p>For example:</p>
<ul>
<li><p>Critical dependency vulnerabilities</p>
</li>
<li><p>Authentication flaws</p>
</li>
<li><p>Secrets committed to source control</p>
</li>
<li><p>Dangerous container vulnerabilities</p>
</li>
<li><p>Failed security tests</p>
</li>
</ul>
<h3>2. It Has a Clear Failure Threshold</h3>
<p>Teams should know exactly what causes the gate to fail.</p>
<p>For example:</p>
<pre><code class="language-text">Critical vulnerability → Block deployment
High vulnerability → Block deployment
Medium vulnerability → Review or warning
Low vulnerability → Track for remediation
</code></pre>
<p>The exact policy will depend on your application's risk level.</p>
<h3>3. It Actually Stops the Pipeline</h3>
<p>A scanner that produces a report but always exits successfully is not enforcing anything.</p>
<p>Security tools should return a failing exit code when your organization's defined security threshold is exceeded.</p>
<p>That turns security scanning into a real deployment control.</p>
<hr />
<h2>Final Thoughts</h2>
<p>Strong <strong>CI/CD security gates</strong> make security part of software delivery instead of leaving it until the end of the release cycle.</p>
<p>Start with a few meaningful checks:</p>
<ul>
<li><p>Automated testing</p>
</li>
<li><p>Dependency scanning</p>
</li>
<li><p>Static security testing</p>
</li>
<li><p>Container image scanning</p>
</li>
<li><p>Production approval</p>
</li>
</ul>
<p>As your application and security requirements grow, these practices can become part of a broader <a href="https://sdlccorp.com/services/digital-transformation-service">digital transformation strategy</a> focused on secure, automated, and scalable software delivery.</p>
<p>The goal is not to create the most complicated CI/CD pipeline.</p>
<p>The goal is to make sure serious security problems <strong>stop a release before they reach production</strong>.</p>
]]></content:encoded></item><item><title><![CDATA[Migrating REST to GraphQL Incrementally]]></title><description><![CDATA[Migrating from REST to GraphQL does not mean you need to rewrite your entire API at once.
A safer REST to GraphQL migration is to introduce GraphQL gradually, keep existing REST services running, and ]]></description><link>https://sdlccorp-softwaredev.hashnode.dev/migrating-rest-to-graphql-incrementally</link><guid isPermaLink="true">https://sdlccorp-softwaredev.hashnode.dev/migrating-rest-to-graphql-incrementally</guid><dc:creator><![CDATA[Samcorp]]></dc:creator><pubDate>Mon, 31 Aug 2026 07:21:58 GMT</pubDate><content:encoded><![CDATA[<img src="https://cdn.hashnode.com/uploads/covers/685a6d69d4d0ea366852534c/a6949261-671e-4cb3-ba8d-cf2f984f6796.png" alt="" style="display:block;margin:0 auto" />

<p>Migrating from REST to GraphQL does not mean you need to rewrite your entire API at once.</p>
<p>A safer <strong>REST to GraphQL migration</strong> is to introduce GraphQL gradually, keep existing REST services running, and move one feature at a time.</p>
<p>This approach reduces production risk and gives teams enough time to test performance, authentication, caching, and client behavior.</p>
<hr />
<h2>Why Migrate Incrementally?</h2>
<p>A full REST replacement can affect:</p>
<ul>
<li><p>Backend services</p>
</li>
<li><p>Frontend applications</p>
</li>
<li><p>Authentication</p>
</li>
<li><p>Caching</p>
</li>
<li><p>Monitoring</p>
</li>
<li><p>Third-party integrations</p>
</li>
</ul>
<p>Instead of changing everything together, migrate in small steps.</p>
<p>For example:</p>
<pre><code class="language-plaintext">User Profile → Orders → Products → Mutations → Remaining APIs
</code></pre>
<p>Each phase can be tested and rolled back independently.</p>
<hr />
<h2>Step 1: Review Existing REST APIs</h2>
<p>Start by identifying the endpoints your application actually uses.</p>
<p>Example:</p>
<pre><code class="language-plaintext">GET /users/:id
GET /users/:id/orders
POST /orders
</code></pre>
<p>Choose a simple and low-risk endpoint first.</p>
<p>A read-only feature such as a user profile is usually better than starting with payments or other critical transactions.</p>
<hr />
<h2>Step 2: Design the GraphQL Schema</h2>
<p>Avoid copying REST endpoints directly into GraphQL.</p>
<p>Instead of:</p>
<pre><code class="language-plaintext">type Query {
  getUser(id: ID!): User
  getUserOrders(id: ID!): [Order]
}
</code></pre>
<p>Use relationships:</p>
<pre><code class="language-plaintext">type User {
  id: ID!
  name: String!
  email: String!
  orders: [Order!]!
}

type Order {
  id: ID!
  total: Float!
  status: String!
}

type Query {
  user(id: ID!): User
}
</code></pre>
<p>Now the frontend works with business objects instead of individual endpoints.</p>
<hr />
<h2>Step 3: Add GraphQL Over Existing REST</h2>
<p>You do not need to remove REST immediately.</p>
<p>GraphQL can act as a layer in front of your current APIs.</p>
<pre><code class="language-plaintext">Frontend
   |
GraphQL API
   |
Existing REST APIs
</code></pre>
<p>For example, a resolver can call the current REST endpoint:</p>
<pre><code class="language-plaintext">const resolvers = {
  Query: {
    user: async (_, { id }) =&gt; {
      const response = await fetch(
        `https://api.example.com/users/${id}`
      );

      return response.json();
    },
  },
};
</code></pre>
<p>Your existing backend continues working while clients slowly move to GraphQL.</p>
<p>For larger modernization projects, professional <a href="https://sdlccorp.com/custom-api-development-integration-services/">custom API development and integration</a> can help teams design, secure, and gradually migrate REST APIs to GraphQL.</p>
<hr />
<h2>Step 4: Migrate One Frontend Feature</h2>
<p>Suppose your application currently makes two REST calls:</p>
<pre><code class="language-plaintext">GET /users/42
GET /users/42/orders
</code></pre>
<p>With GraphQL, the client can request both together:</p>
<pre><code class="language-plaintext">query GetUser {
  user(id: "42") {
    id
    name
    email
    orders {
      id
      total
      status
    }
  }
}
</code></pre>
<p>Start by migrating only one page or workflow.</p>
<p>Other parts of the application can continue using REST.</p>
<p>For applications that require scalable frontend and backend integration, modern <a href="https://sdlccorp.com/web-development-company/">web development</a> can combine GraphQL, REST APIs, and application architecture within the same development stack.</p>
<hr />
<h2>Step 5: Use Feature Flags</h2>
<p>Feature flags make migrations easier to control.</p>
<pre><code class="language-plaintext">if (features.useGraphQL) {
  return loadWithGraphQL();
}

return loadWithREST();
</code></pre>
<p>You can gradually enable GraphQL for:</p>
<pre><code class="language-plaintext">Internal users
↓
Beta users
↓
25% traffic
↓
50% traffic
↓
100% traffic
</code></pre>
<p>If something goes wrong, switch back to REST quickly.</p>
<hr />
<h2>Step 6: Compare REST and GraphQL Results</h2>
<p>Before fully switching traffic, compare both implementations.</p>
<p>Check for:</p>
<ul>
<li><p>Missing fields</p>
</li>
<li><p>Incorrect values</p>
</li>
<li><p>Permission differences</p>
</li>
<li><p>Null handling</p>
</li>
<li><p>Pagination issues</p>
</li>
<li><p>Error differences</p>
</li>
</ul>
<p>This is especially useful for read operations.</p>
<p>Avoid sending the same write through both REST and GraphQL because it may create duplicate records.</p>
<hr />
<h2>Step 7: Keep Authentication Consistent</h2>
<p>If REST currently uses:</p>
<pre><code class="language-plaintext">Authorization: Bearer &lt;token&gt;
</code></pre>
<p>the GraphQL layer should forward the same identity to downstream services.</p>
<p>During early migration, keeping existing authorization rules reduces unnecessary risk.</p>
<p>You can redesign authorization later once the GraphQL layer is stable.</p>
<hr />
<h2>Step 8: Watch for the N+1 Problem</h2>
<p>GraphQL can accidentally create too many backend requests.</p>
<p>For example:</p>
<pre><code class="language-plaintext">query {
  users {
    name
    orders {
      id
    }
  }
}
</code></pre>
<p>If you have 100 users, a poorly designed resolver might make 100 separate requests for orders.</p>
<p>Use techniques such as:</p>
<ul>
<li><p>Request batching</p>
</li>
<li><p>Caching</p>
</li>
<li><p>DataLoader</p>
</li>
<li><p>Batch REST endpoints</p>
</li>
</ul>
<p>Always measure downstream traffic, not just frontend requests.</p>
<hr />
<h2>Step 9: Monitor the Migration</h2>
<p>Track important metrics such as:</p>
<ul>
<li><p>GraphQL error rate</p>
</li>
<li><p>REST traffic</p>
</li>
<li><p>API latency</p>
</li>
<li><p>Payload size</p>
</li>
<li><p>Backend request count</p>
</li>
<li><p>Cache performance</p>
</li>
<li><p>Client failures</p>
</li>
</ul>
<p>Do not assume GraphQL is faster just because the browser makes fewer HTTP requests.</p>
<p>Measure the complete request path.</p>
<hr />
<h2>Step 10: Migrate Mutations Later</h2>
<p>Once GraphQL reads are stable, start migrating write operations.</p>
<p>For example:</p>
<pre><code class="language-plaintext">mutation CreateOrder($input: CreateOrderInput!) {
  createOrder(input: $input) {
    id
    status
  }
}
</code></pre>
<p>Initially, GraphQL can still call:</p>
<pre><code class="language-plaintext">POST /orders
</code></pre>
<p>Later, the resolver can connect directly to the service layer.</p>
<p>The client does not need to know when the internal implementation changes.</p>
<hr />
<h2>Step 11: Deprecate REST Gradually</h2>
<p>Do not remove REST endpoints immediately.</p>
<p>Monitor usage first.</p>
<p>Example:</p>
<pre><code class="language-plaintext">Week 1 → 100,000 REST requests
Week 2 → 45,000
Week 3 → 8,000
Week 4 → 0
</code></pre>
<p>Confirm that all web apps, mobile apps, internal tools, and integrations have migrated.</p>
<p>Only then remove the endpoint.</p>
<hr />
<h2>Recommended Migration Flow</h2>
<p>A practical <strong>REST to GraphQL migration</strong> looks like this:</p>
<pre><code class="language-plaintext">Review REST APIs
      ↓
Choose one feature
      ↓
Design GraphQL schema
      ↓
Connect GraphQL to REST
      ↓
Migrate one client
      ↓
Test and monitor
      ↓
Optimize performance
      ↓
Migrate more features
      ↓
Deprecate unused REST APIs
</code></pre>
<hr />
<h2>Common Mistakes to Avoid</h2>
<p>Avoid these common problems:</p>
<ul>
<li><p>Rewriting the complete backend first</p>
</li>
<li><p>Copying REST endpoints directly into GraphQL</p>
</li>
<li><p>Migrating every frontend screen together</p>
</li>
<li><p>Ignoring N+1 requests</p>
</li>
<li><p>Changing authentication during the migration</p>
</li>
<li><p>Removing REST without checking traffic</p>
</li>
<li><p>Assuming GraphQL is automatically faster</p>
</li>
</ul>
<p>Keep each migration step small and measurable.</p>
<hr />
<h2>Final Thoughts</h2>
<p>A successful <strong>REST to GraphQL migration</strong> is not about replacing REST as quickly as possible.</p>
<p>Start with one useful GraphQL feature, keep existing REST services running, test the result, monitor production traffic, and gradually expand.</p>
<p>GraphQL can first work as a layer over REST and later connect directly to backend services.</p>
<p>This approach gives you the benefits of GraphQL without turning API modernization into a risky full-system rewrite.</p>
]]></content:encoded></item><item><title><![CDATA[Unreal Blueprints to C++: When and How to Convert]]></title><description><![CDATA[A practical guide to deciding which Unreal Engine Blueprint systems belong in C++, how to migrate them safely, and why converting everything is usually the wrong approach.


Blueprints are one of Unre]]></description><link>https://sdlccorp-softwaredev.hashnode.dev/unreal-blueprints-to-c-when-and-how-to-convert</link><guid isPermaLink="true">https://sdlccorp-softwaredev.hashnode.dev/unreal-blueprints-to-c-when-and-how-to-convert</guid><dc:creator><![CDATA[Samcorp]]></dc:creator><pubDate>Fri, 28 Aug 2026 11:49:02 GMT</pubDate><content:encoded><![CDATA[<p>A practical guide to deciding which Unreal Engine Blueprint systems belong in C++, how to migrate them safely, and why converting everything is usually the wrong approach.</p>
<img src="https://cdn.hashnode.com/uploads/covers/685a6d69d4d0ea366852534c/07073bf8-5e5e-4863-964c-cfbd66b7d110.png" alt="" style="display:block;margin:0 auto" />

<p>Blueprints are one of Unreal Engine's biggest productivity advantages.</p>
<p>You can prototype gameplay, connect events, expose designer controls, and test ideas without waiting on a traditional code-build cycle.</p>
<p>But as a project grows, a Blueprint that started like this:</p>
<pre><code class="language-text">Input
  ↓
Move Player
  ↓
Play Animation
</code></pre>
<p>can eventually become:</p>
<pre><code class="language-text">Event Tick
  ↓
Branch
  ↓
Loop
  ↓
Multiple Casts
  ↓
AI Logic
  ↓
Inventory Logic
  ↓
Movement Logic
  ↓
More Branches...
</code></pre>
<p>At that point, developers often ask:</p>
<blockquote>
<p>Should I convert this Blueprint to C++?</p>
</blockquote>
<p>The answer is usually <strong>not all of it</strong>.</p>
<p>A good <strong>Unreal Blueprint to C++</strong> migration moves the right responsibilities into native code while keeping Blueprint where visual scripting still provides faster iteration.</p>
<hr />
<h2>Blueprint vs C++ Is Not an Either/Or Decision</h2>
<p>Unreal Engine is designed for the two systems to work together.</p>
<p>A useful production architecture is:</p>
<pre><code class="language-text">        C++
         │
 ┌───────┴────────┐
 │ Core Systems   │
 │ Gameplay Logic │
 │ Algorithms     │
 │ Networking     │
 └───────┬────────┘
         │
         ▼
     Blueprints
         │
 ┌───────┴────────┐
 │ Configuration  │
 │ Events         │
 │ UI             │
 │ Designer Logic │
 └────────────────┘
</code></pre>
<p>This gives programmers control over architecture while designers retain Blueprint's rapid iteration.</p>
<p>That hybrid approach is also common in professional <a href="https://sdlccorp.com/services/games/unreal-engine-game-development-company/">Unreal Engine game development</a>, where C++ and Blueprints can serve different responsibilities rather than competing for ownership of the entire project.</p>
<hr />
<h2>When Should You Move Blueprint Logic to C++?</h2>
<p>There are several good signals.</p>
<h2>1. The Blueprint Has Become Too Complex</h2>
<p>Large Blueprint graphs become difficult to navigate.</p>
<p>You may see:</p>
<pre><code class="language-text">Functions → Macros → Events → Casts
     ↓
More Functions
     ↓
Nested Logic
     ↓
More Dependencies
</code></pre>
<p>If developers spend more time understanding the graph than modifying it, moving the underlying system to C++ may improve maintainability.</p>
<p>C++ also works better with normal text-based source-control workflows, code reviews, diffs, and merges.</p>
<hr />
<h2>2. The Logic Runs Very Frequently</h2>
<p>Blueprint code executes through Unreal's Blueprint virtual machine, while C++ is compiled into native machine code.</p>
<p>The difference matters most when you're performing substantial work repeatedly.</p>
<p>Examples include:</p>
<pre><code class="language-text">Large loops
Complex calculations
Many actors ticking
Large datasets
AI processing
Procedural systems
Simulation logic
</code></pre>
<p>Consider this Blueprint-style pattern:</p>
<pre><code class="language-text">Event Tick
   ↓
Get All Enemies
   ↓
For Each Enemy
   ↓
Calculate Distance
   ↓
Check Visibility
   ↓
Update State
</code></pre>
<p>With hundreds of actors, this can become expensive.</p>
<p>That logic may belong in C++—although eliminating unnecessary Tick work should often be your first optimization.</p>
<hr />
<h2>Profile Before You Convert</h2>
<p>Don't rewrite Blueprints because someone says:</p>
<blockquote>
<p>C++ is faster.</p>
</blockquote>
<p>Measure first.</p>
<p>Use tools such as <strong>Unreal Insights</strong> to determine where frame time is actually going.</p>
<p>Your problem might be:</p>
<pre><code class="language-text">GPU rendering
Physics
Animation
Asset loading
Blueprint VM
Networking
</code></pre>
<p>If rendering is consuming most of the frame, converting an unrelated Blueprint won't magically improve performance.</p>
<p>A better workflow is:</p>
<pre><code class="language-text">Profile
   ↓
Identify Bottleneck
   ↓
Understand Cause
   ↓
Optimize
   ↓
Measure Again
</code></pre>
<p>Convert only when Blueprint execution or architecture is actually part of the problem.</p>
<hr />
<h2>What Should Usually Stay in Blueprint?</h2>
<p>Blueprint remains excellent for:</p>
<ul>
<li><p>designer-controlled gameplay</p>
</li>
<li><p>simple event sequences</p>
</li>
<li><p>UI behavior</p>
</li>
<li><p>animation integration</p>
</li>
<li><p>level scripting</p>
</li>
<li><p>configuration</p>
</li>
<li><p>prototyping</p>
</li>
<li><p>visual effects triggers</p>
</li>
</ul>
<p>For example:</p>
<pre><code class="language-text">Player Enters Trigger
       ↓
Open Door
       ↓
Play Sound
       ↓
Trigger VFX
</code></pre>
<p>doesn't necessarily need a C++ rewrite.</p>
<p>Blueprint is doing exactly what it's good at: expressing straightforward gameplay behavior visually.</p>
<p>Understanding these strengths before migration is important; this overview of <a href="https://sdlccorp.com/post/the-role-of-blueprints-in-unreal-engine-game-development/">Blueprints in Unreal Engine development</a> provides additional context around prototyping, visual scripting, and designer-friendly development.</p>
<hr />
<h2>A Better Migration Strategy</h2>
<p>Suppose you currently have:</p>
<pre><code class="language-text">BP_PlayerCharacter
</code></pre>
<p>containing:</p>
<pre><code class="language-text">Movement
Health
Combat
Inventory
Interaction
Animation Events
VFX
Audio
</code></pre>
<p>Don't immediately recreate the entire Blueprint in C++.</p>
<p>Instead, create a native foundation.</p>
<pre><code class="language-text">APlayerCharacterBase (C++)
          │
          ▼
BP_PlayerCharacter
</code></pre>
<p>Move stable system-level logic into the C++ parent while allowing the Blueprint child to handle presentation and tuning.</p>
<hr />
<h2>Step 1: Add C++ to the Project</h2>
<p>A Blueprint project doesn't need to be abandoned or recreated.</p>
<p>Add a new C++ class through Unreal Editor and choose the appropriate parent class.</p>
<p>For a character:</p>
<pre><code class="language-cpp">#include "GameFramework/Character.h"
#include "PlayerCharacterBase.generated.h"

UCLASS()
class MYGAME_API APlayerCharacterBase
    : public ACharacter
{
    GENERATED_BODY()

public:

    APlayerCharacterBase();

};
</code></pre>
<p>Adding a C++ class sets up the native code environment while your existing Blueprint assets can remain in the project.</p>
<hr />
<h2>Step 2: Move Stable Variables Into C++</h2>
<p>Suppose Blueprint contains:</p>
<pre><code class="language-text">Health
MaxHealth
MovementSpeed
Damage
</code></pre>
<p>Move the structural variables into C++:</p>
<pre><code class="language-cpp">UPROPERTY(
    EditAnywhere,
    BlueprintReadWrite,
    Category="Player"
)
float MaxHealth = 100.0f;

UPROPERTY(
    BlueprintReadOnly,
    Category="Player"
)
float CurrentHealth;
</code></pre>
<p>The key is the Unreal reflection system.</p>
<p><code>UPROPERTY</code> allows C++ to define the architecture while still exposing appropriate values to Blueprint.</p>
<hr />
<h2>Step 3: Move Core Functions</h2>
<p>Suppose damage handling has grown into a complicated Blueprint graph.</p>
<p>Move the core calculation:</p>
<pre><code class="language-cpp">UFUNCTION(BlueprintCallable)
void ApplyPlayerDamage(float DamageAmount);
</code></pre>
<p>Implementation:</p>
<pre><code class="language-cpp">void APlayerCharacterBase::ApplyPlayerDamage(
    float DamageAmount)
{
    CurrentHealth -= DamageAmount;

    CurrentHealth = FMath::Clamp(
        CurrentHealth,
        0.0f,
        MaxHealth
    );
}
</code></pre>
<p>Blueprint can still call:</p>
<pre><code class="language-text">Apply Player Damage
</code></pre>
<p>as a normal node.</p>
<p>Now you get:</p>
<pre><code class="language-text">C++
 └── Damage calculation

Blueprint
 └── Animation
 └── Sound
 └── VFX
 └── UI feedback
</code></pre>
<p>This separation scales much better.</p>
<hr />
<h2>Step 4: Let Blueprint Implement Presentation</h2>
<p>Sometimes C++ should define <strong>when</strong> something happens without deciding <strong>how it looks</strong>.</p>
<p>For example:</p>
<pre><code class="language-cpp">UFUNCTION(BlueprintImplementableEvent)
void OnPlayerDeath();
</code></pre>
<p>C++:</p>
<pre><code class="language-cpp">if (CurrentHealth &lt;= 0.0f)
{
    OnPlayerDeath();
}
</code></pre>
<p>Blueprint can implement:</p>
<pre><code class="language-text">On Player Death
      ↓
Play Animation
      ↓
Spawn VFX
      ↓
Play Sound
      ↓
Show Game Over UI
</code></pre>
<p>This creates a useful boundary:</p>
<p><strong>C++ owns the system. Blueprint owns presentation.</strong></p>
<hr />
<h2>Step 5: Reparent the Existing Blueprint</h2>
<p>Once the C++ base class exists, your Blueprint can inherit from it.</p>
<p>Conceptually:</p>
<pre><code class="language-text">Before

BP_PlayerCharacter
       ↓
   Character


After

BP_PlayerCharacter
       ↓
APlayerCharacterBase
       ↓
   Character
</code></pre>
<p>Then migrate Blueprint functionality gradually.</p>
<p>Don't move twenty systems at once.</p>
<p>Try:</p>
<pre><code class="language-text">Health
  ↓
Test

Movement
  ↓
Test

Combat
  ↓
Test

Inventory
  ↓
Test
</code></pre>
<p>Small migrations make regressions much easier to locate.</p>
<hr />
<h2>Blueprint Header View Can Help</h2>
<p>Current Unreal Engine versions include <strong>Blueprint Header View</strong>.</p>
<p>You can right-click a Blueprint class or struct and use:</p>
<pre><code class="language-text">Preview Equivalent C++ Header
</code></pre>
<p>to see C++-style declarations representing Blueprint elements such as:</p>
<pre><code class="language-text">Variables
Functions
Components
Event Dispatchers
</code></pre>
<p>But there is an important limitation:</p>
<p><strong>it does not automatically convert your entire Blueprint implementation into clean production C++.</strong></p>
<p>Think of it as a migration aid.</p>
<p>You still need to implement and architect the actual C++ logic.</p>
<hr />
<h2>Don't Convert Everything</h2>
<p>One of the easiest mistakes during an <strong>Unreal Blueprint to C++</strong> migration is turning it into a rewrite.</p>
<p>You don't need:</p>
<pre><code class="language-text">Blueprint
    ↓
Delete Everything
    ↓
100% C++
</code></pre>
<p>A healthier target is often:</p>
<pre><code class="language-text">              GAME
               │
        ┌──────┴──────┐
        │             │
       C++        Blueprint
        │             │
Core systems       UI
Algorithms         VFX
Performance        Events
Networking         Tuning
Architecture       Content
</code></pre>
<p>The exact split depends on your project and team.</p>
<hr />
<h2>A Simple Decision Rule</h2>
<p>Before converting a Blueprint, ask four questions.</p>
<pre><code class="language-text">Is it performance-critical?
        │
        YES → Consider C++

Is the graph difficult to maintain?
        │
        YES → Consider C++

Is it a reusable core system?
        │
        YES → C++ is often better

Does a designer need frequent iteration?
        │
        YES → Keep Blueprint exposure
</code></pre>
<p>This avoids converting code simply for the sake of having more C++.</p>
<hr />
<h2>Common Migration Mistakes</h2>
<p>The biggest problems usually aren't syntax problems.</p>
<p>They are architecture problems.</p>
<p>Avoid:</p>
<ul>
<li><p>converting without profiling</p>
</li>
<li><p>rewriting every Blueprint</p>
</li>
<li><p>moving presentation logic unnecessarily</p>
</li>
<li><p>creating giant C++ classes</p>
</li>
<li><p>breaking Blueprint asset references</p>
</li>
<li><p>exposing every C++ property publicly</p>
</li>
<li><p>migrating multiple large systems simultaneously</p>
</li>
<li><p>forgetting to test after reparenting</p>
</li>
</ul>
<p>Also remember that moving a Blueprint into C++ may require updating existing references. For larger refactors, Unreal's <strong>Core Redirects</strong> can help remap references when classes or other reflected elements change.</p>
<hr />
<h2>Final Takeaway</h2>
<p>An <strong>Unreal Blueprint to C++</strong> conversion should be a refactor, not a declaration that Blueprint failed.</p>
<p>Blueprint is excellent for:</p>
<pre><code class="language-text">Prototype
Design
Configure
Script
Iterate
</code></pre>
<p>C++ becomes valuable for:</p>
<pre><code class="language-text">Architecture
Performance
Complex algorithms
Reusable systems
Large-scale maintenance
</code></pre>
<p>The strongest Unreal projects often use both.</p>
<p>Prototype quickly in Blueprint, identify which systems become stable or expensive, profile before optimizing, move the right foundation into C++, and expose clean interfaces back to Blueprint.</p>
<p>That gives programmers the control of native C++ without removing the iteration speed that makes Blueprint so valuable in the first place.</p>
]]></content:encoded></item><item><title><![CDATA[Building a WebGL Playable Ad From an Existing Game]]></title><description><![CDATA[How to turn an existing game into a lightweight, fast-loading WebGL playable ad without trying to squeeze the entire game into a browser ad.


Playable ads give users something normal ads cannot: a ch]]></description><link>https://sdlccorp-softwaredev.hashnode.dev/building-a-webgl-playable-ad-from-an-existing-game</link><guid isPermaLink="true">https://sdlccorp-softwaredev.hashnode.dev/building-a-webgl-playable-ad-from-an-existing-game</guid><dc:creator><![CDATA[Samcorp]]></dc:creator><pubDate>Fri, 28 Aug 2026 10:30:52 GMT</pubDate><content:encoded><![CDATA[<p>How to turn an existing game into a lightweight, fast-loading WebGL playable ad without trying to squeeze the entire game into a browser ad.</p>
<img src="https://cdn.hashnode.com/uploads/covers/685a6d69d4d0ea366852534c/d8962262-bd79-4c24-aa43-d8eb96c3f575.png" alt="" style="display:block;margin:0 auto" />

<p>Playable ads give users something normal ads cannot: a chance to experience the game before installing it.</p>
<p>Instead of:</p>
<pre><code class="language-text">Watch Ad → Click → Store → Install
</code></pre>
<p>the journey becomes:</p>
<pre><code class="language-text">Play Mini Experience → Understand Game → CTA → Install
</code></pre>
<p>That sounds simple, especially when the game already exists.</p>
<p>But building a <strong>WebGL playable ad</strong> is not the same as exporting the complete game to WebGL.</p>
<p>The real challenge is deciding which part of the game should survive the conversion—and then making that experience small, fast, understandable, and persuasive.</p>
<p>Let's walk through a practical approach.</p>
<hr />
<h2>What Is a WebGL Playable Ad?</h2>
<p>A WebGL playable ad is a small interactive game experience that runs inside a browser or advertising environment without requiring the user to install the complete game first.</p>
<p>A typical structure looks like:</p>
<pre><code class="language-text">Ad Loads
   ↓
Instant Gameplay
   ↓
Simple Objective
   ↓
Win / Fail Moment
   ↓
Call to Action
   ↓
Install Full Game
</code></pre>
<p>The playable should communicate the game's core appeal within a very short interaction.</p>
<p>For example, if your original game is a runner:</p>
<pre><code class="language-text">Full Game
├── 40 levels
├── Character upgrades
├── Shop
├── Daily rewards
├── Multiple environments
└── Missions
</code></pre>
<p>your playable might contain only:</p>
<pre><code class="language-text">Playable Ad
├── One character
├── One short track
├── Basic movement
├── Coins
├── One obstacle type
└── Install CTA
</code></pre>
<p>That's the first important principle:</p>
<blockquote>
<p><strong>Extract the core loop. Don't port the entire game.</strong></p>
</blockquote>
<hr />
<h2>Step 1: Find the Most Marketable Gameplay Loop</h2>
<p>Before touching WebGL settings, decide what users should actually experience.</p>
<p>Ask:</p>
<p><strong>What makes the game fun within the first few seconds?</strong></p>
<p>For a puzzle game, that might be:</p>
<pre><code class="language-text">Move → Match → Reward
</code></pre>
<p>For a runner:</p>
<pre><code class="language-text">Swipe → Avoid → Collect
</code></pre>
<p>For a strategy game:</p>
<pre><code class="language-text">Place Unit → Fight → Win
</code></pre>
<p>For a merge game:</p>
<pre><code class="language-text">Drag → Merge → Upgrade
</code></pre>
<p>Your playable needs one clear loop.</p>
<p>Avoid including:</p>
<ul>
<li><p>account systems</p>
</li>
<li><p>inventories</p>
</li>
<li><p>settings</p>
</li>
<li><p>complex tutorials</p>
</li>
<li><p>multiplayer</p>
</li>
<li><p>unnecessary menus</p>
</li>
<li><p>progression systems</p>
</li>
<li><p>large maps</p>
</li>
</ul>
<p>The playable isn't a demo of every feature.</p>
<p>It's a focused advertisement built around interaction.</p>
<hr />
<h2>Step 2: Create a Separate Playable Scene</h2>
<p>Don't start deleting systems from the production game.</p>
<p>Create a dedicated scene:</p>
<pre><code class="language-text">Assets/
├── MainGame/
│
└── PlayableAd/
    ├── Scenes/
    ├── Scripts/
    ├── Art/
    └── Audio/
</code></pre>
<p>Then build only what the playable requires.</p>
<p>For example:</p>
<pre><code class="language-text">PlayableScene
│
├── Camera
├── Player
├── Small Environment
├── Gameplay Controller
├── UI
└── CTA Screen
</code></pre>
<p>This makes optimization much easier because unnecessary game systems never enter the playable build.</p>
<p>Teams already using Unity can also reuse gameplay logic while creating a browser-specific build pipeline. A broader <a href="https://sdlccorp.com/services/games/unity-game-development-company/">Unity game development workflow</a> can target WebGL alongside mobile and other platforms while keeping platform-specific performance requirements separate.</p>
<hr />
<h2>Step 3: Simplify the Gameplay</h2>
<p>Suppose the original game contains:</p>
<pre><code class="language-csharp">PlayerController
InventorySystem
AchievementManager
CloudSaveManager
AnalyticsManager
MultiplayerManager
ShopManager
DailyRewardManager
</code></pre>
<p>The playable may only need:</p>
<pre><code class="language-csharp">PlayerController
PlayableGameManager
PlayableUI
</code></pre>
<p>Remove dependencies aggressively.</p>
<p>A playable ad should behave more like a small standalone game than a compressed production build.</p>
<hr />
<h2>Step 4: Reduce Asset Weight</h2>
<p>Assets are often responsible for most of the downloadable size.</p>
<p>Start with textures.</p>
<p>A production asset might be:</p>
<pre><code class="language-text">Environment.png
2048 × 2048
</code></pre>
<p>For the playable, perhaps:</p>
<pre><code class="language-text">Environment.png
512 × 512
</code></pre>
<p>Also consider:</p>
<pre><code class="language-text">Texture atlases
Lower-resolution textures
Simpler shaders
Smaller meshes
Reduced animation frames
Compressed audio
Fewer particle effects
</code></pre>
<p>If the original scene uses ten environment variations, keep only the one shown in the playable.</p>
<p>Every asset should answer:</p>
<blockquote>
<p>Does the user need this to understand why the game is fun?</p>
</blockquote>
<p>If not, remove it.</p>
<hr />
<h2>Step 5: Optimize for WebGL</h2>
<p>Once the playable scene is isolated, switch the project to WebGL.</p>
<p>In Unity:</p>
<pre><code class="language-text">File
  ↓
Build Settings
  ↓
WebGL
  ↓
Switch Platform
</code></pre>
<p>For production builds, focus on reducing downloadable size.</p>
<p>Useful settings include:</p>
<pre><code class="language-text">Code Optimization → Size

Strip Engine Code → Enabled

Compression → Brotli or Gzip

Development Build → Disabled
</code></pre>
<p>Development builds should only be used while debugging because they contain additional development information and are much larger.</p>
<hr />
<h2>Step 6: Design for Fast Startup</h2>
<p>Playable ads compete for seconds of attention.</p>
<p>A user shouldn't stare at:</p>
<pre><code class="language-text">Loading... 13%
Loading... 27%
Loading... 44%
</code></pre>
<p>for several seconds before seeing anything useful.</p>
<p>Aim for:</p>
<pre><code class="language-text">Load
 ↓
Visual Feedback
 ↓
Gameplay
</code></pre>
<p>as quickly as possible.</p>
<p>This is why the first scene should contain only essential assets.</p>
<p>For browser-focused projects, <a href="https://sdlccorp.com/services/games/html-5-game-development-company/">HTML5 and WebGL game development</a> follows the same general principle: browser experiences need lightweight assets and performance-conscious rendering because users expect interaction without an installation step.</p>
<hr />
<h2>Step 7: Make the First Interaction Obvious</h2>
<p>Don't require a long tutorial.</p>
<p>Use simple visual guidance.</p>
<p>For example:</p>
<pre><code class="language-text">👆 SWIPE TO MOVE
</code></pre>
<p>or:</p>
<pre><code class="language-text">TAP TO JUMP
</code></pre>
<p>Then let the user interact immediately.</p>
<p>A good playable experience often looks like:</p>
<pre><code class="language-text">0–2 sec
Load + visual instruction

2–5 sec
User understands control

5–15 sec
Core gameplay

15–25 sec
Reward / challenge

25+ sec
CTA
</code></pre>
<p>The exact timing depends on the game and ad network, but the principle is universal:</p>
<p><strong>reach gameplay quickly.</strong></p>
<hr />
<h2>Step 8: Build a Clear CTA</h2>
<p>Eventually, the playable needs to become an advertisement again.</p>
<p>After a success moment:</p>
<pre><code class="language-text">LEVEL COMPLETE!

Continue the Adventure

[ PLAY NOW ]
</code></pre>
<p>Or after failure:</p>
<pre><code class="language-text">SO CLOSE!

Think You Can Beat It?

[ TRY THE FULL GAME ]
</code></pre>
<p>The CTA should feel connected to the gameplay rather than randomly interrupting it.</p>
<hr />
<h2>Step 9: Connect JavaScript and Unity</h2>
<p>Playable environments often require communication between Unity and the surrounding web page.</p>
<p>Conceptually:</p>
<pre><code class="language-text">Unity WebGL
     │
     ▼
JavaScript Bridge
     │
     ▼
Ad Environment
     │
     ▼
Store / CTA
</code></pre>
<p>For example, your game could call a JavaScript function when the user taps the CTA:</p>
<pre><code class="language-javascript">function openStore() {
    // Trigger the ad platform's
    // supported click-through behavior.
}
</code></pre>
<p>Keep this integration isolated.</p>
<p>Something like:</p>
<pre><code class="language-text">PlayableAdBridge.cs
</code></pre>
<p>should handle browser/ad-specific behavior rather than mixing it throughout gameplay code.</p>
<hr />
<h2>Step 10: Test on Real Mobile Hardware</h2>
<p>A playable running smoothly on a developer PC proves very little.</p>
<p>Test it on:</p>
<pre><code class="language-text">Low-end Android
Mid-range Android
Recent Android
Older iPhone
Recent iPhone
</code></pre>
<p>Check:</p>
<pre><code class="language-text">Startup time
FPS
Memory
Touch input
Orientation
Audio
CTA
Browser compatibility
</code></pre>
<p>Also test slow network conditions.</p>
<p>A playable that works perfectly on office Wi-Fi may behave very differently over a weak mobile connection.</p>
<hr />
<h2>Step 11: Measure More Than Clicks</h2>
<p>Once the playable runs correctly, track the interaction funnel.</p>
<p>For example:</p>
<pre><code class="language-text">Playable Loaded
      ↓
First Interaction
      ↓
50% Completed
      ↓
Gameplay Completed
      ↓
CTA Clicked
</code></pre>
<p>Useful metrics include:</p>
<pre><code class="language-text">Load completion rate
Interaction rate
Completion rate
Average play duration
CTA click-through rate
Install conversion rate
</code></pre>
<p>Now you can compare playable variants.</p>
<p>For example:</p>
<pre><code class="language-text">Version A
Immediate gameplay
CTA conversion: 6.1%

Version B
5-second tutorial
CTA conversion: 3.8%
</code></pre>
<p>That tells you something actionable.</p>
<hr />
<h2>Common WebGL Playable Ad Mistakes</h2>
<h2>Porting the Entire Game</h2>
<p>The biggest mistake is treating the playable as a normal WebGL release.</p>
<p>Instead:</p>
<pre><code class="language-text">Existing Game
     ↓
Extract Core Loop
     ↓
Simplify
     ↓
Optimize
     ↓
Playable Ad
</code></pre>
<hr />
<h2>Using Production-Quality Assets Everywhere</h2>
<p>Playable assets need to look good, but they don't always need the same resolution or complexity as the full game.</p>
<p>Optimize for the actual viewing environment.</p>
<hr />
<h2>Waiting Too Long for Interaction</h2>
<p>Users should understand what to do almost immediately.</p>
<p>Don't waste their attention on logos, menus, and long tutorials.</p>
<hr />
<h2>Ignoring Ad-Network Requirements</h2>
<p>Different ad platforms can impose different requirements around:</p>
<pre><code class="language-text">Bundle size
External requests
Audio
Orientation
Click behavior
File structure
JavaScript APIs
</code></pre>
<p>Always check the requirements of the network where the playable will actually run before final packaging.</p>
<hr />
<h2>Optimizing Only for File Size</h2>
<p>A tiny playable isn't automatically a good playable.</p>
<p>You still need:</p>
<pre><code class="language-text">Fast loading
+
Smooth gameplay
+
Clear mechanics
+
Strong creative
+
Effective CTA
</code></pre>
<p>The goal is conversion—not winning a compression contest.</p>
<hr />
<h2>A Practical Production Workflow</h2>
<p>A useful workflow looks like:</p>
<pre><code class="language-text">Existing Game
      │
      ▼
Identify Core Loop
      │
      ▼
Create Playable Scene
      │
      ▼
Remove Dependencies
      │
      ▼
Optimize Assets
      │
      ▼
Build WebGL
      │
      ▼
Integrate Ad Bridge
      │
      ▼
Test Devices
      │
      ▼
Measure Engagement
      │
      ▼
Iterate
</code></pre>
<p>Keeping the playable isolated from the production game makes each step easier to control.</p>
<h2>Final Takeaway</h2>
<p>Building a <strong>WebGL playable ad</strong> from an existing game isn't mainly an export task.</p>
<p>It's an editing task.</p>
<p>You are taking a large game experience and reducing it to the smallest interaction that still communicates:</p>
<blockquote>
<p><strong>This game is fun. I want more.</strong></p>
</blockquote>
<ul>
<li><p>Start with the strongest gameplay loop.</p>
</li>
<li><p>Build a separate playable scene.</p>
</li>
<li><p>Remove everything the interaction doesn't need.</p>
</li>
<li><p>Compress assets and strip unused code.</p>
</li>
<li><p>Get users playing quickly.</p>
</li>
</ul>
<p>Then finish with a clear CTA and measure what they actually do.</p>
<p>The best playable ad isn't the one that reproduces the most features from the original game.</p>
<p>It's the one that delivers the <strong>right few seconds of gameplay</strong> well enough to make users want the full experience.</p>
]]></content:encoded></item><item><title><![CDATA[Odoo API Auth in Practice: XML-RPC vs JSON-RPC]]></title><description><![CDATA[A practical look at Odoo XML-RPC and JSON-RPC authentication, API calls, security, and what developers should use for new integrations.


  
Connecting an external application to Odoo usually starts w]]></description><link>https://sdlccorp-softwaredev.hashnode.dev/odoo-api-auth-in-practice-xml-rpc-vs-json-rpc</link><guid isPermaLink="true">https://sdlccorp-softwaredev.hashnode.dev/odoo-api-auth-in-practice-xml-rpc-vs-json-rpc</guid><dc:creator><![CDATA[Samcorp]]></dc:creator><pubDate>Fri, 28 Aug 2026 10:08:28 GMT</pubDate><content:encoded><![CDATA[<p>A practical look at Odoo XML-RPC and JSON-RPC authentication, API calls, security, and what developers should use for new integrations.</p>
<img src="https://cdn.hashnode.com/uploads/covers/685a6d69d4d0ea366852534c/26d1f10c-8cdf-4d60-a1f6-90edc72d7ea8.png" alt="" style="display:block;margin:0 auto" />

  
<p>Connecting an external application to Odoo usually starts with one question:</p>
<p><strong>How should the application authenticate and communicate with Odoo?</strong></p>
<p>For years, two common answers have been <strong>XML-RPC and JSON-RPC</strong>.</p>
<p>Both can connect external applications with Odoo models, but they differ in payload format, client implementation, and developer experience.</p>
<p>There is also an important change for modern Odoo projects: Odoo 19 deprecates the legacy external XML-RPC and JSON-RPC endpoints in favor of the newer <strong>JSON-2 API</strong>.</p>
<p>So if you're working with <strong>Odoo XMLRPC JSONRPC</strong> integrations today, understanding both the existing authentication flow and the migration direction matters.</p>
<hr />
<h2>The Basic Odoo API Flow</h2>
<p>A traditional external Odoo integration usually follows this pattern:</p>
<pre><code class="language-text">External Application
        │
        ▼
 Authentication
        │
        ▼
      User ID
        │
        ▼
 Odoo Object Service
        │
        ▼
     Odoo Model
        │
        ▼
      Records
</code></pre>
<p>Typical connection information includes:</p>
<pre><code class="language-text">Odoo URL
Database
Username
Password / API Key
</code></pre>
<p>Once authenticated, the integration can interact with models such as:</p>
<pre><code class="language-text">res.partner
sale.order
product.product
account.move
stock.picking
</code></pre>
<p>The user's normal Odoo access rights and record rules still determine what the integration can access.</p>
<p>For larger integrations involving CRM, eCommerce, payments, logistics, or other enterprise systems, a structured <a href="https://sdlccorp.com/services/odoo-services/odoo-integration-services/">Odoo integration approach</a> is important because authentication is only one part of reliable data synchronization.</p>
<hr />
<h2>XML-RPC Authentication</h2>
<p>XML-RPC is one of Odoo's long-standing external API mechanisms.</p>
<p>In Python, the standard library already provides an XML-RPC client.</p>
<pre><code class="language-python">import xmlrpc.client

url = "https://example.odoo.com"
db = "example"
username = "integration@example.com"
api_key = "YOUR_API_KEY"

common = xmlrpc.client.ServerProxy(
    f"{url}/xmlrpc/2/common"
)

uid = common.authenticate(
    db,
    username,
    api_key,
    {}
)

print(uid)
</code></pre>
<p>If authentication succeeds, Odoo returns a user ID:</p>
<pre><code class="language-text">7
</code></pre>
<p>That <code>uid</code> is then used for model operations.</p>
<hr />
<h2>Calling an Odoo Model With XML-RPC</h2>
<p>Create another proxy:</p>
<pre><code class="language-python">models = xmlrpc.client.ServerProxy(
    f"{url}/xmlrpc/2/object"
)
</code></pre>
<p>Now we can query contacts:</p>
<pre><code class="language-python">partners = models.execute_kw(
    db,
    uid,
    api_key,
    "res.partner",
    "search_read",
    [[["is_company", "=", True]]],
    {
        "fields": ["name", "email"],
        "limit": 5
    }
)

print(partners)
</code></pre>
<p>Conceptually, the call is:</p>
<pre><code class="language-text">Application
     │
     ▼
/xmlrpc/2/object
     │
     ▼
execute_kw()
     │
     ▼
res.partner.search_read()
</code></pre>
<p>The approach is straightforward, particularly for Python scripts and existing integrations.</p>
<hr />
<h2>What About API Keys?</h2>
<p>For integrations, avoid hardcoding a user's main password whenever possible.</p>
<p>Odoo supports API keys that can replace the password in traditional RPC calls.</p>
<p>So instead of:</p>
<pre><code class="language-python">password = "user-password"
</code></pre>
<p>you can use:</p>
<pre><code class="language-python">api_key = os.environ["ODOO_API_KEY"]
</code></pre>
<p>and authenticate with that credential.</p>
<p>Keep it outside your source code:</p>
<pre><code class="language-bash">export ODOO_API_KEY="..."
</code></pre>
<p>Treat the API key like a password.</p>
<p>Anyone who obtains it may receive the permissions associated with that Odoo user.</p>
<hr />
<h2>JSON-RPC Authentication</h2>
<p>JSON-RPC uses JSON rather than XML to represent requests and responses.</p>
<p>A typical request structure looks conceptually like this:</p>
<pre><code class="language-json">{
  "jsonrpc": "2.0",
  "method": "call",
  "params": {
    "...": "..."
  },
  "id": 1
}
</code></pre>
<p>Because JSON maps naturally to JavaScript objects, JSON-RPC has historically been convenient for web-oriented integrations.</p>
<p>A request can be made using a normal HTTP client:</p>
<pre><code class="language-python">import requests

payload = {
    "jsonrpc": "2.0",
    "method": "call",
    "params": {
        "service": "common",
        "method": "authenticate",
        "args": [
            db,
            username,
            api_key,
            {}
        ]
    },
    "id": 1
}

response = requests.post(
    f"{url}/jsonrpc",
    json=payload
)

uid = response.json()["result"]
</code></pre>
<p>The authentication concept remains similar:</p>
<pre><code class="language-text">Database
   +
Username
   +
Credential
   ↓
Authenticate
   ↓
User ID
</code></pre>
<p>The major difference is how the RPC request is encoded and transported.</p>
<hr />
<h2>XML-RPC vs JSON-RPC</h2>
<p>Here's the practical comparison:</p>
<table>
<thead>
<tr>
<th>Area</th>
<th>XML-RPC</th>
<th>JSON-RPC</th>
</tr>
</thead>
<tbody><tr>
<td>Encoding</td>
<td>XML</td>
<td>JSON</td>
</tr>
<tr>
<td>Payload</td>
<td>More verbose</td>
<td>Generally lighter</td>
</tr>
<tr>
<td>Python support</td>
<td>Excellent</td>
<td>Excellent</td>
</tr>
<tr>
<td>JavaScript friendliness</td>
<td>Moderate</td>
<td>High</td>
</tr>
<tr>
<td>Human readability</td>
<td>Lower</td>
<td>Higher</td>
</tr>
<tr>
<td>Legacy Odoo integrations</td>
<td>Very common</td>
<td>Common</td>
</tr>
<tr>
<td>Odoo 19 external RPC status</td>
<td>Deprecated</td>
<td>Deprecated</td>
</tr>
<tr>
<td>Long-term direction</td>
<td>JSON-2</td>
<td>JSON-2</td>
</tr>
</tbody></table>
<p>If you're maintaining an older Python integration, XML-RPC can still be perfectly understandable and functional for supported versions.</p>
<p>For a new long-lived integration, however, protocol choice shouldn't be made without considering Odoo's deprecation roadmap.</p>
<hr />
<h2>The Important Odoo 19 Change</h2>
<p>This is where older Odoo API tutorials can become misleading.</p>
<p>Starting with Odoo 19, the external endpoints:</p>
<pre><code class="language-text">/xmlrpc
/xmlrpc/2
/jsonrpc
</code></pre>
<p>are deprecated.</p>
<p>Odoo's replacement is the <strong>External JSON-2 API</strong>.</p>
<p>The newer architecture looks more like:</p>
<pre><code class="language-text">POST /json/2/&lt;model&gt;/&lt;method&gt;
</code></pre>
<p>with an API key supplied through the authorization header.</p>
<p>For example:</p>
<pre><code class="language-python">import requests

response = requests.post(
    f"{url}/json/2/res.partner/search_read",
    headers={
        "Authorization": f"bearer {api_key}"
    },
    json={
        "domain": [
            ["is_company", "=", True]
        ],
        "fields": [
            "name",
            "email"
        ],
        "limit": 5
    }
)

partners = response.json()
</code></pre>
<p>Notice what's missing:</p>
<pre><code class="language-text">username
password
uid
execute_kw
</code></pre>
<p>Authentication becomes API-key based.</p>
<p>The request itself identifies the model and method:</p>
<pre><code class="language-text">/json/2/res.partner/search_read
        │
        ├── Model  → res.partner
        │
        └── Method → search_read
</code></pre>
<p>This produces a cleaner HTTP integration model.</p>
<hr />
<h2>Authentication: Old RPC vs JSON-2</h2>
<p>The difference is easier to see side by side.</p>
<h3>XML-RPC / Legacy JSON-RPC</h3>
<pre><code class="language-text">Database
   +
Username
   +
Password/API Key
       │
       ▼
  authenticate()
       │
       ▼
      UID
       │
       ▼
   execute_kw()
</code></pre>
<h3>JSON-2</h3>
<pre><code class="language-text">API Key
   │
   ▼
Authorization: bearer &lt;key&gt;
   │
   ▼
/json/2/model/method
   │
   ▼
Odoo
</code></pre>
<p>For new Odoo 19+ integrations, this newer model deserves serious consideration.</p>
<hr />
<h2>Use a Dedicated Integration User</h2>
<p>Whichever API style you use, don't automatically connect integrations through an administrator account.</p>
<p>Create a dedicated user such as:</p>
<pre><code class="language-text">erp-integration-bot
</code></pre>
<p>and grant only the permissions required by the integration.</p>
<p>For example, an application that synchronizes customers may need:</p>
<pre><code class="language-text">Contacts → Read
Sales → Read
</code></pre>
<p>but probably doesn't need:</p>
<pre><code class="language-text">Settings → Administration
Accounting → Full Access
Users → Manage
</code></pre>
<p>This follows the principle of least privilege.</p>
<p>If an integration credential is compromised, the account's permissions determine the potential impact.</p>
<hr />
<h2>Keep Authentication Configuration Outside Code</h2>
<p>Avoid this:</p>
<pre><code class="language-python">username = "admin@example.com"
api_key = "abcd1234..."
</code></pre>
<p>Prefer environment variables or a secrets manager:</p>
<pre><code class="language-python">import os

url = os.environ["ODOO_URL"]
db = os.environ["ODOO_DB"]
username = os.environ["ODOO_USER"]
api_key = os.environ["ODOO_API_KEY"]
</code></pre>
<p>Your repository then contains integration logic—not production credentials.</p>
<p>For custom modules and workflows that require API hooks or external data exchange, <a href="https://sdlccorp.com/services/odoo-services/odoo-development-company/">custom Odoo development</a> can also separate integration logic from core ERP functionality and make future upgrades easier to manage.</p>
<h2>Handle Authentication Failures Clearly</h2>
<p>Don't assume authentication always succeeds.</p>
<p>For XML-RPC:</p>
<pre><code class="language-python">uid = common.authenticate(
    db,
    username,
    api_key,
    {}
)

if not uid:
    raise RuntimeError(
        "Odoo authentication failed"
    )
</code></pre>
<p>For HTTP-based APIs, also check:</p>
<pre><code class="language-text">HTTP status
Timeouts
Invalid JSON
Expired credentials
Access errors
</code></pre>
<p>A production integration should distinguish between:</p>
<pre><code class="language-text">Authentication failure
Authorization failure
Network failure
Odoo server failure
Invalid request
</code></pre>
<p>They require different fixes.</p>
<hr />
<h2>Which Should You Use?</h2>
<p>The answer depends largely on the Odoo version and whether you're maintaining or creating the integration.</p>
<h3>Existing XML-RPC integration</h3>
<p>If it's stable and running against a supported Odoo version, you don't necessarily need an immediate rewrite.</p>
<p>But plan for migration.</p>
<h3>Existing JSON-RPC integration</h3>
<p>The same principle applies.</p>
<p>Keep it operational while preparing for JSON-2.</p>
<h3>New Odoo 19+ integration</h3>
<p>Prefer evaluating <strong>JSON-2</strong> first.</p>
<p>Building a brand-new integration around an already deprecated API creates avoidable future migration work.</p>
<h3>Older Odoo environment</h3>
<p>XML-RPC may remain the simplest option, especially for Python automation.</p>
<p>Compatibility with your actual Odoo version matters more than following the newest API blindly.</p>
<hr />
<h2>A Practical Decision Guide</h2>
<pre><code class="language-text">What are you building?
        │
        ▼
Existing integration?
   │           │
  YES          NO
   │           │
   ▼           ▼
XML/JSON RPC   Odoo 19+?
still works?    │
   │         ┌─┴─┐
  YES       YES  NO
   │         │    │
   ▼         ▼    ▼
Maintain   JSON-2  Check version
+
Plan migration
</code></pre>
<p>The key is not simply choosing XML or JSON.</p>
<p>It's choosing an API strategy that matches the lifecycle of your Odoo installation.</p>
<hr />
<h2>Security Checklist</h2>
<p>Before shipping an Odoo API integration:</p>
<ul>
<li><p>Use HTTPS.</p>
</li>
<li><p>Prefer API keys over embedding user passwords.</p>
</li>
<li><p>Store credentials outside source code.</p>
</li>
<li><p>Use a dedicated integration account.</p>
</li>
<li><p>Apply minimum required permissions.</p>
</li>
<li><p>Rotate and revoke credentials when appropriate.</p>
</li>
<li><p>Handle authentication errors explicitly.</p>
</li>
<li><p>Log failures without logging secrets.</p>
</li>
<li><p>Test record rules and access permissions.</p>
</li>
<li><p>Plan migration away from deprecated APIs.</p>
</li>
</ul>
<p>Authentication is only secure when the entire integration respects the same trust boundary.</p>
<hr />
<h2>Final Takeaway</h2>
<p>The <strong>Odoo XMLRPC JSONRPC</strong> comparison is no longer simply about XML versus JSON.</p>
<p>For existing Odoo integrations:</p>
<pre><code class="language-text">XML-RPC → Mature and widely used
JSON-RPC → JSON-based alternative
</code></pre>
<p>But for modern Odoo development:</p>
<pre><code class="language-text">XML-RPC
     \
      → Deprecated → JSON-2
     /
JSON-RPC
</code></pre>
<p>If you're maintaining an older integration, understand its authentication flow, protect API credentials, and keep permissions narrow.</p>
<p>If you're starting a new integration on Odoo 19 or later, evaluate JSON-2 before committing to either legacy RPC interface.</p>
<p>That small architecture decision today can save a much larger migration later.</p>
]]></content:encoded></item><item><title><![CDATA[Setting Up LLM Observability Without a Vendor]]></title><description><![CDATA[Build a vendor-neutral observability stack for LLM applications using traces, metrics, structured logs, OpenTelemetry, and your own dashboards.An LLM application fails in production.  


The API retur]]></description><link>https://sdlccorp-softwaredev.hashnode.dev/setting-up-llm-observability-without-a-vendor</link><guid isPermaLink="true">https://sdlccorp-softwaredev.hashnode.dev/setting-up-llm-observability-without-a-vendor</guid><dc:creator><![CDATA[Samcorp]]></dc:creator><pubDate>Thu, 27 Aug 2026 10:06:51 GMT</pubDate><content:encoded><![CDATA[<p>Build a vendor-neutral observability stack for LLM applications using traces, metrics, structured logs, OpenTelemetry, and your own dashboards.An LLM application fails in production.  </p>
<img src="https://cdn.hashnode.com/uploads/covers/685a6d69d4d0ea366852534c/d4826192-7edc-4ba7-ae5c-24ceef503390.png" alt="" style="display:block;margin:0 auto" />

<p>The API returned <code>200 OK</code>.</p>
<p>Your infrastructure dashboard is green.</p>
<p>CPU looks normal.</p>
<p>Memory looks normal.</p>
<p>Yet the user waited 18 seconds and received a poor answer.</p>
<p>What happened?</p>
<ul>
<li><p>Maybe the model was slow.</p>
</li>
<li><p>Maybe the prompt became enormous.</p>
</li>
<li><p>Maybe retrieval returned irrelevant context.</p>
</li>
<li><p>Maybe an agent called the same tool four times.</p>
</li>
<li><p>Maybe a retry silently doubled the cost.</p>
</li>
</ul>
<p>Traditional application monitoring doesn't always answer these questions.</p>
<p>That's where <strong>LLM observability</strong> comes in.</p>
<p>And you don't necessarily need to send every prompt, response, and trace to a specialized observability vendor to get started.</p>
<p>In this guide, we'll build a practical <strong>LLM observability setup</strong> around open telemetry concepts so that we can understand:</p>
<ul>
<li><p>requests</p>
</li>
<li><p>model calls</p>
</li>
<li><p>latency</p>
</li>
<li><p>tokens</p>
</li>
<li><p>estimated cost</p>
</li>
<li><p>errors</p>
</li>
<li><p>retrieval</p>
</li>
<li><p>tool calls</p>
</li>
<li><p>end-to-end traces</p>
</li>
</ul>
<p>while keeping control over where our telemetry goes.</p>
<hr />
<h2>What Is LLM Observability?</h2>
<p>LLM observability is the ability to understand what happens inside an LLM-powered application by collecting and correlating telemetry.</p>
<p>For a basic application:</p>
<pre><code class="language-text">User
  │
  ▼
Application
  │
  ▼
LLM
  │
  ▼
Response
</code></pre>
<p>we may want to know:</p>
<pre><code class="language-text">Which model handled the request?

How long did generation take?

How many input tokens were consumed?

How many output tokens were generated?

Did the request fail?

How much did it approximately cost?
</code></pre>
<p>For an agent or RAG application, the execution path becomes more complicated:</p>
<pre><code class="language-text">User
 │
 ▼
Agent
 │
 ├── LLM Call
 │
 ├── Vector Search
 │
 ├── LLM Call
 │
 ├── Tool Call
 │
 ├── Tool Result
 │
 └── Final LLM Call
 │
 ▼
Response
</code></pre>
<p>Now observability must explain the entire execution, not just the final API request.</p>
<hr />
<h2>Monitoring vs. Observability</h2>
<p>These terms are related but aren't identical.</p>
<p>Monitoring tells you:</p>
<pre><code class="language-text">p95 latency = 4.8 seconds
error rate = 2.1%
requests/minute = 1,200
</code></pre>
<p>Observability helps answer:</p>
<blockquote>
<p>Why did p95 latency increase?</p>
</blockquote>
<p>Perhaps traces reveal:</p>
<pre><code class="language-text">Retrieval          180 ms
First LLM call    1,420 ms
Tool call         3,900 ms  ← bottleneck
Second LLM call   1,610 ms
</code></pre>
<p>The metric tells us something is wrong.</p>
<p>The trace helps explain where it happened.</p>
<p>A useful LLM stack therefore combines:</p>
<pre><code class="language-text">Metrics + Traces + Logs + Evaluation Signals
</code></pre>
<hr />
<h2>Why Build It Without a Specialized Vendor?</h2>
<p>Specialized LLM observability platforms can be useful.</p>
<p>But building the basic telemetry layer yourself has several advantages.</p>
<h3>Portability</h3>
<p>Your instrumentation doesn't have to depend on one dashboard provider.</p>
<h3>Data control</h3>
<p>Prompts and responses may contain:</p>
<pre><code class="language-text">customer information
source code
internal documents
financial information
credentials accidentally supplied by users
personal data
</code></pre>
<p>Keeping telemetry within infrastructure you control may simplify some privacy and security requirements.</p>
<h3>Existing infrastructure</h3>
<p>Your organization may already operate:</p>
<pre><code class="language-text">OpenTelemetry
Prometheus
Grafana
Jaeger
Tempo
Loki
Elasticsearch
</code></pre>
<p>Adding LLM signals to the same stack may be easier than creating another monitoring silo.</p>
<h3>Understanding</h3>
<p>Building the first version yourself forces you to decide which signals actually matter.</p>
<p>That's valuable even if you eventually adopt a specialized platform.</p>
<hr />
<h2>The Architecture We're Building</h2>
<p>Our initial architecture will look like this:</p>
<pre><code class="language-text">                    LLM Application
                           │
                           │ telemetry
                           ▼
                 OpenTelemetry SDK
                           │
                           ▼
                  OTel Collector
                    /      |      \
                   /       |       \
                  ▼        ▼        ▼
              Traces    Metrics    Logs
                 │         │         │
                 ▼         ▼         ▼
               Tempo   Prometheus   Loki
                  \        |        /
                   \       |       /
                    ▼      ▼      ▼
                       Grafana
</code></pre>
<p>You don't have to use these exact backends.</p>
<p>That's one of the advantages of the architecture.</p>
<p>The application emits standardized telemetry.</p>
<p>The backend is replaceable.</p>
<hr />
<h2>Step 1: Decide What You Need to Observe</h2>
<p>Don't start by logging everything.</p>
<p>Start with questions.</p>
<p>For example:</p>
<blockquote>
<p>Why is this request slow?</p>
</blockquote>
<blockquote>
<p>Which model is consuming the most tokens?</p>
</blockquote>
<blockquote>
<p>Which prompt version causes the most failures?</p>
</blockquote>
<blockquote>
<p>How often are tools failing?</p>
</blockquote>
<blockquote>
<p>How much does one successful request cost?</p>
</blockquote>
<blockquote>
<p>Which retrieval step adds the most latency?</p>
</blockquote>
<p>Those questions define your telemetry requirements.</p>
<p>For a first version, collect:</p>
<pre><code class="language-text">Request ID
Trace ID
Model
Operation
Latency
Input tokens
Output tokens
Finish reason
Status
Error type
Prompt version
Application version
Environment
</code></pre>
<p>For agents, add:</p>
<pre><code class="language-text">Tool name
Tool duration
Tool result status
Agent turns
Retry count
</code></pre>
<p>For RAG:</p>
<pre><code class="language-text">Retriever
Retrieval duration
Number of documents
Embedding model
Top-k
Context size
</code></pre>
<p>That's already enough to investigate many production problems.</p>
<hr />
<h2>Step 2: Don't Log Full Prompts by Default</h2>
<p>This deserves its own step.</p>
<p>The easiest observability implementation is:</p>
<pre><code class="language-python">logger.info(prompt)
logger.info(response)
</code></pre>
<p>It's also potentially dangerous.</p>
<p>Prompts may contain:</p>
<pre><code class="language-text">PII
customer records
internal documents
source code
access tokens
medical information
financial information
</code></pre>
<p>Instead, begin with metadata:</p>
<pre><code class="language-json">{
  "trace_id": "8c94...",
  "model": "example-model",
  "prompt_version": "support-v12",
  "input_tokens": 812,
  "output_tokens": 184,
  "duration_ms": 1420,
  "status": "ok"
}
</code></pre>
<p>If you later decide that prompt capture is necessary, make it an explicit feature with:</p>
<pre><code class="language-text">redaction
sampling
access controls
retention policies
encryption
audit logging
</code></pre>
<p>Observability shouldn't become a new data-leakage system.</p>
<hr />
<h2>Step 3: Start With OpenTelemetry</h2>
<p>OpenTelemetry gives us a vendor-neutral model for telemetry.</p>
<p>Instead of wiring our application directly to one backend:</p>
<pre><code class="language-text">Application
    │
    ▼
Vendor-specific SDK
    │
    ▼
Vendor
</code></pre>
<p>we can use:</p>
<pre><code class="language-text">Application
    │
    ▼
OpenTelemetry
    │
    ▼
OTLP
    │
    ├── Backend A
    ├── Backend B
    └── Self-hosted stack
</code></pre>
<p>This separation is one of the most important design choices in a vendor-neutral observability system.</p>
<hr />
<h2>Step 4: Instrument the Application</h2>
<p>Let's use Python for a minimal example.</p>
<p>Install OpenTelemetry packages:</p>
<pre><code class="language-bash">pip install \
  opentelemetry-api \
  opentelemetry-sdk \
  opentelemetry-exporter-otlp
</code></pre>
<p>Create a tracer:</p>
<pre><code class="language-python">from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace.export import (
    BatchSpanProcessor
)
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
    OTLPSpanExporter
)

resource = Resource.create({
    "service.name": "llm-api",
    "service.version": "1.0.0",
    "deployment.environment": "production"
})

provider = TracerProvider(resource=resource)

provider.add_span_processor(
    BatchSpanProcessor(
        OTLPSpanExporter(
            endpoint="http://otel-collector:4317",
            insecure=True
        )
    )
)

trace.set_tracer_provider(provider)

tracer = trace.get_tracer("llm-observability")
</code></pre>
<p>Now the application can create spans.</p>
<hr />
<h2>Step 5: Trace an LLM Call</h2>
<p>Wrap model execution in a span.</p>
<pre><code class="language-python">def call_llm(client, model, messages):

    with tracer.start_as_current_span(
        "llm.generate"
    ) as span:

        span.set_attribute(
            "gen_ai.request.model",
            model
        )

        response = client.generate(
            model=model,
            messages=messages
        )

        span.set_attribute(
            "gen_ai.usage.input_tokens",
            response.usage.input_tokens
        )

        span.set_attribute(
            "gen_ai.usage.output_tokens",
            response.usage.output_tokens
        )

        return response
</code></pre>
<p>Now a model request isn't just:</p>
<pre><code class="language-text">POST /chat
</code></pre>
<p>It becomes part of a distributed trace.</p>
<hr />
<h1>Step 6: Trace the Whole Request</h1>
<p>The model call should normally be a child of the user request.</p>
<pre><code class="language-python">def handle_request(question):

    with tracer.start_as_current_span(
        "answer_question"
    ):

        context = retrieve_context(question)

        answer = call_llm(
            client,
            MODEL,
            build_messages(
                question,
                context
            )
        )

        return answer
</code></pre>
<p>The resulting trace might look like:</p>
<pre><code class="language-text">answer_question                 2.83s
│
├── retrieve_context            0.31s
│
└── llm.generate                2.47s
</code></pre>
<p>This is where tracing becomes much more useful than disconnected logs.</p>
<p>You can see where the time went.</p>
<hr />
<h1>Step 7: Instrument Retrieval</h1>
<p>For a RAG system:</p>
<pre><code class="language-python">def retrieve_context(query):

    with tracer.start_as_current_span(
        "rag.retrieve"
    ) as span:

        results = vector_store.search(
            query,
            top_k=5
        )

        span.set_attribute(
            "rag.top_k",
            5
        )

        span.set_attribute(
            "rag.documents.returned",
            len(results)
        )

        return results
</code></pre>
<p>Now the trace becomes:</p>
<pre><code class="language-text">answer_question
│
├── rag.retrieve
│
└── llm.generate
</code></pre>
<p>Later you might add:</p>
<pre><code class="language-text">embedding generation
reranking
document filtering
context construction
</code></pre>
<p>as separate spans.</p>
<hr />
<h2>Step 8: Instrument Tool Calls</h2>
<p>Agent applications need another layer.</p>
<p>Suppose the model calls:</p>
<pre><code class="language-text">get_order_status
</code></pre>
<p>Trace it separately:</p>
<pre><code class="language-python">def execute_tool(name, arguments):

    with tracer.start_as_current_span(
        f"tool.{name}"
    ) as span:

        span.set_attribute(
            "tool.name",
            name
        )

        try:

            result = tool_registry[name](
                **arguments
            )

            span.set_attribute(
                "tool.status",
                "success"
            )

            return result

        except Exception as error:

            span.record_exception(error)

            span.set_attribute(
                "tool.status",
                "error"
            )

            raise
</code></pre>
<p>Now an agent trace might show:</p>
<pre><code class="language-text">agent.run                         7.4s
│
├── llm.generate                 1.8s
│
├── tool.get_order_status        3.2s
│
└── llm.generate                 2.1s
</code></pre>
<p>Immediately we know the model wasn't responsible for most of the delay.</p>
<p>The internal tool was.</p>
<hr />
<h2>Step 9: Add Metrics</h2>
<p>Traces explain individual requests.</p>
<p>Metrics reveal patterns across thousands of them.</p>
<p>At minimum, track:</p>
<pre><code class="language-text">LLM request count
LLM request duration
Input tokens
Output tokens
Errors
Tool duration
Tool errors
</code></pre>
<p>Useful dashboards might include:</p>
<pre><code class="language-text">Requests/min
p50 latency
p95 latency
p99 latency
Token usage/min
Average tokens/request
Errors by model
Latency by model
Tool failure rate
</code></pre>
<p>For production AI systems, observability becomes part of the broader deployment lifecycle rather than something added only after incidents. This is why a mature <a href="https://sdlccorp.com/generative-ai-development-services/">generative AI development process</a> typically combines testing, deployment, monitoring, guardrails, and ongoing optimization.</p>
<hr />
<h2>Step 10: Estimate Cost Yourself</h2>
<p>If you know a model's current token pricing, cost calculation is straightforward.</p>
<p>Conceptually:</p>
<pre><code class="language-python">def estimate_cost(
    input_tokens,
    output_tokens,
    input_price,
    output_price
):
    return (
        input_tokens / 1_000_000
        * input_price
    ) + (
        output_tokens / 1_000_000
        * output_price
    )
</code></pre>
<p>Then attach the result:</p>
<pre><code class="language-python">span.set_attribute(
    "llm.estimated_cost_usd",
    cost
)
</code></pre>
<p>You can now answer:</p>
<pre><code class="language-text">What does one request cost?

Which endpoint costs the most?

Which model consumes the most budget?

Did the latest prompt increase token usage?
</code></pre>
<p>Keep pricing configuration outside application code so it can be updated without changing instrumentation.</p>
<hr />
<h2>Step 11: Track Prompt Versions, Not Necessarily Prompt Text</h2>
<p>Suppose production currently uses:</p>
<pre><code class="language-text">support-agent-v17
</code></pre>
<p>Record:</p>
<pre><code class="language-python">span.set_attribute(
    "app.prompt.version",
    "support-agent-v17"
)
</code></pre>
<p>rather than storing the entire system prompt on every request.</p>
<p>Now you can compare:</p>
<pre><code class="language-text">Prompt v15
p95 latency: 2.8s
avg input: 720 tokens

Prompt v16
p95 latency: 3.0s
avg input: 890 tokens

Prompt v17
p95 latency: 4.4s
avg input: 1,620 tokens
</code></pre>
<p>Suddenly a latency regression has an obvious clue.</p>
<p>Prompt versioning gives you useful correlation without automatically storing sensitive content.</p>
<hr />
<h2>Step 12: Add Structured Logs</h2>
<p>You still need logs.</p>
<p>Just make them structured.</p>
<p>Instead of:</p>
<pre><code class="language-text">LLM failed!!!
</code></pre>
<p>write:</p>
<pre><code class="language-json">{
  "level": "error",
  "event": "llm_request_failed",
  "trace_id": "83a21...",
  "model": "example-model",
  "prompt_version": "support-v17",
  "error_type": "timeout",
  "retry_count": 2
}
</code></pre>
<p>The most important field is often:</p>
<pre><code class="language-text">trace_id
</code></pre>
<p>It connects logs with traces.</p>
<p>A useful debugging workflow becomes:</p>
<pre><code class="language-text">Alert
  ↓
Metric
  ↓
Trace
  ↓
Span
  ↓
Correlated logs
</code></pre>
<hr />
<h2>Step 13: Deploy an OpenTelemetry Collector</h2>
<p>Don't have every application export directly to storage.</p>
<p>Put a collector between them.</p>
<pre><code class="language-text">Applications
     │
     │ OTLP
     ▼
+---------------------+
| OpenTelemetry       |
| Collector           |
+---------------------+
     │
     ├── traces
     ├── metrics
     └── logs
</code></pre>
<p>The collector gives you a central place for:</p>
<pre><code class="language-text">batching
filtering
sampling
redaction
routing
retries
export
</code></pre>
<p>Your applications only need to know where the collector lives.</p>
<hr />
<h2>Step 14: Configure the Collector</h2>
<p>A simplified configuration might look like:</p>
<pre><code class="language-yaml">receivers:
  otlp:
    protocols:
      grpc:
      http:

processors:
  batch:

exporters:
  otlp/tempo:
    endpoint: tempo:4317
    tls:
      insecure: true

service:
  pipelines:

    traces:
      receivers:
        - otlp

      processors:
        - batch

      exporters:
        - otlp/tempo
</code></pre>
<p>You can later add separate pipelines for:</p>
<pre><code class="language-text">metrics
logs
</code></pre>
<p>and additional processors for filtering or sampling.</p>
<hr />
<h2>Step 15: Build a Local Stack</h2>
<p>A useful self-hosted stack is:</p>
<pre><code class="language-text">OpenTelemetry Collector
        │
        ├── Tempo      → traces
        ├── Prometheus → metrics
        └── Loki       → logs
                       │
                       ▼
                    Grafana
</code></pre>
<p>Another option is:</p>
<pre><code class="language-text">OpenTelemetry
      │
      ▼
Jaeger
</code></pre>
<p>for a simpler tracing-focused setup.</p>
<p>The important part isn't the exact backend.</p>
<p>It's keeping instrumentation separate from storage.</p>
<hr />
<h2>Step 16: Create Your First Dashboard</h2>
<p>Don't build 40 charts.</p>
<p>Start with a small operational dashboard.</p>
<h3>Traffic</h3>
<pre><code class="language-text">Requests/minute
Requests by model
</code></pre>
<h3>Latency</h3>
<pre><code class="language-text">p50
p95
p99
</code></pre>
<h3>Tokens</h3>
<pre><code class="language-text">Input tokens/min
Output tokens/min
Tokens/request
</code></pre>
<h3>Reliability</h3>
<pre><code class="language-text">Error rate
Timeout rate
Retry rate
</code></pre>
<h3>Cost</h3>
<pre><code class="language-text">Estimated cost/hour
Estimated cost/request
Cost by model
</code></pre>
<h3>Agent Tools</h3>
<pre><code class="language-text">Tool calls
Tool latency
Tool failures
</code></pre>
<p>That's enough to answer many operational questions.</p>
<hr />
<h1>Step 17: Add Alerts</h1>
<p>Dashboards require someone to look at them.</p>
<p>Alerts tell you when something changes.</p>
<p>For example:</p>
<pre><code class="language-text">p95 latency &gt; 8 seconds
for 10 minutes
</code></pre>
<p>or:</p>
<pre><code class="language-text">LLM error rate &gt; 5%
</code></pre>
<p>or:</p>
<pre><code class="language-text">Tool failure rate &gt; 10%
</code></pre>
<p>Token anomalies can also be useful:</p>
<pre><code class="language-text">Average input tokens increased
more than 40% from baseline
</code></pre>
<p>This can detect:</p>
<pre><code class="language-text">prompt changes
retrieval explosions
conversation-history growth
agent loops
</code></pre>
<p>before the cost increase becomes obvious on the monthly bill.</p>
<hr />
<h2>Step 18: Add Sampling Before Volume Explodes</h2>
<p>Keeping every trace forever is rarely necessary.</p>
<p>A simple strategy might be:</p>
<pre><code class="language-text">Successful request → sample 5%

Error → keep 100%

Very slow request → keep 100%

Critical workflow → keep 100%
</code></pre>
<p>This is much more useful than blindly sampling every request at the same rate.</p>
<p>The interesting traces are often the unusual ones.</p>
<hr />
<h2>Step 19: Redact at the Collector</h2>
<p>Suppose telemetry accidentally contains:</p>
<pre><code class="language-text">user.email
authorization.header
customer.phone
</code></pre>
<p>You don't necessarily want those values reaching storage.</p>
<p>A useful architecture is:</p>
<pre><code class="language-text">Application
     │
     ▼
OTel Collector
     │
     ├── Remove sensitive attributes
     ├── Redact values
     ├── Sample
     └── Route
     │
     ▼
Telemetry Storage
</code></pre>
<p>This creates a centralized privacy control point.</p>
<p>Still, prevention at the application layer is better than relying exclusively on downstream redaction.</p>
<hr />
<h2>Step 20: Observe RAG as a Pipeline</h2>
<p>A common mistake is treating RAG as one LLM request.</p>
<p>It's actually a pipeline:</p>
<pre><code class="language-text">Question
   │
   ▼
Embedding
   │
   ▼
Vector Search
   │
   ▼
Reranking
   │
   ▼
Context Construction
   │
   ▼
LLM
</code></pre>
<p>Trace these separately:</p>
<pre><code class="language-text">rag.request
│
├── embedding.create
├── vector.search
├── reranker.rank
├── context.build
└── llm.generate
</code></pre>
<p>Now when RAG quality deteriorates, you can investigate the individual stages.</p>
<hr />
<h2>Step 21: Observe Agents as Trees</h2>
<p>Agent executions are even more interesting.</p>
<p>A trace could look like:</p>
<pre><code class="language-text">agent.run
│
├── llm.generate
│
├── tool.search_customer
│
├── llm.generate
│
├── tool.get_orders
│
├── llm.generate
│
└── response
</code></pre>
<p>From that single trace you can calculate:</p>
<pre><code class="language-text">number of LLM calls
number of tool calls
total tokens
total latency
tool latency
retries
estimated cost
</code></pre>
<p>This makes traces especially valuable for debugging agentic systems.</p>
<hr />
<h2>Step 22: Detect Agent Loops</h2>
<p>Imagine this trace:</p>
<pre><code class="language-text">agent.run
│
├── llm.generate
├── tool.search
├── llm.generate
├── tool.search
├── llm.generate
├── tool.search
├── llm.generate
├── tool.search
├── ...
</code></pre>
<p>Your application is probably stuck.</p>
<p>Create metrics such as:</p>
<pre><code class="language-text">agent_turns
tool_calls_per_run
llm_calls_per_run
</code></pre>
<p>Then alert on abnormal values.</p>
<p>For example:</p>
<pre><code class="language-text">agent_turns &gt; 12
</code></pre>
<p>or:</p>
<pre><code class="language-text">same tool called &gt; 5 times
</code></pre>
<p>Observability can reveal problems that ordinary API monitoring completely misses.</p>
<hr />
<h2>Step 23: Add Quality Signals</h2>
<p>Infrastructure telemetry answers:</p>
<pre><code class="language-text">Was it fast?

Did it fail?

How many tokens did it use?
</code></pre>
<p>But an LLM can be:</p>
<pre><code class="language-text">fast
cheap
error-free
</code></pre>
<p>and still give a terrible answer.</p>
<p>So eventually add evaluation signals:</p>
<pre><code class="language-text">groundedness
correctness
user feedback
task completion
hallucination rate
retrieval quality
</code></pre>
<p>Then correlate them with traces.</p>
<p>For example:</p>
<pre><code class="language-text">Trace ID: abc123

Latency:       2.8s
Cost:          $0.009
Tokens:        1,240
Groundedness:  0.91
User rating:   positive
</code></pre>
<p>Now you're observing both <strong>system performance</strong> and <strong>AI behavior</strong>.</p>
<hr />
<h2>Step 24: Define LLM SLOs</h2>
<p>Traditional systems have SLOs.</p>
<p>LLM systems should too.</p>
<p>For example:</p>
<h3>Reliability</h3>
<pre><code class="language-text">99.5% successful model calls
</code></pre>
<h3>Latency</h3>
<pre><code class="language-text">95% of requests complete &lt; 6 seconds
</code></pre>
<h3>Cost</h3>
<pre><code class="language-text">95% of requests cost &lt; $0.03
</code></pre>
<h3>Agent behavior</h3>
<pre><code class="language-text">99% complete within 8 turns
</code></pre>
<h3>Quality</h3>
<pre><code class="language-text">groundedness score &gt; agreed threshold
</code></pre>
<p>The exact values depend on the application.</p>
<p>The important part is defining acceptable behavior before an incident.</p>
<p>For larger production AI systems, this operating layer should be planned alongside model deployment, security, evaluation, and lifecycle management. SDLC Corp's <a href="https://sdlccorp.com/ai-development-services/">AI development services</a> similarly describe deployment and monitoring as part of the broader AI lifecycle rather than treating monitoring as a separate afterthought.</p>
<hr />
<h2>Step 25: Build a Debugging Workflow</h2>
<p>The biggest benefit of observability isn't the dashboard.</p>
<p>It's reducing the time between:</p>
<pre><code class="language-text">Something is wrong.
</code></pre>
<p>and:</p>
<pre><code class="language-text">We know why.
</code></pre>
<p>A useful workflow looks like:</p>
<pre><code class="language-text">Alert
  │
  ▼
Find affected metric
  │
  ▼
Filter by model/version
  │
  ▼
Open representative trace
  │
  ▼
Find slow/failing span
  │
  ▼
Inspect correlated logs
  │
  ▼
Identify root cause
</code></pre>
<p>For example:</p>
<pre><code class="language-text">ALERT:
p95 latency &gt; 8s

        ↓

FILTER:
prompt_version = support-v24

        ↓

TRACE:
rag.retrieve = 5.4s

        ↓

ROOT CAUSE:
vector search latency regression
</code></pre>
<p>That's observability doing useful engineering work.</p>
<hr />
<h2>What Should You Avoid Collecting?</h2>
<p>Your telemetry policy should explicitly define what should <strong>not</strong> be collected.</p>
<p>Usually that includes:</p>
<pre><code class="language-text">Passwords
API keys
Authorization headers
Access tokens
Raw secrets
Payment details
Unnecessary PII
Sensitive documents
</code></pre>
<p>For prompt and completion content, make a deliberate risk-based decision.</p>
<p>Don't enable content capture simply because the instrumentation supports it.</p>
<hr />
<h2>A Practical Production Architecture</h2>
<p>A mature setup could eventually look like:</p>
<pre><code class="language-text">                  LLM Application
                         │
                         ▼
               OpenTelemetry SDK
                         │
                         ▼
                 OTel Collector
                         │
             ┌───────────┼───────────┐
             │           │           │
             ▼           ▼           ▼
          Metrics      Traces       Logs
             │           │           │
             ▼           ▼           ▼
        Prometheus     Tempo        Loki
             │           │           │
             └───────────┼───────────┘
                         │
                         ▼
                      Grafana
                         │
             ┌───────────┼───────────┐
             ▼           ▼           ▼
         Alerts      Dashboards   Debugging
</code></pre>
<p>Then add:</p>
<pre><code class="language-text">Evaluation results
User feedback
Cost data
Release metadata
Prompt versions
Model versions
</code></pre>
<p>as your system matures.</p>
<hr />
<h2>LLM Observability Setup Checklist</h2>
<p>Before calling the setup complete, verify:</p>
<h3>Instrumentation</h3>
<ul>
<li><p>End-to-end requests have trace IDs</p>
</li>
<li><p>LLM calls are individual spans</p>
</li>
<li><p>Retrieval operations are traced</p>
</li>
<li><p>Tool calls are traced</p>
</li>
<li><p>Errors are recorded</p>
</li>
</ul>
<h3>Metrics</h3>
<ul>
<li><p>Request volume</p>
</li>
<li><p>Latency</p>
</li>
<li><p>Input tokens</p>
</li>
<li><p>Output tokens</p>
</li>
<li><p>Error rate</p>
</li>
<li><p>Retry rate</p>
</li>
<li><p>Estimated cost</p>
</li>
</ul>
<h3>Metadata</h3>
<ul>
<li><p>Model</p>
</li>
<li><p>Prompt version</p>
</li>
<li><p>Application version</p>
</li>
<li><p>Environment</p>
</li>
</ul>
<h3>Privacy</h3>
<ul>
<li><p>Secrets are excluded</p>
</li>
<li><p>PII policy exists</p>
</li>
<li><p>Prompt capture is intentional</p>
</li>
<li><p>Retention is defined</p>
</li>
<li><p>Telemetry access is restricted</p>
</li>
</ul>
<h3>Operations</h3>
<ul>
<li><p>Dashboard exists</p>
</li>
<li><p>Alerts exist</p>
</li>
<li><p>Sampling is configured</p>
</li>
<li><p>Engineers know how to find a trace from an incident</p>
</li>
</ul>
<hr />
<h2>Common LLM Observability Mistakes</h2>
<h2>Logging Everything</h2>
<p>More data isn't automatically more observability.</p>
<p>Collect information that helps answer operational questions.</p>
<hr />
<h2>Capturing Prompts Without a Privacy Plan</h2>
<p>Prompt logging can expose sensitive information.</p>
<p>Start metadata-first.</p>
<hr />
<h2>Tracking Only Latency</h2>
<p>A fast LLM request can still be expensive or incorrect.</p>
<p>Track:</p>
<pre><code class="language-text">latency + tokens + cost + errors + quality
</code></pre>
<hr />
<h2>Tracking Only the LLM</h2>
<p>For RAG and agents, the model is only one part of the system.</p>
<p>Trace:</p>
<pre><code class="language-text">retrieval
tools
databases
external APIs
model calls
</code></pre>
<p>together.</p>
<hr />
<h2>Ignoring Versions</h2>
<p>Always record enough information to correlate regressions with changes:</p>
<pre><code class="language-text">model version
prompt version
application version
retrieval configuration
</code></pre>
<hr />
<h2>Building Vendor-Specific Instrumentation Too Early</h2>
<p>If possible, instrument around open telemetry standards and adapt at the export layer.</p>
<p>That keeps future backend changes much easier.</p>
<hr />
<h2>What You Actually Need to Start</h2>
<p>You don't need a huge LLMOps platform on day one.</p>
<p>Start with:</p>
<pre><code class="language-text">OpenTelemetry SDK
        ↓
OTel Collector
        ↓
Traces + Metrics
        ↓
Dashboard
</code></pre>
<p>Instrument:</p>
<pre><code class="language-text">LLM calls
retrieval
tools
</code></pre>
<p>and record:</p>
<pre><code class="language-text">latency
tokens
errors
model
prompt version
trace ID
</code></pre>
<p>That's already enough to answer a surprising number of production questions.</p>
<p>Add complexity only when a real problem requires it.</p>
<hr />
<h2>Final Takeaway</h2>
<p>A good <strong>LLM observability setup</strong> isn't about collecting every possible detail from every prompt.</p>
<p>It's about being able to answer:</p>
<blockquote>
<p><strong>What happened, where did it happen, why did it happen, and what changed?</strong></p>
</blockquote>
<p>Build around open telemetry rather than a specific dashboard.</p>
<p>Start with metadata rather than sensitive content.</p>
<p>Trace the entire application rather than only the model call.</p>
<p>Measure tokens and cost alongside traditional reliability metrics.</p>
<p>Correlate prompts, models, retrieval, and application releases through version metadata.</p>
<p>Then gradually connect operational telemetry with evaluation results and user feedback.</p>
<p>The final goal isn't a beautiful dashboard.</p>
<p>It's reaching the point where an LLM application behaves strangely in production and your team can explain <strong>why</strong> without guessing.</p>
]]></content:encoded></item><item><title><![CDATA[Implementing MCP Servers for Internal Tools: A Practical Developer Guide]]></title><description><![CDATA[Learn how to design and implement an MCP server that safely connects AI applications to internal APIs, databases, services, and enterprise workflows.


Giving an AI assistant access to company systems]]></description><link>https://sdlccorp-softwaredev.hashnode.dev/implementing-mcp-servers-for-internal-tools-a-practical-developer-guide</link><guid isPermaLink="true">https://sdlccorp-softwaredev.hashnode.dev/implementing-mcp-servers-for-internal-tools-a-practical-developer-guide</guid><dc:creator><![CDATA[Samcorp]]></dc:creator><pubDate>Thu, 27 Aug 2026 09:14:20 GMT</pubDate><content:encoded><![CDATA[<p>Learn how to design and implement an MCP server that safely connects AI applications to internal APIs, databases, services, and enterprise workflows.</p>
<img src="https://cdn.hashnode.com/uploads/covers/685a6d69d4d0ea366852534c/a7eeea8a-deae-4fc5-b9e8-9ee18215fb34.png" alt="" style="display:block;margin:0 auto" />

<p>Giving an AI assistant access to company systems sounds simple:</p>
<pre><code class="language-text">LLM → Internal API → Result
</code></pre>
<p>In practice, it quickly becomes messy.</p>
<p>One application needs access to customer records. Another needs Jira-style project data. A developer assistant needs deployment information. An operations agent needs monitoring tools.</p>
<p>Before long, every AI application has its own integrations:</p>
<pre><code class="language-text">AI Assistant ───────→ CRM API
Developer Agent ────→ Deployment API
Support Agent ──────→ Ticket System
Analytics Agent ────→ Internal Database
</code></pre>
<p>Each integration requires its own schemas, authentication logic, error handling, permissions, and documentation.</p>
<p><strong>Model Context Protocol (MCP)</strong> gives us another approach.</p>
<p>Instead of teaching every AI application how every internal system works, we can put an MCP server between them.</p>
<pre><code class="language-text">AI Application
       │
       │ MCP
       ▼
+------------------+
|    MCP Server    |
+------------------+
   │      │      │
   ▼      ▼      ▼
 CRM     DB     Internal API
</code></pre>
<p>The server exposes carefully controlled capabilities that an MCP-compatible application can discover and use.</p>
<p>In this tutorial, we'll walk through <strong>MCP server implementation</strong> for internal tools, starting with architecture and ending with security, testing, and production deployment.</p>
<hr />
<h2>What Is an MCP Server?</h2>
<p>Model Context Protocol is an open protocol for connecting AI applications with external systems and context.</p>
<p>An MCP server sits on the system side of that relationship.</p>
<p>Instead of exposing an entire internal application directly to an AI model, the server exposes specific MCP capabilities.</p>
<p>The three primitives you'll encounter most often are:</p>
<h3>Tools</h3>
<p>Tools allow the AI application to perform actions or calculations.</p>
<p>Examples:</p>
<pre><code class="language-text">get_customer
search_tickets
create_support_ticket
check_deployment
restart_service
generate_report
</code></pre>
<h3>Resources</h3>
<p>Resources expose information that can be read as context.</p>
<p>Examples:</p>
<pre><code class="language-text">internal documentation
configuration
database-backed records
service information
project metadata
runbooks
</code></pre>
<h3>Prompts</h3>
<p>Prompts expose reusable interaction templates.</p>
<p>For example:</p>
<pre><code class="language-text">summarize_incident
review_deployment
prepare_customer_report
</code></pre>
<p>The important architectural idea is that the model doesn't need to understand your internal API implementation.</p>
<p>It only needs to understand the MCP interface you expose.</p>
<hr />
<h1>Why MCP Makes Sense for Internal Tools</h1>
<p>Imagine your organization has:</p>
<pre><code class="language-text">CRM
ERP
Git repositories
CI/CD platform
Analytics database
Knowledge base
Monitoring system
Support platform
Internal REST APIs
</code></pre>
<p>Without a common integration layer, every AI application may need separate connectors.</p>
<p>With MCP:</p>
<pre><code class="language-text">                    ┌───────────────┐
                    │ AI Assistant  │
                    └───────┬───────┘
                            │
                    ┌───────▼───────┐
                    │  MCP Client   │
                    └───────┬───────┘
                            │
                           MCP
                            │
                    ┌───────▼───────┐
                    │  MCP Server   │
                    └───────┬───────┘
                            │
           ┌────────────────┼────────────────┐
           │                │                │
           ▼                ▼                ▼
          CRM            Database        DevOps API
</code></pre>
<p>The MCP server becomes a controlled interface between AI applications and business infrastructure.</p>
<p>This pattern fits particularly well with broader <a href="https://sdlccorp.com/enterprise-ai-development-company/">enterprise AI development</a> where LLMs need to interact with existing internal platforms while organizations still need access control, governance, and secure integration.</p>
<hr />
<h2>Step 1: Decide What the AI Actually Needs</h2>
<p>A common mistake is starting with:</p>
<blockquote>
<p>"Let's expose our internal API through MCP."</p>
</blockquote>
<p>That's too broad.</p>
<p>Start with specific tasks instead.</p>
<p>Suppose employees repeatedly ask:</p>
<pre><code class="language-text">What's the status of order 58421?

Find the customer associated with this email.

Is checkout-api deployed in production?

Show me unresolved P1 incidents.
</code></pre>
<p>These translate naturally into tools:</p>
<pre><code class="language-text">get_order_status
find_customer
get_deployment_status
list_critical_incidents
</code></pre>
<p>This gives us a much safer boundary.</p>
<p>Instead of exposing:</p>
<pre><code class="language-text">execute_database_query
call_internal_api
run_shell_command
</code></pre>
<p>we expose business-level operations:</p>
<pre><code class="language-text">get_order
find_customer
check_service_health
</code></pre>
<p>That difference is critical.</p>
<p>The first group gives an AI broad execution capabilities.</p>
<p>The second gives it narrowly defined actions.</p>
<hr />
<h2>Step 2: Choose Your Transport</h2>
<p>The transport determines how an MCP client communicates with the server.</p>
<p>For many local developer integrations, <strong>stdio</strong> is the simplest choice.</p>
<p>Conceptually:</p>
<pre><code class="language-text">MCP Host
   │
   ├── starts server process
   │
   ▼
MCP Server
stdin  ← requests
stdout → protocol responses
</code></pre>
<p>This works well when the host launches the server locally.</p>
<p>For a centrally hosted internal MCP service used by multiple clients, an HTTP-based deployment is usually more appropriate.</p>
<p>Conceptually:</p>
<pre><code class="language-text">Developer Laptop
       │
       │ HTTPS
       ▼
Company MCP Endpoint
       │
       ├── CRM
       ├── Database
       └── Internal APIs
</code></pre>
<p>Keep transport decisions separate from your business logic so the same tools can eventually be exposed through a different deployment model.</p>
<hr />
<h2>Step 3: Create the Project</h2>
<p>We'll use TypeScript.</p>
<p>A simple structure might be:</p>
<pre><code class="language-text">internal-mcp/
│
├── src/
│   ├── index.ts
│   ├── tools/
│   │   ├── customer.ts
│   │   ├── deployments.ts
│   │   └── incidents.ts
│   │
│   ├── services/
│   │   ├── crm.ts
│   │   └── devops.ts
│   │
│   └── security/
│       └── permissions.ts
│
├── package.json
└── tsconfig.json
</code></pre>
<p>Install the current MCP server package and Zod:</p>
<pre><code class="language-bash">npm install @modelcontextprotocol/server zod
</code></pre>
<p>During development, you'll also want your normal TypeScript tooling.</p>
<p>Keeping tools separate from service integrations will become important as the server grows.</p>
<hr />
<h2>Step 4: Create the MCP Server</h2>
<p>Start with a server factory.</p>
<pre><code class="language-typescript">import { McpServer } from "@modelcontextprotocol/server";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import * as z from "zod/v4";

function createServer() {
  const server = new McpServer({
    name: "internal-tools",
    version: "1.0.0"
  });

  return server;
}

void serveStdio(createServer);
</code></pre>
<p>This gives us the foundation.</p>
<p>Right now the server isn't particularly useful because it doesn't expose anything.</p>
<p>Let's fix that.</p>
<hr />
<h2>Step 5: Implement Your First Tool</h2>
<p>Suppose we have an internal deployment service.</p>
<p>We want an AI assistant to answer:</p>
<blockquote>
<p>"What's currently deployed for checkout-api?"</p>
</blockquote>
<p>Our tool should represent that exact business operation.</p>
<pre><code class="language-typescript">server.registerTool(
  "get-deployment-status",
  {
    title: "Get Deployment Status",

    description:
      "Get the current deployment status for an internal service.",

    inputSchema: {
      service: z.string().min(1),
      environment: z.enum([
        "development",
        "staging",
        "production"
      ])
    }
  },

  async ({ service, environment }) =&gt; {
    const deployment = await deploymentService.getStatus(
      service,
      environment
    );

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(deployment)
        }
      ]
    };
  }
);
</code></pre>
<p>An MCP client can now discover that capability and understand its input requirements.</p>
<p>For example:</p>
<pre><code class="language-json">{
  "service": "checkout-api",
  "environment": "production"
}
</code></pre>
<p>could return:</p>
<pre><code class="language-json">{
  "service": "checkout-api",
  "environment": "production",
  "version": "v2.18.4",
  "status": "healthy"
}
</code></pre>
<hr />
<h2>Step 6: Keep MCP Logic Thin</h2>
<p>Don't put all your business logic inside tool handlers.</p>
<p>Avoid turning this:</p>
<pre><code class="language-typescript">server.registerTool(...)
</code></pre>
<p>into hundreds of lines of API calls, database queries, authorization checks, transformations, and retries.</p>
<p>Instead, keep a service layer:</p>
<pre><code class="language-typescript">class DeploymentService {
  async getStatus(
    service: string,
    environment: string
  ) {
    // Call internal deployment platform.

    return {
      service,
      environment,
      version: "v2.18.4",
      status: "healthy"
    };
  }
}
</code></pre>
<p>Then the MCP layer does only a few things:</p>
<pre><code class="language-text">Validate input
      ↓
Check permission
      ↓
Call service
      ↓
Transform result
      ↓
Return MCP response
</code></pre>
<p>Your architecture becomes:</p>
<pre><code class="language-text">MCP Protocol
     │
     ▼
Tool Handler
     │
     ▼
Service Layer
     │
     ▼
Internal System
</code></pre>
<p>This separation also makes unit testing much easier.</p>
<hr />
<h2>Step 7: Add a Customer Lookup Tool</h2>
<p>Now let's connect another internal system.</p>
<pre><code class="language-typescript">server.registerTool(
  "find-customer",
  {
    title: "Find Customer",

    description:
      "Find a customer using their company email address.",

    inputSchema: {
      email: z.string().email()
    }
  },

  async ({ email }) =&gt; {
    const customer = await crmService.findByEmail(email);

    if (!customer) {
      return {
        content: [
          {
            type: "text",
            text: "Customer not found."
          }
        ]
      };
    }

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify({
            id: customer.id,
            name: customer.name,
            accountStatus: customer.accountStatus
          })
        }
      ]
    };
  }
);
</code></pre>
<p>Notice something important.</p>
<p>Our CRM record might contain:</p>
<pre><code class="language-text">Customer ID
Name
Email
Phone
Billing address
Internal notes
Payment information
Account status
Support history
</code></pre>
<p>But the MCP tool returns only:</p>
<pre><code class="language-text">ID
Name
Account status
</code></pre>
<p>That is intentional.</p>
<hr />
<h2>Step 8: Apply Data Minimization</h2>
<p>One of the biggest mistakes in internal AI integrations is returning everything simply because the backend API provides it.</p>
<p>Don't do this:</p>
<pre><code class="language-typescript">return entireCustomerObject;
</code></pre>
<p>Instead:</p>
<pre><code class="language-typescript">return {
  id: customer.id,
  name: customer.name,
  accountStatus: customer.accountStatus
};
</code></pre>
<p>The question should always be:</p>
<blockquote>
<p>What is the minimum information necessary for this tool to complete its job?</p>
</blockquote>
<p>This reduces:</p>
<ul>
<li><p>accidental data exposure</p>
</li>
<li><p>unnecessary model context</p>
</li>
<li><p>token usage</p>
</li>
<li><p>privacy risk</p>
</li>
<li><p>confusing responses</p>
</li>
</ul>
<p>MCP does not remove your responsibility to design safe application boundaries.</p>
<p>It gives you a standardized interface through which to enforce them.</p>
<hr />
<h2>Step 9: Use Resources for Read-Only Context</h2>
<p>Not everything needs to be a tool.</p>
<p>Suppose your engineering assistant needs access to an internal deployment policy.</p>
<p>A resource is often a better abstraction.</p>
<p>Conceptually:</p>
<pre><code class="language-text">resource:
internal://policies/deployment
</code></pre>
<p>could return:</p>
<pre><code class="language-markdown"># Production Deployment Policy

- Production deployments require approval.
- Deployments must pass integration tests.
- Rollback procedures must be documented.
</code></pre>
<p>Think about the distinction like this:</p>
<pre><code class="language-text">Need information?
      │
      └── Resource

Need to perform an operation?
      │
      └── Tool

Need a reusable interaction workflow?
      │
      └── Prompt
</code></pre>
<p>This produces a cleaner MCP interface.</p>
<hr />
<h2>Step 10: Design Tool Names for Models, Not APIs</h2>
<p>Your internal API might have an endpoint named:</p>
<pre><code class="language-text">GET /api/v4/env/service/current
</code></pre>
<p>Don't expose that mental model.</p>
<p>Expose:</p>
<pre><code class="language-text">get-deployment-status
</code></pre>
<p>Similarly:</p>
<pre><code class="language-text">POST /crm/v3/search/entity
</code></pre>
<p>becomes:</p>
<pre><code class="language-text">find-customer
</code></pre>
<p>Tool names and descriptions are part of the interface the model reasons about.</p>
<p>Make them:</p>
<ul>
<li><p>explicit</p>
</li>
<li><p>narrow</p>
</li>
<li><p>predictable</p>
</li>
<li><p>action-oriented</p>
</li>
</ul>
<p>Avoid ambiguous tools such as:</p>
<pre><code class="language-text">execute
process
manage
run
perform
</code></pre>
<p>Prefer:</p>
<pre><code class="language-text">get-order-status
create-support-ticket
list-open-incidents
get-deployment-status
</code></pre>
<hr />
<h2>Step 11: Write Precise Tool Descriptions</h2>
<p>Compare these:</p>
<pre><code class="language-text">Gets service information.
</code></pre>
<p>and:</p>
<pre><code class="language-text">Returns the currently deployed version and health
status for an internal service in development,
staging, or production.
</code></pre>
<p>The second description gives the model much more information about when the tool is appropriate.</p>
<p>Descriptions should answer:</p>
<pre><code class="language-text">What does this tool do?
When should it be used?
What does it return?
What important limitations exist?
</code></pre>
<p>Don't hide important business rules only inside the implementation.</p>
<hr />
<h2>Step 12: Add Authorization Before Real Actions</h2>
<p>Internal tools often have very different risk levels.</p>
<p>Reading service status is not equivalent to restarting production.</p>
<p>We can classify operations:</p>
<table>
<thead>
<tr>
<th>Tool</th>
<th>Risk</th>
</tr>
</thead>
<tbody><tr>
<td><code>get-deployment-status</code></td>
<td>Low</td>
</tr>
<tr>
<td><code>search-customer</code></td>
<td>Medium</td>
</tr>
<tr>
<td><code>create-ticket</code></td>
<td>Medium</td>
</tr>
<tr>
<td><code>deploy-service</code></td>
<td>High</td>
</tr>
<tr>
<td><code>restart-production-service</code></td>
<td>Critical</td>
</tr>
</tbody></table>
<p>Your authorization layer should reflect that.</p>
<p>Conceptually:</p>
<pre><code class="language-typescript">async function authorize(
  user: User,
  action: string
) {
  const allowed = await permissionService.can(
    user.id,
    action
  );

  if (!allowed) {
    throw new Error("Permission denied");
  }
}
</code></pre>
<p>Then:</p>
<pre><code class="language-typescript">await authorize(
  currentUser,
  "deployment:read"
);
</code></pre>
<p>For sensitive operations, you may need additional approval or confirmation before the action is executed.</p>
<p>The important rule is:</p>
<blockquote>
<p><strong>Never treat access to the MCP server itself as authorization to every tool it exposes.</strong></p>
</blockquote>
<hr />
<h2>Step 13: Separate Read and Write Operations</h2>
<p>A useful first production milestone is a <strong>read-only MCP server</strong>.</p>
<p>Start with:</p>
<pre><code class="language-text">get_customer
get_order
list_incidents
read_documentation
check_deployment
get_service_health
</code></pre>
<p>Then introduce writes gradually:</p>
<pre><code class="language-text">create_ticket
update_customer
trigger_build
restart_service
deploy_application
</code></pre>
<p>Write operations deserve stricter controls because their failures change real systems.</p>
<p>A useful pattern is:</p>
<pre><code class="language-text">Read
 ↓
Prepare action
 ↓
Validate
 ↓
Authorize
 ↓
Confirm when necessary
 ↓
Execute
 ↓
Audit
</code></pre>
<hr />
<h2>Step 14: Never Give the Model Raw Database Access</h2>
<p>This is tempting:</p>
<pre><code class="language-text">execute_sql(query)
</code></pre>
<p>It's also an unnecessarily large security boundary.</p>
<p>A better approach is:</p>
<pre><code class="language-text">get_customer_orders(customer_id)
</code></pre>
<p>instead of:</p>
<pre><code class="language-sql">SELECT *
FROM orders
WHERE customer_id = ...
</code></pre>
<p>Likewise:</p>
<pre><code class="language-text">get_monthly_revenue(month)
</code></pre>
<p>is safer than:</p>
<pre><code class="language-text">execute_analytics_sql
</code></pre>
<p>The MCP server should act as a controlled application layer—not simply turn the model into a database administrator.</p>
<hr />
<h2>Step 15: Treat Tool Arguments as Untrusted Input</h2>
<p>A model-generated tool call is still input.</p>
<p>Validate it exactly as you would validate an external API request.</p>
<p>Zod schemas help here:</p>
<pre><code class="language-typescript">inputSchema: {
  service: z
    .string()
    .min(1)
    .max(100),

  environment: z.enum([
    "development",
    "staging",
    "production"
  ])
}
</code></pre>
<p>For identifiers, define formats:</p>
<pre><code class="language-typescript">ticketId: z
  .string()
  .regex(/^TICKET-[0-9]+$/)
</code></pre>
<p>Don't rely on prompts such as:</p>
<pre><code class="language-text">Please only provide valid service names.
</code></pre>
<p>Prompts are not validation.</p>
<p>Code is.</p>
<hr />
<h2>Step 16: Protect Against Tool Chaining Risks</h2>
<p>Consider this sequence:</p>
<pre><code class="language-text">1. Read internal document
2. Extract instruction from document
3. Call another tool
4. Modify production system
</code></pre>
<p>If untrusted content influences tool selection, an AI agent could potentially execute actions you never intended.</p>
<p>This is one reason internal MCP security isn't simply:</p>
<pre><code class="language-text">Authentication = solved
</code></pre>
<p>You also need to consider:</p>
<pre><code class="language-text">Authentication
Authorization
Input validation
Output filtering
Least privilege
Confirmation
Auditability
Tool interactions
Data classification
</code></pre>
<p>The MCP layer becomes part of your security architecture.</p>
<hr />
<h2>Step 17: Add Audit Logging</h2>
<p>For production internal tools, you should be able to reconstruct what happened.</p>
<p>A useful audit event might contain:</p>
<pre><code class="language-json">{
  "timestamp": "2026-08-27T08:30:00Z",
  "actor": "user-1842",
  "tool": "get-deployment-status",
  "arguments": {
    "service": "checkout-api",
    "environment": "production"
  },
  "result": "success",
  "duration_ms": 183
}
</code></pre>
<p>For write operations, consider recording:</p>
<pre><code class="language-text">who initiated the request
which tool was invoked
which target was affected
authorization result
approval/confirmation
execution outcome
timestamp
request/correlation ID
</code></pre>
<p>Be careful not to create a second privacy problem by logging secrets or sensitive payloads.</p>
<hr />
<h2>Step 18: Handle Errors Deliberately</h2>
<p>Internal systems fail.</p>
<p>Your CRM may timeout.</p>
<p>Your deployment API may be unavailable.</p>
<p>A requested customer may not exist.</p>
<p>Don't convert everything into:</p>
<pre><code class="language-text">Internal server error
</code></pre>
<p>Instead, create predictable error categories:</p>
<pre><code class="language-text">NOT_FOUND
PERMISSION_DENIED
INVALID_INPUT
UPSTREAM_TIMEOUT
RATE_LIMITED
SERVICE_UNAVAILABLE
</code></pre>
<p>Then map internal errors into safe messages.</p>
<p>For example:</p>
<pre><code class="language-typescript">try {
  const result = await deploymentService.getStatus(
    service,
    environment
  );

  return toMcpResult(result);
} catch (error) {
  logger.error({
    error,
    service,
    environment
  });

  return {
    isError: true,
    content: [
      {
        type: "text",
        text: "Deployment status is temporarily unavailable."
      }
    ]
  };
}
</code></pre>
<p>The user receives useful information without exposing stack traces, credentials, internal hostnames, or infrastructure details.</p>
<hr />
<h2>Step 19: Add Timeouts</h2>
<p>Never assume an internal service will respond.</p>
<p>Conceptually:</p>
<pre><code class="language-typescript">const result = await withTimeout(
  deploymentService.getStatus(
    service,
    environment
  ),
  5000
);
</code></pre>
<p>Without timeouts, a single unhealthy dependency can leave tool execution hanging.</p>
<p>For production integrations, also consider:</p>
<pre><code class="language-text">timeouts
retry policies
circuit breakers
rate limits
connection limits
bulkheads
fallback behavior
</code></pre>
<hr />
<h2>Step 20: Keep Credentials Out of Tool Arguments</h2>
<p>Avoid tool schemas like:</p>
<pre><code class="language-json">{
  "apiKey": "...",
  "customerId": "..."
}
</code></pre>
<p>Credentials should come from your server-side environment or identity infrastructure.</p>
<p>The flow should be:</p>
<pre><code class="language-text">MCP Client
    │
    │ authorized request
    ▼
MCP Server
    │
    ├── identity
    ├── permissions
    └── server-side credentials
             │
             ▼
       Internal Service
</code></pre>
<p>not:</p>
<pre><code class="language-text">Model → Secret → Tool → Internal API
</code></pre>
<p>The model should receive as few secrets as possible—ideally none.</p>
<hr />
<h2>Step 21: Design Around Least Privilege</h2>
<p>Suppose an AI coding assistant needs to inspect deployment state.</p>
<p>It probably needs:</p>
<pre><code class="language-text">deployment:read
</code></pre>
<p>It probably does not need:</p>
<pre><code class="language-text">deployment:create
deployment:delete
production:restart
secret:read
</code></pre>
<p>The same principle applies to backend credentials.</p>
<p>If the MCP server only reads tickets, its service account shouldn't have permission to delete them.</p>
<p>Use least privilege at multiple layers:</p>
<pre><code class="language-text">User
 ↓
MCP tool permissions
 ↓
Service identity
 ↓
Internal API permissions
 ↓
Database permissions
</code></pre>
<p>If one layer fails, another still limits the blast radius.</p>
<hr />
<h2>Step 22: Test the MCP Server</h2>
<p>For local development, the MCP Inspector is extremely useful.</p>
<p>You can launch an stdio server through the Inspector:</p>
<pre><code class="language-bash">npx @modelcontextprotocol/inspector npx tsx src/index.ts
</code></pre>
<p>Then inspect available tools and execute them manually.</p>
<p>Test at least:</p>
<pre><code class="language-text">valid requests
invalid arguments
missing records
unauthorized requests
upstream failures
timeouts
malformed upstream responses
sensitive-data filtering
write confirmation paths
</code></pre>
<p>Don't test only:</p>
<pre><code class="language-text">Does the tool work?
</code></pre>
<p>Also test:</p>
<pre><code class="language-text">Can the tool do something it shouldn't?
</code></pre>
<hr />
<h2>Step 23: Be Careful With stdio Logging</h2>
<p>When your server runs over stdio, standard output is part of the protocol channel.</p>
<p>So this can cause problems:</p>
<pre><code class="language-typescript">console.log("Server started");
</code></pre>
<p>Use stderr for development logging instead:</p>
<pre><code class="language-typescript">console.error("Internal MCP server started");
</code></pre>
<p>And in production, route structured logs through an appropriate logging pipeline.</p>
<p>Small transport details like this can save a surprising amount of debugging time.</p>
<hr />
<h2>Step 24: Move Toward a Shared Internal MCP Service</h2>
<p>A local stdio server is excellent for development.</p>
<p>Organizations may eventually want something more centralized:</p>
<pre><code class="language-text">                 ┌─────────────────┐
                 │ AI Application  │
                 └────────┬────────┘
                          │
                 ┌────────▼────────┐
                 │ Identity Layer  │
                 └────────┬────────┘
                          │
                 ┌────────▼────────┐
                 │   MCP Gateway   │
                 └────────┬────────┘
                          │
             ┌────────────┼────────────┐
             ▼            ▼            ▼
          CRM MCP      DevOps MCP    Data MCP
             │            │            │
             ▼            ▼            ▼
            CRM         CI/CD       Analytics
</code></pre>
<p>Now you can apply centralized:</p>
<pre><code class="language-text">authentication
authorization
observability
rate limiting
auditing
network policies
secret management
</code></pre>
<p>while keeping individual MCP servers focused on specific domains.</p>
<p>This is also where MCP implementation becomes part of a larger AI integration strategy. Organizations building <a href="https://sdlccorp.com/services/ai-as-a-service/">AI services integrated with existing business systems</a> need to think beyond the model itself and design secure integration, deployment, monitoring, access-control, and governance layers around the AI system.</p>
<hr />
<h2>Step 25: Avoid Building One Giant MCP Server</h2>
<p>It can be tempting to create:</p>
<pre><code class="language-text">company-mcp-server
</code></pre>
<p>containing 300 tools.</p>
<p>That quickly becomes difficult to reason about.</p>
<p>A domain-oriented design is often cleaner:</p>
<pre><code class="language-text">crm-mcp
├── find_customer
├── get_account
└── list_customer_tickets

devops-mcp
├── get_deployment
├── list_builds
└── get_service_health

support-mcp
├── search_tickets
├── get_ticket
└── create_ticket

knowledge-mcp
├── search_docs
└── read_document
</code></pre>
<p>Benefits include:</p>
<pre><code class="language-text">smaller permission boundaries
clearer ownership
simpler deployments
easier testing
better tool discovery
reduced blast radius
</code></pre>
<p>Your organization can then decide which AI applications receive access to which servers.</p>
<hr />
<h2>A Production-Oriented Architecture</h2>
<p>Once the prototype works, the architecture might evolve into:</p>
<pre><code class="language-text">                  AI Application
                         │
                         ▼
                 MCP Client / Host
                         │
                         ▼
              ┌─────────────────────┐
              │ Authentication      │
              ├─────────────────────┤
              │ Authorization       │
              ├─────────────────────┤
              │ MCP Server          │
              ├─────────────────────┤
              │ Tool Validation     │
              ├─────────────────────┤
              │ Service Layer       │
              ├─────────────────────┤
              │ Audit / Metrics     │
              └──────────┬──────────┘
                         │
              ┌──────────┼──────────┐
              ▼          ▼          ▼
             CRM      Internal DB   DevOps
</code></pre>
<p>Each layer has a specific job.</p>
<p>The MCP protocol defines the interface.</p>
<p>Your application still defines the trust boundary.</p>
<hr />
<h2>MCP Server Implementation Checklist</h2>
<p>Before exposing an internal MCP server, ask:</p>
<h3>Interface</h3>
<ul>
<li><p>Are tools narrowly scoped?</p>
</li>
<li><p>Are names understandable?</p>
</li>
<li><p>Are descriptions precise?</p>
</li>
<li><p>Are schemas strict?</p>
</li>
<li><p>Are read and write operations clearly separated?</p>
</li>
</ul>
<h3>Security</h3>
<ul>
<li><p>Is authentication enforced?</p>
</li>
<li><p>Is authorization checked per operation?</p>
</li>
<li><p>Are permissions least-privileged?</p>
</li>
<li><p>Are secrets kept outside model context?</p>
</li>
<li><p>Are dangerous operations protected?</p>
</li>
<li><p>Are sensitive outputs filtered?</p>
</li>
</ul>
<h3>Reliability</h3>
<ul>
<li><p>Are upstream calls timed out?</p>
</li>
<li><p>Are failures handled predictably?</p>
</li>
<li><p>Are retries bounded?</p>
</li>
<li><p>Are dependencies observable?</p>
</li>
</ul>
<h3>Governance</h3>
<ul>
<li><p>Are important actions audited?</p>
</li>
<li><p>Can you identify the requesting actor?</p>
</li>
<li><p>Can security teams investigate an incident?</p>
</li>
<li><p>Is sensitive information excluded from logs?</p>
</li>
</ul>
<h3>Testing</h3>
<ul>
<li><p>Have invalid inputs been tested?</p>
</li>
<li><p>Have unauthorized actions been tested?</p>
</li>
<li><p>Have upstream failures been simulated?</p>
</li>
<li><p>Have write operations been tested safely?</p>
</li>
</ul>
<p>If several of those answers are "no," the server probably isn't ready for production.</p>
<hr />
<h2>Common MCP Implementation Mistakes</h2>
<h2>1. Exposing Generic API Tools</h2>
<p>Avoid:</p>
<pre><code class="language-text">call_api
execute_sql
run_command
</code></pre>
<p>Prefer narrow business operations.</p>
<hr />
<h2>2. Treating the Model as Trusted</h2>
<p>Tool arguments are untrusted input.</p>
<p>Validate everything.</p>
<hr />
<h2>3. Returning Entire Backend Objects</h2>
<p>Return only the fields necessary for the task.</p>
<hr />
<h2>4. Giving Every User Every Tool</h2>
<p>Tool availability should reflect authorization.</p>
<hr />
<h2>5. Starting With High-Risk Write Operations</h2>
<p>Begin read-only where possible.</p>
<p>Introduce mutations after your security and auditing model is mature.</p>
<hr />
<h2>6. Ignoring Observability</h2>
<p>When an agent behaves unexpectedly, you need enough telemetry to reconstruct the interaction.</p>
<hr />
<h2>7. Putting Business Logic in MCP Handlers</h2>
<p>Keep the MCP layer thin and move business operations into reusable services.</p>
<hr />
<h2>What MCP Changes and What It Doesn't</h2>
<p>MCP solves an important integration problem:</p>
<pre><code class="language-text">How can AI applications interact with external
systems through a standardized interface?
</code></pre>
<p>It does <strong>not</strong> automatically solve:</p>
<pre><code class="language-text">Who should have access?

What data should the model see?

Which operations are safe?

Should an action require confirmation?

How should credentials be managed?

What should be logged?

How do we recover from failures?
</code></pre>
<p>Those remain engineering and security decisions.</p>
<p>That's especially important for internal tools because the systems being exposed may control customer data, production infrastructure, financial information, or operational workflows.</p>
<hr />
<h2>Final Takeaway</h2>
<p>A good <strong>MCP server implementation</strong> isn't simply an adapter around an existing REST API.</p>
<p>It's a carefully designed boundary between AI applications and real systems.</p>
<p>Start small:</p>
<pre><code class="language-text">1. Identify a real internal workflow
2. Define narrow MCP tools
3. Validate every input
4. Return minimum necessary data
5. Start with read-only operations
6. Enforce authorization
7. Add timeouts and error handling
8. Audit important actions
9. Test failure and abuse scenarios
10. Expand only after the boundary is trustworthy
</code></pre>
<p>The code required to expose the first MCP tool can be surprisingly small.</p>
<p>The real engineering work is deciding <strong>what the AI should be allowed to do</strong>.</p>
<p>Get that boundary right, and MCP can turn disconnected internal APIs and services into a reusable tool layer for AI applications without requiring every new assistant or agent to reinvent those integrations.</p>
]]></content:encoded></item><item><title><![CDATA[Step-by-Step: Building an LLM Eval Harness From Scratch]]></title><description><![CDATA[Build a lightweight evaluation system that runs repeatable LLM tests, scores responses, tracks failures, and catches regressions before they reach production.
LLM applications are surprisingly easy to]]></description><link>https://sdlccorp-softwaredev.hashnode.dev/step-by-step-building-an-llm-eval-harness-from-scratch</link><guid isPermaLink="true">https://sdlccorp-softwaredev.hashnode.dev/step-by-step-building-an-llm-eval-harness-from-scratch</guid><dc:creator><![CDATA[Samcorp]]></dc:creator><pubDate>Thu, 27 Aug 2026 08:48:58 GMT</pubDate><content:encoded><![CDATA[<img src="https://cdn.hashnode.com/uploads/covers/685a6d69d4d0ea366852534c/1b350ba1-b7a0-49e1-b25d-1163ec3608e6.png" alt="" style="display:block;margin:0 auto" />

  
  
<p>Build a lightweight evaluation system that runs repeatable LLM tests, scores responses, tracks failures, and catches regressions before they reach production.</p>
<p>LLM applications are surprisingly easy to demo and surprisingly difficult to measure.</p>
<p>You change a system prompt and the answers look better.</p>
<p>Then you switch models and three edge cases quietly get worse.</p>
<p>You modify retrieval settings and hallucinations decrease—but response latency jumps.</p>
<p>Without structured evaluation, development quickly becomes:</p>
<blockquote>
<p>Change something → try a few prompts → read the answers → decide whether it "feels better."</p>
</blockquote>
<p>That works during the first prototype. It does not work when an LLM becomes part of a production system.</p>
<p>What we need instead is an <strong>eval harness</strong>: infrastructure that repeatedly executes test cases, captures outputs, grades them, and turns the results into something we can compare.</p>
<p>In this tutorial, we'll <strong>build an LLM eval harness</strong> from scratch using Python.</p>
<p>By the end, we'll have something like this:</p>
<pre><code class="language-text">Evaluation Dataset
       |
       v
+------------------+
|   Eval Runner    |
+------------------+
       |
       v
+------------------+
|  LLM Under Test  |
+------------------+
       |
       v
+------------------+
|     Graders      |
+------------------+
       |
       v
+------------------+
| Results + Logs   |
+------------------+
       |
       v
 Regression Report
</code></pre>
<p>No large evaluation framework is required to understand the fundamentals.</p>
<p>Let's build the pieces ourselves.</p>
<hr />
<h2>What Exactly Is an LLM Eval Harness?</h2>
<p>An evaluation harness is the infrastructure responsible for running evaluations consistently.</p>
<p>At minimum, it needs to answer four questions:</p>
<ol>
<li><p><strong>What should we test?</strong></p>
</li>
<li><p><strong>How do we execute the model?</strong></p>
</li>
<li><p><strong>How do we determine whether the result is good?</strong></p>
</li>
<li><p><strong>How do we compare this run with previous runs?</strong></p>
</li>
</ol>
<p>That distinction matters.</p>
<p>An <strong>eval dataset</strong> contains test cases.</p>
<p>A <strong>grader</strong> evaluates an individual result.</p>
<p>An <strong>eval suite</strong> groups related tests.</p>
<p>The <strong>eval harness</strong> connects everything together: loading tests, executing requests, collecting responses, invoking graders, recording metadata, and aggregating results.</p>
<p>For teams building production LLM applications, evaluation should ideally be part of the broader development lifecycle rather than a final QA activity. This is particularly important when building <a href="https://sdlccorp.com/generative-ai-development-services/">generative AI applications</a> where prompts, models, retrieval, tools, and application logic can all affect output quality.</p>
<hr />
<h2>Step 1: Define What "Good" Means</h2>
<p>Don't start by writing the runner.</p>
<p>Start with the behavior you want to measure.</p>
<p>Suppose we're building a customer-support assistant.</p>
<p>We might care about:</p>
<pre><code class="language-text">Correctness
Instruction following
Groundedness
Required information
Formatting
Safety
Latency
Cost
</code></pre>
<p>A vague requirement such as:</p>
<pre><code class="language-text">The assistant should give good answers.
</code></pre>
<p>is almost impossible to evaluate.</p>
<p>Instead, convert it into observable requirements:</p>
<pre><code class="language-text">The assistant must answer using the supplied policy.
The assistant must not invent refund rules.
The assistant must mention the refund window.
The response must not exceed 150 words.
</code></pre>
<p>Now we have properties that can actually be tested.</p>
<p>This is the first major principle of eval design:</p>
<blockquote>
<p><strong>Evaluate specific behaviors, not a vague impression of quality.</strong></p>
</blockquote>
<hr />
<h2>Step 2: Create the Evaluation Dataset</h2>
<p>Let's start with JSON.</p>
<p>Create:</p>
<pre><code class="language-text">evals.json
</code></pre>
<p>Then add a few cases:</p>
<pre><code class="language-json">[
  {
    "id": "refund_001",
    "category": "refund",
    "input": "Can I return an unopened product after 14 days?",
    "expected_contains": [
      "return",
      "14"
    ]
  },
  {
    "id": "password_001",
    "category": "account",
    "input": "How do I reset my password?",
    "expected_contains": [
      "reset",
      "password"
    ]
  },
  {
    "id": "shipping_001",
    "category": "shipping",
    "input": "How can I track my order?",
    "expected_contains": [
      "track",
      "order"
    ]
  }
]
</code></pre>
<p>This is intentionally simple.</p>
<p>A production dataset could contain considerably more information:</p>
<pre><code class="language-json">{
  "id": "refund_001",
  "category": "refund",
  "input": "...",
  "context": "...",
  "reference_answer": "...",
  "expected_contains": [],
  "forbidden_contains": [],
  "difficulty": "medium",
  "tags": ["policy", "refund"]
}
</code></pre>
<h3>Where Should Eval Cases Come From?</h3>
<p>Your strongest eval cases often come from actual product behavior:</p>
<ul>
<li><p>production failures</p>
</li>
<li><p>user complaints</p>
</li>
<li><p>support tickets</p>
</li>
<li><p>manually discovered edge cases</p>
</li>
<li><p>expected product behaviors</p>
</li>
<li><p>adversarial inputs</p>
</li>
<li><p>previously fixed bugs</p>
</li>
</ul>
<p>Don't create 1,000 artificial examples simply because 1,000 sounds more scientific.</p>
<p>A smaller set of meaningful tests is usually more useful at the beginning.</p>
<hr />
<h2>Step 3: Build a Model Adapter</h2>
<p>We don't want evaluation logic tightly coupled to one model provider.</p>
<p>Instead of doing this everywhere:</p>
<pre><code class="language-python">response = some_provider_specific_call(...)
</code></pre>
<p>create a small abstraction:</p>
<pre><code class="language-python">class ModelAdapter:
    def generate(self, prompt: str) -&gt; str:
        raise NotImplementedError
</code></pre>
<p>Then implement the provider:</p>
<pre><code class="language-python">class MyLLM(ModelAdapter):

    def __init__(self, client, model):
        self.client = client
        self.model = model

    def generate(self, prompt: str) -&gt; str:

        response = self.client.responses.create(
            model=self.model,
            input=prompt
        )

        return response.output_text
</code></pre>
<p>Now the rest of our evaluation code doesn't need to know which provider we're using.</p>
<p>That makes experiments much easier:</p>
<pre><code class="language-text">Model A
Model B
Model C
New model version
Different temperature
Different system prompt
</code></pre>
<p>can all run through the same harness.</p>
<hr />
<h2>Step 4: Write the First Deterministic Grader</h2>
<p>Before reaching for another LLM to grade responses, use deterministic checks whenever they can reliably measure the requirement.</p>
<p>Let's create a keyword grader:</p>
<pre><code class="language-python">def contains_grader(output, expected):

    output = output.lower()

    matches = [
        item.lower() in output
        for item in expected
    ]

    return {
        "score": sum(matches) / len(matches),
        "passed": all(matches)
    }
</code></pre>
<p>For example:</p>
<pre><code class="language-python">output = """
You can reset your password from the account settings page.
"""

expected = ["reset", "password"]
</code></pre>
<p>Result:</p>
<pre><code class="language-json">{
  "score": 1.0,
  "passed": true
}
</code></pre>
<p>Deterministic graders are excellent for requirements such as:</p>
<pre><code class="language-text">JSON validity
Required fields
Exact values
Regex patterns
Schema validation
Maximum length
Forbidden phrases
Tool outcomes
Database state
Unit tests
</code></pre>
<p>They're fast, inexpensive, repeatable, and easy to debug.</p>
<hr />
<h2>Step 5: Add Multiple Graders</h2>
<p>A single metric rarely captures everything we care about.</p>
<p>Let's introduce another grader:</p>
<pre><code class="language-python">def length_grader(output, max_words=150):

    word_count = len(output.split())

    return {
        "word_count": word_count,
        "passed": word_count &lt;= max_words
    }
</code></pre>
<p>And perhaps a forbidden-content grader:</p>
<pre><code class="language-python">def forbidden_phrase_grader(output, phrases):

    output_lower = output.lower()

    found = [
        phrase
        for phrase in phrases
        if phrase.lower() in output_lower
    ]

    return {
        "found": found,
        "passed": len(found) == 0
    }
</code></pre>
<p>Our evaluation can now measure multiple dimensions instead of compressing everything into "good" or "bad."</p>
<hr />
<h2>Step 6: Build the Eval Runner</h2>
<p>Now we're ready for the core harness.</p>
<pre><code class="language-python">import json
import time

def load_evals(path):

    with open(path, "r") as file:
        return json.load(file)


def run_eval_case(model, case):

    start = time.perf_counter()

    output = model.generate(case["input"])

    latency = time.perf_counter() - start

    keyword_result = contains_grader(
        output,
        case.get("expected_contains", [])
    )

    length_result = length_grader(output)

    return {
        "id": case["id"],
        "category": case["category"],
        "input": case["input"],
        "output": output,
        "latency_seconds": latency,
        "graders": {
            "keywords": keyword_result,
            "length": length_result
        }
    }
</code></pre>
<p>Then execute the complete suite:</p>
<pre><code class="language-python">def run_suite(model, cases):

    results = []

    for case in cases:

        try:
            result = run_eval_case(model, case)
            results.append(result)

        except Exception as error:
            results.append({
                "id": case["id"],
                "error": str(error)
            })

    return results
</code></pre>
<p>Notice the <code>try/except</code>.</p>
<p>One failed API request should not destroy an entire evaluation run.</p>
<hr />
<h2>Step 7: Aggregate the Results</h2>
<p>Individual cases are useful for debugging.</p>
<p>Aggregate results are useful for decisions.</p>
<pre><code class="language-python">def summarize(results):

    completed = [
        r for r in results
        if "error" not in r
    ]

    if not completed:
        return {
            "total": len(results),
            "completed": 0
        }

    keyword_scores = [
        r["graders"]["keywords"]["score"]
        for r in completed
    ]

    latencies = [
        r["latency_seconds"]
        for r in completed
    ]

    return {
        "total": len(results),
        "completed": len(completed),
        "keyword_score": (
            sum(keyword_scores) / len(keyword_scores)
        ),
        "average_latency": (
            sum(latencies) / len(latencies)
        )
    }
</code></pre>
<p>An evaluation run could now produce:</p>
<pre><code class="language-json">{
  "total": 50,
  "completed": 50,
  "keyword_score": 0.91,
  "average_latency": 1.84
}
</code></pre>
<p>That already gives us something much more useful than manually reading five responses.</p>
<hr />
<h2>Step 8: Save Every Run</h2>
<p>Evaluation becomes much more powerful when results are historical.</p>
<pre><code class="language-python">from datetime import datetime

def save_results(results):

    timestamp = datetime.now().strftime(
        "%Y%m%d_%H%M%S"
    )

    path = f"results/eval_{timestamp}.json"

    with open(path, "w") as file:
        json.dump(results, file, indent=2)

    return path
</code></pre>
<p>Now we can compare:</p>
<pre><code class="language-text">Prompt v1 → 82%
Prompt v2 → 89%
Prompt v3 → 91%
</code></pre>
<p>Or:</p>
<pre><code class="language-text">Model A → 93% / 2.1 sec
Model B → 91% / 1.2 sec
Model C → 95% / 4.8 sec
</code></pre>
<p>Suddenly model selection becomes an engineering decision rather than a preference.</p>
<hr />
<h2>Step 9: Track Cost and Tokens</h2>
<p>Quality isn't the only thing worth evaluating.</p>
<p>Production LLM systems also operate under constraints.</p>
<p>Extend the adapter so it returns metadata:</p>
<pre><code class="language-python">{
    "text": "...",
    "input_tokens": 430,
    "output_tokens": 120,
    "model": "model-name"
}
</code></pre>
<p>Then calculate:</p>
<pre><code class="language-python">cost = (
    input_tokens * input_rate
    + output_tokens * output_rate
)
</code></pre>
<p>Your report can now compare:</p>
<table>
<thead>
<tr>
<th>Version</th>
<th>Quality</th>
<th>Latency</th>
<th>Cost/Request</th>
</tr>
</thead>
<tbody><tr>
<td>A</td>
<td>0.88</td>
<td>1.4s</td>
<td>$0.006</td>
</tr>
<tr>
<td>B</td>
<td>0.92</td>
<td>2.0s</td>
<td>$0.012</td>
</tr>
<tr>
<td>C</td>
<td>0.91</td>
<td>1.2s</td>
<td>$0.007</td>
</tr>
</tbody></table>
<p>This prevents a common mistake: optimizing quality while ignoring the operational price of achieving it.</p>
<hr />
<h2>Step 10: Add LLM-as-a-Judge</h2>
<p>Some properties cannot be captured reliably with string matching.</p>
<p>Consider:</p>
<pre><code class="language-text">Is the answer helpful?
Is it grounded in the supplied context?
Did it correctly explain the policy?
Did it hallucinate?
Does the answer satisfy the user's request?
</code></pre>
<p>These require semantic evaluation.</p>
<p>We can use another model as a grader.</p>
<pre><code class="language-python">def judge_response(judge_model, question, answer):

    prompt = f"""
You are evaluating an AI assistant.

Question:
{question}

Answer:
{answer}

Score the answer from 1 to 5.

Evaluate:
- correctness
- relevance
- completeness

Return JSON only:

{{
  "score": 1,
  "reason": ""
}}
"""

    return judge_model.generate(prompt)
</code></pre>
<p>For production use, make the rubric much more specific.</p>
<p>Instead of:</p>
<pre><code class="language-text">Is this a good answer?
</code></pre>
<p>prefer:</p>
<pre><code class="language-text">Score factual correctness from 1-5.

5 = completely correct
4 = correct with a minor omission
3 = partially correct
2 = major factual problems
1 = incorrect
</code></pre>
<p>A judge is itself an LLM, so its verdict isn't automatically ground truth.</p>
<p>Validate judge behavior against human-labeled examples before trusting it.</p>
<hr />
<h2>Step 11: Separate Capability Evals From Regression Evals</h2>
<p>These serve different purposes.</p>
<h3>Capability Evals</h3>
<p>Capability evals answer:</p>
<blockquote>
<p><strong>How well can the system perform this behavior?</strong></p>
</blockquote>
<p>You intentionally include difficult examples.</p>
<p>A low initial score isn't necessarily bad—it gives the team room to improve.</p>
<h3>Regression Evals</h3>
<p>Regression evals answer:</p>
<blockquote>
<p><strong>Did something we changed break behavior that previously worked?</strong></p>
</blockquote>
<p>These tests should normally remain highly stable.</p>
<p>For example:</p>
<pre><code class="language-text">evals/
├── capability/
│   ├── complex_reasoning.json
│   └── difficult_support.json
│
└── regression/
    ├── refund_policy.json
    ├── formatting.json
    └── known_failures.json
</code></pre>
<p>Whenever you fix an important production failure, consider converting it into a regression case.</p>
<p>That's how your eval suite becomes more valuable over time.</p>
<hr />
<h2>Step 12: Add Category-Level Reporting</h2>
<p>A global score can hide serious failures.</p>
<p>Imagine:</p>
<pre><code class="language-text">Overall score: 91%
</code></pre>
<p>Looks great.</p>
<p>But underneath:</p>
<pre><code class="language-text">General FAQ       99%
Shipping          97%
Account            95%
Refund             72%
Safety             61%
</code></pre>
<p>Now the picture is completely different.</p>
<p>Aggregate results by category:</p>
<pre><code class="language-python">from collections import defaultdict

def category_scores(results):

    categories = defaultdict(list)

    for result in results:

        if "error" in result:
            continue

        score = result["graders"]["keywords"]["score"]

        categories[result["category"]].append(score)

    return {
        category: sum(scores) / len(scores)
        for category, scores in categories.items()
    }
</code></pre>
<p>This tells us <strong>where</strong> the system is improving or deteriorating.</p>
<hr />
<h2>Step 13: Make Runs Reproducible</h2>
<p>If two runs aren't comparable, the resulting numbers aren't very useful.</p>
<p>Record the configuration used for every run:</p>
<pre><code class="language-json">{
  "run_id": "2026-08-27-001",
  "model": "model-name",
  "temperature": 0,
  "prompt_version": "support-v7",
  "dataset_version": "3.2",
  "grader_version": "2.1",
  "commit": "abc123"
}
</code></pre>
<p>You should be able to answer:</p>
<blockquote>
<p>"Why did our score fall from 92% to 86%?"</p>
</blockquote>
<p>without guessing.</p>
<p>Possible causes might include:</p>
<pre><code class="language-text">Model changed
Prompt changed
Dataset changed
Retriever changed
Tool behavior changed
Grader changed
Harness changed
</code></pre>
<p>Version them.</p>
<hr />
<h2>Step 14: Keep Evaluation Environments Isolated</h2>
<p>This becomes especially important when evaluating agents.</p>
<p>Imagine an agent that:</p>
<ol>
<li><p>creates files,</p>
</li>
<li><p>modifies a database,</p>
</li>
<li><p>calls tools,</p>
</li>
<li><p>updates application state.</p>
</li>
</ol>
<p>If the next evaluation starts with artifacts left by the previous run, the cases aren't independent anymore.</p>
<p>Each trial should therefore begin from a known state:</p>
<pre><code class="language-python">def setup_environment():
    reset_database()
    clear_temp_files()
    seed_test_data()


def run_isolated_case(case):

    setup_environment()

    try:
        return execute(case)

    finally:
        cleanup_environment()
</code></pre>
<p>For larger systems, this may involve:</p>
<pre><code class="language-text">containers
temporary databases
mock APIs
filesystem snapshots
sandbox environments
</code></pre>
<p>The closer your evaluation environment resembles the relevant parts of production, the more useful your results become.</p>
<hr />
<h2>Step 15: Don't Over-Grade the Agent's Path</h2>
<p>Suppose an agent needs to find a customer's order.</p>
<p>A brittle eval might require:</p>
<pre><code class="language-text">1. call lookup_customer
2. call get_orders
3. call get_order_status
4. generate response
</code></pre>
<p>But what if the agent discovers an equally valid two-step solution?</p>
<p>It shouldn't fail simply because it solved the task differently.</p>
<p>When possible, grade the <strong>outcome</strong>:</p>
<pre><code class="language-python">assert order_status == "shipped"
assert response_contains_tracking_number
</code></pre>
<p>rather than requiring one exact reasoning trajectory.</p>
<p>For agents, the final environment state can often be more meaningful than the final text response.</p>
<hr />
<h2>Step 16: Turn Production Failures Into Tests</h2>
<p>This may be the most valuable habit in the entire process.</p>
<p>Suppose a user discovers:</p>
<pre><code class="language-text">The assistant invents a refund policy
when an order is older than 90 days.
</code></pre>
<p>Don't just patch the prompt.</p>
<p>Create:</p>
<pre><code class="language-text">regression/refund_over_90_days.json
</code></pre>
<p>Then fix the system.</p>
<p>Now that failure can never silently return without showing up in your evaluation results.</p>
<p>Over time:</p>
<pre><code class="language-text">Production failure
       ↓
Root-cause analysis
       ↓
New eval case
       ↓
System fix
       ↓
Regression test
       ↓
Continuous evaluation
</code></pre>
<p>Your evaluation dataset becomes a record of what your system has learned not to break.</p>
<p>Teams developing larger LLM platforms can apply the same principle across model selection, RAG, prompting, integrations, validation, and monitoring. A broader <a href="https://sdlccorp.com/services/ai-as-a-service/private-large-language-model-development-company/">LLM development workflow</a> should therefore treat evaluation as an ongoing engineering loop rather than a one-time benchmark.</p>
<hr />
<h2>Step 17: Add the Harness to CI</h2>
<p>Eventually, evals should run automatically.</p>
<p>A simple CI rule might be:</p>
<pre><code class="language-python">if regression_score &lt; 0.95:
    raise SystemExit("Regression threshold failed")
</code></pre>
<p>Or compare against the baseline:</p>
<pre><code class="language-python">if new_score &lt; baseline_score - 0.02:
    raise SystemExit("Quality regression detected")
</code></pre>
<p>A deployment workflow might become:</p>
<pre><code class="language-text">Developer changes prompt/model/RAG
            |
            v
       Pull Request
            |
            v
       Unit Tests
            |
            v
       LLM Evals
            |
       +----+----+
       |         |
      Pass      Fail
       |         |
       v         v
     Merge     Review
</code></pre>
<p>Don't necessarily block deployments on every experimental capability metric.</p>
<p>Regression suites are usually better suited for hard CI thresholds.</p>
<hr />
<h2>A Better Project Structure</h2>
<p>Once the harness grows, keep its components separate.</p>
<pre><code class="language-text">llm-evals/
│
├── datasets/
│   ├── capability.json
│   └── regression.json
│
├── graders/
│   ├── keyword.py
│   ├── schema.py
│   ├── judge.py
│   └── safety.py
│
├── models/
│   └── adapter.py
│
├── runners/
│   └── runner.py
│
├── reports/
│   └── summary.py
│
├── results/
│
├── config.yaml
│
└── main.py
</code></pre>
<p>This makes it easier to modify one part without quietly changing the others.</p>
<hr />
<h2>What Should You Log?</h2>
<p>At minimum, save:</p>
<pre><code class="language-text">Run ID
Test case ID
Dataset version
Model
Model configuration
Prompt version
Input
Context
Output
Grader results
Latency
Token usage
Cost
Errors
Timestamp
</code></pre>
<p>For agentic applications, you may additionally need:</p>
<pre><code class="language-text">Tool calls
Tool results
Number of turns
Retries
Final environment state
Resource budget
</code></pre>
<p>Logs aren't merely debugging information.</p>
<p>They become raw material for future evals.</p>
<hr />
<h2>Common Mistakes When Building an Eval Harness</h2>
<h3>1. Testing Only Happy Paths</h3>
<p>If every example is easy, your score tells you very little.</p>
<p>Include boundary cases and known failures.</p>
<h3>2. Using Only One Aggregate Score</h3>
<p>A 94% average can hide a critical category running at 60%.</p>
<p>Break results down by behavior.</p>
<h3>3. Using LLM Judges for Everything</h3>
<p>Use deterministic evaluation when the requirement itself is deterministic.</p>
<p>Save model-based grading for semantic judgments.</p>
<h3>4. Changing the Harness During Model Comparisons</h3>
<p>If Model A gets different tools, prompts, retries, or budgets than Model B, you're no longer performing a clean comparison.</p>
<h3>5. Trusting the Grader Without Testing It</h3>
<p>A broken grader produces confident-looking but meaningless numbers.</p>
<p>Validate graders too.</p>
<h3>6. Ignoring Variability</h3>
<p>LLM outputs are probabilistic.</p>
<p>For important tests, consider multiple trials rather than assuming one response represents expected behavior.</p>
<h3>7. Building Evals Too Late</h3>
<p>If evaluation begins only after launch, you're forced to reconstruct expected behavior from an already complex system.</p>
<p>Start small and start early.</p>
<hr />
<h2>From Tiny Script to Production Eval Platform</h2>
<p>Our first version was essentially:</p>
<pre><code class="language-text">JSON
  ↓
Model
  ↓
Grader
  ↓
Score
</code></pre>
<p>A mature system might look more like:</p>
<pre><code class="language-text">Versioned datasets
       ↓
Parallel execution
       ↓
Model / Agent
       ↓
Deterministic graders
       +
LLM judges
       +
Environment checks
       ↓
Results database
       ↓
Dashboards
       ↓
Baseline comparison
       ↓
CI/CD quality gate
</code></pre>
<p>The architecture becomes more sophisticated, but the core idea doesn't change:</p>
<blockquote>
<p><strong>Define expected behavior, execute consistently, measure the result, preserve the evidence, and compare changes.</strong></p>
</blockquote>
<hr />
<h2>Final Takeaway</h2>
<p>If you want to <strong>build an LLM eval harness</strong>, don't begin by searching for the perfect evaluation framework.</p>
<p>Begin with five pieces:</p>
<pre><code class="language-text">1. A small dataset of meaningful tasks
2. A repeatable model runner
3. Simple, trustworthy graders
4. Detailed result logging
5. Baselines for comparison
</code></pre>
<p>Then improve the system as your application grows.</p>
<p>Add semantic judges when deterministic checks aren't enough.</p>
<p>Add isolated environments when agents modify state.</p>
<p>Add multiple trials when variance matters.</p>
<p>Add cost and latency when production economics matter.</p>
<p>Add regression gates when your team starts shipping frequently.</p>
<p>Most importantly, turn real failures into permanent tests.</p>
<p>The goal isn't to create an impressive evaluation dashboard.</p>
<p>The goal is to know whether your LLM application is <strong>actually getting better</strong>.</p>
]]></content:encoded></item><item><title><![CDATA[Automating Odoo Deployments With Docker and CI]]></title><description><![CDATA[Deploying Odoo manually can work for small projects, but as custom modules and environments grow, repeatable deployments become far more valuable.
With an Odoo Docker deployment, you can package Odoo ]]></description><link>https://sdlccorp-softwaredev.hashnode.dev/automating-odoo-deployments-with-docker-and-ci</link><guid isPermaLink="true">https://sdlccorp-softwaredev.hashnode.dev/automating-odoo-deployments-with-docker-and-ci</guid><dc:creator><![CDATA[Samcorp]]></dc:creator><pubDate>Wed, 19 Aug 2026 12:25:38 GMT</pubDate><content:encoded><![CDATA[<img src="https://cdn.hashnode.com/uploads/covers/685a6d69d4d0ea366852534c/53d70556-78d6-4b79-a90d-2be56698f2c0.png" alt="" style="display:block;margin:0 auto" />

  
<p>Deploying Odoo manually can work for small projects, but as custom modules and environments grow, repeatable deployments become far more valuable.</p>
<p>With an <strong>Odoo Docker deployment</strong>, you can package Odoo and its dependencies into a consistent container, while CI/CD can automatically test, build, publish, and deploy each approved change.</p>
<p>A practical workflow looks like this:</p>
<pre><code class="language-text">Developer Push
      ↓
CI Pipeline
      ↓
Validate &amp; Test
      ↓
Build Docker Image
      ↓
Push to Registry
      ↓
Deploy to Server
      ↓
Health Check
</code></pre>
<p>Odoo maintains an official Docker image repository, including current Docker definitions for Odoo 17, 18, and 19. The Odoo 19 image also provides dedicated paths for persistent application data and custom addons.</p>
<hr />
<h2>Why Use Docker for Odoo Deployment?</h2>
<p>Without containers, two Odoo servers can slowly become different because of:</p>
<ul>
<li><p>Python packages</p>
</li>
<li><p>System dependencies</p>
</li>
<li><p>Odoo configuration</p>
</li>
<li><p>Custom addon versions</p>
</li>
<li><p>OS-level changes</p>
</li>
</ul>
<p>Docker gives the deployment a more consistent runtime.</p>
<p>Instead of repeatedly configuring the server manually, you define the environment as code:</p>
<pre><code class="language-text">Dockerfile
+
Compose configuration
+
Odoo configuration
+
Custom addons
</code></pre>
<p>The same image can then move through testing, staging, and production.</p>
<hr />
<h2>Step 1: Organize the Odoo Project</h2>
<p>A simple project structure could be:</p>
<pre><code class="language-text">odoo-project/
├── custom-addons/
├── config/
│   └── odoo.conf
├── Dockerfile
├── compose.yaml
├── compose.production.yaml
└── .github/
    └── workflows/
        └── deploy.yml
</code></pre>
<p>Keep custom modules in version control so every deployment is connected to an exact code revision.</p>
<hr />
<h2>Step 2: Create the Odoo Dockerfile</h2>
<p>For Odoo 19, you can build on the Odoo image and add your own modules.</p>
<pre><code class="language-dockerfile">FROM odoo:19.0

COPY ./custom-addons /mnt/extra-addons
COPY ./config/odoo.conf /etc/odoo/odoo.conf
</code></pre>
<p>The official Odoo 19 Docker definition uses <code>/mnt/extra-addons</code> for user addons and <code>/var/lib/odoo</code> for persistent Odoo data.</p>
<p>For real projects, keep the image focused.</p>
<p>Avoid installing unnecessary packages just because they may be useful later.  </p>
<p>A structured <a href="https://sdlccorp.com/services/odoo-services/odoo-development-company/"><strong>Odoo development and deployment</strong></a> workflow can combine Docker-based environments, staging, automated CI/CD pipelines, and controlled production releases.</p>
<hr />
<h2>Step 3: Add PostgreSQL With Docker Compose</h2>
<p>Odoo requires PostgreSQL, so Compose can define both services.</p>
<pre><code class="language-yaml">services:

  db:
    image: postgres:16
    restart: unless-stopped

    environment:
      POSTGRES_DB: postgres
      POSTGRES_USER: odoo
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

    volumes:
      - postgres_data:/var/lib/postgresql/data

    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U odoo"]
      interval: 10s
      timeout: 5s
      retries: 5

  odoo:
    build: .
    restart: unless-stopped

    depends_on:
      db:
        condition: service_healthy

    ports:
      - "8069:8069"

    environment:
      HOST: db
      USER: odoo
      PASSWORD: ${POSTGRES_PASSWORD}

    volumes:
      - odoo_data:/var/lib/odoo

volumes:
  postgres_data:
  odoo_data:
</code></pre>
<p>The health check is useful because starting PostgreSQL does not automatically mean it is ready to accept connections. Docker Compose supports <code>condition: service_healthy</code> so a dependent application can wait for a dependency's health check to succeed.</p>
<hr />
<h2>Step 4: Keep Important Data Persistent</h2>
<p>Containers should be replaceable.</p>
<p>Your business data should not disappear when a container is recreated.</p>
<p>Persist at least:</p>
<pre><code class="language-text">PostgreSQL data
Odoo filestore
</code></pre>
<p>The official Odoo image exposes <code>/var/lib/odoo</code> specifically for persistent Odoo data, including the filestore.</p>
<p>The deployment model should therefore be:</p>
<pre><code class="language-text">Disposable Container
        +
Persistent Database
        +
Persistent Filestore
</code></pre>
<p>This makes rebuilding the Odoo container much safer.</p>
<hr />
<h2>Step 5: Separate Development and Production Configuration</h2>
<p>Production usually needs different settings from local development.</p>
<p>Docker recommends using an additional Compose configuration for production-specific changes.</p>
<p>For example:</p>
<pre><code class="language-text">compose.yaml
compose.production.yaml
</code></pre>
<p>You might use the production file to:</p>
<ul>
<li><p>Remove unnecessary exposed ports</p>
</li>
<li><p>Configure restart policies</p>
</li>
<li><p>Use production images</p>
</li>
<li><p>Connect to a reverse proxy</p>
</li>
<li><p>Apply production resources or volumes</p>
</li>
</ul>
<p>Then deploy with:</p>
<pre><code class="language-bash">docker compose \
  -f compose.yaml \
  -f compose.production.yaml \
  up -d
</code></pre>
<p>This keeps environment-specific changes clear.</p>
<hr />
<h2>Step 6: Keep Secrets Out of the Image</h2>
<p>Never write production credentials directly into your Dockerfile:</p>
<pre><code class="language-dockerfile">ENV POSTGRES_PASSWORD=super-secret-password
</code></pre>
<p>Avoid committing them to Git as well.</p>
<p>Docker recommends using dedicated secrets mechanisms for sensitive values rather than ordinary environment variables where possible.</p>
<p>Sensitive values may include:</p>
<pre><code class="language-text">PostgreSQL password
Odoo master password
Registry credentials
SSH keys
API tokens
SMTP passwords
</code></pre>
<p>Your CI system should also keep deployment credentials in its protected secret store.</p>
<hr />
<h1>Step 7: Add CI Before Automatic Deployment</h1>
<p>The important idea behind CI is simple:</p>
<blockquote>
<p>Don't deploy code that has not passed validation.</p>
</blockquote>
<p>A useful pipeline might be:</p>
<pre><code class="language-text">Git Push
   ↓
Checkout
   ↓
Build
   ↓
Run Tests
   ↓
Build Production Image
   ↓
Push Image
   ↓
Deploy
</code></pre>
<p>Docker provides official GitHub Actions specifically for building and publishing container images, and GitHub also documents workflows for publishing images to Docker registries.</p>
<hr />
<h2>Step 8: Create a GitHub Actions Workflow</h2>
<p>Here is a simplified example.</p>
<pre><code class="language-yaml">name: Odoo CI

on:
  push:
    branches:
      - production

jobs:

  build:
    runs-on: ubuntu-latest

    steps:

      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build Odoo image
        uses: docker/build-push-action@v6
        with:
          context: .
          push: false
          tags: my-odoo:test
</code></pre>
<p>At this point, the workflow proves that the Docker image can be built.</p>
<p>That alone already catches problems such as:</p>
<ul>
<li><p>Invalid Dockerfile instructions</p>
</li>
<li><p>Missing copied files</p>
</li>
<li><p>Broken dependencies</p>
</li>
<li><p>Build-time failures</p>
</li>
</ul>
<hr />
<h2>Step 9: Test Before Publishing</h2>
<p>A better pipeline should validate the image before pushing it to your registry.</p>
<p>Docker provides a documented <strong>test-before-push</strong> workflow pattern specifically for this reason.</p>
<p>For Odoo, testing might include:</p>
<pre><code class="language-bash">odoo \
  -d test_db \
  -i custom_module \
  --test-enable \
  --stop-after-init
</code></pre>
<p>Depending on your project, you can test:</p>
<ul>
<li><p>Module installation</p>
</li>
<li><p>Python tests</p>
</li>
<li><p>Access rights</p>
</li>
<li><p>XML loading</p>
</li>
<li><p>Business workflows</p>
</li>
<li><p>Upgrade scripts</p>
</li>
</ul>
<p>Your pipeline becomes:</p>
<pre><code class="language-text">Build
   ↓
Test
   ↓
Pass?
 ┌─┴─┐
No  Yes
│     │
Stop  Push Image
</code></pre>
<p>This prevents a large class of deployment problems from reaching production.</p>
<hr />
<h2>Step 10: Push the Approved Image</h2>
<p>After validation passes, authenticate to your container registry.</p>
<pre><code class="language-yaml">- name: Login to registry
  uses: docker/login-action@v3
  with:
    username: ${{ secrets.REGISTRY_USERNAME }}
    password: ${{ secrets.REGISTRY_PASSWORD }}
</code></pre>
<p>Then publish:</p>
<pre><code class="language-yaml">- name: Build and push
  uses: docker/build-push-action@v6
  with:
    context: .
    push: true
    tags: |
      company/odoo:${{ github.sha }}
      company/odoo:production
</code></pre>
<p>Using the Git commit SHA as an image tag is helpful:</p>
<pre><code class="language-text">company/odoo:a5b3c74...
</code></pre>
<p>because it gives you a clear connection between:</p>
<pre><code class="language-text">Running container
      ↓
Docker image
      ↓
Git commit
</code></pre>
<p>That makes debugging and rollback easier.</p>
<hr />
<h2>Step 11: Deploy the New Image</h2>
<p>Once the registry contains the approved image, production can pull it.</p>
<p>For example:</p>
<pre><code class="language-bash">docker compose pull odoo
docker compose up -d --no-deps odoo
</code></pre>
<p>Docker's production Compose guidance supports recreating an updated service without unnecessarily recreating its dependencies.</p>
<p>Your PostgreSQL container and persistent volumes remain separate from the replaceable Odoo application container.</p>
<hr />
<h2>Step 12: Add a Deployment Health Check</h2>
<p>Automation should not end with:</p>
<pre><code class="language-text">Container started
</code></pre>
<p>You also want to know:</p>
<pre><code class="language-text">Is Odoo actually responding?
</code></pre>
<p>A simple deployment check might be:</p>
<pre><code class="language-bash">curl --fail http://localhost:8069/web/login
</code></pre>
<p>Conceptually:</p>
<pre><code class="language-text">Deploy
   ↓
Wait for Odoo
   ↓
Health Check
   ↓
Success → Finish
Failure → Investigate/Roll Back
</code></pre>
<p>This gives the pipeline a clear success condition.</p>
<hr />
<h2>Step 13: Put a Reverse Proxy in Front of Odoo</h2>
<p>For an internet-facing production deployment, avoid treating Odoo's application port as your complete production web stack.</p>
<p>Odoo's production deployment documentation covers deployment behind a proxy and production configuration such as <code>proxy_mode</code>.</p>
<p>A common architecture is:</p>
<pre><code class="language-text">Internet
   ↓
HTTPS
   ↓
Nginx / Reverse Proxy
   ↓
Odoo Container
   ↓
PostgreSQL
</code></pre>
<p>The reverse proxy can handle:</p>
<ul>
<li><p>TLS</p>
</li>
<li><p>Domain routing</p>
</li>
<li><p>Request forwarding</p>
</li>
<li><p>Security headers</p>
</li>
</ul>
<p>Your PostgreSQL service generally remains internal to the Docker network rather than being exposed publicly.</p>
<hr />
<h1>Step 14: Make Rollback Part of CI/CD</h1>
<p>Automation is valuable only when recovery is also predictable.</p>
<p>Suppose the new version is:</p>
<pre><code class="language-text">company/odoo:8fd21ab
</code></pre>
<p>and the previous stable version was:</p>
<pre><code class="language-text">company/odoo:42ac718
</code></pre>
<p>A rollback can switch the application image back to:</p>
<pre><code class="language-text">company/odoo:42ac718
</code></pre>
<p>and recreate the Odoo service.</p>
<p>However, remember an important Odoo-specific point:</p>
<blockquote>
<p>Container rollback and database rollback are not always the same thing.</p>
</blockquote>
<p>If a deployment upgraded modules or changed database structures, simply returning to an older Docker image may not be enough.</p>
<p>For database-changing releases, take a verified backup before deployment.  </p>
<p>For upgrades or database-changing releases, <a href="https://sdlccorp.com/services/odoo-services/odoo-migration-services/"><strong>Odoo migration and deployment automation</strong></a> can combine reproducible Docker environments, automated validation, backups, and rollback planning before production cutover.</p>
<hr />
<h1>A Practical Odoo Docker Deployment Pipeline</h1>
<p>The final process could look like this:</p>
<pre><code class="language-text">Developer
   │
   ▼
Git Push
   │
   ▼
CI Pipeline
   │
   ├── Build Docker Image
   │
   ├── Start Test Database
   │
   ├── Install Modules
   │
   └── Run Tests
   │
   ▼
Push Versioned Image
   │
   ▼
Production Server
   │
   ├── Backup
   ├── Pull Image
   ├── Deploy
   └── Health Check
   │
   ▼
Odoo Live
</code></pre>
<p>This creates a much clearer deployment history than manually copying addons directly onto a production server.</p>
<hr />
<h2>Common Odoo Docker Deployment Mistakes</h2>
<h3>Using <code>latest</code> for everything</h3>
<p>Use predictable versioning so you know exactly which image is running.</p>
<h3>Keeping PostgreSQL inside the Odoo container</h3>
<p>Keep database and application responsibilities separate.</p>
<h3>Forgetting persistent volumes</h3>
<p>Containers are disposable. Business data is not.</p>
<h3>Deploying before testing</h3>
<p>Build and validate before publishing the production image.</p>
<h3>Storing credentials in Git</h3>
<p>Use your CI/CD secret store and appropriate runtime secret management.</p>
<h3>Updating modules without a backup</h3>
<p>Database-changing deployments deserve a verified rollback plan.</p>
<h3>Rebuilding directly on production</h3>
<p>Build once in CI and promote the tested artifact whenever possible.</p>
<hr />
<h2>Final Thoughts</h2>
<p>A reliable <strong>Odoo Docker deployment</strong> becomes much easier when deployment is treated as a repeatable pipeline rather than a sequence of manual server commands.</p>
<p>A solid starting point is:</p>
<p><strong>Code → Test → Build → Push → Backup → Deploy → Verify</strong></p>
<p>Docker gives you a consistent Odoo runtime, while CI/CD gives every change the same validation and deployment process. Docker's official CI tooling supports automated image building and publishing, and its production Compose guidance supports controlled service updates.</p>
<p>Start with a simple Odoo + PostgreSQL container setup.</p>
<p>Then automate one stage at a time.</p>
<p>Once your pipeline consistently builds, tests, versions, deploys, and verifies the same artifact, Odoo releases become easier to repeat, audit, and recover.</p>
]]></content:encoded></item><item><title><![CDATA[Building Your First Odoo Connector: Shopify Order Sync]]></title><description><![CDATA[A well-designed Odoo Shopify integration can automatically bring Shopify orders into Odoo and reduce repetitive manual work.
For a first connector, the best approach is to keep the workflow simple:
Sh]]></description><link>https://sdlccorp-softwaredev.hashnode.dev/building-your-first-odoo-connector-shopify-order-sync</link><guid isPermaLink="true">https://sdlccorp-softwaredev.hashnode.dev/building-your-first-odoo-connector-shopify-order-sync</guid><dc:creator><![CDATA[Samcorp]]></dc:creator><pubDate>Tue, 18 Aug 2026 11:34:45 GMT</pubDate><content:encoded><![CDATA[<img src="https://cdn.hashnode.com/uploads/covers/685a6d69d4d0ea366852534c/667c35e5-6448-4540-b29d-1aae54d0edd1.png" alt="" style="display:block;margin:0 auto" />

  
<p>A well-designed <strong>Odoo Shopify integration</strong> can automatically bring Shopify orders into Odoo and reduce repetitive manual work.</p>
<p>For a first connector, the best approach is to keep the workflow simple:</p>
<pre><code class="language-text">Shopify Order
   ↓
orders/create Webhook
   ↓
Verify HMAC
   ↓
Check Duplicate
   ↓
Map Customer &amp; Product
   ↓
Create Odoo Sales Order
   ↓
Store Shopify Order ID
</code></pre>
<p>This gives you a reliable foundation that can later be extended to inventory, fulfillment, refunds, and customer synchronization.</p>
<hr />
<h2>Step 1: Create the Odoo Connector Module</h2>
<p>Start with a small custom Odoo module.</p>
<pre><code class="language-text">shopify_odoo_connector/
├── __init__.py
├── __manifest__.py
├── controllers/
├── models/
├── security/
└── views/
</code></pre>
<p>Your module can depend on the main applications required for order creation:</p>
<pre><code class="language-python">{
    "name": "Shopify Odoo Connector",
    "version": "1.0.0",
    "depends": [
        "sale_management",
        "stock",
        "contacts",
    ],
    "installable": True,
}
</code></pre>
<p>Keeping the first version focused makes development and testing much easier.</p>
<hr />
<h2>Step 2: Configure the Shopify Store</h2>
<p>Create a model for the Shopify instance.</p>
<pre><code class="language-python">from odoo import fields, models


class ShopifyInstance(models.Model):
    _name = "shopify.instance"
    _description = "Shopify Instance"

    name = fields.Char(required=True)
    shop_domain = fields.Char(required=True)
    active = fields.Boolean(default=True)
</code></pre>
<p>Store Shopify credentials securely rather than placing access tokens directly inside Python code.</p>
<hr />
<h2>Step 3: Receive Shopify Orders With a Webhook</h2>
<p>When a new Shopify order is created, Shopify can send an <code>orders/create</code> webhook to Odoo.</p>
<p>Example endpoint:</p>
<pre><code class="language-text">POST /shopify/webhook/orders/create
</code></pre>
<p>A basic Odoo controller may look like:</p>
<pre><code class="language-python">from odoo import http
from odoo.http import request


class ShopifyWebhookController(http.Controller):

    @http.route(
        "/shopify/webhook/orders/create",
        type="http",
        auth="public",
        methods=["POST"],
        csrf=False,
    )
    def shopify_order_create(self, **kwargs):

        raw_body = request.httprequest.data

        # Verify and process the webhook

        return request.make_response("OK", status=200)
</code></pre>
<p>Using webhooks allows orders to move into Odoo soon after they are created.  </p>
<p>For a production-ready reference, this <a href="https://sdlccorp.com/products/shopify-odoo-integration-app/"><strong>Shopify to Odoo Connector</strong></a> demonstrates webhook-based synchronization for orders, customers, products, and inventory.</p>
<hr />
<h2>Step 4: Verify the Shopify Webhook</h2>
<p>Before processing an order, verify Shopify's HMAC signature.</p>
<p>Shopify sends the signature in:</p>
<pre><code class="language-text">X-Shopify-Hmac-SHA256
</code></pre>
<p>A simplified verification function is:</p>
<pre><code class="language-python">import base64
import hashlib
import hmac


def verify_webhook(raw_body, received_hmac, secret):

    digest = hmac.new(
        secret.encode(),
        raw_body,
        hashlib.sha256,
    ).digest()

    calculated = base64.b64encode(digest).decode()

    return hmac.compare_digest(
        calculated,
        received_hmac,
    )
</code></pre>
<p>This helps ensure that your Odoo endpoint processes genuine Shopify requests.</p>
<hr />
<h2>Step 5: Prevent Duplicate Orders</h2>
<p>Webhook events can occasionally be delivered more than once.</p>
<p>Use Shopify's webhook ID and Shopify order ID to make the integration idempotent.</p>
<p>Before creating an Odoo order:</p>
<pre><code class="language-python">existing_order = env["sale.order"].search([
    ("shopify_order_id", "=", str(payload["id"]))
], limit=1)
</code></pre>
<p>If the order already exists, simply skip the duplicate request.</p>
<p>This ensures:</p>
<pre><code class="language-text">1 Shopify Order
      ↓
1 Odoo Order
</code></pre>
<hr />
<h2>Step 6: Match Shopify Products With Odoo</h2>
<p>A simple starting point is matching the Shopify SKU with the Odoo internal reference.</p>
<pre><code class="language-python">product = env["product.product"].search([
    ("default_code", "=", shopify_sku)
], limit=1)
</code></pre>
<p>For example:</p>
<pre><code class="language-text">Shopify SKU: TSHIRT-BLACK-M
        ↓
Odoo Internal Reference: TSHIRT-BLACK-M
</code></pre>
<p>If a product cannot be matched, log the issue clearly instead of silently creating an incorrect product.</p>
<hr />
<h2>Step 7: Find or Create the Customer</h2>
<p>Search for the Shopify customer in Odoo using a reliable identifier such as email or a stored Shopify customer ID.</p>
<pre><code class="language-python">partner = env["res.partner"].search([
    ("email", "=", customer_email)
], limit=1)
</code></pre>
<p>If no customer exists:</p>
<pre><code class="language-python">partner = env["res.partner"].create({
    "name": customer_name,
    "email": customer_email,
    "phone": customer_phone,
})
</code></pre>
<p>Storing Shopify's customer ID in Odoo can make future synchronization even more reliable.</p>
<hr />
<h2>Step 8: Create the Odoo Sales Order</h2>
<p>Once products and customers are mapped, build the order lines.</p>
<pre><code class="language-python">order_lines.append((
    0,
    0,
    {
        "product_id": product.id,
        "product_uom_qty": quantity,
        "price_unit": price,
    }
))
</code></pre>
<p>Then create the sales order:</p>
<pre><code class="language-python">sale_order = env["sale.order"].create({
    "partner_id": partner.id,
    "client_order_ref": shopify_order_name,
    "order_line": order_lines,
})
</code></pre>
<p>For an initial connector, keeping imported orders as quotations can be useful while you validate payment, tax, inventory, and shipping rules.</p>
<hr />
<h2>Step 9: Store Shopify References</h2>
<p>Save the external Shopify order ID in Odoo.</p>
<pre><code class="language-python">shopify_order_id = fields.Char(
    index=True,
    copy=False,
)
</code></pre>
<p>This helps with:</p>
<ul>
<li><p>Duplicate prevention</p>
</li>
<li><p>Order updates</p>
</li>
<li><p>Cancellations</p>
</li>
<li><p>Refunds</p>
</li>
<li><p>Fulfillment synchronization</p>
</li>
</ul>
<p>Stable external IDs make future connector features much easier to manage.</p>
<hr />
<h2>Step 10: Add Background Processing</h2>
<p>For a small prototype, orders can be processed directly.</p>
<p>For production, a stronger architecture is:</p>
<pre><code class="language-text">Webhook Received
      ↓
Verify HMAC
      ↓
Store Event
      ↓
Return Success
      ↓
Process in Background
      ↓
Create Odoo Records
</code></pre>
<p>This keeps webhook responses fast and makes order processing easier to retry if an issue occurs.</p>
<hr />
<h2>Step 11: Add a Reconciliation Job</h2>
<p>Webhooks are excellent for real-time events, but a scheduled reconciliation job adds another layer of reliability.</p>
<p>For example:</p>
<pre><code class="language-text">Webhook Sync
    +
Hourly Reconciliation
</code></pre>
<p>The reconciliation process can compare recently updated Shopify orders with Odoo and recover anything that was missed during temporary downtime.  </p>
<p>You can also follow this detailed guide to <a href="https://sdlccorp.com/post/how-to-configure-shopify-odoo-connector/"><strong>configure a Shopify Odoo connector</strong></a>, including API authentication, store registration, SKU mapping, order synchronization, and scheduler setup.</p>
<hr />
<h2>Testing Your Odoo Shopify Integration</h2>
<p>Before going live, test a few important scenarios.</p>
<h3>Standard Order</h3>
<p>Known customer + known product.</p>
<p><strong>Expected:</strong> One Odoo quotation is created.</p>
<h3>Multiple Products</h3>
<p>Several Shopify order lines.</p>
<p><strong>Expected:</strong> All products appear correctly in one Odoo order.</p>
<h3>Unknown SKU</h3>
<p><strong>Expected:</strong> The connector records a clear mapping issue.</p>
<h3>Duplicate Webhook</h3>
<p>Send the same event twice.</p>
<p><strong>Expected:</strong> Only one Odoo order exists.</p>
<h3>Invalid HMAC</h3>
<p><strong>Expected:</strong> The request is rejected and no record is created.</p>
<p>These tests help confirm that the connector behaves safely before real orders are synchronized.</p>
<hr />
<h2>Common Integration Mistakes to Avoid</h2>
<p>Keep these points in mind:</p>
<ul>
<li><p>Store API credentials securely</p>
</li>
<li><p>Verify every Shopify webhook</p>
</li>
<li><p>Prevent duplicate order creation</p>
</li>
<li><p>Use stable product identifiers</p>
</li>
<li><p>Log mapping issues clearly</p>
</li>
<li><p>Test taxes, shipping, and payments before automatic confirmation</p>
</li>
<li><p>Keep a reconciliation process for recovery</p>
</li>
</ul>
<p>Each of these practices makes the integration easier to maintain as it grows.</p>
<hr />
<h2>What Can You Add Next?</h2>
<p>Once Shopify order sync is stable, you can expand the connector gradually:</p>
<pre><code class="language-text">Phase 1: Orders
Phase 2: Customers
Phase 3: Products &amp; Variants
Phase 4: Inventory
Phase 5: Order Updates
Phase 6: Fulfillment
Phase 7: Refunds
Phase 8: Monitoring &amp; Reconciliation
</code></pre>
<p>A phased approach keeps the <strong>Odoo Shopify integration</strong> easier to test, debug, and improve.</p>
<hr />
<h2>Final Thoughts</h2>
<p>Building your first Shopify connector becomes much simpler when you start with one reliable workflow.</p>
<p>Focus first on:</p>
<p><strong>Verify → Deduplicate → Map → Import → Log</strong></p>
<p>Once Shopify orders are consistently reaching Odoo, you can confidently extend the connector to customers, inventory, fulfillment, and refunds.</p>
<p>A small, dependable integration is a much stronger starting point than trying to synchronize everything at once.</p>
]]></content:encoded></item><item><title><![CDATA[Odoo 16 to 18 Upgrade: Step-by-Step Guide for Custom Modules]]></title><description><![CDATA[An Odoo 16 to 18 upgrade is more than a database migration when custom modules are involved. Your database may upgrade successfully, but custom Python code, XML views, JavaScript, integrations, and hi]]></description><link>https://sdlccorp-softwaredev.hashnode.dev/odoo-16-to-18-upgrade-step-by-step-guide-for-custom-modules</link><guid isPermaLink="true">https://sdlccorp-softwaredev.hashnode.dev/odoo-16-to-18-upgrade-step-by-step-guide-for-custom-modules</guid><dc:creator><![CDATA[Samcorp]]></dc:creator><pubDate>Tue, 18 Aug 2026 09:12:04 GMT</pubDate><content:encoded><![CDATA[<img src="https://cdn.hashnode.com/uploads/covers/685a6d69d4d0ea366852534c/c86ced7f-e40f-4055-bd29-5b31ed1da251.png" alt="" style="display:block;margin:0 auto" />

<p>An <strong>Odoo 16 to 18 upgrade</strong> is more than a database migration when custom modules are involved. Your database may upgrade successfully, but custom Python code, XML views, JavaScript, integrations, and historical data can still break.</p>
<p>Odoo officially supports upgrading older databases to newer versions, including a 16.0 to 18.0 migration path. Custom code, however, still needs to be reviewed, updated, and tested separately.</p>
<h2>Step 1: Audit Your Odoo 16 Setup</h2>
<p>Before upgrading, make a list of everything customized in your current system:</p>
<ul>
<li><p>Custom modules</p>
</li>
<li><p>Third-party apps</p>
</li>
<li><p>Odoo Studio changes</p>
</li>
<li><p>External integrations</p>
</li>
<li><p>Scheduled actions</p>
</li>
<li><p>Custom reports</p>
</li>
<li><p>Security rules</p>
</li>
<li><p>Payment or shipping connectors</p>
</li>
</ul>
<p>Also remove customizations that are no longer needed. Odoo 18 may already provide some functionality that previously required custom development.</p>
<h2>Step 2: Take a Full Backup</h2>
<p>Create a complete backup before making any changes.</p>
<p>Back up:</p>
<ul>
<li><p>PostgreSQL database</p>
</li>
<li><p>Odoo filestore</p>
</li>
<li><p>Custom addons</p>
</li>
<li><p>Configuration files</p>
</li>
<li><p>Environment variables</p>
</li>
<li><p>Integration settings</p>
</li>
</ul>
<p>A typical PostgreSQL backup command is:</p>
<pre><code class="language-bash">pg_dump -Fc production_db &gt; odoo16_backup.dump
</code></pre>
<p>Always keep the matching filestore with the database backup.</p>
<h2>Step 3: Create a Separate Odoo 18 Environment</h2>
<p>Do not upgrade directly on your live Odoo 16 server.</p>
<p>Create a clean Odoo 18 staging environment and configure the required addon paths.</p>
<p>For example:</p>
<pre><code class="language-ini">addons_path = /opt/odoo18/odoo/addons,/opt/odoo18/enterprise,/opt/odoo18/custom-addons
</code></pre>
<p>First confirm that Odoo 18 starts correctly before introducing migrated data.</p>
<h2>Step 4: Make Custom Modules Odoo 18 Compatible</h2>
<p>Test each custom module on a fresh Odoo 18 database.</p>
<pre><code class="language-bash">./odoo-bin \
  -d odoo18_test \
  -i custom_module \
  --stop-after-init
</code></pre>
<p>Fix compatibility issues such as:</p>
<ul>
<li><p>Removed or renamed fields</p>
</li>
<li><p>Changed model methods</p>
</li>
<li><p>Broken Python imports</p>
</li>
<li><p>Invalid XML views</p>
</li>
<li><p>XPath failures</p>
</li>
<li><p>Asset changes</p>
</li>
<li><p>JavaScript or OWL errors</p>
</li>
<li><p>Missing dependencies</p>
<p>If your environment contains heavily modified modules, reviewing them through an <a href="https://sdlccorp.com/services/odoo-services/odoo-customization-services/"><strong>Odoo customization services</strong></a> workflow can help identify outdated dependencies, unsafe core modifications, view changes, and upgrade-sensitive business logic before migration.</p>
</li>
</ul>
<p>Also update the module manifest where necessary.</p>
<pre><code class="language-python">{
    "name": "Custom Sales Extension",
    "version": "18.0.1.0.0",
    "depends": ["sale_management", "stock"],
    "installable": True,
}
</code></pre>
<p>A module should install cleanly on Odoo 18 before you test it with migrated production data.</p>
<h2>Step 5: Review XML and Frontend Customizations</h2>
<p>Custom views often fail because the parent Odoo view has changed.</p>
<p>For example:</p>
<pre><code class="language-xml">&lt;xpath expr="//field[@name='partner_id']" position="after"&gt;
    &lt;field name="custom_reference"/&gt;
&lt;/xpath&gt;
</code></pre>
<p>If the Odoo 18 parent view is different, this XPath may stop working.</p>
<p>Review:</p>
<ul>
<li><p>Form and list views</p>
</li>
<li><p>Search views</p>
</li>
<li><p>Kanban views</p>
</li>
<li><p>QWeb templates</p>
</li>
<li><p>Reports</p>
</li>
<li><p>OWL components</p>
</li>
<li><p>JavaScript patches</p>
</li>
<li><p>POS customizations</p>
</li>
</ul>
<p>Use browser developer tools to identify frontend errors.</p>
<h2>Step 6: Upgrade a Test Database</h2>
<p>Once your custom modules are mostly compatible, create an upgraded test database using Odoo's supported upgrade process.</p>
<p>For complex environments involving custom modules, integrations, and production data, a structured <a href="https://sdlccorp.com/services/odoo-services/odoo-migration-services/"><strong>Odoo migration services</strong></a> approach can help organize staging, validation, reconciliation, and rollback planning before the final cutover.</p>
<p>Do not move directly to production.</p>
<p>Use the test database to identify issues with:</p>
<ul>
<li><p>Existing records</p>
</li>
<li><p>Custom fields</p>
</li>
<li><p>Accounting data</p>
</li>
<li><p>Inventory</p>
</li>
<li><p>Attachments</p>
</li>
<li><p>Workflows</p>
</li>
<li><p>Integrations</p>
</li>
</ul>
<p>Make sure the correct filestore is also restored.</p>
<h2>Step 7: Write Migration Scripts for Custom Data</h2>
<p>Code changes do not automatically transform historical records.</p>
<p>Suppose Odoo 16 stores:</p>
<pre><code class="language-text">state = legacy
</code></pre>
<p>but the Odoo 18 module expects:</p>
<pre><code class="language-text">state = active
</code></pre>
<p>You may need an upgrade script:</p>
<pre><code class="language-python">def migrate(cr, version):
    cr.execute("""
        UPDATE custom_record
        SET state = 'active'
        WHERE state = 'legacy'
    """)
</code></pre>
<p>Migration scripts are commonly required when:</p>
<ul>
<li><p>Fields are renamed</p>
</li>
<li><p>Models change</p>
</li>
<li><p>Selection values change</p>
</li>
<li><p>Data moves between models</p>
</li>
<li><p>Relationships change</p>
</li>
<li><p>Old fields are removed</p>
</li>
</ul>
<p>Odoo also supports pre, post, and end migration phases for handling these transformations.</p>
<h2>Step 8: Test Complete Business Workflows</h2>
<p>A successful module installation does not mean the business process still works.</p>
<p>Test complete workflows such as:</p>
<p><strong>Sales</strong></p>
<pre><code class="language-text">Quotation → Sales Order → Delivery → Invoice → Payment
</code></pre>
<p><strong>Purchase</strong></p>
<pre><code class="language-text">RFQ → Purchase Order → Receipt → Vendor Bill
</code></pre>
<p><strong>Inventory</strong></p>
<p>Check receipts, deliveries, transfers, lots, serial numbers, and valuation.</p>
<p><strong>Accounting</strong></p>
<p>Validate taxes, journal entries, reconciliation, credit notes, payments, and reports.</p>
<p>Also test every custom workflow created specifically for your organization.</p>
<h2>Step 9: Test External Integrations</h2>
<p>Verify all connected services, including:</p>
<ul>
<li><p>Payment gateways</p>
</li>
<li><p>Shipping providers</p>
</li>
<li><p>Marketplaces</p>
</li>
<li><p>REST APIs</p>
</li>
<li><p>Webhooks</p>
</li>
<li><p>SSO</p>
</li>
<li><p>BI tools</p>
</li>
<li><p>Email services</p>
</li>
</ul>
<p>Check authentication, payload formats, scheduled synchronization, and error handling.</p>
<h2>Step 10: Rehearse the Migration</h2>
<p>Run the complete upgrade again using a fresh Odoo 16 backup.</p>
<p>Record how long each stage takes:</p>
<pre><code class="language-text">Backup
Database upgrade
Restore
Custom module update
Migration scripts
Testing
Final validation
</code></pre>
<p>This helps you plan a realistic production maintenance window.</p>
<h2>Step 11: Prepare a Rollback Plan</h2>
<p>Before production migration, make sure you can return to Odoo 16 if necessary.</p>
<p>Your rollback plan should include:</p>
<ol>
<li><p>Final Odoo 16 database backup</p>
</li>
<li><p>Matching filestore backup</p>
</li>
<li><p>Original Odoo 16 server kept intact</p>
</li>
<li><p>Reversible proxy or DNS configuration</p>
</li>
<li><p>Clear rollback decision criteria</p>
</li>
</ol>
<h2>Step 12: Perform the Production Upgrade</h2>
<p>Once staging is approved, follow a controlled sequence:</p>
<pre><code class="language-text">1. Stop user access
2. Take final backup
3. Upgrade the database
4. Deploy Odoo 18-compatible custom modules
5. Restore the filestore
6. Run migration scripts
7. Upgrade modules
8. Run smoke tests
9. Verify integrations
10. Reopen the system
</code></pre>
<p>Deploy only the code that has already been tested in staging.</p>
<h2>Common Odoo 16 to 18 Upgrade Mistakes</h2>
<p>Avoid these common problems:</p>
<ul>
<li><p>Treating the upgrade as database-only</p>
</li>
<li><p>Testing directly in production</p>
</li>
<li><p>Migrating unnecessary custom modules</p>
</li>
<li><p>Ignoring the filestore</p>
</li>
<li><p>Skipping custom data migration</p>
</li>
<li><p>Checking only whether modules install</p>
</li>
<li><p>Forgetting integrations</p>
</li>
<li><p>Testing individual screens instead of complete workflows</p>
</li>
</ul>
<h2>Can You Upgrade Directly From Odoo 16 to Odoo 18?</h2>
<p>Yes. Odoo supports upgrading a database from Odoo 16 to Odoo 18.</p>
<p>The important part is understanding that the database upgrade and custom-code migration are different tasks.</p>
<p>A safer workflow is:</p>
<pre><code class="language-text">Odoo 16
   ↓
Database Upgrade
   ↓
Odoo 18 Database
   +
Compatible Custom Modules
   +
Data Migration Scripts
   +
Business Testing
</code></pre>
<h2>Final Thoughts</h2>
<p>A reliable <strong>Odoo 16 to 18 upgrade</strong> should follow a controlled process:</p>
<p><strong>Audit → Backup → Update Custom Modules → Upgrade Test Database → Migrate Data → Test → Rehearse → Go Live</strong></p>
<p>The biggest mistake is assuming that a database that successfully starts on Odoo 18 is fully migrated.</p>
<p>Custom modules, integrations, accounting workflows, inventory processes, and existing data should all be tested before production cutover.</p>
]]></content:encoded></item><item><title><![CDATA[How to Choose the Best Odoo Customization Company in India: A Complete Guide]]></title><description><![CDATA[What is Odoo Development?
Odoo development refers to building, modifying, or extending the capabilities of the Odoo ERP platform. As an open-source and modular system, Odoo provides applications for finance, sales, inventory, HR, project management, ...]]></description><link>https://sdlccorp-softwaredev.hashnode.dev/how-to-choose-the-best-odoo-customization-company-in-india-a-complete-guide</link><guid isPermaLink="true">https://sdlccorp-softwaredev.hashnode.dev/how-to-choose-the-best-odoo-customization-company-in-india-a-complete-guide</guid><category><![CDATA[Odoo India  ERP Development in India  Odoo Consulting India  Indian ERP Consultants  Odoo Experts in India]]></category><category><![CDATA[Choosing Odoo Partner  Odoo Selection Guide  Odoo Consulting Services  ERP System Selection  Choosing ERP Vendor]]></category><category><![CDATA[Odoo Customization  Best Odoo Implementation Company  Odoo Customization Services  Odoo Development in India  Odoo Customization Experts]]></category><category><![CDATA[Odoo ERP Customization  ERP Solution Providers  Odoo Customization Best Practices  Odoo Implementation Services  Odoo Modules Development]]></category><dc:creator><![CDATA[Samcorp]]></dc:creator><pubDate>Tue, 01 Jul 2025 11:58:05 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-what-is-odoo-development"><strong>What is Odoo Development?</strong></h2>
<p>Odoo development refers to building, modifying, or extending the capabilities of the Odoo ERP platform. As an open-source and modular system, Odoo provides applications for finance, sales, inventory, HR, project management, and more. Through Odoo customization, businesses can tailor these modules to match unique workflows, integrate with third-party tools, or even develop completely new features. Professional Odoo development services include module customization, API integrations, migration, performance tuning, and deployment support to ensure that the ERP ecosystem functions smoothly and meets your precise operational requirements.</p>
<p><a target="_blank" href="https://sdlccorp.com/services/odoo-services/odoo-customization-services/"><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXc6ctlmmhhaYfj4rNYXqN3y7ClUYB3EBg8B6uZQtf9QivNXIDuDgd9mXD9Z8tOVHYH8Tg0bTAIzG8Ec9vyiAo3rP_X113vMK9N8RiZabtDV2xTQ8Ne-nkzcGNpGQ6lQQIA2M_8tFw?key=0i76YSbhJatqS--oIvrEzg" alt /></a></p>
<h3 id="heading-why-do-businesses-need-odoo-development-companies"><strong>Why Do Businesses Need Odoo Development Companies?</strong></h3>
<p>While Odoo’s modular architecture is powerful, implementing and adapting it to complex, real-world business processes often requires specialized skills. This is where an experienced <a target="_blank" href="https://sdlccorp.com/services/odoo-services/odoo-customization-services/">Odoo customization company</a> becomes crucial. Such companies combine functional consultants with technical developers to:</p>
<ul>
<li><p>Understand and map your business workflows</p>
</li>
<li><p>Perform efficient module configuration</p>
</li>
<li><p>Deliver Odoo customization services tailored to your business rules</p>
</li>
<li><p>Ensure data security and migration integrity</p>
</li>
<li><p>Provide long-term maintenance and support</p>
</li>
</ul>
<p>Without expert guidance, implementing Odoo can result in poor configurations, data loss, or compatibility issues. Partnering with a professional team guarantees a robust, scalable, and future-ready ERP implementation.</p>
<h3 id="heading-what-are-the-types-of-odoo-development-services-available"><strong>What are the Types of Odoo Development Services Available?</strong></h3>
<p>Top Odoo development companies typically offer a broad range of services, including:</p>
<ul>
<li><p><strong>Odoo customization</strong> (tailoring standard modules to fit your workflows)</p>
</li>
<li><p><strong>Custom module development</strong> (building new features from scratch)</p>
</li>
<li><p><strong>Odoo migration services</strong> (upgrading to newer Odoo versions with secure data migration)</p>
</li>
<li><p><strong>Third-party API integrations</strong> (connecting Odoo with external apps like payment gateways, CRMs, or shipping providers)</p>
</li>
<li><p><strong>Performance optimization</strong> (improving load times and data processing speed)</p>
</li>
<li><p><strong>Odoo training</strong> (enabling your staff to use the platform effectively)</p>
</li>
<li><p><strong>Odoo support and maintenance</strong> (continuous updates, bug fixes, and upgrades)</p>
</li>
</ul>
<p>These Odoo customization services empower businesses to extend Odoo’s default capabilities and align the system precisely with their operations.</p>
<h3 id="heading-basic-factors-when-choosing-an-odoo-development-company"><strong>Basic Factors When Choosing an Odoo Development Company</strong></h3>
<p>Choosing the best Odoo customization company in India requires assessing several critical factors:</p>
<ul>
<li><p><strong>Technical expertise</strong>: Verify their experience with Odoo’s frameworks (Python, PostgreSQL, XML) and modern development practices.</p>
</li>
<li><p><strong>Functional knowledge</strong>: Ensure they have consultants who understand business domains like manufacturing, trading, e-commerce, or accounting.</p>
</li>
<li><p><strong>Portfolio</strong>: Review their previous Odoo customization projects, complexity of integrations, and real-world results.</p>
</li>
<li><p><strong>Certifications &amp; partnerships</strong>: Prefer certified Odoo partners or official contributors to the Odoo community.</p>
</li>
<li><p><strong>Communication &amp; project management</strong>: Evaluate their ability to deliver milestones transparently and manage change requests effectively.</p>
</li>
<li><p><strong>Post-deployment support</strong>: Confirm their service-level agreements for long-term maintenance, bug fixing, and updates.</p>
</li>
</ul>
<p><a target="_blank" href="https://sdlccorp.com/services/odoo-services/odoo-customization-services/"><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXfuAtZ18fJynyC-hynGJdRwgeWFy5Li9oleMcHCR7pYHYnV49XB9nfflnmS-xq-z0qANpmkYpe0R4vI2yHeyLzMKREV6ofYa3LsAxpemTJiYSoX7H5HXEB2hf4gbABLT0uGtfUxdg?key=0i76YSbhJatqS--oIvrEzg" alt /></a></p>
<h3 id="heading-evaluating-the-companys-portfolio-and-case-studies"><strong>Evaluating the Company’s Portfolio and Case Studies</strong></h3>
<p>A reputable Odoo customization company should have a clear, verifiable portfolio that demonstrates:</p>
<ul>
<li><p>End-to-end Odoo implementations</p>
</li>
<li><p>Complex module customizations</p>
</li>
<li><p>Cross-industry experience</p>
</li>
<li><p>Successful Odoo migration projects</p>
</li>
<li><p>Scalability and performance improvements</p>
</li>
</ul>
<p>Reviewing detailed case studies is an essential step. Ask for success stories relevant to your domain, measurable outcomes achieved, and references from their past clients. This due diligence will help you assess whether their <a target="_blank" href="https://sdlccorp.com/services/odoo-services/odoo-customization-services/">Odoo customization service</a> truly meets your needs.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>Odoo is a powerful ERP platform, but to unlock its full potential, a skilled partner is essential. The best Odoo customization companies in India combine technical excellence, industry knowledge, and a commitment to ongoing support. By carefully evaluating their skills, portfolio, and support structures, you can select a reliable Odoo customization service provider that aligns with your business goals, ensuring a secure, scalable, and efficient ERP solution for years to come.</p>
]]></content:encoded></item></channel></rss>