<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: John Ughiovhe</title>
    <description>The latest articles on DEV Community by John Ughiovhe (@johnughiovhe).</description>
    <link>https://dev.to/johnughiovhe</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3978145%2F7934388c-e53b-4f4b-b5ae-1fcc09ae2468.jpeg</url>
      <title>DEV Community: John Ughiovhe</title>
      <link>https://dev.to/johnughiovhe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/johnughiovhe"/>
    <language>en</language>
    <item>
      <title>We built an agent that turns messy RFQ emails into priced quotes, and shipped it on Alibaba Cloud</title>
      <dc:creator>John Ughiovhe</dc:creator>
      <pubDate>Mon, 20 Jul 2026 18:40:59 +0000</pubDate>
      <link>https://dev.to/johnughiovhe/we-built-an-agent-that-turns-messy-rfq-emails-into-priced-quotes-and-shipped-it-on-alibaba-cloud-2oip</link>
      <guid>https://dev.to/johnughiovhe/we-built-an-agent-that-turns-messy-rfq-emails-into-priced-quotes-and-shipped-it-on-alibaba-cloud-2oip</guid>
      <description>&lt;p&gt;Every distributor we spoke to has the same quiet bottleneck, and none of them call it a problem. They call it Tuesday.&lt;/p&gt;

&lt;p&gt;A request for quote lands in a shared inbox. Sometimes it is a tidy bulleted list. More often it is three lines of text from someone's phone, or a PDF that was scanned at an angle. Someone on the sales desk reads it, works out which catalog part each line actually refers to, checks pricing, and types up a quote. A busy desk does this thirty or forty times a day.&lt;/p&gt;

&lt;p&gt;It is slow, it is boring, and it is exactly the kind of work where a tired person on a Friday afternoon quotes the wrong bolt and nobody notices until the shipment arrives.&lt;/p&gt;

&lt;p&gt;We spent three weeks building &lt;strong&gt;Distill.ai&lt;/strong&gt; to do that job. This is what we learned, including the parts that went badly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we actually built
&lt;/h2&gt;

&lt;p&gt;You paste an email or upload a PDF. From there a seven stage pipeline runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;parse -&amp;gt; extract -&amp;gt; classify -&amp;gt; match -&amp;gt; price -&amp;gt; policy -&amp;gt; score
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parse cleans the document into text. Extract pulls out the individual line items, quantities, and specs. Classify works out what kind of request this is. Match maps each line to a real catalog SKU. Price applies the pricing rules. Policy runs the business checks. Score attaches a confidence value to every match.&lt;/p&gt;

&lt;p&gt;The interesting part is not the happy path. It is what happens when the model is unsure.&lt;/p&gt;

&lt;p&gt;Any line that scores below a &lt;strong&gt;0.70 match threshold&lt;/strong&gt; does not get quoted. It gets flagged with a reason and routed to a human review queue. A person confirms or corrects it, and the quote goes out clean.&lt;/p&gt;

&lt;p&gt;That one decision is the difference between a demo and something a sales desk would actually put its name on. An agent that is confidently wrong 5% of the time is worse than useless in procurement, because someone has to check all 100% of the output anyway. An agent that says "I got 47 of these 50 lines, here are the 3 I could not resolve" saves real hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Qwen, and how we wired it up
&lt;/h2&gt;

