<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://sigagent.ai/feed.xml" rel="self" type="application/atom+xml" /><link href="https://sigagent.ai/" rel="alternate" type="text/html" /><updated>2026-03-08T16:06:00+00:00</updated><id>https://sigagent.ai/feed.xml</id><title type="html">SigAgent.AI</title><subtitle>SigAgent.AI provides advanced monitoring and telemetry solutions  specifically designed for Claude Agents.
</subtitle><author><name>Andrei Radulescu-Banu</name><email>andrei@analytiqhub.com</email></author><entry><title type="html">Self-Hosted MongoDB on Kubernetes with Atlas Search (mongot)</title><link href="https://sigagent.ai/tech/kubernetes/devops/mongodb/self-hosted-mongodb-kubernetes-atlas-search/" rel="alternate" type="text/html" title="Self-Hosted MongoDB on Kubernetes with Atlas Search (mongot)" /><published>2026-03-08T00:00:00+00:00</published><updated>2026-03-08T00:00:00+00:00</updated><id>https://sigagent.ai/tech/kubernetes/devops/mongodb/self-hosted-mongodb-kubernetes-atlas-search</id><content type="html" xml:base="https://sigagent.ai/tech/kubernetes/devops/mongodb/self-hosted-mongodb-kubernetes-atlas-search/"><![CDATA[<p>For air-gapped environments, on-premises clusters, or any deployment where MongoDB Atlas is not an option, you can run a production-grade MongoDB replica set with optional <strong>Atlas Search</strong> (full-text and vector indexes) entirely inside Kubernetes. This post describes the <a href="https://github.com/analytiq-hub/analytiq-charts"><code class="language-plaintext highlighter-rouge">mongodb-atlas-local</code></a> Helm chart and the operational details we learned running it on EKS and elsewhere.</p>

<p>If you’re new to Kubernetes, the <a href="/tech/kubernetes/devops/kubernetes-for-docker-users-primer/">Kubernetes for Docker Users primer</a> covers Pods, Deployments, Services, PVCs, and Helm basics. For packaging and GitOps, see <a href="/tech/kubernetes/devops/kubernetes-packaging-helm-gitops/">Kubernetes Packaging and Deployment</a>.</p>

<h2 id="why-not-bitnami">Why not Bitnami?</h2>

<p>The obvious choice for an in-cluster MongoDB is the Bitnami chart, which is widely used and simple to install. The problem is <strong>vector search</strong>. Applications that need semantic search or Atlas-style indexes require the <code class="language-plaintext highlighter-rouge">mongot</code> process — a sidecar that runs alongside <code class="language-plaintext highlighter-rouge">mongod</code> and handles full-text and vector indexes. Bitnami deploys a plain community MongoDB without <code class="language-plaintext highlighter-rouge">mongot</code>, so Atlas Search is simply not available.</p>

<p>The only supported path to <code class="language-plaintext highlighter-rouge">mongot</code> in a self-hosted environment is the <a href="https://github.com/mongodb/mongodb-kubernetes-operator">MongoDB Kubernetes Operator</a>, which introduces the <code class="language-plaintext highlighter-rouge">MongoDBCommunity</code> and <code class="language-plaintext highlighter-rouge">MongoDBSearch</code> custom resources. The operator manages the StatefulSet, replica set initialization, user creation, and TLS — and, when <code class="language-plaintext highlighter-rouge">MongoDBSearch</code> is enabled, injects the <code class="language-plaintext highlighter-rouge">mongot</code> sidecar with the right configuration.</p>

<p>Our chart wraps the operator’s CRDs with sensible defaults and a single <code class="language-plaintext highlighter-rouge">helm upgrade --install</code> interface, so operators don’t need to understand the operator’s internals to get a working cluster. You can run MongoDB with or without search; if you don’t need vector or full-text search, you can disable the <code class="language-plaintext highlighter-rouge">mongot</code> sidecar and save resources.</p>

<h2 id="two-phase-install">Two-phase install</h2>

<p><code class="language-plaintext highlighter-rouge">mongot</code> requires a running, authenticated replica set to connect to — it cannot start on a fresh cluster. The install therefore happens in two phases:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Phase 1: bring up the replica set without search</span>
helm upgrade <span class="nt">--install</span> mongodb oci://ghcr.io/analytiq-hub/mongodb-atlas-local <span class="se">\</span>
  <span class="nt">--version</span> 2.0.1 <span class="nt">--namespace</span> mongodb <span class="se">\</span>
  <span class="nt">--set</span> mongodb.adminPassword<span class="o">=</span><span class="s2">"..."</span> <span class="se">\</span>
  <span class="nt">--set</span> mongodb.appUser.password<span class="o">=</span><span class="s2">"..."</span> <span class="se">\</span>
  <span class="nt">--set</span> search.enabled<span class="o">=</span><span class="nb">false</span>

<span class="c"># Wait for replica set Ready</span>
kubectl <span class="nb">wait</span> <span class="nt">--for</span><span class="o">=</span><span class="nv">condition</span><span class="o">=</span>ready pod <span class="nt">-l</span> <span class="nv">app</span><span class="o">=</span>mongodb-mongodb-atlas-local <span class="se">\</span>
  <span class="nt">-n</span> mongodb <span class="nt">--timeout</span><span class="o">=</span>300s

<span class="c"># Phase 2: enable search</span>
helm upgrade mongodb oci://ghcr.io/analytiq-hub/mongodb-atlas-local <span class="se">\</span>
  <span class="nt">--version</span> 2.0.1 <span class="nt">--namespace</span> mongodb <span class="nt">--reuse-values</span> <span class="se">\</span>
  <span class="nt">--set</span> search.enabled<span class="o">=</span><span class="nb">true</span>
</code></pre></div></div>

<p>Attempting a single-phase install with <code class="language-plaintext highlighter-rouge">search.enabled=true</code> results in <code class="language-plaintext highlighter-rouge">mongot</code> crash-looping because the replica set isn’t ready to accept its connection.</p>

<h2 id="node-sizing-for-stateful-workloads">Node sizing for stateful workloads</h2>

<p>Adding MongoDB changes the cluster sizing arithmetic considerably. Each replica pod runs two containers: <code class="language-plaintext highlighter-rouge">mongod</code> (500m CPU, 400Mi) and <code class="language-plaintext highlighter-rouge">mongodb-agent</code> (500m CPU, 400Mi), plus a <code class="language-plaintext highlighter-rouge">mongot</code> sidecar (250m CPU, 250Mi) when search is enabled. A 3-replica set therefore requests ~2.25 vCPU and ~3.15 Gi of memory, on top of whatever other workloads you run.</p>

<p>The scheduler must fit the entire pod on one node. On a cluster with two <code class="language-plaintext highlighter-rouge">t3.medium</code> nodes (2 vCPU / 4 Gi each), if existing workloads already consume ~1.7 vCPU in requests, there may be ~2.2 vCPU free across both nodes — but never more than ~740m on a single node. A MongoDB pod that needs ~750m CPU cannot be scheduled. Adding a third node (or sizing nodes with enough headroom) resolves it.</p>

<p>The practical lesson: <strong>account for stateful pods when sizing the initial node group</strong>, or ensure the autoscaler can provision new nodes quickly enough not to block workloads.</p>

<h2 id="ebs-csi-driver-and-the-gp2-trap-eks">EBS CSI Driver and the gp2 trap (EKS)</h2>

<p>When we added MongoDB to an EKS cluster, PVCs sat in <code class="language-plaintext highlighter-rouge">Pending</code> indefinitely with the error:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>no persistent volumes available for this claim and no storage class is set
</code></pre></div></div>

<p>EKS creates a <code class="language-plaintext highlighter-rouge">gp2</code> StorageClass by default, but it has two problems. First, it is not marked as the default class — PVCs with an empty <code class="language-plaintext highlighter-rouge">storageClassName</code> get no provisioner assigned. Second, and more importantly, <code class="language-plaintext highlighter-rouge">gp2</code> uses the legacy in-tree <code class="language-plaintext highlighter-rouge">kubernetes.io/aws-ebs</code> provisioner, which was removed in Kubernetes 1.27. On EKS 1.35, it is simply gone.</p>

<p>The fix is to create a <code class="language-plaintext highlighter-rouge">gp3</code> StorageClass backed by the EBS CSI driver (<code class="language-plaintext highlighter-rouge">ebs.csi.aws.com</code>) and mark it as the cluster default:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">storage.k8s.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">StorageClass</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">gp3</span>
  <span class="na">annotations</span><span class="pi">:</span>
    <span class="na">storageclass.kubernetes.io/is-default-class</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
<span class="na">provisioner</span><span class="pi">:</span> <span class="s">ebs.csi.aws.com</span>
<span class="na">volumeBindingMode</span><span class="pi">:</span> <span class="s">WaitForFirstConsumer</span>
<span class="na">allowVolumeExpansion</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">parameters</span><span class="pi">:</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">gp3</span>
  <span class="na">encrypted</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">WaitForFirstConsumer</code> is important — it delays EBS volume creation until the pod is actually scheduled to a node, which ensures the volume is created in the correct availability zone. <code class="language-plaintext highlighter-rouge">allowVolumeExpansion: true</code> enables online resizing without pod restarts.</p>

<p>Provision this StorageClass (and the EBS CSI driver) via Terraform or your preferred IaC so new clusters get it automatically.</p>

<h2 id="summary">Summary</h2>

<table>
  <thead>
    <tr>
      <th>Topic</th>
      <th>Takeaway</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Chart</strong></td>
      <td><code class="language-plaintext highlighter-rouge">mongodb-atlas-local</code> on <a href="https://github.com/analytiq-hub/analytiq-charts">analytiq-charts</a> — replica set + optional <code class="language-plaintext highlighter-rouge">mongot</code> for Atlas Search</td>
    </tr>
    <tr>
      <td><strong>Install</strong></td>
      <td>Two-phase: bring up replica set with <code class="language-plaintext highlighter-rouge">search.enabled=false</code>, then enable search</td>
    </tr>
    <tr>
      <td><strong>Sizing</strong></td>
      <td>Reserve enough CPU/memory per node for the full MongoDB pod; scheduler places whole pod on one node</td>
    </tr>
    <tr>
      <td><strong>EKS storage</strong></td>
      <td>Use a <code class="language-plaintext highlighter-rouge">gp3</code> StorageClass with <code class="language-plaintext highlighter-rouge">ebs.csi.aws.com</code>; don’t rely on the default <code class="language-plaintext highlighter-rouge">gp2</code></td>
    </tr>
  </tbody>
</table>

<p>We use this chart for <a href="https://docrouter.ai">Doc Router</a> and other applications that need MongoDB with vector search. For the full Doc Router deployment story (Helm chart, workers, CI/CD, multi-cloud), see <a href="/tech/kubernetes/devops/docrouter/deploying-doc-router-on-kubernetes/">Deploying Doc Router on Kubernetes</a>.</p>

<hr />

<p><em>Andrei Radulescu-Banu is the founder of <a href="https://docrouter.ai">DocRouter.AI</a> (document processing with LLMs) and <a href="https://sigagent.ai">SigAgent.AI</a> (Claude Agent monitoring). His company <a href="https://analytiqhub.com">AnalytiqHub.com</a> provides consulting services for cloud and AI engineering.</em></p>]]></content><author><name>Andrei Radulescu-Banu</name></author><category term="tech" /><category term="kubernetes" /><category term="devops" /><category term="mongodb" /><summary type="html"><![CDATA[Run a production-grade MongoDB replica set with optional Atlas Search (vector and full-text) inside Kubernetes — for air-gapped, on-prem, or any environment where Atlas isn't an option.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://sigagent.ai/assets/images/self-hosted-mongodb-kubernetes-atlas-search-splash.png" /><media:content medium="image" url="https://sigagent.ai/assets/images/self-hosted-mongodb-kubernetes-atlas-search-splash.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Deploying Doc Router on Kubernetes: From Docker Compose to EKS and Digital Ocean</title><link href="https://sigagent.ai/tech/kubernetes/devops/docrouter/deploying-doc-router-on-kubernetes/" rel="alternate" type="text/html" title="Deploying Doc Router on Kubernetes: From Docker Compose to EKS and Digital Ocean" /><published>2026-03-07T00:00:00+00:00</published><updated>2026-03-07T00:00:00+00:00</updated><id>https://sigagent.ai/tech/kubernetes/devops/docrouter/deploying-doc-router-on-kubernetes</id><content type="html" xml:base="https://sigagent.ai/tech/kubernetes/devops/docrouter/deploying-doc-router-on-kubernetes/"><![CDATA[<p>We recently added production-grade Kubernetes support to Doc Router. This post walks through the key decisions and challenges we encountered along the way.</p>

<p>If you’re new to Kubernetes, start with <a href="/tech/kubernetes/devops/kubernetes-for-docker-users-primer/">Kubernetes for Docker Users: A Practical Primer</a>, which covers the core concepts — Pods, Deployments, Services, Namespaces, Secrets, PVCs, Helm, and Kind — before diving into this post. For packaging and GitOps (Kustomize, Helm, Flux), see <a href="/tech/kubernetes/devops/kubernetes-packaging-helm-gitops/">Kubernetes Packaging and Deployment</a>.</p>

<h2 id="why-kubernetes">Why Kubernetes?</h2>

<p>Doc Router was originally deployed using Docker Compose, which worked well for single-node setups. As we started onboarding enterprise customers with availability and scalability requirements, we needed:</p>

<ul>
  <li><strong>Horizontal scaling</strong> — multiple replicas behind a load balancer</li>
  <li><strong>Automated failover</strong> — pods restarted on failure without manual intervention</li>
  <li><strong>Rolling deployments</strong> — zero-downtime upgrades</li>
  <li><strong>Resource isolation</strong> — CPU and memory limits per component</li>
</ul>

<h2 id="architecture">Architecture</h2>

<p>The production deployment consists of two main workloads:</p>

<ul>
  <li><strong>Frontend</strong> — Next.js server (SSR + API routes via NextAuth)</li>
  <li><strong>Backend</strong> — FastAPI application with embedded background workers</li>
</ul>

<p>Both run as Kubernetes Deployments behind a shared nginx ingress with TLS terminated by cert-manager (Let’s Encrypt).</p>

<p>MongoDB can run outside the cluster (MongoDB Atlas) or in-cluster via our <a href="https://github.com/analytiq-hub/analytiq-charts"><code class="language-plaintext highlighter-rouge">mongodb-atlas-local</code></a> Helm chart — see <a href="/tech/kubernetes/devops/mongodb/self-hosted-mongodb-kubernetes-atlas-search/">Self-Hosted MongoDB on Kubernetes with Atlas Search</a> for the install guide. AWS S3 remains an external dependency.</p>

<h2 id="helm-chart">Helm Chart</h2>

<p>We packaged the deployment as a Helm chart (<code class="language-plaintext highlighter-rouge">deploy/charts/doc-router</code>) published to GitHub Container Registry (ghcr.io) as an OCI artifact. The chart is versioned independently of the Docker images, so we can update deployment configuration without rebuilding the application.</p>

<p>Key design decisions:</p>

<ul>
  <li><strong>Single <code class="language-plaintext highlighter-rouge">values.yaml</code></strong> with sensible defaults — operators override only what differs per cluster</li>
  <li><strong>ConfigMap for non-secret config</strong> — <code class="language-plaintext highlighter-rouge">NEXTAUTH_URL</code>, <code class="language-plaintext highlighter-rouge">FASTAPI_ROOT_PATH</code>, worker count, S3 bucket</li>
  <li><strong>Kubernetes Secret for credentials</strong> — MongoDB URI, API keys, NextAuth secret — created by the deploy script, never stored in the chart</li>
  <li><strong>Ingress host derived from <code class="language-plaintext highlighter-rouge">APP_HOST</code></strong> — a single variable drives the entire URL configuration</li>
</ul>

<h2 id="choosing-a-container-registry">Choosing a Container Registry</h2>

<p>We evaluated two natural options: <strong>Amazon ECR</strong> (since we’re already on AWS/EKS) and <strong>GitHub Container Registry (ghcr.io)</strong> (since our source is on GitHub).</p>

<p><strong>ECR</strong> has one significant operational advantage for EKS: nodes authenticate via IAM role, so there is no image pull secret to manage. Costs are low — $0.10/GB stored, with no data transfer charge for pulls within the same AWS region. However, ECR is tightly coupled to AWS. A second deployment on Digital Ocean or a customer’s on-premises cluster would need separate registry credentials and mirroring, making it a poor fit for a multi-cloud or self-hosted product.</p>

<p><strong>ghcr.io</strong> is cloud-neutral — any cluster anywhere can pull images with a single token. It integrates naturally with GitHub Actions (the <code class="language-plaintext highlighter-rouge">GITHUB_TOKEN</code> secret already has <code class="language-plaintext highlighter-rouge">packages: write</code> permission), so publishing images is zero-configuration. The chart package also appears directly on the repository’s GitHub page alongside the source code and releases, which is the right home for an open-source project.</p>

<p>The catch: ghcr.io packages are <strong>private by default</strong> for organizations, and GitHub’s free tier includes only 500 MB storage and 1 GB transfer per month. For clusters that pull large images repeatedly, those limits are reached quickly. Making packages public eliminates the cost entirely, but requires an organization admin to enable public package creation in the org settings — it is disabled by default.</p>

<p>We chose ghcr.io and made our packages public. The images contain no secrets — only application code — so public visibility is appropriate and keeps infrastructure simple. Clusters pull anonymously with no credentials required.</p>

<p>For customers who need private images (for example, an enterprise build with proprietary integrations), the <code class="language-plaintext highlighter-rouge">REGISTRY_PROVIDER</code> variable in the overlay <code class="language-plaintext highlighter-rouge">.env</code> file can be switched to <code class="language-plaintext highlighter-rouge">aws</code> or <code class="language-plaintext highlighter-rouge">do</code> to use ECR or Digital Ocean Container Registry instead, with registry login handled automatically by the deploy scripts.</p>

<h2 id="merging-workers-into-fastapi">Merging Workers into FastAPI</h2>

<p>The original architecture ran the background workers (OCR, LLM, KB indexing, webhooks) as a separate process alongside uvicorn. In Kubernetes, this meant each backend pod ran two Python processes, consuming ~375 MB of memory.</p>

<p>We merged the workers into the FastAPI lifespan using <code class="language-plaintext highlighter-rouge">asyncio.create_task</code>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="n">asynccontextmanager</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">lifespan</span><span class="p">(</span><span class="n">app</span><span class="p">):</span>
    <span class="c1"># startup
</span>    <span class="n">worker_tasks</span> <span class="o">=</span> <span class="n">start_workers</span><span class="p">(</span><span class="n">n_workers</span><span class="p">)</span>
    <span class="k">yield</span>
    <span class="c1"># shutdown
</span>    <span class="k">for</span> <span class="n">task</span> <span class="ow">in</span> <span class="n">worker_tasks</span><span class="p">:</span>
        <span class="n">task</span><span class="p">.</span><span class="n">cancel</span><span class="p">()</span>
    <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">gather</span><span class="p">(</span><span class="o">*</span><span class="n">worker_tasks</span><span class="p">,</span> <span class="n">return_exceptions</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
</code></pre></div></div>

<p>This halved per-pod memory usage (~190 MB) and eliminated the process management overhead. The workers share the same event loop as the API, which is safe because all worker I/O is already async.</p>

<h2 id="worker-polling-optimization">Worker Polling Optimization</h2>

<p>With multiple replicas, each pod runs a full set of worker coroutines polling MongoDB queues. At idle with 4 workers per pod, that was ~80 MongoDB queries per second cluster-wide.</p>

<p>We implemented exponential backoff with shared state across parallel workers:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">_queue_idle_sleep</span><span class="p">:</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">float</span><span class="p">]</span> <span class="o">=</span> <span class="p">{}</span>  <span class="c1"># shared across all workers on a queue
</span>
<span class="c1"># on idle: back off
</span><span class="n">sleep</span> <span class="o">=</span> <span class="n">_queue_idle_sleep</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"ocr"</span><span class="p">,</span> <span class="n">POLL_MIN_SLEEP</span><span class="p">)</span>
<span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="n">sleep</span><span class="p">)</span>
<span class="n">_queue_idle_sleep</span><span class="p">[</span><span class="s">"ocr"</span><span class="p">]</span> <span class="o">=</span> <span class="nb">min</span><span class="p">(</span><span class="n">sleep</span> <span class="o">*</span> <span class="mi">2</span><span class="p">,</span> <span class="n">POLL_MAX_SLEEP</span><span class="p">)</span>

<span class="c1"># on message found: reset for all workers on this queue
</span><span class="n">_queue_idle_sleep</span><span class="p">[</span><span class="s">"ocr"</span><span class="p">]</span> <span class="o">=</span> <span class="n">POLL_MIN_SLEEP</span>
</code></pre></div></div>

<p>This reduces idle polling to near-zero while keeping response latency low when work arrives.</p>

<h2 id="graceful-shutdown">Graceful Shutdown</h2>

<p>When Kubernetes scales down a pod (HPA scale-in or rolling update), it sends SIGTERM. We needed in-flight jobs to be marked as failed rather than silently abandoned.</p>

<p>Since workers are asyncio tasks, cancellation arrives as <code class="language-plaintext highlighter-rouge">asyncio.CancelledError</code> — a <code class="language-plaintext highlighter-rouge">BaseException</code>, not caught by <code class="language-plaintext highlighter-rouge">except Exception</code>. We added explicit handling in each worker:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span><span class="p">:</span>
    <span class="k">await</span> <span class="n">ad</span><span class="p">.</span><span class="n">msg_handlers</span><span class="p">.</span><span class="n">process_ocr_msg</span><span class="p">(</span><span class="n">analytiq_client</span><span class="p">,</span> <span class="n">msg</span><span class="p">)</span>
