<?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: Nandawula Kabali-Kagwa</title>
    <description>The latest articles on DEV Community by Nandawula Kabali-Kagwa (@nanda-regine).</description>
    <link>https://dev.to/nanda-regine</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%2F3706972%2F43b6285f-dd21-440c-a25a-75ddf1e0aa54.jpg</url>
      <title>DEV Community: Nandawula Kabali-Kagwa</title>
      <link>https://dev.to/nanda-regine</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nanda-regine"/>
    <language>en</language>
    <item>
      <title>I Rebuilt JarvisOS Marketing Wing From Scratch Mid-Sprint — Here's Every Trade-Off</title>
      <dc:creator>Nandawula Kabali-Kagwa</dc:creator>
      <pubDate>Fri, 11 Sep 2026 10:31:14 +0000</pubDate>
      <link>https://dev.to/nanda-regine/i-rebuilt-jarvisos-marketing-wing-from-scratch-mid-sprint-heres-every-trade-off-5b25</link>
      <guid>https://dev.to/nanda-regine/i-rebuilt-jarvisos-marketing-wing-from-scratch-mid-sprint-heres-every-trade-off-5b25</guid>
      <description>&lt;p&gt;There is a specific kind of dread that arrives when a &lt;code&gt;save + publish&lt;/code&gt; button silently breaks in production. No 500 in the logs. No red banner for the user. The data just... stops moving. That was Monday.&lt;/p&gt;

&lt;p&gt;This is the technical record of a week inside JarvisOS — an AI operating system I'm building for African founders — where the marketing wing collapsed under its own accumulated shortcuts and I had to decide: patch it again, or tear the architecture down and rebuild it correctly.&lt;/p&gt;

&lt;p&gt;I rebuilt it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Root Cause: Schema Drift
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;save + publish&lt;/code&gt; failure traced back to a schema mismatch. Columns I was writing to in the API layer didn't exist in the actual database table — specifically &lt;code&gt;thumbnail_url&lt;/code&gt;, &lt;code&gt;image_brief&lt;/code&gt;, and &lt;code&gt;external_url&lt;/code&gt;. The inserts were silently swallowed because the ORM wasn't set to strict mode. The user saw a success toast. The row was incomplete. Every downstream read that touched those fields returned a 400.&lt;/p&gt;

&lt;p&gt;This is a category of bug that feels embarrassing in retrospect but is genuinely easy to accumulate when you are shipping fast and migrating features across wings of a larger system. The fix was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add the missing columns via migration&lt;/li&gt;
&lt;li&gt;Audit every SELECT and INSERT that touched the marketing content table&lt;/li&gt;
&lt;li&gt;Add a &lt;code&gt;NOT NULL&lt;/code&gt; constraint with a safe default on &lt;code&gt;external_url&lt;/code&gt; to make future drift loud and immediate&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Trade-off:&lt;/strong&gt; Strict constraints slow down exploratory schema work. I accepted that cost. At production scale, silent data corruption is worse than a loud migration failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five Live Crashes, One Sweep
&lt;/h2&gt;

&lt;p&gt;Once I was in the codebase, I ran a full sweep. What I found:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;withCostLogging&lt;/code&gt; not a function&lt;/strong&gt; — a utility I'd refactored had changed its export signature. Call sites weren't updated. Classic named-vs-default export drift in a JavaScript monorepo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;visual_brief.slice()&lt;/code&gt; crash&lt;/strong&gt; — &lt;code&gt;visual_brief&lt;/code&gt; was returning &lt;code&gt;null&lt;/code&gt; from the DB on older rows. I was calling &lt;code&gt;.slice()&lt;/code&gt; directly without a null guard. Fixed with optional chaining: &lt;code&gt;visual_brief?.slice(0, 200) ?? ''&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;brand_posts&lt;/code&gt; lane skipped&lt;/strong&gt; — when &lt;code&gt;content_calendar&lt;/code&gt; was empty, the lane generation function returned early before populating brand posts. The condition was checking the wrong array. One line fix, embarrassing in scale, real in consequence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TikTok &lt;code&gt;StrategyTab&lt;/code&gt; syntax errors&lt;/strong&gt; — apostrophes in JSX string props that weren't escaped. South African copy tends to contract heavily (&lt;code&gt;it's&lt;/code&gt;, &lt;code&gt;you're&lt;/code&gt;, &lt;code&gt;brand's&lt;/code&gt;) and I hadn't sanitised the AI-generated content before rendering it as JSX attributes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON crash on paste queue hashtags&lt;/strong&gt; — &lt;code&gt;JSON.parse()&lt;/code&gt; on a value that was already a parsed object. &lt;code&gt;typeof&lt;/code&gt; guard added.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Five crashes. One sweep. Three hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cockpit Architecture Rebuild
&lt;/h2&gt;