&lt;p&gt;We used two models from Alibaba Cloud Model Studio:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Qwen-Plus&lt;/strong&gt; for extraction and classification, including tool calling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;text-embedding-v4&lt;/strong&gt; for the 1024 dimension embeddings behind catalog matching&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Model Studio exposes an OpenAI-compatible endpoint, which mattered more than we expected. Our provider layer is a thin &lt;code&gt;fetch&lt;/code&gt; wrapper, and switching models is a config change rather than a rewrite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LLM_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/chat/completions`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LLM_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LLM_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="nx"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;maxTokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;LLM_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://dashscope-intl.aliyuncs.com/compatible-mode/v1
&lt;span class="nv"&gt;LLM_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;qwen-plus
&lt;span class="nv"&gt;EMBEDDINGS_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;text-embedding-v4
&lt;span class="nv"&gt;EMBEDDINGS_DIMENSIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1024
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The embeddings go into Postgres with pgvector, so catalog matching is a similarity search against real SKU rows rather than keyword guessing. "M8 hex bolt, grade 8.8, zinc plated" and "Bolt, hexagon head, M8x50, 8.8, ZP" are not a string match, but they are close neighbours in vector space.&lt;/p&gt;

&lt;p&gt;One deliberate choice: we deployed in &lt;strong&gt;Singapore (ap-southeast-1)&lt;/strong&gt; specifically to sit next to the Model Studio endpoint. On a pipeline that makes several model calls per request, the round trips add up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;A NestJS modular monolith in TypeScript, split into two processes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API&lt;/strong&gt; takes the request, validates it, persists it, and enqueues a job&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker&lt;/strong&gt; consumes from Redis via BullMQ and runs all seven stages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Splitting them matters because parsing a 40 page PDF and calling a model six times is not something you do inside an HTTP request. The API answers immediately and the browser watches progress over Server-Sent Events, so you see each stage light up in real time instead of staring at a spinner.&lt;/p&gt;

&lt;p&gt;Everything is containerized: api, worker, client (React and Vite behind Nginx), Postgres with pgvector, and Redis. The whole thing runs on a single Alibaba Cloud ECS instance via docker-compose, with Caddy in front for TLS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it actually went wrong
&lt;/h2&gt;

&lt;p&gt;The pipeline was the fun part. Deployment is where we lost days.&lt;/p&gt;

&lt;h3&gt;
  
  
  The migration step that could never have worked
&lt;/h3&gt;

&lt;p&gt;Our deploy job ran database migrations inside the production container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-T&lt;/span&gt; api pnpm migration:run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That script resolves to &lt;code&gt;ts-node -r tsconfig-paths/register node_modules/typeorm/cli.js migration:run -d src/database/data-source.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now look at the production Dockerfile. The runner stage installs &lt;code&gt;--prod&lt;/code&gt; dependencies only and copies &lt;code&gt;dist/&lt;/code&gt;, not &lt;code&gt;src/&lt;/code&gt;. So in the production image there is no &lt;code&gt;ts-node&lt;/code&gt;, and there is no TypeScript data source. The command was guaranteed to fail the moment it touched a real server, and it had been sitting in the workflow the whole time because nobody had run a real deploy yet.&lt;/p&gt;

&lt;p&gt;The fix is to point the TypeORM CLI at the compiled data source, which only needs packages that exist in the runtime image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-T&lt;/span&gt; api &lt;span class="se"&gt;\&lt;/span&gt;
  node node_modules/typeorm/cli.js migration:run &lt;span class="nt"&gt;-d&lt;/span&gt; dist/database/data-source.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We checked that &lt;code&gt;dist/database/data-source.js&lt;/code&gt; resolves entities and migrations through &lt;code&gt;__dirname&lt;/code&gt;-relative globs, and confirmed no &lt;code&gt;@&lt;/code&gt;-alias requires survive compilation. All 21 migrations then applied cleanly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; your production image is a different computer. Any command in your deploy pipeline that you have only ever run locally is an untested command.&lt;/p&gt;

&lt;h3&gt;
  
  
  Boot loop over an empty string
&lt;/h3&gt;

&lt;p&gt;The api and worker containers came up and immediately died, over and over:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;Invalid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;environment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;variables:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;SENTRY_DSN:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'Invalid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;url'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our env schema validates &lt;code&gt;SENTRY_DSN&lt;/code&gt; as a URL. We were not using Sentry in this deployment, so the value was an empty string, and an empty string is not a valid URL. The variable was optional in spirit but not in schema.&lt;/p&gt;

&lt;p&gt;Deleting the line entirely fixed it. Optional means absent, not blank.&lt;/p&gt;

&lt;h3&gt;
  
  
  A 4 GB box and an out of memory kill
&lt;/h3&gt;

&lt;p&gt;We build images on the instance itself. On a 2 vCPU / 4 GB box, the client build got OOM killed partway through. Adding 4 GB of swap got the builds through. Not elegant, but it was the difference between shipping and not shipping.&lt;/p&gt;

&lt;h3&gt;
  
  
  Branch protection versus a deadline
&lt;/h3&gt;

&lt;p&gt;Our &lt;code&gt;staging&lt;/code&gt; and &lt;code&gt;main&lt;/code&gt; branches require a pull request and three passing checks, and none of us can self merge. Correct policy. Also completely immovable at 11pm with a deadline coming.&lt;/p&gt;

&lt;p&gt;So we brought the first release up by hand: &lt;code&gt;git archive&lt;/code&gt;, scp to the box, &lt;code&gt;docker compose build&lt;/code&gt;, migrate, &lt;code&gt;up -d&lt;/code&gt;. Then we wrote the pipeline fix back as a proper reviewed PR, so the next deploy goes through CI like it should.&lt;/p&gt;

&lt;p&gt;We do not regret the branch protection. Shipping around your own safety rails once, deliberately, and then closing the gap properly afterwards, is very different from not having the rails.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTTPS without paying for a domain
&lt;/h3&gt;

&lt;p&gt;We used Caddy with a DuckDNS subdomain. Caddy handles the ACME challenge and certificate renewal on its own, so the entire TLS configuration is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;distill-ai.duckdns.org {
  reverse_proxy client:8080
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lines and a real Let's Encrypt certificate. Then we locked SSH to a single operator IP and closed the raw API port, so the only public surface is the HTTPS front door.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we would tell ourselves three weeks ago
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Deploy on day two, not day nineteen.&lt;/strong&gt; Every bug above was a deployment bug, not a logic bug. None of them were findable locally. The pipeline worked on our machines the whole time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confidence scoring is the product, not a feature.&lt;/strong&gt; We nearly shipped without the review queue. The moment we added it, the whole thing changed from a party trick into something you could show a customer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An OpenAI-compatible endpoint is worth real money in engineering time.&lt;/strong&gt; We never wrote a Qwen-specific client. We wrote an HTTP client and changed a base URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"It runs locally" and "it runs in production" are separated by a surprising amount of unglamorous work.&lt;/strong&gt; Swap space. Empty strings. Missing dev dependencies. None of it is interesting and all of it is required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The deployment is live and open, no login required:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://distill-ai.duckdns.org" rel="noopener noreferrer"&gt;https://distill-ai.duckdns.org&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The seeded catalog is zinc-plated fasteners, so write your RFQ around M6, M8, or M10 bolts, nuts, and washers. Here is one to paste:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hi team, please quote the following for our new assembly line:
- 1500 x M10 hex bolts, grade 8.8, zinc plated
- 1500 x M10 hex nuts, zinc plated
- 3000 x M10 flat washers, zinc plated

We need delivery within two weeks. Kindly include pricing and lead time.
Regards, Sarah Bennett, Procurement, Northgate Industrial Ltd.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch the trace run, then open the review page. If one of the lines lands in the review queue rather than the quote, that is not a bug. That is the whole point.&lt;/p&gt;

&lt;p&gt;Built with NestJS, TypeScript, React, PostgreSQL with pgvector, Redis, BullMQ, Docker, and Caddy, running on Alibaba Cloud ECS with Qwen-Plus and text-embedding-v4 via Alibaba Cloud Model Studio.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>showdev</category>
      <category>typescript</category>
      <category>devops</category>
    </item>
    <item>
      <title>The 16-Day System and the 3-Day Feature: Two Internship Tasks That Changed How I Think About Backend Engineering</title>
      <dc:creator>John Ughiovhe</dc:creator>
      <pubDate>Fri, 12 Jun 2026 02:25:46 +0000</pubDate>
      <link>https://dev.to/johnughiovhe/the-16-day-system-and-the-3-day-feature-two-internship-tasks-that-changed-how-i-think-about-1nn5</link>
      <guid>https://dev.to/johnughiovhe/the-16-day-system-and-the-3-day-feature-two-internship-tasks-that-changed-how-i-think-about-1nn5</guid>
      <description>&lt;p&gt;A reflection on two backend engineering internship projects that shaped my understanding of system design, API architecture, and working within existing codebases.&lt;/p&gt;

&lt;p&gt;When people ask what I worked on during my internship, the easiest answer is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I built APIs, authentication systems, and backend services."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Technically, that is true.&lt;/p&gt;

&lt;p&gt;But that answer misses the most important part.&lt;/p&gt;

&lt;p&gt;The most valuable lessons I learned didn't come from writing code. They came from dealing with complexity, making architectural decisions, and solving problems that were much bigger than they first appeared.&lt;/p&gt;

&lt;p&gt;Looking back, two tasks stand out more than any others.&lt;/p&gt;

&lt;p&gt;The first was &lt;strong&gt;Insighta Labs+&lt;/strong&gt;, a backend platform that took about &lt;strong&gt;16 days of active engineering effort across three progressive stages&lt;/strong&gt; and pushed me deeper into system design than anything I had worked on before.&lt;/p&gt;

&lt;p&gt;The second was building &lt;strong&gt;Funnel Display APIs for SEIL&lt;/strong&gt;, a feature that took about &lt;strong&gt;3 days of focused implementation&lt;/strong&gt; and taught me another important side of backend engineering: how to extend an existing system without breaking its foundation.&lt;/p&gt;

&lt;p&gt;One taught me how to build systems.&lt;/p&gt;

&lt;p&gt;The other taught me how to build within systems.&lt;/p&gt;

&lt;p&gt;Both changed how I approach backend engineering.&lt;/p&gt;




&lt;h2&gt;
  
  
  Task 1: Insighta Labs+ — Building One Backend for Multiple Interfaces
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Why I Picked This Project
&lt;/h2&gt;

&lt;p&gt;If I had to choose the project that stretched me the most during the internship, it would be Insighta Labs+.&lt;/p&gt;

&lt;p&gt;Not because it was simply a large project.&lt;/p&gt;

&lt;p&gt;Not because it used unfamiliar technologies.&lt;/p&gt;

&lt;p&gt;But because it forced me to think beyond individual endpoints.&lt;/p&gt;

&lt;p&gt;Across three stages and about &lt;strong&gt;16 active days of implementation&lt;/strong&gt;, I wasn't just building an API.&lt;/p&gt;

&lt;p&gt;I was building a backend platform that needed to support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A web application&lt;/li&gt;
&lt;li&gt;A command-line interface (CLI)&lt;/li&gt;
&lt;li&gt;Multiple user roles&lt;/li&gt;
&lt;li&gt;Authentication and authorization&lt;/li&gt;
&lt;li&gt;Search workflows&lt;/li&gt;
&lt;li&gt;Data ingestion pipelines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each stage built on the previous one.&lt;/p&gt;

&lt;p&gt;This meant early architectural decisions became foundations for everything that came after.&lt;/p&gt;

&lt;p&gt;The breaks between stages also gave time to review what was already built, identify weaknesses, and approach the next stage with a clearer understanding of the system.&lt;/p&gt;

&lt;p&gt;This was where I started seeing the difference between writing backend code and engineering backend systems.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Was
&lt;/h2&gt;

&lt;p&gt;Insighta Labs+ is a profile intelligence platform built around a shared backend architecture.&lt;/p&gt;

&lt;p&gt;The platform supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub OAuth authentication&lt;/li&gt;
&lt;li&gt;Profile intelligence workflows&lt;/li&gt;
&lt;li&gt;Role-based access control&lt;/li&gt;
&lt;li&gt;CSV profile uploads&lt;/li&gt;
&lt;li&gt;Search and filtering&lt;/li&gt;
&lt;li&gt;Data export&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interesting part was not any single feature.&lt;/p&gt;

&lt;p&gt;The challenge was ensuring every interface depended on the same backend rules.&lt;/p&gt;

&lt;p&gt;A browser user and a CLI user should receive the same:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication behavior&lt;/li&gt;
&lt;li&gt;Authorization decisions&lt;/li&gt;
&lt;li&gt;Data consistency&lt;/li&gt;
&lt;li&gt;API responses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The backend had to become the single source of truth.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem It Was Solving
&lt;/h2&gt;

&lt;p&gt;One common problem in software systems is allowing different clients to slowly become different products.&lt;/p&gt;

&lt;p&gt;The web application starts implementing its own logic.&lt;/p&gt;

&lt;p&gt;The CLI starts duplicating behavior.&lt;/p&gt;

&lt;p&gt;Authentication rules become inconsistent.&lt;/p&gt;

&lt;p&gt;Eventually, nobody can confidently explain how the system actually works.&lt;/p&gt;

&lt;p&gt;The goal with Insighta Labs+ was to prevent that.&lt;/p&gt;

&lt;p&gt;The backend needed to own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Business logic&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Authorization&lt;/li&gt;
&lt;li&gt;Search behavior&lt;/li&gt;
&lt;li&gt;Data access rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every client should consume the backend instead of recreating it.&lt;/p&gt;




&lt;h2&gt;
  
  
  How I Approached It
&lt;/h2&gt;

&lt;p&gt;My approach was guided by one principle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every client should depend on the backend, not the other way around.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This affected every major decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication Across Web and CLI
&lt;/h2&gt;

&lt;p&gt;Supporting browser authentication was straightforward.&lt;/p&gt;

&lt;p&gt;Supporting CLI authentication was more interesting.&lt;/p&gt;

&lt;p&gt;The browser flow used traditional GitHub OAuth.&lt;/p&gt;

&lt;p&gt;The CLI flow required PKCE (Proof Key for Code Exchange), allowing secure authentication without exposing client secrets.&lt;/p&gt;

&lt;p&gt;The backend had to manage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OAuth state&lt;/li&gt;
&lt;li&gt;PKCE verification&lt;/li&gt;
&lt;li&gt;Authorization code exchange&lt;/li&gt;
&lt;li&gt;Callback validation&lt;/li&gt;
&lt;li&gt;Session creation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simple login feature became a system of interacting parts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Secure Token Handling
&lt;/h2&gt;

&lt;p&gt;I avoided storing raw tokens directly.&lt;/p&gt;

&lt;p&gt;The system used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Opaque access tokens&lt;/li&gt;
&lt;li&gt;Refresh token rotation&lt;/li&gt;
&lt;li&gt;Token hashing&lt;/li&gt;
&lt;li&gt;Session lifecycle management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal was to make authentication secure while keeping the developer experience predictable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Role-Based Authorization
&lt;/h2&gt;

&lt;p&gt;The platform supported different user roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Analysts&lt;/li&gt;
&lt;li&gt;Administrators&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of scattering permission checks throughout the application, authorization was handled centrally.&lt;/p&gt;

&lt;p&gt;This made the system easier to maintain and reduced the chance of inconsistent access rules.&lt;/p&gt;




&lt;h2&gt;
  
  
  Natural Language Search
&lt;/h2&gt;

&lt;p&gt;One of the most interesting parts was building deterministic search.&lt;/p&gt;

&lt;p&gt;Instead of using AI models, I built a parser that could understand queries like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"young males from Nigeria"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and convert them into structured filters.&lt;/p&gt;

&lt;p&gt;The parser handled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gender detection&lt;/li&gt;
&lt;li&gt;Age ranges&lt;/li&gt;
&lt;li&gt;Country extraction&lt;/li&gt;
&lt;li&gt;Validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal wasn't to understand every possible sentence.&lt;/p&gt;

&lt;p&gt;The goal was to understand expected user intent reliably.&lt;/p&gt;




&lt;h2&gt;
  
  
  Performance Improvements
&lt;/h2&gt;

&lt;p&gt;As the system grew, repeated queries became expensive.&lt;/p&gt;

&lt;p&gt;To improve efficiency, I introduced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Query normalization&lt;/li&gt;
&lt;li&gt;Cache key standardization&lt;/li&gt;
&lt;li&gt;Response caching&lt;/li&gt;
&lt;li&gt;Cache invalidation after updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The challenge was not only making things faster.&lt;/p&gt;

&lt;p&gt;It was making them faster without serving outdated information.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Broke
&lt;/h2&gt;

&lt;p&gt;A lot.&lt;/p&gt;

&lt;p&gt;And that was where the real learning happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  OAuth Complexity
&lt;/h2&gt;

&lt;p&gt;OAuth becomes significantly harder when supporting multiple clients.&lt;/p&gt;

&lt;p&gt;The browser flow and CLI flow introduced different requirements.&lt;/p&gt;

&lt;p&gt;Small mistakes in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Redirect URLs&lt;/li&gt;
&lt;li&gt;State validation&lt;/li&gt;
&lt;li&gt;Token exchange&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;could completely break authentication.&lt;/p&gt;




&lt;h2&gt;
  
  
  Search Edge Cases
&lt;/h2&gt;

&lt;p&gt;Users rarely search exactly how developers expect.&lt;/p&gt;

&lt;p&gt;Some queries created conflicting filters.&lt;/p&gt;

&lt;p&gt;Others were ambiguous.&lt;/p&gt;

&lt;p&gt;Instead of guessing, I made the parser fail clearly and predictably.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cache Consistency
&lt;/h2&gt;

&lt;p&gt;Caching solved performance issues.&lt;/p&gt;

&lt;p&gt;But it introduced a new problem:&lt;/p&gt;

&lt;p&gt;How do you know when cached data is no longer correct?&lt;/p&gt;

&lt;p&gt;The solution was automatic cache invalidation after data mutations.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Took Away
&lt;/h2&gt;

&lt;p&gt;Insighta Labs+ changed my mindset.&lt;/p&gt;

&lt;p&gt;Before this project, I thought mainly in terms of features.&lt;/p&gt;

&lt;p&gt;After it, I started thinking in terms of systems.&lt;/p&gt;

&lt;p&gt;I learned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication is a system, not just a login endpoint.&lt;/li&gt;
&lt;li&gt;Authorization is an architecture decision.&lt;/li&gt;
&lt;li&gt;Caching is a consistency problem.&lt;/li&gt;
&lt;li&gt;APIs become products when multiple clients depend on them.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Task 2: Funnel Display APIs — Building Features That Fit Existing Systems
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Why I Picked This Project
&lt;/h2&gt;

&lt;p&gt;Compared to Insighta Labs+, this task was smaller in scope and took about &lt;strong&gt;3 days of focused implementation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But it represented another important side of backend engineering.&lt;/p&gt;

&lt;p&gt;In real-world teams, you rarely build everything from scratch.&lt;/p&gt;

&lt;p&gt;Most of the time, you are extending an existing codebase.&lt;/p&gt;

&lt;p&gt;The challenge is not just making your feature work.&lt;/p&gt;

&lt;p&gt;The challenge is making it belong.&lt;/p&gt;

&lt;p&gt;That is why I picked this project.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Was
&lt;/h2&gt;

&lt;p&gt;The Funnel Display API was a backend feature built for SEIL to expose funnel and stage information required by the frontend.&lt;/p&gt;

&lt;p&gt;The implementation introduced four authenticated endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retrieve available funnels&lt;/li&gt;
&lt;li&gt;Retrieve full funnel details&lt;/li&gt;
&lt;li&gt;Retrieve stages for a funnel&lt;/li&gt;
&lt;li&gt;Retrieve a specific stage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The work also included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database migration&lt;/li&gt;
&lt;li&gt;Service logic&lt;/li&gt;
&lt;li&gt;Controller implementation&lt;/li&gt;
&lt;li&gt;Unit test updates&lt;/li&gt;
&lt;li&gt;Swagger documentation&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Problem It Was Solving
&lt;/h2&gt;

&lt;p&gt;The frontend needed a clean way to display funnel information.&lt;/p&gt;

&lt;p&gt;Instead of exposing database structures directly, the backend needed to provide meaningful product-level data.&lt;/p&gt;

&lt;p&gt;The API needed to answer questions like:&lt;/p&gt;

&lt;p&gt;"Show this user's funnel."&lt;/p&gt;

&lt;p&gt;"Show the stages inside this funnel."&lt;/p&gt;

&lt;p&gt;"Show details about this specific stage."&lt;/p&gt;

&lt;p&gt;The backend became the bridge between stored data and the user experience.&lt;/p&gt;




&lt;h2&gt;
  
  
  How I Approached It
&lt;/h2&gt;

&lt;p&gt;Before writing code, I studied the existing architecture.&lt;/p&gt;

&lt;p&gt;SEIL already had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NestJS&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;PostgreSQL&lt;/li&gt;
&lt;li&gt;JWT authentication&lt;/li&gt;
&lt;li&gt;Repository-based data access&lt;/li&gt;
&lt;li&gt;Swagger documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal wasn't to introduce a new style.&lt;/p&gt;

&lt;p&gt;The goal was to follow the existing one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Database Migration
&lt;/h2&gt;

&lt;p&gt;The first step was adding the required database structures.&lt;/p&gt;

&lt;p&gt;Instead of manually modifying the database, I created a migration.&lt;/p&gt;

&lt;p&gt;This made schema changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repeatable&lt;/li&gt;
&lt;li&gt;Version controlled&lt;/li&gt;
&lt;li&gt;Safe across environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A database change is not just a database change.&lt;/p&gt;

&lt;p&gt;It becomes part of the application lifecycle.&lt;/p&gt;




&lt;h2&gt;
  
  
  API Design
&lt;/h2&gt;

&lt;p&gt;The endpoints were designed around frontend needs.&lt;/p&gt;

&lt;p&gt;A frontend does not think:&lt;/p&gt;

&lt;p&gt;"Return rows from the funnel table."&lt;/p&gt;

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

&lt;p&gt;"Display this funnel and its progress."&lt;/p&gt;

&lt;p&gt;That difference influenced the API structure.&lt;/p&gt;

&lt;p&gt;The routes focused on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User ownership&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Meaningful responses&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Service Layer Design
&lt;/h2&gt;

&lt;p&gt;I kept responsibilities separated.&lt;/p&gt;

&lt;p&gt;Controllers handled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request handling&lt;/li&gt;
&lt;li&gt;Validation&lt;/li&gt;
&lt;li&gt;Responses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Services handled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Business logic&lt;/li&gt;
&lt;li&gt;Data retrieval&lt;/li&gt;
&lt;li&gt;Rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This made the feature easier to test and maintain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;I updated unit tests to verify funnel service behavior.&lt;/p&gt;

&lt;p&gt;I also manually tested the endpoints through Swagger:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start the development server&lt;/li&gt;
&lt;li&gt;Authenticate with a JWT&lt;/li&gt;
&lt;li&gt;Call each endpoint&lt;/li&gt;
&lt;li&gt;Verify responses and status codes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result was a frontend-ready API surface.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Broke
&lt;/h2&gt;

&lt;p&gt;The biggest challenge wasn't a failed build.&lt;/p&gt;

&lt;p&gt;It was ensuring the feature matched the existing system.&lt;/p&gt;

&lt;p&gt;A backend feature is not successful just because it works.&lt;/p&gt;

&lt;p&gt;It should feel like it was always part of the application.&lt;/p&gt;

&lt;p&gt;That meant paying attention to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Existing patterns&lt;/li&gt;
&lt;li&gt;Response formats&lt;/li&gt;
&lt;li&gt;Error handling&lt;/li&gt;
&lt;li&gt;Authentication rules&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What I Took Away
&lt;/h2&gt;

&lt;p&gt;This task taught me that backend engineering is also about integration.&lt;/p&gt;

&lt;p&gt;I learned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Migrations are part of feature development.&lt;/li&gt;
&lt;li&gt;API design should reflect user actions.&lt;/li&gt;
&lt;li&gt;Existing architecture deserves respect.&lt;/li&gt;
&lt;li&gt;Good features fit naturally into systems.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;These two projects represented two different sides of backend engineering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Insighta Labs+&lt;/strong&gt; taught me how to build systems that evolve across multiple stages, interfaces, and requirements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Funnel Display APIs&lt;/strong&gt; taught me how to contribute to an existing system and extend it safely.&lt;/p&gt;

&lt;p&gt;One required creating foundations that could support future growth.&lt;/p&gt;

&lt;p&gt;The other required understanding an existing foundation and building on top of it.&lt;/p&gt;

&lt;p&gt;Together, they reinforced a lesson I will carry forward:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A good backend engineer doesn't just write code. They understand the system they are changing and make that system better.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>backend</category>
      <category>softwareengineering</category>
      <category>internship</category>
    </item>
  </channel>
</rss>