<span class="k">except</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">CancelledError</span><span class="p">:</span>
    <span class="n">logger</span><span class="p">.</span><span class="n">warning</span><span class="p">(</span><span class="sa">f</span><span class="s">"Worker cancelled mid-flight on msg </span><span class="si">{</span><span class="n">msg</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'_id'</span><span class="p">)</span><span class="si">}</span><span class="s">, marking failed"</span><span class="p">)</span>
    <span class="k">await</span> <span class="n">ad</span><span class="p">.</span><span class="n">queue</span><span class="p">.</span><span class="n">delete_msg</span><span class="p">(</span><span class="n">analytiq_client</span><span class="p">,</span> <span class="s">"ocr"</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">msg</span><span class="p">[</span><span class="s">"_id"</span><span class="p">]),</span> <span class="n">status</span><span class="o">=</span><span class="s">"failed"</span><span class="p">)</span>
    <span class="k">raise</span>  <span class="c1"># allow the task to actually cancel
</span></code></pre></div></div>

<p>The failed job can then be retried on another pod.</p>

<h2 id="database-migrations-as-a-helm-pre-upgrade-hook">Database Migrations as a Helm Pre-Upgrade Hook</h2>

<p>Running database migrations safely in a multi-replica environment requires that migrations complete before any new application code starts serving traffic. In Docker Compose this is handled by startup ordering, but in Kubernetes rolling updates, new pods can start before old ones are gone — with no guarantee about migration timing.</p>

<p>We solved this with a Helm hook Job that runs <code class="language-plaintext highlighter-rouge">migrate.py</code> using the same backend image, annotated to execute before the upgrade rolls out:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">annotations</span><span class="pi">:</span>
  <span class="s2">"</span><span class="s">helm.sh/hook"</span><span class="err">:</span> <span class="s">pre-upgrade,pre-rollback</span>
  <span class="s">"helm.sh/hook-weight"</span><span class="err">:</span> <span class="s2">"</span><span class="s">-5"</span>
  <span class="s2">"</span><span class="s">helm.sh/hook-delete-policy"</span><span class="err">:</span> <span class="s">hook-succeeded,before-hook-creation</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">pre-upgrade</code> hook ensures migrations run and complete successfully before Helm touches any Deployment. If the migration Job fails, Helm aborts the upgrade entirely — the old version keeps running. <code class="language-plaintext highlighter-rouge">hook-delete-policy: hook-succeeded</code> cleans up the completed Job automatically, keeping the namespace tidy. The <code class="language-plaintext highlighter-rouge">before-hook-creation</code> policy ensures the old Job is removed if a previous run left one behind.</p>

<p>One subtlety: at pre-upgrade time, the ConfigMap has not yet been updated by Helm (hooks run before regular resources). The migration Job therefore mounts only the Secret — which contains <code class="language-plaintext highlighter-rouge">MONGODB_URI</code> — and not the ConfigMap:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">envFrom</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">secretRef</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">doc-router-secrets</span>
<span class="c1"># ConfigMap intentionally omitted — not yet updated at hook time</span>
</code></pre></div></div>

<p>This means <code class="language-plaintext highlighter-rouge">migrate.py</code> must be written to need only the database connection string, with no dependency on application config values.</p>

<p>The result is a safe, atomic upgrade sequence: <strong>migrate → roll out new pods → terminate old pods</strong> — with automatic rollback if the migration fails.</p>

<h2 id="hpa-tuning">HPA Tuning</h2>

<p>We configured Horizontal Pod Autoscaler on the backend with both CPU and memory targets:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">metrics</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">Resource</span>
  <span class="na">resource</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">cpu</span>
    <span class="na">target</span><span class="pi">:</span>
      <span class="na">type</span><span class="pi">:</span> <span class="s">Utilization</span>
      <span class="na">averageUtilization</span><span class="pi">:</span> <span class="m">80</span>
<span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">Resource</span>
  <span class="na">resource</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">memory</span>
    <span class="na">target</span><span class="pi">:</span>
      <span class="na">type</span><span class="pi">:</span> <span class="s">Utilization</span>
      <span class="na">averageUtilization</span><span class="pi">:</span> <span class="m">80</span>
</code></pre></div></div>

<p>A subtle issue: HPA scale-down uses <code class="language-plaintext highlighter-rouge">ceil(currentReplicas × currentUtil / targetUtil)</code>. With 5 pods at 72% memory utilization against an 80% target, <code class="language-plaintext highlighter-rouge">ceil(5 × 72/80) = ceil(4.5) = 5</code> — the ceiling arithmetic created a deadlock where the cluster could never scale below 5 pods.</p>

<p>The fix was increasing the memory request from 512 Mi to 768 Mi. After the worker merge reduced actual usage to ~190 MB, utilization dropped to ~25% — well below the threshold — and the cluster scaled back down to the minimum of 2 replicas.</p>

<h2 id="environment-configuration">Environment Configuration</h2>

<p>Next.js <code class="language-plaintext highlighter-rouge">NEXT_PUBLIC_*</code> variables are baked into the browser bundle at build time, not injected at runtime. This caused a subtle bug: our local <code class="language-plaintext highlighter-rouge">.env.local</code> file set <code class="language-plaintext highlighter-rouge">NEXT_PUBLIC_FASTAPI_FRONTEND_URL=http://127.0.0.1:8000</code>. Because <code class="language-plaintext highlighter-rouge">.env.local</code> wasn’t listed in <code class="language-plaintext highlighter-rouge">.dockerignore</code>, it was copied into the Docker build context and read by Next.js during <code class="language-plaintext highlighter-rouge">npm run build</code> — silently overriding the intended production value and baking the localhost URL into every image.</p>

<p>We fixed this in two steps:</p>

<ol>
  <li>
    <p><strong>Exclude all <code class="language-plaintext highlighter-rouge">.env.*</code> files from the Docker build context</strong> by adding <code class="language-plaintext highlighter-rouge">**/.env.*</code> to <code class="language-plaintext highlighter-rouge">.dockerignore</code>, so local development env files can never leak into images.</p>
  </li>
  <li>
    <p><strong>Remove <code class="language-plaintext highlighter-rouge">NEXT_PUBLIC_FASTAPI_FRONTEND_URL</code> entirely.</strong> Rather than baking an absolute URL into the bundle, the frontend now always calls <code class="language-plaintext highlighter-rouge">/fastapi</code> — a relative path that works from any hostname. Next.js rewrites proxy <code class="language-plaintext highlighter-rouge">/fastapi/:path*</code> to the backend service URL at the server layer:</p>
  </li>
</ol>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// next.config.mjs</span>
<span class="k">async</span> <span class="nx">rewrites</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">[{</span>
    <span class="na">source</span><span class="p">:</span> <span class="dl">'</span><span class="s1">/fastapi/:path*</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">destination</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">FASTAPI_BACKEND_URL</span><span class="p">}</span><span class="s2">/fastapi/:path*`</span><span class="p">,</span>
  <span class="p">}];</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">FASTAPI_BACKEND_URL</code> is a server-side runtime variable (not <code class="language-plaintext highlighter-rouge">NEXT_PUBLIC_</code>) pointing to the in-cluster backend service (<code class="language-plaintext highlighter-rouge">http://backend.&lt;namespace&gt;.svc.cluster.local:8000</code>). It is never exposed to the browser. The result is a truly environment-agnostic frontend image that requires no rebuild when moving between clusters.</p>

<h2 id="cicd-pipeline">CI/CD Pipeline</h2>

<h3 id="structure">Structure</h3>

<p>We use three GitHub Actions workflows:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">backend-tests.yml</code></strong> — runs Python tests against a local MongoDB Atlas instance (with vector search via <code class="language-plaintext highlighter-rouge">mongodb-atlas-local</code>) plus TypeScript tests. Triggered by <code class="language-plaintext highlighter-rouge">workflow_call</code> or <code class="language-plaintext highlighter-rouge">workflow_dispatch</code>.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">frontend-build.yml</code></strong> — runs <code class="language-plaintext highlighter-rouge">npm run build</code> for the Next.js frontend. Also triggered by <code class="language-plaintext highlighter-rouge">workflow_call</code> or <code class="language-plaintext highlighter-rouge">workflow_dispatch</code>.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">ci.yml</code></strong> — runs both test workflows on every pull request to <code class="language-plaintext highlighter-rouge">main</code>.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">release.yml</code></strong> — triggered on semver tags (<code class="language-plaintext highlighter-rouge">v[0-9]*.[0-9]*.[0-9]*</code>). Runs both test workflows first, then builds and pushes Docker images if they pass.</li>
</ul>

<h3 id="why-semver-tags-not-branch-pushes">Why semver tags, not branch pushes</h3>

<p>An early version of the pipeline ran tests on every push to <code class="language-plaintext highlighter-rouge">main</code> and triggered builds from there. This caused two problems:</p>

<ol>
  <li><strong>Tests ran twice per release</strong> — once on the branch push, once triggered by the tag.</li>
  <li><strong>The tag trigger didn’t wait for tests</strong> — if a tag was pushed immediately after a commit, the build could race ahead of a still-running test run.</li>
</ol>

<p>The current design avoids both: <code class="language-plaintext highlighter-rouge">release.yml</code> is only triggered by a semver tag, and the <code class="language-plaintext highlighter-rouge">build-push</code> job declares <code class="language-plaintext highlighter-rouge">needs: [test-backend, test-frontend]</code>, so Docker images are never built unless all tests pass on that exact commit. Tests run exactly once per release.</p>

<p>The <code class="language-plaintext highlighter-rouge">ci.yml</code> workflow handles the PR gate separately — developers get test feedback on their branch without triggering a build.</p>

<h3 id="reusable-test-workflows">Reusable test workflows</h3>

<p>Making the test workflows <code class="language-plaintext highlighter-rouge">workflow_call</code>-able (rather than duplicating the job definitions in both <code class="language-plaintext highlighter-rouge">ci.yml</code> and <code class="language-plaintext highlighter-rouge">release.yml</code>) keeps the test logic in one place. Both workflows call the same definitions; any change to the test steps is automatically reflected in both gates.</p>

<p><code class="language-plaintext highlighter-rouge">workflow_dispatch</code> is kept on each test workflow so that individual test suites can be re-run manually from the GitHub Actions UI without needing to push a commit or tag.</p>

<h3 id="image-tagging">Image tagging</h3>

<p>The build step computes image tags from the git tag:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">TAG="$"</span>          <span class="c1"># e.g. v27.0.1-rc2 or v27.0.1</span>
<span class="s">FRONTEND_TAGS="${FRONTEND}:${TAG}"</span>
<span class="c1"># :latest only for stable releases (no pre-release suffix)</span>
<span class="s">if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then</span>
  <span class="s">FRONTEND_TAGS="${FRONTEND_TAGS},${FRONTEND}:latest"</span>
<span class="s">fi</span>
</code></pre></div></div>

<p>Release candidates (<code class="language-plaintext highlighter-rouge">v27.0.1-rc2</code>) get a versioned tag only. Stable releases (<code class="language-plaintext highlighter-rouge">v27.0.1</code>) also update <code class="language-plaintext highlighter-rouge">:latest</code>. This means a cluster running <code class="language-plaintext highlighter-rouge">:latest</code> auto-updates on the next <code class="language-plaintext highlighter-rouge">helm upgrade</code>, while a cluster pinned to a specific tag is unaffected.</p>

<h3 id="helm-chart-publishing-is-manual">Helm chart publishing is manual</h3>

<p>The Helm chart is published separately with <code class="language-plaintext highlighter-rouge">./deploy/scripts/publish-chart.sh &lt;overlay&gt;</code>. We kept this manual for two reasons: the chart version is independent of the app version (you might push 10 image releases without any chart changes), and publishing the chart is a deliberate operator action — it should not happen automatically on every tag.</p>

<h2 id="egress-ips-and-external-service-whitelisting">Egress IPs and External Service Whitelisting</h2>

<p>A practical difference between EKS and DOKS emerged when connecting to MongoDB Atlas, which requires IP whitelisting for all incoming connections.</p>

<p><strong>On EKS</strong>, the cluster’s private node group sits behind a single NAT gateway. All outbound traffic from every pod — regardless of which node it runs on — exits through one stable public IP. Adding that single IP to MongoDB Atlas’s allowlist is all that’s needed, and the IP never changes when nodes are replaced or the cluster scales.</p>

<p><strong>On DOKS</strong>, there is no NAT gateway by default. Each node is assigned its own public IP, and pods reach the internet directly through the node they’re scheduled on. This means:</p>

<ul>
  <li>There is no single egress IP — the source address MongoDB sees depends on which node the backend pod happens to be running on.</li>
  <li>With two nodes, you need two IPs in the allowlist. With autoscaling, new nodes get new IPs, and the allowlist breaks until you add them.</li>
</ul>

<p>For a fixed-size dev cluster, the workaround is to whitelist all current node IPs. For a production DOKS cluster with autoscaling, the correct solution is to provision a <strong>Digital Ocean Load Balancer as a NAT gateway</strong>, routing all cluster egress through a single stable IP. This adds ~$12/month but is the only reliable option when the external service requires a static source address.</p>

<p>For our dev cluster (<code class="language-plaintext highlighter-rouge">doc-router-dev</code>), we whitelist the two node IPs directly. For production DOKS deployments, a managed NAT gateway is required.</p>

<h2 id="overlay-based-deploy-scripts">Overlay-based Deploy Scripts</h2>

<p>Rather than a one-size-fits-all deploy script, we use an overlay pattern:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.env              # shared defaults (local dev values)
.env.eks-test     # overrides for the test EKS cluster
.env.eks-prod     # overrides for production
</code></pre></div></div>

<p>The deploy scripts (<code class="language-plaintext highlighter-rouge">k8s-deploy.sh</code>, <code class="language-plaintext highlighter-rouge">build-push.sh</code>) accept an overlay name and source both files, with the overlay taking precedence. A single variable — <code class="language-plaintext highlighter-rouge">APP_HOST</code> — drives all URL configuration, making it straightforward to add a new environment. <code class="language-plaintext highlighter-rouge">k8s-deploy.sh</code> is idempotent — it uses <code class="language-plaintext highlighter-rouge">helm upgrade --install</code> and handles both fresh installs and rolling updates without any distinction.</p>

<h2 id="whats-next">What’s Next</h2>

<ul>
  <li><strong>On-premises distribution</strong> — Helm chart and images are public on ghcr.io; self-hosted MongoDB is available via the <a href="https://github.com/analytiq-hub/analytiq-charts"><code class="language-plaintext highlighter-rouge">mongodb-atlas-local</code></a> chart (see <a href="/tech/kubernetes/devops/mongodb/self-hosted-mongodb-kubernetes-atlas-search/">Self-Hosted MongoDB on Kubernetes with Atlas Search</a>); documentation for a one-command on-prem install is the next step</li>
  <li><strong>Offline license keys</strong> — JWT-based licenses signed with a private key, verified against a public key baked into the image, for air-gapped installations</li>
  <li><strong>Multi-cloud support</strong> — Digital Ocean Kubernetes is now supported alongside EKS; Azure Kubernetes Service support is planned</li>
</ul>

<hr />