&lt;p&gt;The deeper issue the bug sweep revealed: the marketing wing had grown as a series of bolted-on tabs rather than as a coherent architecture. Each platform (TikTok, Threads, etc.) had its own component with its own data-fetching logic, its own state shape, its own styling overrides.&lt;/p&gt;

&lt;p&gt;I introduced what I'm calling the &lt;strong&gt;platform cockpit architecture&lt;/strong&gt;. The design principle: every social platform is a &lt;em&gt;wing&lt;/em&gt; of the cockpit, and every wing shares a single contract:&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PlatformWing&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;platformId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;strategyTab&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FC&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;WingProps&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;calendarTab&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FC&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;WingProps&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;analyticsTab&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FC&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;WingProps&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;dataFetcher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PlatformData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means adding a new platform — say, LinkedIn — requires implementing the interface, not copy-pasting a component tree. The &lt;code&gt;dataFetcher&lt;/code&gt; contract also forced me to centralise error handling: one boundary, one logging call, one cost-tracking wrapper.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I'd do differently:&lt;/strong&gt; Define this interface at week one, not month four. The refactor cost was non-trivial. The lesson isn't novel but it bears repeating for anyone building multi-platform SaaS: your platforms are a collection, not a list of one-offs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Threads OAuth + Cloudinary Migration
&lt;/h2&gt;

&lt;p&gt;Two infrastructure moves happened in parallel.&lt;/p&gt;

&lt;p&gt;First, Threads OAuth. I shipped the callback route, the deauthorize webhook, and the delete webhook — all required by Meta's API compliance checklist. The deauthorize and delete routes are easy to defer (&lt;code&gt;we'll do it later&lt;/code&gt;) and catastrophic to forget when your app goes for review. They're in now.&lt;/p&gt;

&lt;p&gt;Second, a full storage migration from Supabase Storage to Cloudinary. Supabase Storage is fine for getting started. Under real media volume — campaign thumbnails, visual briefs, CEO wing assets — the transformation pipeline limitations and egress costs became visible. Cloudinary's upload API with eager transformations gives me WebP conversion, responsive breakpoints, and CDN delivery in a single upload call. The migration script ran against existing rows, updated every &lt;code&gt;storage_url&lt;/code&gt; column, and verified the new URLs returned 200 before deleting originals.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cron That Keeps the DB Honest
&lt;/h2&gt;

&lt;p&gt;One small but meaningful addition: a monthly pruning cron that runs at 03:00 SAST on the first of every month, sweeping 7 tables of rows older than the retention window. Running on African infrastructure means cost discipline matters at every layer. A database that quietly grows forever is a bill that quietly grows forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Git Push → Sprint Board
&lt;/h2&gt;

&lt;p&gt;Finally, a quality-of-life engineering feature that I'll write more about separately: push a commit, Haiku (the AI agent inside JarvisOS's engineering wing) reads the commit message, parses the scope and description, and auto-populates the sprint board task. It sounds small. For a solo founder shipping across 8 apps, it removes the gap between &lt;em&gt;doing the work&lt;/em&gt; and &lt;em&gt;tracking the work&lt;/em&gt;. That gap is where context goes to die.&lt;/p&gt;




&lt;p&gt;The marketing wing is stable now. The cockpit architecture is clean. The schema is honest.&lt;/p&gt;

&lt;p&gt;Next week I turn back to HCI — the human-computer interaction layer that makes the AI agents in JarvisOS feel less like tools and more like a team. Phase 4 closed this week. Phase 5 is already open.&lt;/p&gt;

</description>
      <category>nandapersonal</category>
    </item>
    <item>
      <title>Building in Africa</title>
      <dc:creator>Nandawula Kabali-Kagwa</dc:creator>
      <pubDate>Fri, 11 Sep 2026 08:45:35 +0000</pubDate>
      <link>https://dev.to/nanda-regine/building-in-africa-35eo</link>
      <guid>https://dev.to/nanda-regine/building-in-africa-35eo</guid>
      <description>&lt;p&gt;Welcome to a journey of a lifetime, building for Africa in Africa.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>buildforafrica</category>
    </item>
    <item>
      <title>Building in Africa</title>
      <dc:creator>Nandawula Kabali-Kagwa</dc:creator>
      <pubDate>Fri, 11 Sep 2026 08:37:52 +0000</pubDate>
      <link>https://dev.to/nanda-regine/building-in-africa-28m1</link>
      <guid>https://dev.to/nanda-regine/building-in-africa-28m1</guid>
      <description>&lt;p&gt;Welcome to a journey of a lifetime, building for Africa in Africa.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>buildforafrica</category>
    </item>
  </channel>
</rss>