<p><em>Andrei Radulescu-Banu is the founder of <a href="https://docrouter.ai">DocRouter.AI</a> (document processing with LLMs) and <a href="https://sigagent.ai">SigAgent.AI</a> (Claude Agent monitoring). His company <a href="https://analytiqhub.com">AnalytiqHub.com</a> provides consulting services for cloud and AI engineering.</em></p>]]></content><author><name>Andrei Radulescu-Banu</name></author><category term="tech" /><category term="kubernetes" /><category term="devops" /><category term="docrouter" /><summary type="html"><![CDATA[Production-grade Kubernetes support for Doc Router: key decisions, Helm chart, worker merging, graceful shutdown, and multi-cloud deployment.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://sigagent.ai/assets/images/deploying-doc-router-kubernetes-splash.png" /><media:content medium="image" url="https://sigagent.ai/assets/images/deploying-doc-router-kubernetes-splash.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Kubernetes Packaging and Deployment: Kustomize, Helm, and GitOps</title><link href="https://sigagent.ai/tech/kubernetes/devops/kubernetes-packaging-helm-gitops/" rel="alternate" type="text/html" title="Kubernetes Packaging and Deployment: Kustomize, Helm, and GitOps" /><published>2026-03-06T00:00:00+00:00</published><updated>2026-03-06T00:00:00+00:00</updated><id>https://sigagent.ai/tech/kubernetes/devops/kubernetes-packaging-helm-gitops</id><content type="html" xml:base="https://sigagent.ai/tech/kubernetes/devops/kubernetes-packaging-helm-gitops/"><![CDATA[<p>This is the second part of the Kubernetes primer series. The <a href="/tech/kubernetes/devops/kubernetes-for-docker-users-primer/">first part</a> covered the core building blocks — Pods, Deployments, Services, Secrets, PVCs, and Helm basics. This part goes deeper into the two dominant approaches to packaging Kubernetes manifests, and then introduces GitOps as an alternative to running deploy scripts manually.</p>

<hr />

<h2 id="the-manifest-problem">The manifest problem</h2>

<p>A real Kubernetes application needs dozens of YAML files: Deployments, Services, ConfigMaps, Secrets, Ingress rules, HorizontalPodAutoscalers, PodDisruptionBudgets. Writing them by hand is feasible once, but the moment you need the same app running in three environments — local, staging, production — you face a choice:</p>

<ul>
  <li><strong>Copy the files for each environment</strong> and keep them in sync manually (fragile)</li>
  <li><strong>Use a tool that handles the variation</strong> for you</li>
</ul>

<p>Two tools dominate: <strong>Kustomize</strong> and <strong>Helm</strong>. They solve the same problem differently, and many projects use both — Helm for third-party software, Kustomize for their own app.</p>

<hr />

<h2 id="kustomize--layered-yaml-patches">Kustomize — layered YAML patches</h2>

<p>Kustomize ships with <code class="language-plaintext highlighter-rouge">kubectl</code> (no install needed) and works with plain YAML. The idea is a <strong>base + overlays</strong> structure:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>manifests/
  base/
    deployment.yaml      # canonical deployment
    service.yaml
    kustomization.yaml   # lists the resources
  overlays/
    dev/
      kustomization.yaml # patches for dev
      patch-replicas.yaml
    prod/
      kustomization.yaml # patches for prod
      patch-replicas.yaml
      patch-resources.yaml
</code></pre></div></div>

<p>The base defines the resource once. Each overlay patches only what differs. A typical patch looks like:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># overlays/prod/patch-replicas.yaml</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">backend</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">4</span>       <span class="c1"># override base value of 2</span>
</code></pre></div></div>

<p>To deploy the prod overlay:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-k</span> overlays/prod/
</code></pre></div></div>

<p>Kustomize merges the base YAML with all patches before sending anything to the API server. You always see plain, readable YAML — there is no templating language to learn, and the output is predictable.</p>

<h3 id="variable-substitution">Variable substitution</h3>

<p>For values that vary by environment (hostnames, image tags, resource sizes), Kustomize offers <code class="language-plaintext highlighter-rouge">substituteFrom</code>: it reads variables from a ConfigMap or Secret and injects them into the manifests at apply time:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># kustomization.yaml</span>
<span class="na">configurations</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">var-references.yaml</span>
<span class="na">vars</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">APP_DOMAIN</span>
    <span class="na">objref</span><span class="pi">:</span>
      <span class="na">kind</span><span class="pi">:</span> <span class="s">ConfigMap</span>
      <span class="na">name</span><span class="pi">:</span> <span class="s">project-values</span>
      <span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
    <span class="na">fieldref</span><span class="pi">:</span>
      <span class="na">fieldpath</span><span class="pi">:</span> <span class="s">data.domain</span>
</code></pre></div></div>

<p>This is less flexible than Helm’s full templating but keeps the YAML closer to what Kubernetes actually receives.</p>

<h3 id="what-kustomize-does-not-do">What Kustomize does not do</h3>

<p>Kustomize has no concept of a release, no revision history, and no built-in rollback. If you apply a broken overlay, you must fix it and reapply, or manually apply a previous version. For the same reason, there is no <code class="language-plaintext highlighter-rouge">--atomic</code> safety net — if a deployment fails mid-rollout, you notice from <code class="language-plaintext highlighter-rouge">kubectl</code> output, not from the packaging tool.</p>

<hr />

<h2 id="helm--templated-packages">Helm — templated packages</h2>

<p>Helm wraps Kubernetes YAML in a full templating engine (Go templates) and adds lifecycle management on top. A chart is a directory:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>doc-router/
  Chart.yaml          # name, version, appVersion
  values.yaml         # default values
  templates/
    deployment.yaml   # Go template
    service.yaml
    ingress.yaml
    _helpers.tpl      # reusable template fragments
</code></pre></div></div>

<p>A template looks like:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># templates/deployment.yaml</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> 
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">backend</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s2">"</span><span class="s">:"</span>
          <span class="na">resources</span><span class="pi">:</span>
            <span class="na">requests</span><span class="pi">:</span>
              <span class="na">cpu</span><span class="pi">:</span> 
</code></pre></div></div>

<p>To install with custom values:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm upgrade <span class="nt">--install</span> doc-router ./doc-router <span class="se">\</span>
  <span class="nt">--set</span> <span class="nv">replicaCount</span><span class="o">=</span>4 <span class="se">\</span>
  <span class="nt">--set</span> image.tag<span class="o">=</span>v1.2.3
</code></pre></div></div>

<p>Or via an override file:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm upgrade <span class="nt">--install</span> doc-router ./doc-router <span class="nt">-f</span> values-prod.yaml
</code></pre></div></div>

<h3 id="release-history-and-rollback">Release history and rollback</h3>

<p>Helm records every install and upgrade as a numbered revision in the cluster. You can inspect history and roll back:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm <span class="nb">history </span>doc-router <span class="nt">-n</span> doc-router
helm rollback doc-router 2 <span class="nt">-n</span> doc-router   <span class="c"># back to revision 2</span>
</code></pre></div></div>

<p>With <code class="language-plaintext highlighter-rouge">--atomic</code>, a failed upgrade automatically triggers a rollback — the old version keeps running uninterrupted.</p>

<h3 id="publishing-charts-as-oci-artifacts">Publishing charts as OCI artifacts</h3>

<p>A packaged chart can be pushed to any OCI-compatible registry (ghcr.io, ECR, Docker Hub) and pulled from anywhere:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm push doc-router-0.3.7.tgz oci://ghcr.io/analytiq-hub
helm upgrade <span class="nt">--install</span> doc-router oci://ghcr.io/analytiq-hub/doc-router <span class="nt">--version</span> 0.3.7
</code></pre></div></div>

<p>This means a customer cluster can install your app with a single command, pulling both the chart and images from the same registry, with no Git access required.</p>

<hr />

<h2 id="kustomize-vs-helm--when-to-use-each">Kustomize vs Helm — when to use each</h2>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Kustomize</th>
      <th>Helm</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Learning curve</td>
      <td>Low — just YAML</td>
      <td>Higher — Go templates + chart structure</td>
    </tr>
    <tr>
      <td>Flexibility</td>
      <td>Patches and substitutions</td>
      <td>Full templating, conditionals, loops</td>
    </tr>
    <tr>
      <td>Release history</td>
      <td>None</td>
      <td>Built-in, per-revision</td>
    </tr>
    <tr>
      <td>Rollback</td>
      <td>Manual</td>
      <td><code class="language-plaintext highlighter-rouge">helm rollback</code></td>
    </tr>
    <tr>
      <td>Failure safety</td>
      <td>None</td>
      <td><code class="language-plaintext highlighter-rouge">--atomic</code> auto-rollback</td>
    </tr>
    <tr>
      <td>Publishing</td>
      <td>OCI artifact via Flux</td>
      <td><code class="language-plaintext highlighter-rouge">helm push</code> to any OCI registry</td>
    </tr>
    <tr>
      <td>Best for</td>
      <td>Your own first-party manifests</td>
      <td>Distributable packages, third-party software</td>
    </tr>
  </tbody>
</table>

<p>In practice many projects use both: Helm for installing third-party dependencies (ingress-nginx, cert-manager, MongoDB operator), and Kustomize for their own application manifests. The two are compatible — a Kustomize overlay can reference a Helm chart as a generator.</p>

<hr />

<h2 id="gitops--the-cluster-manages-itself">GitOps — the cluster manages itself</h2>

<p>Both Kustomize and Helm, as described so far, are <strong>imperative</strong>: a human (or a CI job) runs a command that pushes changes into the cluster. GitOps flips this model.</p>

<p>In GitOps, the desired cluster state is declared in a Git repository (or an OCI artifact registry). A controller running <em>inside</em> the cluster continuously watches that source and reconciles actual state to match it. No one runs <code class="language-plaintext highlighter-rouge">helm upgrade</code> — the cluster pulls its own updates.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Developer pushes to Git / CI pushes OCI artifact
         ↓
  Source of truth updated
         ↓
  In-cluster controller detects drift
         ↓
  Controller applies the diff
         ↓
  Cluster matches desired state
</code></pre></div></div>

<p>The key property: <strong>the cluster self-heals</strong>. If someone manually deletes a Deployment or edits a ConfigMap, the controller notices the drift and reverts it within seconds. The Git repo (or OCI artifact) is always the authoritative source.</p>

<hr />

<h2 id="flux--a-gitops-controller">Flux — a GitOps controller</h2>

<p><strong>Flux</strong> is one of the two dominant GitOps controllers (the other is Argo CD). It runs as a set of controllers in the cluster and watches sources:</p>

<h3 id="sources">Sources</h3>

<p>Flux can watch:</p>
<ul>
  <li><strong>Git repositories</strong> — on every push, Flux reconciles the cluster</li>
  <li><strong>OCI artifact registries</strong> — on every <code class="language-plaintext highlighter-rouge">flux push artifact</code>, Flux pulls and applies</li>
  <li><strong>Helm repositories</strong> — for managing Helm releases declaratively</li>
</ul>

<h3 id="core-resources">Core resources</h3>

<p><strong>GitRepository / OCIRepository</strong> — defines where Flux watches:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">source.toolkit.fluxcd.io/v1beta2</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">OCIRepository</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">my-app</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">flux-system</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">interval</span><span class="pi">:</span> <span class="s">1m</span>
  <span class="na">url</span><span class="pi">:</span> <span class="s">oci://123456789.dkr.ecr.us-east-1.amazonaws.com/my-app-manifests</span>
  <span class="na">ref</span><span class="pi">:</span>
    <span class="na">tag</span><span class="pi">:</span> <span class="s">latest</span>
</code></pre></div></div>

<p><strong>Kustomization</strong> — tells Flux what to apply from the source:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">kustomize.toolkit.fluxcd.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Kustomization</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">my-app</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">flux-system</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">interval</span><span class="pi">:</span> <span class="s">5m</span>
  <span class="na">sourceRef</span><span class="pi">:</span>
    <span class="na">kind</span><span class="pi">:</span> <span class="s">OCIRepository</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">my-app</span>
  <span class="na">path</span><span class="pi">:</span> <span class="s">./manifests/kubernetes/overlays/prod</span>
  <span class="na">prune</span><span class="pi">:</span> <span class="no">true</span>      <span class="c1"># delete resources removed from source</span>
  <span class="na">healthChecks</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
      <span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
      <span class="na">name</span><span class="pi">:</span> <span class="s">backend</span>
      <span class="na">namespace</span><span class="pi">:</span> <span class="s">my-app</span>
</code></pre></div></div>

<p><strong>HelmRelease</strong> — manages a Helm release declaratively:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">helm.toolkit.fluxcd.io/v2beta1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">HelmRelease</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">ingress-nginx</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">flux-system</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">interval</span><span class="pi">:</span> <span class="s">1h</span>
  <span class="na">chart</span><span class="pi">:</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">chart</span><span class="pi">:</span> <span class="s">ingress-nginx</span>
      <span class="na">version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">4.11.3"</span>
      <span class="na">sourceRef</span><span class="pi">:</span>
        <span class="na">kind</span><span class="pi">:</span> <span class="s">HelmRepository</span>
        <span class="na">name</span><span class="pi">:</span> <span class="s">ingress-nginx</span>
  <span class="na">values</span><span class="pi">:</span>
    <span class="na">controller</span><span class="pi">:</span>
      <span class="na">replicaCount</span><span class="pi">:</span> <span class="m">2</span>
</code></pre></div></div>

<h3 id="cicd-with-flux">CI/CD with Flux</h3>

<p>A typical Flux-based pipeline looks like:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. Developer opens a PR
2. CI runs tests
3. PR merged to main
4. CI builds Docker image → pushes to ECR
5. CI packages Kustomize manifests as OCI artifact → flux push artifact → ECR
6. Flux detects new artifact version
7. Flux applies manifests to cluster
8. Cluster rolls out new Deployment
</code></pre></div></div>

<p>Steps 6–8 happen automatically, inside the cluster, with no deploy script and no human intervention.</p>

<h3 id="flux-vs-running-deploy-scripts">Flux vs running deploy scripts</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Shell script (<code class="language-plaintext highlighter-rouge">helm upgrade</code>)</th>
      <th>Flux GitOps</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Who initiates deploy</td>
      <td>Human or CI job</td>
      <td>Cluster controller</td>
    </tr>
    <tr>
      <td>Drift detection</td>
      <td>None — manual kubectl needed</td>
      <td>Continuous — auto-reverts</td>
    </tr>
    <tr>
      <td>Audit trail</td>
      <td>CI logs</td>
      <td>Git history + Flux events</td>
    </tr>
    <tr>
      <td>Rollback</td>
      <td><code class="language-plaintext highlighter-rouge">helm rollback</code></td>
      <td>Revert commit, Flux reconciles</td>
    </tr>
    <tr>
      <td>Complexity</td>
      <td>Low — just a shell script</td>
      <td>Higher — Flux controllers + CRDs</td>
    </tr>
    <tr>
      <td>Air-gapped / on-prem</td>
      <td>Simple</td>
      <td>Requires Flux + registry access</td>
    </tr>
  </tbody>
</table>

<p>GitOps is the right choice for teams with multiple people deploying to shared clusters, or for production environments where drift must be detected and prevented. For a small team or a self-hosted product where simplicity matters, shell scripts with <code class="language-plaintext highlighter-rouge">helm upgrade --install</code> are easier to understand, debug, and hand off to a customer.</p>

<hr />

<h2 id="summary">Summary</h2>

<table>
  <thead>
    <tr>
      <th>Tool</th>
      <th>Role</th>
      <th>Key strength</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Kustomize</strong></td>
      <td>Overlay-based YAML patching</td>
      <td>Plain YAML, no templates, built into kubectl</td>
    </tr>
    <tr>
      <td><strong>Helm</strong></td>
      <td>Templated package manager</td>
      <td>Release history, rollback, publishable charts</td>
    </tr>
    <tr>
      <td><strong>Flux</strong></td>
      <td>GitOps controller</td>
      <td>Self-healing cluster, drift detection, no manual deploys</td>
    </tr>
    <tr>
      <td><strong>Argo CD</strong></td>
      <td>GitOps controller (alternative to Flux)</td>
      <td>Web UI, application health visualisation</td>
    </tr>
  </tbody>
</table>

<p>A mature production setup typically uses all three: Kustomize or Helm for defining manifests, Flux or Argo CD for reconciling them, and a CI pipeline that produces the artifacts both consume.</p>

<p><strong>Next:</strong> <a href="/tech/kubernetes/devops/docrouter/deploying-doc-router-on-kubernetes/">Deploying Doc Router on Kubernetes</a> walks through a real application deployment (Helm chart, workers, CI/CD, EKS and Digital Ocean). If you need in-cluster MongoDB with vector search, see <a href="/tech/kubernetes/devops/mongodb/self-hosted-mongodb-kubernetes-atlas-search/">Self-Hosted MongoDB on Kubernetes with Atlas Search</a>.</p>

<hr />

<p><em>Andrei Radulescu-Banu is the founder of <a href="https://docrouter.ai">DocRouter.AI</a> (document processing with LLMs) and <a href="https://sigagent.ai">SigAgent.AI</a> (Claude Agent monitoring). His company <a href="https://analytiqhub.com">AnalytiqHub.com</a> provides consulting services for cloud and AI engineering.</em></p>]]></content><author><name>Andrei Radulescu-Banu</name></author><category term="tech" /><category term="kubernetes" /><category term="devops" /><summary type="html"><![CDATA[The second part of the Kubernetes primer series: Kustomize, Helm, and GitOps with Flux — packaging manifests and letting the cluster manage itself.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://sigagent.ai/assets/images/kubernetes-packaging-helm-gitops-splash.png" /><media:content medium="image" url="https://sigagent.ai/assets/images/kubernetes-packaging-helm-gitops-splash.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Kubernetes for Docker Users: A Practical Primer</title><link href="https://sigagent.ai/tech/kubernetes/devops/kubernetes-for-docker-users-primer/" rel="alternate" type="text/html" title="Kubernetes for Docker Users: A Practical Primer" /><published>2026-03-05T00:00:00+00:00</published><updated>2026-03-05T00:00:00+00:00</updated><id>https://sigagent.ai/tech/kubernetes/devops/kubernetes-for-docker-users-primer</id><content type="html" xml:base="https://sigagent.ai/tech/kubernetes/devops/kubernetes-for-docker-users-primer/"><![CDATA[<p>If you’ve used Docker Compose, you already understand the core idea: define your services, wire them together with a network, and let the runtime manage the processes. Kubernetes takes that same idea and extends it to run across a cluster of machines, with built-in handling for failures, scaling, and upgrades.</p>

<p>Here’s how the key concepts map across.</p>

<h2 id="from-containers-to-pods">From containers to Pods</h2>

<p>In Docker Compose, the unit of work is a container. In Kubernetes, it is a <strong>Pod</strong> — a group of one or more containers that always run together on the same machine and share a network namespace. Most Pods contain a single container, but some use sidecars: a main process plus a helper (a log shipper, a proxy, or in our case the <code class="language-plaintext highlighter-rouge">mongot</code> search process alongside <code class="language-plaintext highlighter-rouge">mongod</code>).</p>

<p>Pods are ephemeral. When a Pod dies, Kubernetes replaces it with a new one — possibly on a different machine, with a new IP address. You never SSH into a Pod or rely on its IP being stable.</p>

<h2 id="deployments--the-equivalent-of-a-compose-service">Deployments — the equivalent of a Compose service</h2>

<p>A <strong>Deployment</strong> tells Kubernetes: “keep N replicas of this Pod running at all times.” If a Pod crashes, the Deployment controller starts a replacement. If you push a new image, it performs a rolling update — starting new Pods before terminating old ones so traffic is never interrupted.</p>

<p>In Docker Compose terms, a Deployment is your <code class="language-plaintext highlighter-rouge">service:</code> block plus restart policies and rolling update logic built in.</p>

<h2 id="services--stable-internal-addresses">Services — stable internal addresses</h2>

<p>Because Pod IPs change on every restart, Kubernetes introduces <strong>Services</strong>: stable DNS names and virtual IPs that front a group of Pods. A Service named <code class="language-plaintext highlighter-rouge">backend</code> in the <code class="language-plaintext highlighter-rouge">doc-router</code> namespace is reachable at <code class="language-plaintext highlighter-rouge">backend.doc-router.svc.cluster.local</code> from anywhere in the cluster, regardless of how many backend Pods exist or where they are running.</p>

<p>This replaces the automatic DNS that Docker Compose sets up between containers on the same network.</p>

<h2 id="namespaces--isolation-within-a-cluster">Namespaces — isolation within a cluster</h2>

<p>A <strong>Namespace</strong> is a logical partition of the cluster. Resources in different namespaces don’t collide even if they share a name. A typical setup uses separate namespaces for each concern: <code class="language-plaintext highlighter-rouge">doc-router</code> for the application, <code class="language-plaintext highlighter-rouge">mongodb</code> for the database, <code class="language-plaintext highlighter-rouge">ingress-nginx</code> for the load balancer, <code class="language-plaintext highlighter-rouge">cert-manager</code> for TLS certificates.</p>

<p>In Docker Compose terms, a namespace is roughly equivalent to a separate Compose project — distinct networks and name scopes.</p>

<h2 id="configmaps-and-secrets--environment-variables-at-scale">ConfigMaps and Secrets — environment variables at scale</h2>

<p>Docker Compose lets you set <code class="language-plaintext highlighter-rouge">environment:</code> variables inline or via an <code class="language-plaintext highlighter-rouge">.env</code> file. Kubernetes separates non-sensitive config from sensitive config:</p>

<ul>
  <li><strong>ConfigMap</strong> — key-value pairs mounted as environment variables or files. Used for things like <code class="language-plaintext highlighter-rouge">FASTAPI_ROOT_PATH</code>, worker count, S3 bucket name.</li>
  <li><strong>Secret</strong> — base64-encoded values stored (optionally encrypted at rest) separately from your app manifests. Used for database URIs, API keys, and auth secrets. Pods reference Secrets by name; the values are injected at runtime, never baked into the image.</li>
</ul>

<h2 id="persistentvolumeclaims--durable-storage">PersistentVolumeClaims — durable storage</h2>

<p>Docker Compose uses named volumes (backed by the local filesystem) to persist data across container restarts. Kubernetes uses <strong>PersistentVolumeClaims (PVCs)</strong>: a request for a piece of storage of a given size and access mode. The cluster fulfils the claim by provisioning a real volume — an EBS disk on AWS, a DO Block Storage volume on Digital Ocean — and mounting it into the Pod.</p>

<p>PVCs survive Pod restarts and rescheduling. If a database Pod moves to a different node, the volume is detached and reattached automatically. Storage is provisioned dynamically by a <strong>StorageClass</strong>, which specifies the provisioner (e.g. <code class="language-plaintext highlighter-rouge">ebs.csi.aws.com</code> on EKS) and volume type.</p>

<h2 id="ingress-and-the-load-balancer">Ingress and the load balancer</h2>

<p>In Docker Compose you typically expose one port from one container. In Kubernetes, multiple Services need to be reachable from the outside under different paths or hostnames, all through a single external IP.</p>

<p><strong>ingress-nginx</strong> is a Kubernetes controller that runs an nginx reverse proxy inside the cluster. When deployed on EKS, it automatically provisions an AWS Network Load Balancer with a stable public IP. You define <strong>Ingress</strong> rules — “route <code class="language-plaintext highlighter-rouge">/fastapi</code> to the backend Service, everything else to the frontend Service” — and ingress-nginx handles the routing. On a new cluster, the load balancer is the only resource with a public IP; everything else is internal.</p>

<h2 id="cert-manager--automatic-tls">cert-manager — automatic TLS</h2>

<p>cert-manager is a Kubernetes controller that watches Ingress resources and automatically requests TLS certificates from Let’s Encrypt. When you annotate an Ingress with <code class="language-plaintext highlighter-rouge">cert-manager.io/cluster-issuer: letsencrypt-prod</code>, cert-manager handles the ACME challenge, obtains the certificate, stores it in a Secret, and renews it before it expires. You never touch a certificate manually.</p>

<h2 id="helm--packaging-it-all-together">Helm — packaging it all together</h2>

<p>Kubernetes resources are defined as YAML files. A real application needs dozens of them: Deployments, Services, ConfigMaps, Secrets, Ingress rules, PodDisruptionBudgets. <strong>Helm</strong> is the package manager for Kubernetes — it bundles all those YAML files into a <strong>chart</strong>, parameterises them with a <code class="language-plaintext highlighter-rouge">values.yaml</code> file, and installs or upgrades the whole bundle with a single command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm upgrade <span class="nt">--install</span> doc-router oci://ghcr.io/analytiq-hub/doc-router <span class="se">\</span>
  <span class="nt">--namespace</span> doc-router <span class="nt">--set</span> <span class="nv">appHost</span><span class="o">=</span>example.com ...
</code></pre></div></div>

<p>A chart can be published as an OCI artifact to any container registry alongside the Docker images.</p>

<p>If Docker Compose is a <code class="language-plaintext highlighter-rouge">docker run</code> wrapper, Helm is closer to an apt package: versioned, reproducible, and upgradeable.</p>

<h3 id="how-helm-applies-changes">How Helm applies changes</h3>

<p>Every time you run <code class="language-plaintext highlighter-rouge">helm upgrade</code>, Helm compares the new rendered YAML against what it last applied and sends only the diff to the Kubernetes API — resources that haven’t changed are left untouched. Helm records each upgrade as a numbered <strong>revision</strong>, stored as a Secret in the cluster:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>helm <span class="nb">history </span>doc-router <span class="nt">-n</span> doc-router
REVISION  STATUS     CHART           APP VERSION  DESCRIPTION
1         superseded doc-router-0.3.5  v27.0.0    Install <span class="nb">complete
</span>2         superseded doc-router-0.3.6  v27.0.1    Upgrade <span class="nb">complete
</span>3         deployed   doc-router-0.3.7  v27.0.2    Upgrade <span class="nb">complete</span>
</code></pre></div></div>

<h3 id="rolling-back-to-a-known-good-state">Rolling back to a known-good state</h3>

<p>If an upgrade goes wrong, rolling back to the previous revision is a single command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm rollback doc-router <span class="nt">-n</span> doc-router        <span class="c"># rolls back to revision 2</span>
helm rollback doc-router 1 <span class="nt">-n</span> doc-router      <span class="c"># rolls back to a specific revision</span>
</code></pre></div></div>

<p>Helm re-applies the exact YAML from that revision — the same image tags, the same config values — so the cluster returns to the state that last worked. Using <code class="language-plaintext highlighter-rouge">--atomic</code> during an upgrade makes this automatic: if the new Pods don’t become healthy within the timeout, Helm rolls back on its own without any manual intervention.</p>

<h3 id="zero-downtime-rolling-updates">Zero-downtime rolling updates</h3>

<p>When Helm upgrades a Deployment with a new image, Kubernetes does not restart all Pods at once. It uses a <strong>rolling update</strong> strategy controlled by two parameters:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">strategy</span><span class="pi">:</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">RollingUpdate</span>
  <span class="na">rollingUpdate</span><span class="pi">:</span>
    <span class="na">maxUnavailable</span><span class="pi">:</span> <span class="m">0</span>   <span class="c1"># never take a pod down before a new one is ready</span>
    <span class="na">maxSurge</span><span class="pi">:</span> <span class="m">1</span>         <span class="c1"># allow one extra pod above the desired count during the rollout</span>
</code></pre></div></div>

<p>With <code class="language-plaintext highlighter-rouge">maxUnavailable: 0</code>, Kubernetes starts a new Pod with the new image first. Only after that Pod passes its readiness probe — meaning it is actually serving traffic — does Kubernetes terminate one of the old Pods. This continues one Pod at a time until all replicas are on the new version. At no point does the number of healthy Pods drop below the desired count.</p>

<p>The result: an upgrade from <code class="language-plaintext highlighter-rouge">v27.0.1</code> to <code class="language-plaintext highlighter-rouge">v27.0.2</code> with two replicas proceeds as:</p>

<ol>
  <li>Start new Pod (v27.0.2) — 2 old + 1 new running</li>
  <li>New Pod passes readiness check</li>
  <li>Terminate one old Pod — 1 old + 1 new running</li>
  <li>Start second new Pod — 1 old + 2 new running</li>
  <li>Second new Pod passes readiness — terminate last old Pod</li>
  <li>Rollout complete — 2 new Pods running, zero downtime</li>
</ol>

<p>If the new Pod fails its readiness check at step 2, the rollout pauses. No old Pods have been terminated, so the old version continues serving 100% of traffic. With <code class="language-plaintext highlighter-rouge">--atomic</code>, Helm then rolls the release back automatically.</p>

<h2 id="running-kubernetes-locally-with-kind">Running Kubernetes locally with Kind</h2>

<p>Before deploying to a real cluster, it’s useful to test locally using <strong>Kind</strong> (Kubernetes in Docker). Kind runs an entire Kubernetes cluster — control plane and worker nodes — as Docker containers on your laptop. There is no cloud provider, no load balancer, and no cloud volumes; Kind uses your local filesystem for storage and <code class="language-plaintext highlighter-rouge">NodePort</code> services for external access.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./deploy/scripts/setup-kind.sh   <span class="c"># creates the Kind cluster</span>
./deploy/scripts/deploy-kind.sh  <span class="c"># installs the Helm chart locally</span>
</code></pre></div></div>

<p>The same chart that runs on EKS runs on Kind, with a different <code class="language-plaintext highlighter-rouge">values-kind.yaml</code> override file. This lets you iterate on chart changes without incurring cloud costs or waiting for node provisioning.</p>

<h2 id="summary">Summary</h2>

<table>
  <thead>
    <tr>
      <th>Docker Compose concept</th>
      <th>Kubernetes equivalent</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Container</td>
      <td>Pod (usually 1 container, sometimes with sidecars)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">service:</code> block</td>
      <td>Deployment + Service</td>
    </tr>
    <tr>
      <td>Container DNS (service name)</td>
      <td>Service DNS (<code class="language-plaintext highlighter-rouge">name.namespace.svc.cluster.local</code>)</td>
    </tr>
    <tr>
      <td>Compose project</td>
      <td>Namespace</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">environment:</code> / <code class="language-plaintext highlighter-rouge">.env</code></td>
      <td>ConfigMap (non-secret) + Secret (sensitive)</td>
    </tr>
    <tr>
      <td>Named volume</td>
      <td>PersistentVolumeClaim + StorageClass</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ports:</code> expose</td>
      <td>Ingress + LoadBalancer Service</td>
    </tr>
    <tr>
      <td>Manual TLS</td>
      <td>cert-manager (automatic Let’s Encrypt)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">docker-compose.yml</code></td>
      <td>Helm chart (<code class="language-plaintext highlighter-rouge">values.yaml</code> + templates)</td>
    </tr>
    <tr>
      <td>Local Docker</td>
      <td>Kind (Kubernetes in Docker)</td>
    </tr>
  </tbody>
</table>

<p><strong>Next:</strong> <a href="/tech/kubernetes/devops/kubernetes-packaging-helm-gitops/">Kubernetes Packaging and Deployment: Kustomize, Helm, and GitOps</a> goes deeper into packaging manifests and GitOps with Flux.</p>

<hr />

<p><em>Andrei Radulescu-Banu is the founder of <a href="https://docrouter.ai">DocRouter.AI</a> (document processing with LLMs) and <a href="https://sigagent.ai">SigAgent.AI</a> (Claude Agent monitoring). His company <a href="https://analytiqhub.com">AnalytiqHub.com</a> provides consulting services for cloud and AI engineering.</em></p>]]></content><author><name>Andrei Radulescu-Banu</name></author><category term="tech" /><category term="kubernetes" /><category term="devops" /><summary type="html"><![CDATA[If you've used Docker Compose, you already understand the core idea. Kubernetes takes that same idea and extends it to run across a cluster of machines, with built-in handling for failures, scaling, and upgrades.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://sigagent.ai/assets/images/kubernetes-docker-users-primer-splash.png" /><media:content medium="image" url="https://sigagent.ai/assets/images/kubernetes-docker-users-primer-splash.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">How We Integrated Stripe Into DocRouter.AI</title><link href="https://sigagent.ai/tech/programming/ai/tutorials/how-we-integrated-stripe-into-docrouter-ai/" rel="alternate" type="text/html" title="How We Integrated Stripe Into DocRouter.AI" /><published>2025-10-20T00:00:00+00:00</published><updated>2025-10-20T00:00:00+00:00</updated><id>https://sigagent.ai/tech/programming/ai/tutorials/how-we-integrated-stripe-into-docrouter-ai</id><content type="html" xml:base="https://sigagent.ai/tech/programming/ai/tutorials/how-we-integrated-stripe-into-docrouter-ai/"><![CDATA[<p>At <a href="http://docrouter.ai">DocRouter.AI</a>, we build an AI app for processing documents using LLMs. As our user base grew, we needed a reliable way to handle billing for self-serve customers. This post explains our Stripe integration, focusing on key design choices. We’ll cover why we chose Stripe, how we keep things flexible, pricing decisions, APIs used, and more.</p>

<h2 id="table-of-contents">Table of Contents</h2>

<ul>
  <li><a href="#why-use-stripe">Why Use Stripe?</a></li>
  <li><a href="#how-stripe-payments-get-handled">How Stripe Payments Get Handled</a></li>
  <li><a href="#spu-credits">SPU Credits</a></li>
  <li><a href="#pricing-strategy-for-ai-products">Pricing Strategy For AI Products</a></li>
  <li><a href="#docrouterai-software-stack">DocRouter.AI Software Stack</a></li>
  <li><a href="#free-tier-plans-and-a-la-carte-credits">Free Tier, Plans, and A-La-Carte Credits</a></li>
  <li><a href="#prices-for-large-vs-small-customers">Prices for Large vs. Small Customers</a></li>
  <li><a href="#price-changing-flexibility">Price Changing Flexibility</a></li>
  <li><a href="#environment-variables">Environment Variables</a></li>
  <li><a href="#the-stripe-product-and-price-metadata">The Stripe Product and Price Metadata</a></li>
  <li><a href="#python-apis-for-retrieving-products-and-prices">Python APIs for Retrieving Products and Prices</a></li>
  <li><a href="#stripe-checkout-and-billing-portal">Stripe Checkout and Billing Portal</a></li>
  <li><a href="#webhooks-and-synchronization">Webhooks and Synchronization</a></li>
  <li><a href="#mongodb-schema">MongoDB Schema</a></li>
  <li><a href="#tracking-spu-usage">Tracking SPU Usage</a></li>
  <li><a href="#development-and-testing-with-stripe">Development and Testing with Stripe</a></li>
  <li><a href="#need-help-with-your-pricing-strategy">Need Help with Your Pricing Strategy?</a></li>
</ul>

<h2 id="why-use-stripe">Why Use Stripe?</h2>

<p>Stripe handles the credit card transactions, and handles global currencies/taxes. For an AI app with variable usage, it’s essential — manual billing would be error-prone and slow. Companies using Stripe don’t need to deal with credit cards. They only need to identify the customers by a unique customer ID managed by Stripe.</p>

<h2 id="how-stripe-payments-get-handled">How Stripe Payments Get Handled</h2>

<p>Stripe supports:</p>
<ul>
  <li><strong>subscriptions</strong> for recurring plans</li>
  <li><strong>one-time charges</strong> for credits,</li>
  <li><strong>webhooks</strong> for real-time updates.</li>
</ul>

<p>It also supports <strong>metered</strong> charges, but we will not use the <strong>Stripe</strong> implementation for meters. It is more convenient to implement our own <strong>metered</strong> support, keeping track on our platform of usage, and charge through <strong>Stripe</strong> the monthly cost.</p>

<h2 id="spu-credits">SPU Credits</h2>

<p>On <a href="http://docrouter.ai">DocRouter.AI</a>, customers purchase credits called <strong>SPUs</strong> (Service Processing Units) - this is our abstraction for document page processing, and for LLM token use. Many other SAAS companies use a credit-purchase based mechanism. For example, Databricks measures credits as <strong>DBUs</strong> (Databricks Processing Units), and charges hourly cluster usage a number of <strong>DBUs</strong> that depends on the cluster size.</p>

<p>As long as the credit units are tied to predictable units of operation - for example, number of pages processed in a document - a credit purchase mechanism is a good choice.</p>

<h2 id="pricing-strategy-for-ai-products">Pricing Strategy For AI Products</h2>

<p>When setting up pricing through Stripe, you have to be strategic. You need to cover the needs of Enterprise customers, but also small companies and occasional users. Some customers prefer to purchase credits a-la-carte, while others prefer the predictability of a monthly subscription.</p>

<p>Pricing must be tailored to all types of customers you have at the current stage - and, has to be as simple as possible.</p>

<p>The diagram below describes different pricing strategies practiced in the industry:</p>

<p><img src="/assets/images/stripe_AI_pricing_strategy.png" alt="Pricing Strategies for AI" /></p>

<p>Ideally, products would be priced with an <strong>Outcome-Base Model</strong> (top right quadrant). This works well if a business is vertical in an industry, and if product value has high attribution.</p>

<p>DocRouter.AI is a horizontal, data-layer product - and uses a <strong>Hybrid Pricing Model</strong> based on monthly subscriptions and/or a-la-carte <strong>SPU</strong> credit purchases.</p>

<h2 id="docrouterai-software-stack"><a href="http://docrouter.ai">DocRouter.AI</a> Software Stack</h2>
<ul>
  <li>The DocRouter.AI front end is <strong>Next.JS</strong>, with <strong>Next.Auth</strong> user authentication</li>
  <li>The back end is <strong>Fast API</strong></li>
  <li>Our database is <strong>MongoDB</strong></li>
</ul>

<p>DocRouter supports <a href="https://docrouter.ai/docs/mcp/">agentic <strong>Claude Code</strong> integration and <strong>MCP</strong></a>, allowing document workflows to be controlled through a simple chat interface.</p>

<p>Users control all DocRouter.AI functions either through the UI, or through <a href="https://docrouter.ai/docs/rest-api/"><strong>REST APIs</strong></a>, using access tokens. A <a href="https://docrouter.ai/docs/python-sdk/"><strong>Python SDK</strong></a> and a <a href="https://docrouter.ai/docs/typescript-sdk/"><strong>Typescript SDK</strong></a> are available.</p>

<p>Having the flexibility to control DocRouter programmatically, either through agentic interfacing, or through APIs is great.</p>

<p>However, use tracking (aka metered billing) has to be automated, and the customer needs to be charged a small percentage above the underlying cost charged by LLM and cloud providers.</p>

<p>Stripe integration is, thus, an essential ingredient in making this kind of programmatic integration possible.</p>

<h2 id="free-tier-plans-and-a-la-carte-credits">Free Tier, Plans, and A-La-Carte Credits</h2>

<p>When you are starting, the best pricing is a simple one that your customer understands - especially for self-onboaded customers.</p>

<p>For DocRouter, we want users to start free, upgrade to plans, and be able to buy extra credits without friction. Here’s how we did it:</p>

<p><img src="/assets/images/docrouter_pricing.png" alt="DocRouter Pricing Plans" /></p>

<p>New orgs get 100 granted SPUs (no card needed). Additional credits can be purchased. Users can subscribe to an <strong>Individual</strong> or <strong>Team</strong> plan, at a discount over the a-la-carte credits price. Or, they can select the <strong>Enterprise</strong> plan, which is invoiced outside of Stripe where we can charge based on the customer outcomes (aka outcomes-based pricing).</p>

<h2 id="prices-for-large-vs-small-customers">Prices for large vs. small customers</h2>

<p>The <strong>business</strong> challenge here is for the pricing scheme to be flexible enough to accommodate large <strong>enterprise</strong> usage, at custom prices - while also allowing <strong>self-onboarded</strong> customers.</p>

<p>Consumption waterfall: allowance first, then purchased, then granted. Keeps costs low for light users, upsell for heavy ones.</p>

<p>The prices need to be in line with what is usually charged for document AI processing - while also capturing the value of more advanced, custom document workflows.</p>

<h2 id="price-changing-flexibility">Price changing flexibility</h2>

<p>The <strong>engineering</strong> challenge is, on the other hand, in how to create this pricing structure in <strong>Stripe</strong>, and ensure the variable part of the config (amounts, utilization thresholds) resides in <strong>Stripe</strong> configuration rather than local <strong>DocRouter.AI</strong> code.</p>

<p>Updating amounts or utilization thresholds at a later time should not require coding changes in DocRouter.AI. These updates should be possible by merely chaging price configuration in <strong>Stripe</strong>.</p>

<p>Pricing updates, however, does not impact existing customers (aka grandfathering).</p>

<p>How does it all work?</p>

<h2 id="environment-variables">Environment Variables</h2>

<p>Three Stripe-related environment variables are configured for integration:</p>

<p><strong><code class="language-plaintext highlighter-rouge">STRIPE_SECRET_KEY</code></strong> - Your Stripe API key for authentication. Required to enable Stripe integration.</p>

<p><strong><code class="language-plaintext highlighter-rouge">STRIPE_WEBHOOK_SECRET</code></strong> - Secret for verifying <strong>webhook</strong> signatures. The DocRouter.AI <strong>webhook</strong> is called by Stripe when various events happen: a user started or stopped a subscription, or purchased a-la-carte <strong>SPU</strong> credits.</p>

<p><strong><code class="language-plaintext highlighter-rouge">STRIPE_PRODUCT_TAG</code></strong> - The product identifier in metadata (default: <code class="language-plaintext highlighter-rouge">"doc_router"</code>). Allows filtering prices to find only those belonging to your product.</p>

<p>If <code class="language-plaintext highlighter-rouge">STRIPE_SECRET_KEY</code> is not set, Stripe integration is disabled and DocRouter operates in local-only mode.</p>

<h2 id="the-stripe-product-and-price-metadata">The Stripe Product and Price metadata</h2>

<p><strong>Stripe</strong> uses the following ‘objects’: <strong>Products</strong>, <strong>Prices</strong> (multiple per product), <strong>Users</strong> (one per customer), and <strong>Subscriptions</strong> (each with one or more <strong>Prices</strong>.</p>

<div class="flex justify-center my-4">
  <img src="/assets/images/stripe_doc_router_diagram.png" alt="Stripe DocRouter.AI Diagram" />
</div>

<p>We could identify the <strong>Products</strong> and the <strong>Prices</strong> by stripe IDs saved as environment variables, similar to how we use the <strong>STRIPE_SECRET_KEY</strong>. However, that is not an advantageous way to set things up.</p>

<p>Instead, we detect Products and Prices by reading their <strong>metadata</strong> in <strong>Stripe</strong>, and filtering for the Products and Prices configured for our company.</p>

<p>This way, a single Stripe account can be used as an umbrella for potentially multiple products, allowing for future extensibility.</p>

<ul>
  <li>
    <p>We create a Stripe <strong>DocRouter Product</strong> 
<img src="/assets/images/stripe_product.png" alt="DocRouter Product" /></p>
  </li>
  <li>
    <p>And we assign it a <code class="language-plaintext highlighter-rouge">product=doc_router</code> key/value in the <strong>price metadata</strong>. The <strong>DocRouter.AI</strong> software detects the product using the Stripe Python API, filtering all products to find specifically the one with this key/value.
<img src="/assets/images/stripe_product_metadata.png" alt="DocRouter Product Metadata" /></p>
  </li>
  <li>
    <p>We create two <em>recurring</em> <strong>Prices</strong> we’ll use for monthly subscriptions: the <strong>Individual Price</strong>, and the <strong>Team Price</strong>. We again use <em>metadata</em> to auto-detect the prices:</p>
    <ul>
      <li>The <strong>Individual Price</strong> has metadata <strong>included_spus=5000</strong>, <strong>price_type=base</strong>, <strong>tier=individual</strong>.</li>
      <li>The <strong>Team Price</strong> has <strong>included_spus=25000</strong>, <strong>price_type=base</strong>, <strong>tier=team</strong></li>
    </ul>
  </li>
</ul>

<div class="flex justify-center my-4">
  <img src="/assets/images/stripe_price_metadata.png" alt="DocRouter Price Metadata" class="w-1/2" />
</div>

<p>The DocRouter.AI detects the prices and the tier limits from the metadata.</p>

<h2 id="python-apis-for-retrieving-products-and-prices">Python APIs for Retrieving Products and Prices</h2>

<p>DocRouter.AI uses the Stripe Python SDK to dynamically fetch pricing configuration at startup. The complete implementation can be found in our <a href="https://github.com/analytiq-hub/doc-router/blob/main/packages/python/docrouter_app/routes/payments.py">payments.py</a> file. Here’s how the key parts work:</p>

<h3 id="retrieving-all-prices-with-product-data">Retrieving All Prices with Product Data</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">prices</span> <span class="o">=</span> <span class="n">stripe</span><span class="p">.</span><span class="n">Price</span><span class="p">.</span><span class="nb">list</span><span class="p">(</span><span class="n">active</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">expand</span><span class="o">=</span><span class="p">[</span><span class="s">'data.product'</span><span class="p">])</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">expand=['data.product']</code> parameter loads the full product object (including metadata) alongside each price in a single API call.</p>

<h3 id="filtering-by-product-metadata">Filtering by Product Metadata</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">product_prices</span> <span class="o">=</span> <span class="p">[</span>
    <span class="n">price</span> <span class="k">for</span> <span class="n">price</span> <span class="ow">in</span> <span class="n">prices</span><span class="p">.</span><span class="n">data</span>
    <span class="k">if</span> <span class="n">price</span><span class="p">.</span><span class="n">product</span><span class="p">.</span><span class="n">metadata</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'product'</span><span class="p">)</span> <span class="o">==</span> <span class="s">'doc_router'</span>
<span class="p">]</span>
</code></pre></div></div>

<p>This filters prices to only those belonging to our DocRouter product.</p>

<h3 id="parsing-price-metadata">Parsing Price Metadata</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="n">price</span> <span class="ow">in</span> <span class="n">product_prices</span><span class="p">:</span>
    <span class="n">metadata</span> <span class="o">=</span> <span class="n">price</span><span class="p">.</span><span class="n">metadata</span>
    <span class="n">price_type</span> <span class="o">=</span> <span class="n">metadata</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'price_type'</span><span class="p">)</span>

    <span class="k">if</span> <span class="n">price_type</span> <span class="o">==</span> <span class="s">'base'</span><span class="p">:</span>
        <span class="n">tier</span> <span class="o">=</span> <span class="n">metadata</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'tier'</span><span class="p">)</span>
        <span class="n">included_spus</span> <span class="o">=</span> <span class="n">metadata</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'included_spus'</span><span class="p">)</span>
        <span class="c1"># Store tier limits for subscription plans
</span></code></pre></div></div>

<p>All pricing configuration lives in Stripe—we can update prices and tier limits without code changes.</p>

<h3 id="wrapping-stripe-apis-for-async">Wrapping Stripe APIs for Async</h3>

<p>Since DocRouter.AI uses FastAPI (an async framework), we wrap Stripe’s synchronous API calls to run in a thread pool:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">_run_in_threadpool</span><span class="p">(</span><span class="n">func</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
    <span class="n">loop</span> <span class="o">=</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">get_event_loop</span><span class="p">()</span>
    <span class="k">return</span> <span class="k">await</span> <span class="n">loop</span><span class="p">.</span><span class="n">run_in_executor</span><span class="p">(</span><span class="bp">None</span><span class="p">,</span> <span class="n">partial</span><span class="p">(</span><span class="n">func</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">))</span>
</code></pre></div></div>

<p>This prevents blocking the event loop. In practice, we call:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">prices</span> <span class="o">=</span> <span class="k">await</span> <span class="n">_run_in_threadpool</span><span class="p">(</span>
    <span class="n">stripe</span><span class="p">.</span><span class="n">Price</span><span class="p">.</span><span class="nb">list</span><span class="p">,</span>
    <span class="n">active</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
    <span class="n">expand</span><span class="o">=</span><span class="p">[</span><span class="s">'data.product'</span><span class="p">]</span>
<span class="p">)</span>
</code></pre></div></div>

<p>The wrapper runs the synchronous <code class="language-plaintext highlighter-rouge">stripe.Price.list()</code> in a separate thread, keeping our async FastAPI endpoints responsive.</p>

<h2 id="stripe-checkout-and-billing-portal">Stripe Checkout and Billing Portal</h2>

<h3 id="purchasing-credits">Purchasing Credits</h3>

<p>When users buy a-la-carte credits, we create a Stripe Checkout session:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">session</span> <span class="o">=</span> <span class="n">stripe</span><span class="p">.</span><span class="n">checkout</span><span class="p">.</span><span class="n">Session</span><span class="p">.</span><span class="n">create</span><span class="p">(</span>
    <span class="n">customer</span><span class="o">=</span><span class="n">stripe_customer_id</span><span class="p">,</span>
    <span class="n">payment_method_types</span><span class="o">=</span><span class="p">[</span><span class="s">'card'</span><span class="p">],</span>
    <span class="n">line_items</span><span class="o">=</span><span class="p">[{...}],</span>
    <span class="n">success_url</span><span class="o">=</span><span class="n">success_url</span><span class="p">,</span>
    <span class="n">cancel_url</span><span class="o">=</span><span class="n">cancel_url</span><span class="p">,</span>
    <span class="n">metadata</span><span class="o">=</span><span class="p">{</span><span class="s">'org_id'</span><span class="p">:</span> <span class="n">org_id</span><span class="p">,</span> <span class="s">'credits'</span><span class="p">:</span> <span class="n">credits</span><span class="p">}</span>
<span class="p">)</span>
</code></pre></div></div>

<div class="flex justify-center my-4">
  <img src="/assets/images/stripe_payment_portal.png" alt="Stripe Payment Portal" />
</div>

<p>Users are redirected to Stripe’s hosted checkout page—we never see their credit card details. Stripe handles all payment security.</p>

<h3 id="managing-subscriptions">Managing Subscriptions</h3>

<p>For subscription management, we use Stripe’s Billing Portal:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">session</span> <span class="o">=</span> <span class="n">stripe</span><span class="p">.</span><span class="n">billing_portal</span><span class="p">.</span><span class="n">Session</span><span class="p">.</span><span class="n">create</span><span class="p">(</span>
    <span class="n">customer</span><span class="o">=</span><span class="n">stripe_customer_id</span><span class="p">,</span>
    <span class="n">return_url</span><span class="o">=</span><span class="n">return_url</span>
<span class="p">)</span>
</code></pre></div></div>

<p>The portal lets users update payment methods, view invoices, and cancel subscriptions—all handled by Stripe.</p>

<h2 id="webhooks-and-synchronization">Webhooks and Synchronization</h2>

<p>Stripe sends webhooks for important events. We verify and process them:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">event</span> <span class="o">=</span> <span class="n">stripe</span><span class="p">.</span><span class="n">Webhook</span><span class="p">.</span><span class="n">construct_event</span><span class="p">(</span>
    <span class="n">payload</span><span class="p">,</span>
    <span class="n">signature_header</span><span class="p">,</span>
    <span class="n">webhook_secret</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Key events we handle:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">checkout.session.completed</code> - Add purchased credits</li>
  <li><code class="language-plaintext highlighter-rouge">customer.subscription.updated</code> - Sync subscription changes</li>
  <li><code class="language-plaintext highlighter-rouge">customer.subscription.deleted</code> - Clear subscription data</li>
  <li><code class="language-plaintext highlighter-rouge">invoice.payment_succeeded</code> - Record successful payments</li>
</ul>

<p>To prevent double-crediting, we track processed transactions in the <code class="language-plaintext highlighter-rouge">db.payments_credit_transactions</code> collection.</p>

<p>On startup and via webhooks, we sync <strong>Stripe</strong> data to <strong>MongoDB</strong>. This keeps local data fresh without constant API calls.</p>

<h2 id="mongodb-schema">MongoDB Schema</h2>

<p>We store payment data in MongoDB collections:</p>

<p><strong><code class="language-plaintext highlighter-rouge">payments_customers</code></strong> - One document per organization:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="nl">org_id</span><span class="p">:</span> <span class="dl">"</span><span class="s2">...</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">user_id</span><span class="p">:</span> <span class="dl">"</span><span class="s2">...</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">stripe_customer_id</span><span class="p">:</span> <span class="dl">"</span><span class="s2">cus_...</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">user_name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">...</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">user_email</span><span class="p">:</span> <span class="dl">"</span><span class="s2">...</span><span class="dl">"</span><span class="p">,</span>

  <span class="c1">// Subscription fields</span>
  <span class="nx">subscription_type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">individual</span><span class="dl">"</span> <span class="o">|</span> <span class="dl">"</span><span class="s2">team</span><span class="dl">"</span> <span class="o">|</span> <span class="dl">"</span><span class="s2">enterprise</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">subscription_spu_allowance</span><span class="p">:</span> <span class="mi">5000</span><span class="p">,</span>
  <span class="nx">subscription_spus_used</span><span class="p">:</span> <span class="mi">1234</span><span class="p">,</span>  <span class="c1">// Reset each billing period</span>
  <span class="nx">stripe_subscription_id</span><span class="p">:</span> <span class="dl">"</span><span class="s2">sub_...</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">stripe_subscription_item_id</span><span class="p">:</span> <span class="dl">"</span><span class="s2">si_...</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">stripe_subscription_status</span><span class="p">:</span> <span class="dl">"</span><span class="s2">active</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">stripe_current_billing_period_start</span><span class="p">:</span> <span class="mi">1234567890</span><span class="p">,</span>
  <span class="nx">stripe_current_billing_period_end</span><span class="p">:</span> <span class="mi">1237246290</span><span class="p">,</span>

  <span class="c1">// Credit fields</span>
  <span class="nx">purchased_credits</span><span class="p">:</span> <span class="mi">10000</span><span class="p">,</span>
  <span class="nx">purchased_credits_used</span><span class="p">:</span> <span class="mi">5000</span><span class="p">,</span>
  <span class="nx">granted_credits</span><span class="p">:</span> <span class="mi">100</span><span class="p">,</span>
  <span class="nx">granted_credits_used</span><span class="p">:</span> <span class="mi">50</span><span class="p">,</span>

  <span class="c1">// Metadata</span>
  <span class="nx">created_at</span><span class="p">:</span> <span class="nx">ISODate</span><span class="p">(...),</span>
  <span class="nx">updated_at</span><span class="p">:</span> <span class="nx">ISODate</span><span class="p">(...),</span>
  <span class="nx">subscription_updated_at</span><span class="p">:</span> <span class="nx">ISODate</span><span class="p">(...)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Subscription SPU usage (<code class="language-plaintext highlighter-rouge">subscription_spus_used</code>) is atomically reset each billing period. Subscription allowances renew monthly. Purchased and granted credits persist until consumed.</p>

<p><strong><code class="language-plaintext highlighter-rouge">payments_credit_transactions</code></strong> - Audit trail for credit purchases:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="nl">session_id</span><span class="p">:</span> <span class="dl">"</span><span class="s2">cs_...</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">org_id</span><span class="p">:</span> <span class="dl">"</span><span class="s2">...</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">credits</span><span class="p">:</span> <span class="mi">1000</span><span class="p">,</span>
  <span class="nx">processed_at</span><span class="p">:</span> <span class="nx">ISODate</span><span class="p">(...)</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong><code class="language-plaintext highlighter-rouge">payments_usage_records</code></strong> - Log of all SPU usage:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="nl">org_id</span><span class="p">:</span> <span class="dl">"</span><span class="s2">...</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">spus</span><span class="p">:</span> <span class="mi">42</span><span class="p">,</span>
  <span class="nx">operation</span><span class="p">:</span> <span class="dl">"</span><span class="s2">document_processing</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">source</span><span class="p">:</span> <span class="dl">"</span><span class="s2">backend</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">timestamp</span><span class="p">:</span> <span class="nx">ISODate</span><span class="p">(...),</span>
  <span class="nx">llm_provider</span><span class="p">:</span> <span class="dl">"</span><span class="s2">anthropic</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">llm_model</span><span class="p">:</span> <span class="dl">"</span><span class="s2">claude-3-5-sonnet-20241022</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">prompt_tokens</span><span class="p">:</span> <span class="mi">1234</span><span class="p">,</span>
  <span class="nx">completion_tokens</span><span class="p">:</span> <span class="mi">567</span><span class="p">,</span>
  <span class="nx">total_tokens</span><span class="p">:</span> <span class="mi">1801</span><span class="p">,</span>
  <span class="nx">actual_cost</span><span class="p">:</span> <span class="mf">0.023</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="tracking-spu-usage">Tracking SPU Usage</h2>

<p>When LLM calls are made, we increment SPU usage:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">record_payment_usage</span><span class="p">(</span><span class="n">org_id</span><span class="p">,</span> <span class="n">spus</span><span class="p">):</span>
    <span class="c1"># Consumption order: subscription → purchased → granted
</span>    <span class="n">balances</span> <span class="o">=</span> <span class="k">await</span> <span class="n">get_current_balances</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">org_id</span><span class="p">)</span>
    <span class="n">consumption</span> <span class="o">=</span> <span class="n">calculate_consumption_breakdown</span><span class="p">(</span><span class="n">spus</span><span class="p">,</span> <span class="n">balances</span><span class="p">)</span>

    <span class="c1"># Update balances atomically
</span>    <span class="k">await</span> <span class="n">update_customer_balances</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">customer_id</span><span class="p">,</span> <span class="n">consumption</span><span class="p">)</span>

    <span class="c1"># Save usage record
</span>    <span class="k">await</span> <span class="n">save_complete_usage_record</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">org_id</span><span class="p">,</span> <span class="n">spus</span><span class="p">,</span> <span class="n">consumption</span><span class="p">)</span>
</code></pre></div></div>

<p>The consumption waterfall ensures subscription credits are used first, then purchased, then granted.</p>

<p>Users view their credit utilization on the usage page. All data comes from MongoDB — no Stripe API calls needed to track usage, keeping the UI fast.</p>

<h2 id="development-and-testing-with-stripe">Development and Testing with Stripe</h2>

<p>Stripe provides separate test and production environments. During development, we use <strong>test mode</strong> keys:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># .env for development</span>
<span class="nv">STRIPE_SECRET_KEY</span><span class="o">=</span>sk_test_...
<span class="nv">STRIPE_WEBHOOK_SECRET</span><span class="o">=</span>whsec_...
<span class="nv">STRIPE_PRODUCT_TAG</span><span class="o">=</span>doc_router
</code></pre></div></div>

<p>Test mode keys (<code class="language-plaintext highlighter-rouge">sk_test_*</code>) access a completely separate sandbox with its own customers, subscriptions, and products. You can:</p>

<ul>
  <li>Create test products and prices in the Stripe Dashboard</li>
  <li>Use test credit cards (like <code class="language-plaintext highlighter-rouge">4242 4242 4242 4242</code>) for checkout</li>
  <li>Trigger webhooks manually to test event handling</li>
  <li>View all transactions without affecting production data</li>
</ul>

<p>For production, swap to live keys (<code class="language-plaintext highlighter-rouge">sk_live_*</code>). The same code works in both modes—Stripe automatically routes API calls to the correct environment based on the key prefix.</p>

<p>This separation lets us develop and debug payment flows safely without risking real customer data or charges.</p>

<h2 id="need-help-with-your-pricing-strategy">Need Help with Your Pricing Strategy?</h2>

<p>Take what you are thinking and try <strong>10x that price</strong> for a fixed price. If they say yes, everyone is happy. If they say no, you can talk about fair pricing for the value you are providing for their business (aka Outcome-based pricing) typically aligned of a cost/revenue metric that matters a lot to your customer.</p>

<p><img src="/assets/images/stripe_AI_pricing_strategy.png" alt="Pricing Strategies for AI" /></p>

<p>If you’re implementing your own pricing strategy and need expert advice, Randy Carlton offers consultation on pricing for AI and SaaS businesses.</p>

<div class="bg-gradient-to-br from-blue-50 to-purple-50 rounded-lg shadow-lg p-8 mt-12">
    <div class="text-center">
        <h3 class="text-2xl font-semibold text-gray-900 mb-4">Get Pricing Strategy Advice</h3>
        <p class="text-gray-600 mb-6">
            Book a 30-minute consultation with Randy Carlton to discuss your pricing challenges and get expert guidance on implementing effective pricing strategies for your AI business.
        </p>
        <div class="w-full max-w-sm mx-auto">
            <div class="bg-gradient-to-r from-blue-600 to-purple-600 rounded-xl p-0.5">
                <button onclick="openRandyCalendlyModal()" class="w-full bg-white hover:bg-gray-50 text-blue-600 hover:text-purple-600 text-center px-8 py-4 rounded-xl font-semibold text-lg transition-all duration-200 shadow-lg hover:shadow-xl">
                    Schedule with Randy
                </button>
            </div>
        </div>
    </div>
</div>

<script>
function openRandyCalendlyModal() {
    // Check if we're on mobile
    const isMobile = window.innerWidth <= 768;
    
    if (isMobile) {
        // For mobile, redirect to Calendly URL directly
        const calendlyUrl = 'https://calendly.com/randycarlton/30m-virtual-meeting';
        window.location.href = calendlyUrl;
    } else {
        // For desktop, open in new window
        window.open('https://calendly.com/randycarlton/30m-virtual-meeting', '_blank', 'width=800,height=600,scrollbars=yes,resizable=yes');
    }
}
</script>]]></content><author><name>Andrei Radulescu-Banu &amp; Randy Carlton</name></author><category term="tech" /><category term="programming" /><category term="ai" /><category term="tutorials" /><summary type="html"><![CDATA[At DocRouter.AI, we build an AI app for processing documents using LLMs. As our user base grew, we needed a reliable way to handle billing for self-serve customers. This post explains our Stripe integration, focusing on key design choices. We’ll cover why we chose Stripe, how we keep things flexible, pricing decisions, APIs used, and more.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://sigagent.ai/assets/images/how_to_integrate_stripe.png" /><media:content medium="image" url="https://sigagent.ai/assets/images/how_to_integrate_stripe.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">How To Train Your AI Agent</title><link href="https://sigagent.ai/ai/programming/tutorials/how-to-train-your-ai-agent/" rel="alternate" type="text/html" title="How To Train Your AI Agent" /><published>2025-10-02T00:00:00+00:00</published><updated>2025-10-02T00:00:00+00:00</updated><id>https://sigagent.ai/ai/programming/tutorials/how-to-train-your-ai-agent</id><content type="html" xml:base="https://sigagent.ai/ai/programming/tutorials/how-to-train-your-ai-agent/"><![CDATA[<p>Developing an AI agent comes down to a number of intertwined yet distinct challenges:</p>
<ol>
  <li>Create the <strong>runtime infrastructure</strong> for the agent itself</li>
  <li>Add <strong>tools</strong> for the task</li>
  <li><strong>Planning</strong>: Making agent tackle the <strong>specific problem</strong> you’re aiming to solve</li>
  <li>Create a <strong>knowledge base</strong> the agent can use to inform its execution steps</li>
  <li>Set up an <strong>evaluation infrastructure</strong> to track progress and catch agent mistakes</li>
  <li><strong>Integrate</strong> the agent into the larger application</li>
</ol>

<p>Whether the AI agent is <strong>fully autonomous</strong> or works with <strong>human-in-the-loop</strong> oversight, development includes roughly the steps outlined above. Each step calls for its own set of techniques and tools.</p>

<p><img src="/assets/images/ai_agent_steps_to_develop.png" alt="Steps To Develop An AI Agent" /></p>

<h2 id="examples-windsurf-cursor-and-claude-code">Examples: Windsurf, Cursor and Claude Code</h2>

<p>Let’s start with some examples the reader may be familiar with:</p>
<ul>
  <li>AI Editors like <a href="https://windsurf.com/"><strong>Windsurf</strong></a>, <a href="https://cursor.com/"><strong>Cursor</strong></a>, <a href="https://github.com/features/copilot"><strong>Github Copilot</strong></a></li>
  <li>Command line tools like <a href="https://www.claude.com/product/claude-code"><strong>Claude Code</strong></a>.</li>
</ul>

<p>The latter, <strong>Claude Code</strong>, also has a <a href="https://marketplace.visualstudio.com/items?itemName=anthropic.claude-code"><strong>VSCode</strong> extension</a> by the same name.</p>

<p>In each case, the underlying AI agents are integrated with the text editor (our <strong>Step 6</strong>).</p>

<p>While <strong>Windsurf</strong>, <strong>Cursor</strong> and <strong>Claude Code</strong> each have their own agent infrastructure (our <strong>Step 1</strong>), the latter, <strong>Claude Code</strong> is different in that it is available as a <a href="https://docs.claude.com/en/docs/claude-code/quickstart"><strong>command line tool</strong></a> which can be executed standalone, in <a href="https://docs.claude.com/en/docs/claude-code/interactive-mode">interactive</a> or <a href="https://docs.claude.com/en/docs/claude-code/cli-reference?utm_source=chatgpt.com">non-interactive mode</a>.</p>

<p><strong>Anthropic</strong> has also developed a <a href="https://docs.claude.com/en/api/client-sdks"><strong>Claude Agent SDK</strong></a>, with <strong>Typescript</strong>, <strong>Python</strong> and <strong>shell command</strong> flavors. The <strong>Claude Agent SDK</strong> can be used both for coding tasks and for more general tasks - for example, for document processing, or for support message processing, or for marketing research and outreach.</p>

<p>An Agent SDK is not available for <strong>Windsurf</strong> or <strong>Claude Code</strong>.</p>

<p>The <strong>knowledge base</strong> (<strong>Step 3</strong>) for these agents is embedded into the language models themselves. These coding agents can code in <strong>Python</strong>, <strong>Typescript</strong>, <strong>C</strong>, <strong>C++</strong> or <strong>Java</strong>. The language models are trained on these programming languages.</p>

<p>While these agents do not technically require an external <strong>knowledge base</strong> for pure coding skills, they use a <strong>web search</strong> tool to look up product and API documentation. The web search tool comes handy when implementing <strong>coding tasks</strong> against specific coding libraries and APIs.</p>

<p><strong>Windsurf</strong>, <strong>Cursor</strong>, <strong>Github Copilot</strong> use multiple-vendor language models, in addition to models used in-house. <strong>Claude Code</strong> on the other hand, only uses <strong>Anthropic</strong> language models.</p>

<p>Custom AI Agents, though, don’t have all necessary knowledge baked into their LLM model - and often need an <strong>external knowledge base</strong>, adapted to their task (<strong>Step 3</strong>).</p>

<p>For example - one of our customer companies develops a lab information management system that uses a scripting language to define web forms. The forms are used to manage lab processes in Biotech, Food Manufacturing, or Forensics. The AI Agent we developed employs an <strong>external knowledge base</strong>  to provide full documentation and examples for the scripting language. The generalist LLMs, while trained on <strong>Python</strong>, <strong>Typescript</strong> and other programming languages, would not have baked-in information about the custom scripting language for web forms.</p>

<p>Custom AI agents need to implement their own <strong>external knowledge base</strong> and <strong>problem-solving tools</strong>. These are both integrated into the Agent using an <strong>MCP Server</strong>.</p>

<p><img src="/assets/images/ai_agent_extending_claude_code.png" alt="Extending Claude Code" /></p>

<p><strong>MCP</strong> support is already available in all AI Editors. The editors can act as <strong>MCP Clients</strong>, and can, for example, read external Github repositories through a <strong>Github MCP Server</strong>. Any MCP Server is supported.</p>

<p>An <strong>MCP Server</strong>, thus, can be used to make your AI agent <strong>act</strong> on your <strong>specific tasks</strong>, and to access your <strong>external knowledge base</strong>.</p>

<p>But having the <strong>tools</strong> and the <strong>knowledge base</strong> is not sufficient. The agent must also be steered through <strong>task planning</strong> (our <strong>Step 4</strong>).</p>

<p>The approach each of the AI editors takes to <strong>steer</strong> their <strong>coding task</strong> is proprietary, and slightly different. A lot of the steering is done through proprietary system prompts. Some have <a href="https://beyondthehype.dev/p/inside-claude-code-prompt-engineering-masterpiece">reverse engineered these system prompts</a> through intercepting API calls - but, for the most part, they remain hidden.</p>

<p>Each AI editor, however, supports extending the system prompt through a configuration file: <strong>CLAUDE.md</strong> for Claude Code, <strong>Cursor Rules</strong> for Cursor,<strong>Windsurf Rules</strong> for Windsurf. It is these configuration files - CLAUDE.md in particular - that will be used to <strong>steer our Custom AI Agent</strong>.</p>

<p>Windsurf, Cursor, Github Copilot also use a <strong>tab-completion model</strong>, which employs a smaller, faster LLM acting directly in the editor buffer. Claude Code does not have that feature.</p>

<p>Evaluation for these AI editors is also proprietary (<strong>Step 5</strong>). Custom AI Agents would need their own evaluation infrastructure developed from scratch.</p>

<p>Here is a Comparison Table for the AI editors we described:</p>

<table style="width: 100%; border-collapse: collapse; margin: 1.5rem 0; font-size: 14px;">
  <thead>
    <tr style="background: #1e40af !important; color: white !important;">
      <th style="padding: 12px 16px; text-align: white; font-weight: 600; background: #1e40af !important; color: white !important;">Step</th>
      <th style="padding: 12px 16px; text-align: left; font-weight: 600; background: #1e40af !important; color: white !important;">Windsurf, Cursor, Github Copilot</th>
      <th style="padding: 12px 16px; text-align: left; font-weight: 600; background: #1e40af !important; color: white !important;">Claude Code</th>
      <th style="padding: 12px 16px; text-align: left; font-weight: 600; background: #1e40af !important; color: white !important;">Custom Agent</th>
    </tr>
  </thead>
  <tbody>
    <tr style="background: #fef3c7;">
      <td style="padding: 12px 16px; border-bottom: 1px solid #d1d5db; font-weight: 600; color: #92400e;">1. Runtime Infrastructure</td>
      <td style="padding: 12px 16px; border-bottom: 1px solid #d1d5db; color: #374151;">Proprietary, integrated with text editors</td>
      <td style="padding: 12px 16px; border-bottom: 1px solid #d1d5db; color: #374151;">Proprietary, available as command-line tool + VSCode extension</td>
      <td style="padding: 12px 16px; border-bottom: 1px solid #d1d5db; color: #374151;">Claude Code + Claude Agent SDK (TypeScript/Python/Shell)</td>
    </tr>
    <tr style="background: #dbeafe;">
      <td style="padding: 12px 16px; border-bottom: 1px solid #d1d5db; font-weight: 600; color: #1e40af;">2. Task Planning</td>
      <td style="padding: 12px 16px; border-bottom: 1px solid #d1d5db; color: #374151;">Proprietary system prompt. Windsurf Rules file for Windsurf. Cursor Rules file for Cursor</td>
      <td style="padding: 12px 16px; border-bottom: 1px solid #d1d5db; color: #374151;">Proprietary system prompt. CLAUDE.md for custom planning.</td>
      <td style="padding: 12px 16px; border-bottom: 1px solid #d1d5db; color: #374151;">Custom via CLAUDE.md + editable system prompts (Claude Agent SDK)</td>
    </tr>
    <tr style="background: #f3e8ff;">
      <td style="padding: 12px 16px; border-bottom: 1px solid #d1d5db; font-weight: 600; color: #7c3aed;">3. Knowledge Base</td>
      <td style="padding: 12px 16px; border-bottom: 1px solid #d1d5db; color: #374151;">Embedded in LLM training + web search for API docs. Extensible through MCP.</td>
      <td style="padding: 12px 16px; border-bottom: 1px solid #d1d5db; color: #374151;">Embedded in LLM training + web search for API docs. Extensible through MCP.</td>
      <td style="padding: 12px 16px; border-bottom: 1px solid #d1d5db; color: #374151;">External vector DB (Pinecone/Weaviate/Qdrant) + custom MCP server tools</td>
    </tr>
    <tr style="background: #fecaca;">
      <td style="padding: 12px 16px; border-bottom: 1px solid #d1d5db; font-weight: 600; color: #dc2626;">4. Evaluation Infrastructure</td>
      <td style="padding: 12px 16px; border-bottom: 1px solid #d1d5db; color: #374151;">Proprietary, internal evaluation systems</td>
      <td style="padding: 12px 16px; border-bottom: 1px solid #d1d5db; color: #374151;">Proprietary, internal evaluation systems</td>
      <td style="padding: 12px 16px; border-bottom: 1px solid #d1d5db; color: #374151;">Custom evaluation scripts + LLM Judge + web visualization dashboard</td>
    </tr>
    <tr style="background: #d1fae5;">
      <td style="padding: 12px 16px; font-weight: 600; color: #059669;">5. Integration</td>
      <td style="padding: 12px 16px; color: #374151;">Native text editor integration</td>
      <td style="padding: 12px 16px; color: #374151;">Command-line + VSCode extension</td>
      <td style="padding: 12px 16px; color: #374151;">Custom VSCode extension</td>
    </tr>
  </tbody>
</table>

<h2 id="separation-of-concerns-infrastructure-vs-task-specific-planning">Separation of concerns: Infrastructure vs. Task-Specific Planning</h2>

<p>When it comes to building the agent, we separate out the <strong>Infrastructure</strong> design from the <strong>Task Planning</strong>, for a couple of reasons:</p>
<ul>
  <li>Often times, the <strong>task</strong> solved by the agent needs to be flexible and can change in the product lifecycle. This calls for <strong>infrastructure</strong> to be separated out from the <strong>task customization</strong>. Sometimes, the <strong>task</strong> or its <strong>subtasks</strong> are not even fully known or understood at the outset of the AI Agent project.</li>
  <li>The <strong>infrastructure</strong> is now available ready-made, and may not have to be built from scratch.</li>
</ul>

<p>Adopting a ready-made, battle tested <strong>AI Agent Infrastructure</strong> that is <strong>steerable</strong> for your task will accelerate your AI Agent design.</p>

<p><img src="/assets/images/ai_agent_online_system.png" alt="Claude Code Online System Architecture" /></p>

<h2 id="the-ai-agent-infrastructure">The AI Agent Infrastructure</h2>

<p>As of this writing, our go-to stack for agent creation is the <a href="https://docs.claude.com/en/docs/claude-code/overview">Claude Code</a> shell command, complemented by the <a href="https://www.anthropic.com/engineering/building-agents-with-the-claude-agent-sdk">Claude Agent SDK</a>. Its advantages:</p>
<ul>
  <li>A suite of powerful features like <a href="https://docs.claude.com/en/docs/claude-code/memory#claude-md-imports"><strong>CLAUDE.md</strong></a> customization, <strong>subcommands</strong> for modular execution, <strong>subagents</strong> for hierarchical delegation</li>
  <li><a href="https://docs.claude.com/en/docs/mcp"><strong>MCP server support</strong></a>, which lets you seamlessly extend the agent, and steer it to solve the <strong>task</strong> at hand.</li>
  <li>Built-in <a href="https://docs.claude.com/en/docs/build-with-claude/prompt-caching#how-prompt-caching-works"><strong>prompt caching</strong></a>, and discounted packaged price for utilization, when using a Claude subscription.</li>
</ul>

<p>No agent is complete without a solid <strong>knowledge base</strong>, and without good <strong>evaluation infrastructure</strong>.</p>

<p>The Claude Agent SDK doesn’t ship with either out of the box. You’ll need to craft them yourself.</p>

<p>For the vector database powering the <strong>knowledge base</strong>, we’ve used <a href="https://www.pinecone.io/"><strong>Pinecone</strong></a>, prized for its straightforward SaaS model that gets you up and running quick. Pinecone <a href="https://docs.pinecone.io/guides/get-started/overview">documentation</a> is also very accessible.</p>

<p>That said, there are multiple Vector DB alternatives: <strong>Weaviate</strong>, <strong>Qdrant</strong>, or <strong>ChromaDB</strong>. Even traditional players like <strong>Postgres</strong> or <strong>MongoDB</strong> have jumped on the vector bandwagon, now offering built-in support for vector tables or collections.</p>

<p>In our architecture, we <strong>centralize all custom interfaces</strong> for the agent within the <strong>MCP server</strong>. This ensures portability and testability across setups. The <strong>MCP server</strong> includes a <code class="language-plaintext highlighter-rouge">vector_db_lookup()</code> interface to let the agent look up the <strong>knowledge base</strong>.</p>

<h2 id="indexing-your-knowledge-base">Indexing Your Knowledge Base</h2>

<p>To build the knowledge base, you need:</p>
<ul>
  <li>A collection of knowledge base files - we usually convert them all to <strong>Markdown</strong>, and store them in an <strong>S3</strong> bucket, or in a <strong>MongoDB collection</strong></li>
  <li>A <strong>vector DB index</strong> in your DB of choice</li>
  <li>An <strong>indexing script</strong>, that runs periodically, and uploads the knowledge base markdown files into the vector DB index, mapped through an <strong>LLM embedding</strong>.</li>
</ul>

<p><img src="/assets/images/indexing_flow.png" alt="Knowledge Base Indexing Flow" style="width: 100%; height: auto;" /></p>

<p>The lookup operation is what you’d expect - the searched text is mapped through the same <strong>LLM embedding</strong>, and looked up for <strong>similarity</strong> with existing chunks in the vector db index - returning the most closely similar chunks from the <strong>knowledge base</strong>.</p>

<h2 id="tuning-ai-agent-actions-via-the-mcp-server">Tuning AI Agent Actions via the MCP Server</h2>

<p>With the knowledge base available, and searchable - we now let the AI agent use it through <strong>MCP Server</strong> tools.</p>

<p>These tools can:</p>
<ul>
  <li>Look up knowledge base articles for similarity</li>
  <li>Perform file reads and edits that are specialised to your file format</li>
  <li>Validate file edits, and run unit tests</li>
</ul>

<p>The tools take parameters, and are self-describing. The LLM model can figure out, to a good extent, which tools to call, and which parameters to pass - however, that is not sufficient for good agent design, because:</p>
<ul>
  <li>The same operation may be solved by different tools.</li>
  <li>Or, the tools need to be called in a certain order.</li>
</ul>

<p>This is why step is crucial: <strong>Task Planning</strong>, which steers the agent in how it uses the <strong>MCP Server</strong> tools.</p>

<h2 id="task-planning">Task Planning</h2>

<p>Task planning is designed partly through editing <strong>CLAUDE.md</strong>, and partly through changing the system prompt.</p>
<ul>
  <li>With the command-line <strong>Claude Code</strong> tool, its internal system prompt is not editable.</li>
  <li>But with <strong>Claude Agent SDK</strong>, it is.</li>
</ul>

<p>In <strong>CLAUDE.md</strong>, we specify how to approach the various custom problems the agent might encounter. <strong>Claude Code</strong> has built-in support for the <strong>ToDoWrite</strong> tool. In <strong>CLAUDE.md</strong>, we specify what are the <strong>ToDo</strong> tasks, for each problem the AI Agent might encounter.</p>

<h2 id="the-claude-code-event-loop">The Claude Code Event Loop</h2>

<p>The <strong>Claude Code</strong> agent itself has a baked-in <a href="https://blog.promptlayer.com/claude-code-behind-the-scenes-of-the-master-agent-loop/?utm_source=chatgpt.com">event loop</a>, which takes user input, file attachments and screenshots - puts <strong>CLAUDE.md</strong> in context, determines its <strong>ToDo</strong> tasks, and iterates through <strong>MCP Server</strong> tools and other built-in tools to solve the task.</p>

<p>The user can interrupt the flow at any moment, and continue it with altered instructions.</p>

<p>From there, the agent marches forward, step by step. It can go solo or pause for human nods, calling on more <strong>MCP</strong> and <strong>built-in tools</strong> as needed to create new files, edit existing ones, or query the knowledge base.</p>

<p><img src="/assets/images/ai_agent_event_loop.png" alt="Claude Code Event Loop" style="width: 100%; height: auto;" /></p>

<h2 id="tuning-the-agent-through-evaluation">Tuning the Agent through Evaluation</h2>

<p>Once the <strong>infrastructure</strong> is stood up, the <strong>knowledge base</strong> available, the <strong>MCP server</strong> working, and the agent begins to be integrated in the larger application - you need to <strong>tune</strong> the agent and <strong>adapt</strong> it to the task at hand.</p>

<p><strong>Tuning</strong> can mean:</p>
<ul>
  <li>Adding or updating to the <strong>knowledge base</strong></li>
  <li>Adding or updating the <strong>MCP server tools</strong></li>
  <li>Improving the <strong>CLAUDE.md</strong> design, to steer the running agent through its __ToDo__s.</li>
</ul>

<p><strong>Evaluation</strong> is an essential step here.</p>

<p><img src="/assets/images/ai_agent_offline_evaluation.png" alt="Claude Code Offline System Architecture" style="width: 80%; height: auto;" /></p>

<p>Kick things off by:</p>
<ul>
  <li>Assembling a <strong>ground-truth dataset</strong> — a curated set of benchmark problems mirroring real-world scenarios.</li>
  <li>Create an <strong>evaluation script</strong> that, for each item, spins up a fresh <strong>Claude Code</strong> instance to tackle the problem in pure automated mode, assuming no user intervention.</li>
  <li>Once the <strong>_Claude Code</strong> instance completes processing a <strong>dataset item</strong>, a second forked instance of <strong>Claude Code</strong> runs to evaluate and judge the output, for a number of metrics, as <strong>LLM Judge</strong>.</li>
</ul>

<p>The metrics tracked by the <strong>LLM Judge</strong> are:</p>
<ul>
  <li><strong>Accuracy:</strong> How spot-on are the results?</li>
  <li><strong>Completeness:</strong> Does it cover all requirements without omissions?</li>
  <li>For MCP interfaces leaning on vector DB retrieval: <strong>Accuracy</strong> and <strong>completeness</strong> of the chunks pulled back.</li>
  <li>Completion rate of the <strong>ToDo</strong>’s</li>
  <li>Drilling deeper, for select MCP tools: <strong>Per-tool breakdowns</strong> of response <strong>accuracy</strong> and <strong>completeness</strong>.</li>
  <li><strong>Number of failed validations</strong> during the actual run</li>
  <li><strong>Validation tests</strong> run by the LLM judge at eval time, using the same MCP validation tools you added</li>
  <li>Other statistics: <strong>number of steps</strong> to solve the question,  <strong>LLM models</strong> used, <strong>in</strong> and <strong>out</strong> tokens, <strong>cached</strong> tokens, <strong>cost</strong></li>
</ul>

<h2 id="bringing-eval-to-life-visualization">Bringing Eval to Life: Visualization</h2>

<p>Raw eval logs are hard to monitor without an <strong>at-a-glance view</strong>.</p>

<p>In our approach, we build a custom web visualiser of eval results, with:</p>
<ul>
  <li>A quick-scan table, one row per question, for instant overviews.</li>
  <li>A radar chart aggregating metrics into a visual story of strengths and gaps.</li>
  <li>Distributions to reveal patterns across the board.</li>
  <li>Per-question deep dives on tool usage: At-a-glance summaries of which tools fired, what inputs they took, the outputs they spat, and any ToDo progress where relevant.</li>
</ul>

<h2 id="iteration-and-improvement">Iteration and Improvement</h2>

<p>Here’s how the development loop flows in practice:</p>
<ul>
  <li>Bootstrap your ground-truth dataset with questions to get baseline momentum.</li>
  <li>Start the evaluation suite and scrutinize the metrics</li>
  <li>For each metrics shortfall, triage the root cause:
    <ul>
      <li>Does the <strong>knowledge base</strong> require new or updated data?</li>
      <li>Is the problem caused by Vector DB <strong>chunking</strong>, <strong>indexing</strong>, or <strong>retrieval</strong>?</li>
      <li>Is an <strong>MCP tool</strong> not working properly? Or is a new <strong>MCP tool</strong> needed?</li>
      <li>Or perhaps <strong>CLAUDE.md</strong> itself needs a refresh to better guide the agent?</li>
    </ul>
  </li>
</ul>

<p>As problems are discovered, and fixed, the ground truth dataset is expanded. Re-running over previously-working dataset items ensures the new fixes don’t break old functionality.</p>]]></content><author><name>Andrei Radulescu-Banu</name></author><category term="ai" /><category term="programming" /><category term="tutorials" /><summary type="html"><![CDATA[Developing an AI agent comes down to a number of intertwined yet distinct challenges: Create the runtime infrastructure for the agent itself Add tools for the task Planning: Making agent tackle the specific problem you’re aiming to solve Create a knowledge base the agent can use to inform its execution steps Set up an evaluation infrastructure to track progress and catch agent mistakes Integrate the agent into the larger application]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://sigagent.ai/assets/images/how_to_train_your_ai_agent.png" /><media:content medium="image" url="https://sigagent.ai/assets/images/how_to_train_your_ai_agent.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Navigating Compliance in Startups: A Guide for Regulated Industries</title><link href="https://sigagent.ai/ai/programming/tech/reviews/engineering-with-guardrails-innovating-in-regulated-spaces/" rel="alternate" type="text/html" title="Navigating Compliance in Startups: A Guide for Regulated Industries" /><published>2025-09-10T00:00:00+00:00</published><updated>2025-09-10T00:00:00+00:00</updated><id>https://sigagent.ai/ai/programming/tech/reviews/engineering-with-guardrails-innovating-in-regulated-spaces</id><content type="html" xml:base="https://sigagent.ai/ai/programming/tech/reviews/engineering-with-guardrails-innovating-in-regulated-spaces/"><![CDATA[<p>In the dynamic world of startups, especially those operating in heavily regulated sectors like healthcare, fintech, AI, and consumer hardware, compliance isn’t just a checkbox—it’s a foundational element that safeguards innovation, protects users, and unlocks growth opportunities.</p>

<p>As regulations evolve rapidly — with frameworks like <strong>HIPAA</strong>, <strong>SOC2</strong>, <strong>FDA</strong> approvals, and <strong>GDPR</strong> becoming more stringent — startups must integrate compliance from the outset to avoid costly pitfalls.</p>

<p>In this blog post, we draw from insights shared at <strong>Startup Boston Week 2025</strong>’s <a href="https://streamyard.com/watch/GWAw3FHAfUZy">Engineering with Guardrails</a> panel, featuring <strong>Ilsa Webeck</strong> (Simbex), <strong>Marjan Monfared</strong> (Leaf Guardian), and myself (DocRouter.AI), moderated by <strong>Rabeeh Majidi</strong> (OrthoKinetic Track) - incorporating panel wisdom alongside other general best practices.</p>

<p>Whether you’re a technical founder or CTO, understanding these principles can help you build resilient products that scale without regulatory roadblocks.</p>

<h2 id="how-do-regulatory-frameworks-differ-from-each-other">How Do Regulatory Frameworks Differ From Each Other?</h2>
<p>While all regulatory frameworks aim to promote safety, security, and accountability, it’s vital to understand their key differences, as advice applicable to one may not translate to others.</p>
<ul>
  <li><strong>HIPAA</strong> is a U.S. federal law mandating the protection of protected health information (PHI) through privacy and security rules, including breach notifications, for covered entities like healthcare providers and their business associates.</li>
  <li><strong>FDA</strong> regulations for <em>pharmaceuticals</em> focus on ensuring drug safety and efficacy via rigorous processes like New Drug Applications (NDAs), which often involve extensive clinical trials and can take an average of 12 years.</li>
  <li>In contrast, <strong>FDA</strong> oversight for <em>medical devices</em> is risk-based, with devices classified into <em>Class I</em> (low risk, general controls), <em>Class II</em> (moderate risk, often requiring 510(k) clearance for substantial equivalence to existing devices), and <em>Class III</em> (high risk, needing Premarket Approval with clinical data), typically spanning 3-7 years.</li>
  <li><strong>SOC2</strong>, developed by the <em>AICPA</em>, is an attestation framework for service organizations, evaluating controls across trust services criteria (security, availability, etc.) via <em>Type 1</em> (design) or <em>Type 2</em> (operational effectiveness) reports, and it’s not industry-specific or legally mandated like <strong>HIPAA</strong>.</li>
  <li><strong>HITRUST</strong> is a voluntary, certifiable framework tailored to healthcare, incorporating <strong>HIPAA</strong> along with standards like <strong>NIST</strong> and <strong>ISO</strong> for comprehensive risk management, going beyond <strong>HIPAA</strong>’s requirements with prescriptive controls and global applicability.</li>
</ul>

<p>For example, while <strong>FDA</strong> emphasizes product testing and recalls, <strong>HIPAA</strong> prioritizes data handling, and <strong>SOC2</strong> focuses on organizational controls—highlighting why strategies must be customized to each framework’s unique scope and enforcement.</p>

<h2 id="common-compliance-mistakes---and-how-to-sidestep-them">Common Compliance Mistakes - And How To Sidestep Them</h2>

<p>One of the most frequent errors startups commit is treating compliance as an afterthought, leading to retrofits that drain resources and expose vulnerabilities.</p>

<p>As Ilsa Webeck noted, the biggest pitfall she sees is really around understanding what the compliance landscape is going to look like for your product. Startups often assume their product’s intended use and launch plan align with <strong>FDA</strong> expectations without researching requirements or establishing a quality management system (QMS). This lack of preparation can lead to costly pivots, as some clients she worked with had to change their product’s market approach after regulatory reviews revealed misaligned pathways.</p>

<ul>
  <li>In healthcare, overlooking patient privacy under <strong>HIPAA</strong> can result in breaches from unsecured devices or inadequate employee training—issues like staff snooping on patient records are top violations that have fined organizations millions.</li>
  <li>Fintech startups often fail to address third-party risks in AI integrations, such as unvetted vendors handling sensitive data.</li>
  <li>A critical oversight is disregarding data quality. For multi-stage data processing, this can lead to “Garbage In, Garbage Out”. It can result in flawed fraud detection or biased models that regulators scrutinize.</li>
  <li>A typical mistake, for <strong>SOC2</strong>, is treating it as a one-time checkbox rather than an ongoing process, often skipping a readiness assessment or gap analysis.</li>
  <li>Broader pitfalls include underestimating documentation for <strong>SOC2</strong> audits or assuming global regs like <strong>GDPR</strong> don’t apply early on.</li>
  <li>Other top reasons for failed audits are due to issues like poor vulnerability management or inconsistent employee onboarding/offboarding.</li>
</ul>

<h2 id="non-compliance-costs-can-be-staggering">Non-Compliance Costs Can Be Staggering</h2>

<p><strong>HIPAA</strong> violations can reach $50,000 per incident, with cumulative fines up to $1.5 million annually; <strong>SOC2</strong> lapses contribute to data breaches averaging $4.45 million; and <strong>GDPR</strong> penalties hit 4% of global revenue, starting at $20,500 for small startups but scaling to millions. Overall, non-compliance is 2.71 times more expensive than proactive programs. To avoid this, conduct an early gap analysis and prioritize <em>compliance by design</em>.</p>

<h2 id="how-do-you-balance-moving-fast-with-staying-compliant">How Do You Balance Moving Fast with Staying Compliant?</h2>

<p>Startups thrive on velocity, but <strong>rushing without guardrails invites failure</strong>. General best practices:</p>
<ul>
  <li><strong>Adopt a phased compliance approach</strong>, like starting with <strong>SOC2 Type 1</strong> for quick foundational controls before <strong>Type 2</strong>.</li>
  <li>Aim for <strong>HIPAA-ready cloud services</strong> (e.g., AWS with Business Associate Agreements) to prototype compliantly.</li>
</ul>

<p><em>“For the early traction, you need to work with the hospitals”</em>, Marjan says. <em>“So how we did that is we make sure all the first prototype equipment that we are providing is off-the-shelf but is HIPAA compliant itself. For example, for data collection, we used a device that was already on the list of <strong>HIPAA</strong>-compliant units.”</em></p>

<ul>
  <li><strong>Use automation tools</strong> like Vanta or Drata for cloud compliance monitoring (10-15 similar vendors available). These tools help avoid duplicate effort between <strong>HIPAA</strong>, <strong>SOC2</strong>, <strong>HITRUST</strong> and <strong>GDPR</strong>, and have relationships with familiar auditors.</li>
  <li><strong>Integrate “shift-left security”</strong> into <strong>CI/CD</strong> pipelines for real-time compliance scans, ensuring innovations roll out without rework.</li>
  <li><strong>Designate a compliance point person</strong> to handle audits without halting engineering.</li>
</ul>

<p>And bring external expertise early on:</p>

<p><em>“I talk a lot to clinicians and patients about new technology just when they’re even kind of napkin sketches or little graphic representations,”</em> says Ilsa Webek.</p>

<p><em>“That’s where you get feedback from the potential users about how this is actually going to work in the real world. You can adjust again to understand what could be commercially viable and then reflect back on the regulatory pathway that you need to be successful in moving it down the line. Get many advisors in, and feedback in from all of your main stakeholders. “</em></p>

<h2 id="innovating-within-strict-rules-turning-constraints-into-opportunities">Innovating Within Strict Rules: Turning Constraints into Opportunities</h2>

<p>While AI offers significant innovation potential in the highly regulated healthcare industry, startups should avoid superficially integrating AI just because it’s trendy.</p>
<ul>
  <li>Understand the regulatory frameworks first.</li>
  <li>Also, study existing <strong>FDA</strong>-approved AI applications.</li>
  <li>AI solutions must address genuine unmet needs and be tailored to specific problems, rather than being forced into products without clear purpose.</li>
</ul>

<p>The AI revolution has dramatically accelerated development. With AI editors like Claude Code and Cursor, projects that used to take three months now take a week or half a week. Code review with AI and enhanced development workflows make innovation much faster for engineers.</p>

<p><em>“There’s huge wave of innovation coming and I don’t know how it’s going to impact regulations as a framework, but for sure it’s going to be impact it”</em>, says Andrei. <em>“The companies that build software stacks to help with regulatory evaluation already embed AI in the document processing. It’s now simpler to see of whether you’re on track with your compliance.”</em></p>

<p><em>“For the DocRouter.AI”</em>, says Andrei, <em>“being a horizontal platform means that you can have customers with different security requirements. Supply Chain requirements are going to be different than for Insurance or Medical or Legal. You can make a lot of progress with customers with lesser requirements. Then, the feature you develop is portable to other domains.”</em></p>

<p><em>“It’s a game of figuring out how to work with customers sharing their data, and how to find the right product pilots, so that when you go to a strictly regulated environment with HIPAA data, for example, then you can make quick progress.”</em></p>

<p>Tips for enabling innovation:</p>
<ul>
  <li>For <strong>HIPAA</strong>, use modular architectures where sensitive <strong>PHI</strong> is isolated in compliant modules, allowing rapid iteration on non-sensitive features.</li>
  <li><strong>Adopt privacy-by-design principles</strong>, like anonymizing data for external AI testing, and leverage compliant cloud services (e.g., <strong>AWS</strong> with <strong>BAAs</strong>) to experiment securely.</li>
  <li>For <strong>SOC2</strong>, focus on relevant Trust Services Criteria (e.g., Security and Privacy for data-heavy innovations) to avoid over-scoping.</li>
</ul>

<p><strong>Recommended Tools:</strong></p>
<ul>
  <li>Snyk or Dependabot: Scan for vulnerabilities in code dependencies.</li>
  <li>Trivy or Aqua Security: Check container images for misconfigurations.</li>
  <li>AWS Config or Azure Policy: Enforce compliant cloud configurations (e.g., HIPAA-eligible services). Implement infrastructure scanning.</li>
  <li>Write unit tests to confirm API endpoints enforce role-based access control (RBAC).</li>
  <li>Ensure all automated checks generate logs for audit trails (critical for HIPAA and SOC 2).</li>
  <li>Store results in a centralized compliance dashboard (e.g., Vanta) for easy review during audits.</li>
</ul>

<h2 id="best-practices-for-healthcare-documentation">Best Practices for Healthcare Documentation</h2>

<ul>
  <li><strong>Establish Robust Communication with Healthcare Clients</strong>
    <ul>
      <li><strong>Engage Early with Hospitals:</strong> Initiate and maintain open, consistent communication with hospital clients to understand their unique, often non-public internal regulations. This is critical during both preclinical and clinical phases to align your product or service with their specific requirements.</li>
      <li><strong>Engage Hospital Ethical Committees:</strong> Prior to formal collaboration, ensure discussions with the hospital’s ethical committee are part of the process. Submitting proposals to these committees is a mandatory step post-contract, as their approval is essential to proceed. Be prepared for potential rejections and plan accordingly.</li>
    </ul>
  </li>
  <li><strong>Tailor Documentation to Hospital Needs</strong>
    <ul>
      <li><strong>Customize Documentation:</strong> Recognize that documentation requirements vary across hospitals. Avoid assumptions about universally accepted formats (e.g., PDFs or specific applications). Actively consult with each hospital to determine their preferred reporting formats and classifications to ensure compliance and usability.</li>
      <li><strong>Leverage Technology for Compliance:</strong> For products like those using thermal temperature mapping to detect early signs of pressure injuries (e.g., ischemia or blood flow disruption), integrate AI-driven, physics-informed solutions to enhance accuracy beyond traditional methods. Ensure all patient data logging includes visual components to meet hospital documentation standards.</li>
    </ul>
  </li>
  <li><strong>Resource Management for Startups</strong>
    <ul>
      <li><strong>Optimize Limited Resources:</strong> As a startup with constrained resources, prioritize partnerships with hospitals where your product or service can be delivered effectively within your capabilities. This strategic focus mitigates risks associated with overextension.</li>
      <li><strong>Proactive Inquiry:</strong> Regularly ask hospitals for feedback on documentation and reporting needs to avoid costly missteps. Tailoring solutions to specific hospital requirements enhances compliance and strengthens client relationships.</li>
    </ul>
  </li>
</ul>

<h2 id="best-practices-for-engineering-documentation">Best Practices for Engineering Documentation</h2>
<ul>
  <li><strong>Leverage modern AI tools:</strong> Claude Code and Cursor have significantly improved. Use them to streamline the engineering documentation process.</li>
  <li><strong>Prioritize Design Phase:</strong> Use these tools to first create BRDs and PRDs, then Architecture and System Docs. Creating unit tests ahead of code implementation allows AI agents to iterate efficiently.</li>
</ul>

<h2 id="when-to-bring-in-outside-help-vs-in-house-management">When to Bring in Outside Help (vs. In-House Management)</h2>

<p><strong>Bootstrap until you can’t</strong> — then scale expertise. Bring in external help for HIPAA when your startup lacks internal expertise. Consultants are ideal for mapping controls. Outsource when scaling rapidly or facing overlapping frameworks (e.g., <strong>HIPAA</strong> + <strong>SOC2</strong>).</p>

<p><strong>Keep it in-house for maintenance</strong> once policies are set, using a dedicated officer. Engage outsiders early for readiness assessments ($10,000–$15,000 for HIPAA mocks) if pre-seed, but only for audits (mandatory external for <strong>SOC2 Type 2</strong>). Costs vary: <strong>SOC2</strong> full compliance runs $15,000–$50,000 in 2025, <strong>HIPAA</strong> certification $40,000+ for complex setups. For global operations, hire fractional experts versed in <strong>GDPR</strong> overlaps to align frameworks efficiently, preventing siloed efforts.</p>

<h2 id="designing-tech-for-evolving-rules">Designing Tech for Evolving Rules</h2>

<p><strong>Regulations shift — your stack must adapt</strong>. Design tech for evolving regulations by adopting modular, scalable architectures. Build with compliant platforms (e.g., <strong>HIPAA</strong>-eligible <strong>AWS</strong> services). For <strong>SOC2</strong>, focus on flexible controls and ‘shift-left’ integration: embed compliance checks in DevOps pipelines.</p>

<p><strong>Use AWS Bedrock for compliant LLMs</strong> and conduct quarterly control mappings. In 2025, anticipate AI ethics updates—implement audit trails for traceability. Multi-framework tools like Drata unify <strong>HIPAA</strong>, <strong>SOC2</strong>, and <strong>GDPR</strong> monitoring, reducing adaptation costs. Regular third-party pentests keep you ahead of cyber evolutions.</p>

<h2 id="talking-risk-and-compliance-with-investors-building-credibility">Talking Risk and Compliance with Investors: Building Credibility</h2>

<p><strong>Investors probe compliance to gauge risks</strong> — be ready. Focus on understanding the highest risk areas and putting the best plan in place. Show progress as you fill in knowledge gaps. Always include a slide about your compliance with crystal clear understanding and explanation based on all the regulations why you fall into this category and the ROI.</p>

<p><strong>Quantify costs</strong> (e.g., validation budgets) and mitigations. Pitch to sector-savvy VCs to skip basics. Highlight how compliance enables expansion—e.g., <strong>SOC2</strong> certification boosts trust, closing deals 2x faster.</p>

<h2 id="one-tip-for-your-first-regulatory-submission">One Tip for Your First Regulatory Submission</h2>

<p>Facing <strong>FDA</strong> or <strong>HIPAA</strong>? Start informed. For complicated problems—look aside and see how others do it. Research vendor <strong>SOC2</strong> selection guidance available through industry resources. Understand your pathway and have a clear description of your intent of use.</p>

<p><strong>Know your FDA class early</strong> via 513(g) requests; document thoroughly (treat it as strategic communication); collaborate via Pre-Sub meetings. For <strong>HIPAA</strong>, assess applicability first—many apps aren’t covered. Aim narrow for initial submissions to iterate later, avoiding common failures like incomplete indications.</p>

<h2 id="why-compliance-pays-off-the-bigger-picture">Why Compliance Pays Off: The Bigger Picture</h2>

<p>Non-compliance isn’t just risky—it’s ruinous, with breaches costing tens and hundreds of thousands of dollars for unprepared firms. Yet, proactive startups see ROI: Faster market entry, investor confidence, and scalable innovation. As regs like AI guidelines tighten, embed compliance now. Resources like FDA’s “breakthrough” designated programs lower barriers. For more, revisit the <a href="https://streamyard.com/watch/GWAw3FHAfUZy">panel recording</a> or consult experts—your guardrails today fuel tomorrow’s breakthroughs.</p>

<p><em>Insights blend panel discussions from September 9, 2025, with industry best practices. Always seek tailored legal advice.</em></p>]]></content><author><name>Andrei Radulescu-Banu</name></author><category term="ai" /><category term="programming" /><category term="tech" /><category term="reviews" /><summary type="html"><![CDATA[In the dynamic world of startups, especially those operating in heavily regulated sectors like healthcare, fintech, AI, and consumer hardware, compliance isn’t just a checkbox—it’s a foundational element that safeguards innovation, protects users, and unlocks growth opportunities.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://sigagent.ai/assets/images/engineering_with_guardrails.png" /><media:content medium="image" url="https://sigagent.ai/assets/images/engineering_with_guardrails.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Build Your Company Website with Analytiq Pages</title><link href="https://sigagent.ai/webdev/jekyll/tailwind/github-pages/company-website/analytiq-pages-github-pages-setup/" rel="alternate" type="text/html" title="Build Your Company Website with Analytiq Pages" /><published>2025-08-30T00:00:00+00:00</published><updated>2025-08-30T00:00:00+00:00</updated><id>https://sigagent.ai/webdev/jekyll/tailwind/github-pages/company-website/analytiq-pages-github-pages-setup</id><content type="html" xml:base="https://sigagent.ai/webdev/jekyll/tailwind/github-pages/company-website/analytiq-pages-github-pages-setup/"><![CDATA[<p>Setting up a professional company website shouldn’t require a team of developers or expensive hosting solutions. <strong>Analytiq Pages</strong> is our streamlined approach to building beautiful, fast company websites using GitHub Pages, Jekyll, and Tailwind CSS - completely free and with enterprise-grade reliability.</p>

<p>📢 <a href="https://www.linkedin.com/feed/update/urn:li:activity:7367581674697629697/">Join the Analytiq Pages discussion on LinkedIn</a></p>

<h2 id="why-analytiq-pages">Why Analytiq Pages?</h2>

<p>After building numerous company websites and helping startups establish their web presence, we’ve distilled the best practices into a reproducible system that delivers:</p>

<ul>
  <li><strong>Zero hosting costs</strong> with GitHub Pages</li>
  <li><strong>Professional design</strong> with Tailwind CSS</li>
  <li><strong>Easy content creation</strong> using Markdown</li>
  <li><strong>Git sandbox</strong> edited with Claude Code, Cursor to create content and visualizations</li>
  <li><strong>Fast deployment</strong> with git-based workflows</li>
  <li><strong>Enterprise reliability</strong> backed by GitHub’s infrastructure</li>
</ul>

<h2 id="the-analytiq-pages-stack">The Analytiq Pages Stack</h2>

<div class="grid md:grid-cols-3 gap-8 my-8">
  <div class="bg-white rounded-lg shadow-lg p-6 border border-gray-200">
    <div class="w-12 h-12 bg-gray-900 rounded-lg flex items-center justify-center mb-4">
      <svg class="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24">
        <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
      </svg>
    </div>
    <h3 class="text-xl font-semibold text-gray-900 mb-3">GitHub Pages</h3>
    <p class="text-gray-600">Free, reliable hosting with enterprise-grade infrastructure</p>
  </div>

  <div class="bg-white rounded-lg shadow-lg p-6 border border-gray-200">
    <div class="w-12 h-12 bg-red-600 rounded-lg flex items-center justify-center mb-4">
      <svg class="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24">
        <path d="M12.017 0C5.396 0 .029 5.367.029 11.987c0 5.079 3.158 9.417 7.618 11.024-.105-.949-.199-2.403.041-3.439.219-.937 1.404-5.965 1.404-5.965s-.359-.72-.359-1.781c0-1.663.967-2.911 2.168-2.911 1.024 0 1.518.769 1.518 1.688 0 1.029-.653 2.567-.992 3.992-.285 1.193.6 2.165 1.775 2.165 2.128 0 3.768-2.245 3.768-5.487 0-2.861-2.063-4.869-5.008-4.869-3.41 0-5.409 2.562-5.409 5.199 0 1.033.394 2.143.889 2.747.099.12.112.225.085.345-.09.375-.293 1.199-.334 1.363-.053.225-.172.271-.402.165-1.495-.69-2.433-2.878-2.433-4.646 0-3.776 2.748-7.252 7.92-7.252 4.158 0 7.392 2.967 7.392 6.923 0 4.135-2.607 7.462-6.233 7.462-1.214 0-2.357-.629-2.747-1.378l-.748 2.853c-.271 1.043-1.002 2.35-1.492 3.146C9.57 23.812 10.763 24.009 12.017 24c6.624 0 11.99-5.367 11.99-11.987C24.007 5.367 18.641.001 12.017.001z" />
      </svg>
    </div>
    <h3 class="text-xl font-semibold text-gray-900 mb-3">Jekyll</h3>
    <p class="text-gray-600">Write content in simple Markdown</p>
  </div>

  <div class="bg-white rounded-lg shadow-lg p-6 border border-gray-200">
    <div class="w-12 h-12 bg-cyan-500 rounded-lg flex items-center justify-center mb-4">
      <svg class="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24">
        <path d="M12.001,4.8c-3.2,0-5.2,1.6-6,4.8c1.2-1.6,2.6-2.2,4.2-1.8c0.913,0.228,1.565,0.89,2.288,1.624 C13.666,10.618,15.027,12,18.001,12c3.2,0,5.2-1.6,6-4.8c-1.2,1.6-2.6,2.2-4.2,1.8c-0.913-0.228-1.565-0.89-2.288-1.624 C16.337,6.182,14.976,4.8,12.001,4.8z M6.001,12c-3.2,0-5.2,1.6-6,4.8c1.2-1.6,2.6-2.2,4.2-1.8c0.913,0.228,1.565,0.89,2.288,1.624 c1.177,1.194,2.538,2.576,5.512,2.576c3.2,0,5.2-1.6,6-4.8c-1.2,1.6-2.6,2.2-4.2,1.8c-0.913-0.228-1.565-0.89-2.288-1.624 C10.337,13.382,8.976,12,6.001,12z" />
      </svg>
    </div>
    <h3 class="text-xl font-semibold text-gray-900 mb-3">Tailwind CSS</h3>
    <p class="text-gray-600">Embed HTML+Tailwind directly in Markdown for sophisticated layouts</p>
  </div>
</div>

<h2 id="setting-up-your-company-website">Setting Up Your Company Website</h2>

<p>The fastest way to get started is by using our reference implementation at <a href="https://analytiqhub.com/">analytiqhub.com</a> as your starting point. This site demonstrates all the Analytiq Pages features in action and serves as a complete template for your company website. You’ll clone this proven foundation and customize it with your branding, content, and specific requirements.</p>

<p>Let’s walk through creating a professional company website using the Analytiq Pages approach.</p>

<h3 id="step-1-repository-setup">Step 1: Repository Setup</h3>

<p>Start by forking the Analytiq Pages reference implementation:</p>

<ol>
  <li><strong>Fork the repository</strong>: Go to <a href="https://github.com/analytiq-hub/analytiq-hub.github.io">https://github.com/analytiq-hub/analytiq-hub.github.io</a> and click “Fork”</li>
  <li><strong>Rename your fork</strong>: In your forked repository settings, rename it to <code class="language-plaintext highlighter-rouge">your-company.github.io</code></li>
  <li><strong>Clone for local development</strong>:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Clone your forked repository</span>
git clone https://github.com/your-company/your-company.github.io.git
<span class="nb">cd </span>your-company.github.io
</code></pre></div></div>

<h3 id="step-2-customize-for-your-company">Step 2: Customize for Your Company</h3>

<p>Replace all Analytiq Hub references with your company information:</p>

<p><strong>Key files to update:</strong></p>

<ol>
  <li><strong><code class="language-plaintext highlighter-rouge">_config.yml</code></strong> - Site configuration:
    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">title</span><span class="pi">:</span> <span class="s">Your Company Name</span>
<span class="na">email</span><span class="pi">:</span> <span class="s">contact@yourcompany.com</span>
<span class="na">description</span><span class="pi">:</span> <span class="s">Your company description</span>
<span class="na">baseurl</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>
<span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">http://your-company.github.io"</span>
<span class="na">github_username</span><span class="pi">:</span> <span class="s">yourcompany</span>
</code></pre></div>    </div>
  </li>
  <li><strong><code class="language-plaintext highlighter-rouge">index.md</code></strong> - Homepage content</li>
  <li><strong><code class="language-plaintext highlighter-rouge">about.md</code></strong> - Company information</li>
  <li><strong><code class="language-plaintext highlighter-rouge">_posts/</code></strong> - Replace sample blog posts with your content</li>
  <li><strong><code class="language-plaintext highlighter-rouge">assets/images/</code></strong> - Replace logos and images</li>
  <li><strong><code class="language-plaintext highlighter-rouge">CNAME</code></strong> - Update with your custom domain (if using one)</li>
</ol>

<p><strong>GitHub Pages setup:</strong></p>
<ul>
  <li>Go to your repository Settings → Pages</li>
  <li>Set source to “Deploy from a branch” → main</li>
  <li>Add custom domain if you have one</li>
</ul>

<p>The forked template already includes Tailwind CSS configuration, so your site is ready to run locally with <code class="language-plaintext highlighter-rouge">make dev</code> or deploy immediately to GitHub Pages.</p>

<h3 id="step-3-local-development-setup">Step 3: Local Development Setup</h3>

<p>Install <a href="https://analytiqhub.com/docs/local_development">local development prerequisites</a> and start developing locally:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Install dependencies</span>
make <span class="nb">install</span>

<span class="c"># Start development server</span>
make dev
</code></pre></div></div>

<p>For complete setup instructions including Ruby, Bundler, and troubleshooting, see the full <a href="https://analytiqhub.com/docs/local_development">Local Development Setup guide</a>.</p>

<p>Point your web browser to the local development server at <a href="http://localhost:4000">http://localhost:4000</a>.</p>

<p>After editing files in the sandbox, manually or with Claude Code, Cursor or the preferred AI editor, they are automatically refreshed on the development server.</p>
<ul>
  <li>Changes to the menu or the footing require a restart of the local development server.</li>
  <li>When later the github pages pipeline is setup, a push of local changes will trigger the web site update at https://your-company.github.io</li>
</ul>

<h3 id="step-4-essential-company-pages">Step 4: Essential Company Pages</h3>

<p>Update the core pages with your company information:</p>
<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">index.md</code></strong> - Homepage with compelling hero section and company value proposition</li>
  <li><strong><code class="language-plaintext highlighter-rouge">about.md</code></strong> - Company story, team information, and mission</li>
  <li><strong><code class="language-plaintext highlighter-rouge">contact.md</code></strong> - Contact information and inquiry forms</li>
</ul>

<h3 id="step-5-header-and-footer-customization">Step 5: Header and Footer Customization</h3>

<p>Start by configuring your site navigation in <strong><code class="language-plaintext highlighter-rouge">_config.yml</code></strong>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Header navigation menu</span>
<span class="na">header_pages</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Services"</span>
    <span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">#"</span>
    <span class="na">children</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Consulting"</span>
        <span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/consulting"</span>
      <span class="pi">-</span> <span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Development"</span>
        <span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/development"</span>
  <span class="pi">-</span> <span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Case</span><span class="nv"> </span><span class="s">Studies"</span>
    <span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/case-studies"</span>
  <span class="pi">-</span> <span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Blog"</span>
    <span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/blog"</span>
  <span class="pi">-</span> <span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">About"</span>
    <span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/about"</span>
  <span class="pi">-</span> <span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Contact"</span>
    <span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/contact"</span>
    <span class="na">button_style</span><span class="pi">:</span> <span class="s2">"</span><span class="s">solid"</span>

<span class="c1"># Footer sitemap</span>
<span class="na">site_map</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Services"</span>
    <span class="na">links</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Consulting"</span>
        <span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/consulting"</span>
      <span class="pi">-</span> <span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Case</span><span class="nv"> </span><span class="s">Studies"</span>
        <span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/case-studies"</span>
  <span class="pi">-</span> <span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Company"</span>
    <span class="na">links</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">About"</span>
        <span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/about"</span>
      <span class="pi">-</span> <span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Contact"</span>
        <span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/contact"</span>
</code></pre></div></div>

<p>Then customize the visual elements:</p>
<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">_includes/custom-header.html</code></strong> - Company logo, announcements, search</li>
  <li><strong><code class="language-plaintext highlighter-rouge">_includes/custom-footer.html</code></strong> - Contact info, social links, legal pages</li>
</ul>

<h3 id="step-6-blog-setup">Step 6: Blog Setup</h3>

<p>Jekyll’s blog functionality is ready out of the box:</p>
<ul>
  <li>Create posts in <code class="language-plaintext highlighter-rouge">_posts/</code> using the format: <code class="language-plaintext highlighter-rouge">YYYY-MM-DD-post-title.md</code></li>
  <li>Posts automatically appear on your homepage and <code class="language-plaintext highlighter-rouge">/blog</code> page</li>
  <li>Use front matter to set title, author, categories, and featured images</li>
</ul>

<h3 id="step-7-case-studies">Step 7: Case Studies</h3>

<p>Showcase your work with detailed case studies:</p>
<ul>
  <li>Add case studies to the <code class="language-plaintext highlighter-rouge">_case_studies/</code> collection</li>
  <li>Use the case study template for consistent formatting</li>
  <li>Include client results, project images, and key outcomes</li>
</ul>

<h3 id="step-8-advanced-custom-components">Step 8: (Advanced) Custom Components</h3>

<p>The template includes Tailwind-powered components for:</p>
<ul>
  <li>Call-to-action sections</li>
  <li>Team member profiles</li>
  <li>Service feature cards</li>
  <li>Client testimonials</li>
</ul>

<p>Your AI editor can help create custom components by combining Jekyll’s liquid templating with Tailwind’s utility classes.</p>

<h3 id="step-9-advanced-custom-layouts">Step 9: (Advanced) Custom Layouts</h3>

<p>Create specialized page layouts by extending the base templates:</p>
<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">_layouts/landing.html</code></strong> - For marketing campaigns and product launches</li>
  <li><strong><code class="language-plaintext highlighter-rouge">_layouts/portfolio.html</code></strong> - Showcase projects with image galleries</li>
  <li><strong><code class="language-plaintext highlighter-rouge">_layouts/team.html</code></strong> - Team member profiles with bios and photos</li>
</ul>

<p>Copy existing layouts as starting points and modify with your specific content structure and Tailwind styling.</p>

<h2 id="deployment-and-domain-setup">Deployment and Domain Setup</h2>

<h3 id="github-pages-configuration">GitHub Pages Configuration</h3>

<ol>
  <li><strong>Enable GitHub Pages</strong> in repository settings</li>
  <li><strong>Set source</strong> to “Deploy from a branch” → main</li>
  <li><strong>Custom domain</strong>: Add your company domain in settings</li>
</ol>

<h3 id="domain-configuration">Domain Configuration</h3>

<p><strong>DNS Setup:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># For apex domain (company.com)
A record: 185.199.108.153
A record: 185.199.109.153
A record: 185.199.110.153
A record: 185.199.111.153

# For www subdomain
CNAME record: your-company.github.io
</code></pre></div></div>

<p><strong>CNAME File:</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"your-company.com"</span> <span class="o">&gt;</span> CNAME
</code></pre></div></div>

<h2 id="analytiq-pages-best-practices">Analytiq Pages Best Practices</h2>

<h3 id="content-strategy">Content Strategy</h3>
<ul>
  <li><strong>Homepage</strong>: Clear value proposition and call-to-action</li>
  <li><strong>About</strong>: Company story and team credibility</li>
  <li><strong>Services/Products</strong>: Detailed offering descriptions</li>
  <li><strong>Blog</strong>: Regular insights to build authority</li>
  <li><strong>Contact</strong>: Multiple ways to reach you</li>
</ul>

<h3 id="seo-foundation">SEO Foundation</h3>
<ul>
  <li><strong>Meta descriptions</strong>: Add to each page’s front matter</li>
  <li><strong>Google Analytics</strong>: Add tracking code to <code class="language-plaintext highlighter-rouge">_includes/custom-head.html</code></li>
</ul>

<h2 id="maintenance-and-updates">Maintenance and Updates</h2>

<p>The beauty of Analytiq Pages is its simplicity:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Update content</span>
git add <span class="nb">.</span>
git commit <span class="nt">-m</span> <span class="s2">"Update company blog post"</span>
git push origin main
<span class="c"># Site updates automatically within minutes</span>
</code></pre></div></div>

<h2 id="why-companies-choose-analytiq-pages">Why Companies Choose Analytiq Pages</h2>

<ul>
  <li><strong>Startups</strong>: Get online fast without burning budget on hosting or developers</li>
  <li><strong>Agencies</strong>: Deliver professional sites quickly for clients</li>
  <li><strong>Enterprise</strong>: Maintain security and compliance with git-based workflows</li>
  <li><strong>Content Teams</strong>: Edit in Markdown without technical dependencies</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>Analytiq Pages combines the best of modern web development - GitHub’s reliability, Markdown simplicity, and Tailwind’s design power - into a streamlined system perfect for company websites. Whether you’re launching a startup or refreshing an enterprise web presence, this approach delivers professional results without the complexity.</p>

<p>Ready to build your company website? Check out our <a href="https://github.com/analytiq-hub/analytiq-pages">Analytiq Pages starter template</a> or <a href="/contact">contact us</a> for custom implementation support.</p>

<hr />

<p><em>Want to see Analytiq Pages in action? This very website was built using these exact techniques. View the <a href="https://github.com/analytiq-hub/analytiq-hub.github.io">source code</a> to see how we implement our own recommendations.</em></p>]]></content><author><name>Andrei Radulescu-Banu</name></author><category term="webdev" /><category term="jekyll" /><category term="tailwind" /><category term="github-pages" /><category term="company-website" /><summary type="html"><![CDATA[Setting up a professional company website shouldn’t require a team of developers or expensive hosting solutions. Analytiq Pages is our streamlined approach to building beautiful, fast company websites using GitHub Pages, Jekyll, and Tailwind CSS - completely free and with enterprise-grade reliability.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://sigagent.ai/assets/images/analytiq_pages.png" /><media:content medium="image" url="https://sigagent.ai/assets/images/analytiq_pages.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">From Jekyll Minima to Tailwind: A Seamless Migration Story</title><link href="https://sigagent.ai/webdev/jekyll/tailwind/jekyll-minima-to-tailwind-migration/" rel="alternate" type="text/html" title="From Jekyll Minima to Tailwind: A Seamless Migration Story" /><published>2025-08-20T00:00:00+00:00</published><updated>2025-08-20T00:00:00+00:00</updated><id>https://sigagent.ai/webdev/jekyll/tailwind/jekyll-minima-to-tailwind-migration</id><content type="html" xml:base="https://sigagent.ai/webdev/jekyll/tailwind/jekyll-minima-to-tailwind-migration/"><![CDATA[<p>I can’t believe it. Claude Code was able to update my Jekyll-based site <a href="https://bitdribble.github.io">bitdribble.github.io</a> to use Tailwind pretty much with no intervention. The transformation from the old, less flexible Minima theme to a modern Tailwind-powered setup was vibe coded with a few light touches.</p>

<h2 id="the-challenge-with-minima">The Challenge with Minima</h2>

<p>For years, I’ve been running my personal knowledge repository on Jekyll with the default Minima theme. While Minima served its purpose, it had several limitations. The look and feel was outdated, and layouts were especially rigid.</p>

<h2 id="enter-tailwind-css">Enter Tailwind CSS</h2>

<p>Tailwind CSS has become the go-to utility-first CSS framework for modern web development, and for good reason:</p>
<ul>
  <li>It is much simpler than CSS</li>
  <li>It is responsove out of the box, built with mobile-first principles</li>
  <li>AI editors like Claude Code and Cursor are very fluent with Tailwind.</li>
</ul>

<h2 id="the-migration-process">The Migration Process</h2>

<p>What surprised me most was how seamless the migration turned out to be.</p>

<h3 id="development-environment-setup">Development Environment Setup</h3>

<p>The migration was performed on <strong>Fedora Linux</strong> using a simple but effective workflow:</p>

<ol>
  <li><strong>Repository Setup:</strong> Checked out the Git repository from the command line:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/bitdribble/bitdribble.github.io.git
<span class="nb">cd </span>bitdribble.github.io
</code></pre></div>    </div>
  </li>
  <li>
    <p><strong>IDE Integration:</strong> Loaded the project in <strong>Cursor</strong> (this works equally well in <strong>VSCode</strong>):</p>
  </li>
  <li><strong>Claude Code Extension:</strong> Enabled the Claude Code add-in, which, like Cursor, provides:
    <ul>
      <li>Intelligent code suggestions and refactoring</li>
      <li>Context-aware assistance with Jekyll and Tailwind</li>
      <li>Seamless understanding of project structure and dependencies</li>
    </ul>
  </li>
</ol>

<h3 id="how-claude-code-transformed-the-migration">How Claude Code Transformed the Migration</h3>

<p>The AI assistant proved exceptionally capable at understanding both Jekyll’s architecture and Tailwind’s utility-first approach. Here’s what made it work so well:</p>

<h3 id="1-jekylls-markdown-flexibility">1. Jekyll’s Markdown Flexibility</h3>

<p><strong>Almost all Jekyll pages can be written in pure Markdown</strong>. Take a look at my <a href="https://bitdribble.github.io/markdown/">markdown example page</a> - it’s a full demonstration of how Jekyll processes Markdown content beautifully, even with the new Tailwind styling.</p>
<ul>
  <li>Content creators don’t need to know HTML or CSS</li>
  <li>Blog posts remain simple and focused on content</li>
</ul>

<h3 id="2-inline-html-when-needed">2. Inline HTML When Needed</h3>

<p>When you need more sophisticated layouts or Tailwind-specific components, Jekyll’s Markdown processor allows you to <strong>embed HTML+Tailwind directly in your Markdown files</strong>. For example, I was able to create a sophisticated three-column layout for my about page by simply adding:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"bg-white rounded-lg shadow-lg p-8"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"grid md:grid-cols-3 gap-8 items-start"</span><span class="nt">&gt;</span>
    <span class="c">&lt;!-- Column content here --&gt;</span>
  <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</code></pre></div></div>

<p><strong>Live Example:</strong> Here’s that same three-column layout in action with actual content:</p>

<div class="bg-white rounded-lg shadow-lg p-8 my-6">
  <div class="grid md:grid-cols-3 gap-8 items-start">
    <div class="text-center">
      <div class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4">
        <svg class="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
        </svg>
      </div>
      <h3 class="text-lg font-semibold text-gray-900 mb-2">Markdown Content</h3>
      <p class="text-gray-600 text-sm">Write content in simple Markdown syntax without worrying about styling complexities.</p>
    </div>
    
    <div class="text-center">
      <div class="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
        <svg class="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path>
        </svg>
      </div>
      <h3 class="text-lg font-semibold text-gray-900 mb-2">Tailwind Styling</h3>
      <p class="text-gray-600 text-sm">Add sophisticated layouts with utility-first CSS classes when you need more control.</p>
    </div>
    
    <div class="text-center">
      <div class="w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-4">
        <svg class="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
        </svg>
      </div>
      <h3 class="text-lg font-semibold text-gray-900 mb-2">Jekyll Processing</h3>
      <p class="text-gray-600 text-sm">Jekyll seamlessly processes both Markdown and HTML, giving you complete flexibility.</p>
    </div>
  </div>
</div>

<p>This flexibility gives you the best of both worlds - simple content editing in Markdown, with the power to create complex layouts when needed.</p>

<h3 id="3-enhanced-features-with-jekyll">3. Enhanced Features with Jekyll</h3>

<p>The Jekyll + Tailwind combination also preserves and enhances Jekyll’s powerful features. Take a look at the <a href="https://bitdribble.github.io/markdown/">markdown example page</a> which demonstrates:</p>

<p><strong>Code Syntax Highlighting:</strong> Jekyll automatically highlights code blocks with proper syntax coloring for multiple languages:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">fibonacci</span><span class="p">(</span><span class="n">n</span><span class="p">):</span>
    <span class="k">if</span> <span class="n">n</span> <span class="o">&lt;=</span> <span class="mi">1</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">n</span>
    <span class="k">return</span> <span class="n">fibonacci</span><span class="p">(</span><span class="n">n</span><span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="o">+</span> <span class="n">fibonacci</span><span class="p">(</span><span class="n">n</span><span class="o">-</span><span class="mi">2</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>MathJax Integration:</strong> Mathematical formulas can be enabled by adding <code class="language-plaintext highlighter-rouge">mathjax: true</code> to the page front matter:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">layout</span><span class="pi">:</span> <span class="s">page</span>
<span class="na">title</span><span class="pi">:</span> <span class="s">Your Page Title</span>
<span class="na">mathjax</span><span class="pi">:</span> <span class="no">true</span>
<span class="nn">---</span>
</code></pre></div></div>

<p>Then you can write beautiful mathematical expressions:</p>
<ul>
  <li>Inline math: $E = mc^2$</li>
  <li>Display equations: 
\(\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi}\)</li>
</ul>

<p><strong>Page Headers:</strong> Jekyll’s front matter system makes it easy to configure pages with metadata, layout selection, and feature toggles like MathJax - all while maintaining the flexibility to use Tailwind styling throughout the content.</p>

<h2 id="the-payoff">The Payoff</h2>

<p>The transformation has been dramatic. Here’s a side-by-side comparison of the old Minima theme versus the new Tailwind-powered design:</p>

<div class="grid md:grid-cols-2 gap-8 my-8">
  <div class="space-y-4">
    <h3 class="!text-sm font-semibold text-gray-900">Before: Jekyll Minima Theme</h3>
    <img src="/assets/images/bitdribble_github_io_old.png" alt="Old Jekyll Minima Site" class="w-full rounded-lg shadow-lg border border-gray-200" />
    <p class="text-base text-gray-600 italic">The original site using Jekyll's Minima theme with dark styling</p>
  </div>
  
  <div class="space-y-4">
    <h3 class="!text-sm font-semibold text-gray-900">After: Tailwind CSS Design</h3>
    <img src="/assets/images/bitdribble_github_io_new.png" alt="New Tailwind CSS Site" class="w-full rounded-lg shadow-lg border border-gray-200" />
    <p class="text-base text-gray-600 italic">The modern site with Tailwind CSS featuring improved navigation and visual hierarchy</p>
  </div>
</div>

<p>The site now boasts:</p>
<ul>
  <li><strong>Modern visuals</strong>: Professional and polished</li>
  <li><strong>Performance</strong>: Purged CSS for leaner loads</li>
  <li><strong>Flexibility</strong>: I can create any layout I can imagine without fighting the framework</li>
</ul>

<h2 id="why-jekyll--tailwind-rocks">Why Jekyll + Tailwind Rocks</h2>

<ul>
  <li><strong>Content Teams</strong>: Edit in Markdown, no CSS needed.</li>
  <li><strong>Developers</strong>: Modern tooling, reusable components</li>
  <li><strong>Businesses</strong>: Fast, SEO-friendly, low-cost hosting (GitHub Pages, Netlify)</li>
</ul>

<h2 id="technical-implementation">Technical Implementation</h2>

<p>The migration involved several key technical steps that Claude Code helped orchestrate seamlessly:</p>

<h3 id="1-tailwind-css-integration">1. Tailwind CSS Integration</h3>

<p><strong>Adding the Tailwind CLI:</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Downloaded the standalone Tailwind CLI binary</span>
curl <span class="nt">-sLO</span> https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64
<span class="nb">chmod</span> +x tailwindcss-linux-x64
<span class="nb">mv </span>tailwindcss-linux-x64 tailwindcss
</code></pre></div></div>

<p><strong>Created Tailwind Configuration:</strong></p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// tailwind.config.js</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">content</span><span class="p">:</span> <span class="p">[</span>
    <span class="dl">'</span><span class="s1">./_includes/**/*.html</span><span class="dl">'</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">./_layouts/**/*.html</span><span class="dl">'</span><span class="p">,</span> 
    <span class="dl">'</span><span class="s1">./_posts/**/*.md</span><span class="dl">'</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">./*.html</span><span class="dl">'</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">./*.md</span><span class="dl">'</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">./**/*.md</span><span class="dl">'</span>
  <span class="p">],</span>
  <span class="na">theme</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">extend</span><span class="p">:</span> <span class="p">{</span>
      <span class="c1">// Custom colors and styling</span>
    <span class="p">},</span>
  <span class="p">},</span>
  <span class="na">plugins</span><span class="p">:</span> <span class="p">[],</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Build Process Integration:</strong>
Created a <code class="language-plaintext highlighter-rouge">Makefile</code> to streamline development with proper process management: <code class="language-plaintext highlighter-rouge">make dev</code> starts the web server locally, and <code class="language-plaintext highlighter-rouge">Ctrl-C</code> stops it:</p>
<div class="language-makefile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Development with live reload and signal handling
</span><span class="nl">dev</span><span class="o">:</span>
	<span class="p">@</span><span class="nb">echo</span> <span class="s2">"Starting development environment..."</span>
	<span class="p">@</span><span class="nb">trap</span> <span class="s1">'echo "Stopping all processes..."; kill 0'</span> INT<span class="p">;</span> <span class="se">\</span>
	./tailwindcss <span class="nt">-o</span> assets/css/tailwind.css <span class="nt">--watch</span> &amp; <span class="se">\</span>
	<span class="nv">TAILWIND_PID</span><span class="o">=</span><span class="nv">$$</span><span class="o">!</span><span class="p">;</span> <span class="se">\</span>
	bundle <span class="nb">exec </span>jekyll serve &amp; <span class="se">\</span>
	<span class="nv">JEKYLL_PID</span><span class="o">=</span><span class="nv">$$</span><span class="o">!</span><span class="p">;</span> <span class="se">\</span>
	<span class="nb">echo</span> <span class="s2">"Development server running. Press Ctrl+C to stop both processes."</span><span class="p">;</span> <span class="se">\</span>
	<span class="nb">wait</span>

<span class="c"># Production build
</span><span class="nl">build</span><span class="o">:</span>
	./tailwindcss <span class="nt">-o</span> assets/css/tailwind.css <span class="nt">--minify</span>
	bundle <span class="nb">exec </span>jekyll build
</code></pre></div></div>

<h3 id="2-replacing-the-minima-theme">2. Replacing the Minima Theme</h3>

<p><strong>Removed Theme Dependency:</strong></p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># _config.yml - Commented out the minima theme</span>
<span class="c1"># theme: minima  # Removed - using Tailwind CSS instead</span>
</code></pre></div></div>

<p><strong>Layout System Redesign:</strong></p>
<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">_layouts/default.html</code></strong>: Created a new base layout with Tailwind styling</li>
  <li><strong><code class="language-plaintext highlighter-rouge">_layouts/home.html</code></strong>: Redesigned blog listing with card-based design</li>
  <li><strong><code class="language-plaintext highlighter-rouge">_layouts/page.html</code></strong>: Clean page layout with proper typography</li>
  <li><strong><code class="language-plaintext highlighter-rouge">_layouts/post.html</code></strong>: Enhanced blog post layout with better readability</li>
</ul>

<h3 id="3-component-migration-strategy">3. Component Migration Strategy</h3>

<p><strong>Navigation System:</strong>
Replaced Minima’s navigation with a modern Tailwind-based header featuring:</p>
<ul>
  <li>Responsive dropdown menus</li>
  <li>Clean typography and spacing</li>
</ul>

<h3 id="4-content-preservation">4. Content Preservation</h3>

<p><strong>Front Matter Compatibility:</strong>
All existing blog posts and pages continued to work without modification. Jekyll’s front matter system remained unchanged:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">layout</span><span class="pi">:</span> <span class="s">post</span>
<span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">My</span><span class="nv"> </span><span class="s">Blog</span><span class="nv"> </span><span class="s">Post"</span>
<span class="na">date</span><span class="pi">:</span> <span class="s">2025-01-20</span>
<span class="na">categories</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">webdev</span><span class="pi">,</span> <span class="nv">jekyll</span><span class="pi">]</span>
<span class="nn">---</span>
</code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>

<p>Jekyll + Tailwind is a powerhouse for blogs and company sites, blending content simplicity with design flexibility. Claude Code nailed the migration, delivering a stunning result with minimal effort. Check the <a href="https://github.com/bitdribble/bitdribble.github.io">source code</a> or <a href="https://bitdribble.github.io">live site</a> to see it in action!</p>]]></content><author><name>Andrei Radulescu-Banu</name></author><category term="webdev" /><category term="jekyll" /><category term="tailwind" /><summary type="html"><![CDATA[I can’t believe it. Claude Code was able to update my Jekyll-based site bitdribble.github.io to use Tailwind pretty much with no intervention. The transformation from the old, less flexible Minima theme to a modern Tailwind-powered setup was vibe coded with a few light touches.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://sigagent.ai/assets/images/jekyll-tailwind.png" /><media:content medium="image" url="https://sigagent.ai/assets/images/jekyll-tailwind.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">DocRouter.AI: Adventures in CSS and AI Coding</title><link href="https://sigagent.ai/ai/programming/tech/reviews/docrouter-cursor-claude/" rel="alternate" type="text/html" title="DocRouter.AI: Adventures in CSS and AI Coding" /><published>2025-07-29T00:00:00+00:00</published><updated>2025-07-29T00:00:00+00:00</updated><id>https://sigagent.ai/ai/programming/tech/reviews/docrouter-cursor-claude</id><content type="html" xml:base="https://sigagent.ai/ai/programming/tech/reviews/docrouter-cursor-claude/"><![CDATA[<p>DocRouter.AI transforms messy, multi-layout business documents into clean, structured data using large language models (LLMs) and schema-driven orchestration. We focus on regulated industries like insurance, healthcare, and supply chain, where precision is non-negotiable. It’s a horizontal data layer application that plugs into vertical-specific apps, acting as an AI accelerator across sectors.</p>

<h2 id="docrouterai-user-experience">DocRouter.AI User Experience</h2>

<p><img src="/assets/images/doc_router_user_experience.png" alt="DocRouter.AI User Experience" style="display: block; margin: 2rem auto; width: 80%; border-radius: 0.5rem; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);" /></p>

<p>We’re developing DocRouter.AI as open source because experience shows it leads to better-designed, more resilient code over time. Business value comes from our SaaS version at app.docrouter.ai and enterprise on-VPC installations. We also offer consulting to help other companies build software using similar styles and tools.</p>

<p><img src="/assets/images/doc_router_repo.png" alt="DocRouter.AI Repository" style="display: block; margin: 2rem auto; width: 80%; border-radius: 0.5rem; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);" /></p>

<p><strong>GitHub Repository:</strong> <a href="https://github.com/analytiq-hub/doc-router">github.com/analytiq-hub/doc-router</a></p>

<p>A revolution is underway in software development with tools like Cursor and Claude Code. I’ll dive into how we built DocRouter.AI, sharing lessons that could apply to your projects. (I’ve detailed my tool usage in a previous LinkedIn post, so I won’t repeat it here.)</p>

<h2 id="my-background-from-embedded-systems-to-ai-driven-front-ends">My Background: From Embedded Systems to AI-Driven Front Ends</h2>

<p>I come from a world of cloud, back-end, and embedded work, starting long ago with Linux kernel programming, computer networks, then high performance computing for Wall St, followed by robotics/ROS/computer vision.</p>

<p>In short, my expertise is embedded back-end, and data science. When starting DocRouter.AI, I had zero hands-on experience with front-end development— no JavaScript, TypeScript, or React.</p>

<p>But tools like GitHub Copilot (which helped me learn Apache Spark and Terraform from scratch in prior roles) paved the way.</p>

<p>By fall of last year, Cursor enabled me to build DocRouter.AI with a Next.js front end, FastAPI back end, and AWS via Terraform. Claude Code exploded about a month ago, and I’ve began using it intensively.</p>

<h2 id="lessons-from-ai-coding-tools-what-works-and-what-doesnt">Lessons from AI Coding Tools: What Works and What Doesn’t</h2>

<p><img src="/assets/images/claude_code_vs_cursor.png" alt="Claude Code vs Cursor" style="display: block; margin: 2rem auto; width: 80%; border-radius: 0.5rem; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);" /></p>

<p>We often hear engineers claiming they can “zero-shot vibe code” entire apps from scratch. That’s possible for simple, UI-focused apps, but for anything complex, the human engineer must stay in the loop. Knowing when to let AI take over and when to intervene is becoming a black art.</p>

<p>For DocRouter.AI, we use FastAPI on the back end, with all functions in a single file (<code class="language-plaintext highlighter-rouge">main.py</code>) to simplify AI editing—easier for pattern searching and consistency. If designing for humans, I’d split it into 8-10 files. But, for AI editors, a single large file simplifies things.</p>

<p>AI agents have “personalities.” Claude Code Agent outperforms Cursor Agent right now. I use Cursor in manual mode, attaching specific files. Keeping FastAPI in one file reduces attachment hassle.</p>

<p>AI editors love “improving” code inconsistently. Most of <code class="language-plaintext highlighter-rouge">main.py</code>, our FastAPI interface, was built with Cursor. But for web form support (more on why below), I tasked Claude Code with creating similar FastAPI endpoints (create, list, get, update, delete) to our existing schema ones.</p>

<p>Claude did great but switched to identifying things with UUIDs instead of our MongoDB <code class="language-plaintext highlighter-rouge">_id</code> scheme. I almost did not notice it. I caught it by asking Claude Code to explain in practice how the new APIs are used.</p>

<h2 id="why-schemas-web-forms-and-mongodb">Why Schemas, Web Forms, and MongoDB?</h2>

<p>I’m a fan of the simplest tool for the job. DocRouter.AI stores documents, LLM prompts, and extraction schemas—MongoDB collections are ideal. Mongo handles blobs via GridFS, avoiding external storage like S3 (which Postgres would require).</p>

<p><strong>Schemas:</strong> LLMs can output free-form or structured JSON. Providers support JSON schemas for precise LLM output. For docs, we classify types (e.g., via LLM prompt) then extract a specific schema—like date, patient name, DOB, address, diagnostics, doctor details in a medical prescription.</p>

<p>DocRouter.AI lets users configure extraction schemas for all doc types.</p>

<p><strong>Web Forms:</strong> Schemas are too abstract for end users accustomed to ERPs (e.g., Epic for EHR, Salesforce for CRM). End users are instead familiar with entering data into web forms tailored to their process.</p>

<p>We had to add web form support to DocRouter.AI: LLM extractions map to pre-populated form fields. LLM-as-judge scores confidence, so users focus on low-confidence fields—cutting effort by up to 90%.</p>

<p>To stay horizontal (not vertical-specific), we avoid hard-coding web forms. Instead, we integrate a schema builder, and a web form builder.</p>

<p>The web form builder was pretty complicated.</p>

<p>But the schema builder was vibe-coded in Cursor (manual mode, incremental development: first do the FastAPI back end, then the Axios APIs, then the UI components, and the unit testers). It worked out of the box!</p>

<p>For comparison, Base44 solo entrepreneur Maor Shlomo (podcast interview) uses MongoDB too, seeing it as AI-era friendly over SQL. He uses plain React and JavaScript; I use Next.js and TypeScript. His simpler stack might ease things—worth pondering.</p>

<p>Note that Next.js has server-side capabilities, but we use FastAPI for most back-end functions.</p>

<h2 id="docrouterai-document-processing">DocRouter.AI Document Processing</h2>

<h3 id="the-need-for-human-in-the-loop">The Need for Human-in-the-Loop</h3>

<p>LLMs are precise with context, but humans must correct rare errors.</p>

<p>The goal is to make human reviews simple - and focus them on likely mistakes.</p>

<p>Ease of adoption is also key. We must adapt to existing workflows, — and plug DocRouter gradually in the customer process.</p>

<p>Our multi-vertical approach gets many more use cases compared to a vertical approach - but demands flexibility and portable software design.</p>

<h3 id="how-docrouterai-works">How DocRouter.AI Works</h3>

<p>Here is an example DocRouter.AI use from one of our pilots with an insurance company:</p>

<p>We process Acord forms for personal/commercial insurance: We extract insured name/address, insurance type, and can extract coverage limits, loss runs, etc.</p>

<ul>
  <li>Files arrive as email attachments; tools like n8n upload via REST APIs to DocRouter workspace.</li>
  <li>We configure the extraction formats (e.g., insured details) and prompts (descriptions, examples, counter-examples).</li>
  <li>These are stored as JSON schemas</li>
  <li>We upload representative datasets of documents, and iterate schema &amp; prompt design, as well as vendor-agnostic LLM choice, for best accuracy/cost.</li>
  <li>We monitor accuracy, and tweak prompts (add examples/counter-examples) for edge cases.</li>
</ul>

<p>Extracted data can be corrected by the human-in-the-loop, and is available via REST APIs for ERP upload (e.g., TMS for insurance, Epic for hospitals).</p>

<h3 id="the-user-perspective-streamlining-reviews">The User Perspective: Streamlining Reviews</h3>

<p>Before DocRouter, users had to manually enter PDF data into ERP web/UI forms—a laborious process!</p>

<p>DocRouter pre-fills 90%+ fields with LLM extractions and, when configured, with confidence scores from LLM-as-Judge. Users then focus on low-confidence fields, ensuring perfect accuracy with minimal effort.</p>

<p>The human-in-the-loop is critical in regulated fields (healthcare, insurance, fintech, supply chain, legal) where errors in amounts, inventories, or obligations are unacceptable.</p>

<h3 id="achieving-accuracy-and-flexibility">Achieving Accuracy and Flexibility</h3>

<p>But direct LLM review is impractical. Users prefer familiar web forms.</p>

<p>We are designing the system so project managers can configure tailored web forms linked to extractions and confidences. For each doc, users review pre-populated forms flagged by confidence metrics.</p>

<h2 id="implementing-the-web-form-builder-css-adventures">Implementing the Web Form Builder: CSS Adventures</h2>

<p>Vibe coding couldn’t handle this yet—AI tools aren’t ready for complex builders.</p>

<h3 id="formio-builder-in-docrouterai">FormIO Builder in DocRouter.AI</h3>

<p>We integrated FormIO (nice UI npm package). But issues arose:</p>

<ul>
  <li>FormIO elements didn’t display right. Cursor/Claude couldn’t fix immediately.</li>
  <li>Root cause: FormIO uses Bootstrap; DocRouter uses Tailwind. Global CSS conflicts.</li>
</ul>

<p>Added <code class="language-plaintext highlighter-rouge">@tsed/react-formio</code> (React support) and <code class="language-plaintext highlighter-rouge">@tsed/tailwind-formio</code> (Bootstrap-Tailwind fix). Forms showed, but Tailwind broke—AI couldn’t diagnose.</p>

<p>Expert friends suggested including FormIO components through iframes or a shadow DOM.</p>

<ul>
  <li><strong>Shadow DOM:</strong> Claude iterated but drag-and-drop failed across the boundary with the thin DOM (FormIO incompatibility).</li>
  <li><strong>iframes:</strong> Overkill with API messaging for config/state.</li>
</ul>

<p>Stuck, I fiddled with CSS via browser inspector. Solution: Redefine Tailwind breakpoints last in global.css order:</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">//</span> <span class="nt">FormIO</span> <span class="o">+</span> <span class="nt">Bootstrap</span>
<span class="k">@import</span> <span class="s2">'formiojs/dist/formio.full.min.css'</span><span class="p">;</span>

<span class="o">//</span> <span class="nt">Tailwind</span>
<span class="k">@tailwind</span> <span class="n">base</span><span class="p">;</span> 
<span class="k">@tailwind</span> <span class="n">components</span><span class="p">;</span>
<span class="k">@tailwind</span> <span class="n">utilities</span><span class="p">;</span>

<span class="o">//</span> <span class="nt">Correction</span> <span class="nt">so</span> <span class="nt">Bootstrap</span> <span class="nt">works</span> <span class="nt">in</span> <span class="nt">FormIO</span>
<span class="k">@import</span> <span class="s1">"~@tsed/tailwind-formio/styles/index.css"</span><span class="p">;</span> 

<span class="o">//</span> <span class="nt">Correction</span> <span class="nt">to</span> <span class="nt">correction</span> 
<span class="o">//</span> <span class="nt">so</span> <span class="nt">Tailwind</span> <span class="nt">responsiveness</span> <span class="nt">works</span>
<span class="k">@import</span> <span class="s2">'./formio-custom.css'</span><span class="p">;</span> 
</code></pre></div></div>

<h2 id="key-takeaway-ai-tools-are-game-changers-but-humans-are-essential">Key Takeaway: AI Tools Are Game-Changers, But Humans Are Essential</h2>

<p>The programmer must guide AI—tools are amazing, but tough problems (like CSS conflicts) need human expertise. We’re not fully autonomous coding yet, especially for intricate integrations.</p>

<p>If you’re building AI-accelerated tools or facing similar challenges, let’s connect! What are your experiences with Cursor, Claude Code, or CSS headaches? Share in the comments.</p>]]></content><author><name>Andrei Radulescu-Banu</name></author><category term="ai" /><category term="programming" /><category term="tech" /><category term="reviews" /><summary type="html"><![CDATA[DocRouter.AI transforms messy, multi-layout business documents into clean, structured data using large language models (LLMs) and schema-driven orchestration. We focus on regulated industries like insurance, healthcare, and supply chain, where precision is non-negotiable. It’s a horizontal data layer application that plugs into vertical-specific apps, acting as an AI accelerator across sectors.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://sigagent.ai/assets/images/doc_router_cursor_claude.png" /><media:content medium="image" url="https://sigagent.ai/assets/images/doc_router_cursor_claude.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>