<?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>Why I Built an Automated Marketing Desk Inside My Monorepo</title>
      <dc:creator>Nandawula Kabali-Kagwa</dc:creator>
      <pubDate>Thu, 22 Oct 2026 13:30:31 +0000</pubDate>
      <link>https://dev.to/nanda-regine/why-i-built-an-automated-marketing-desk-inside-my-monorepo-487a</link>
      <guid>https://dev.to/nanda-regine/why-i-built-an-automated-marketing-desk-inside-my-monorepo-487a</guid>
      <description>&lt;p&gt;As developers, we love building the product. The database schema, the API routes, the buttery-smooth UI transitions, that is our playground. But once you ship eight production apps and run a registered software studio from East London, you realize a brutal truth: code that nobody sees solves no problems.&lt;/p&gt;

&lt;p&gt;Instead of hiring out or getting sucked into daily manual social media loops, I decided to engineer my way out of the problem. This week inside JarvisOS, I shipped a complete marketing department into the codebase.&lt;/p&gt;

&lt;p&gt;Here is what the stack looks like now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;B2 Weekly Auto-Plan:&lt;/strong&gt; A deterministic slot planner executing on a Sunday cron at 18:00.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;B3 Generators:&lt;/strong&gt; Reading directly from our internal markdown corpus slots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;B4 Command Center:&lt;/strong&gt; A full API and UI for dispatching content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;B5 Account Portfolio:&lt;/strong&gt; Tracking roles, strategies, streaks, and best-performing posts per account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CF1 Cloudflare Browser Run:&lt;/strong&gt; Offloading PNG/PDF renders to the edge with a local Chrome fallback.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One of the most satisfying engineering moments this week was fixing our corpus reading pipelines. An unanchored gitignore rule had silently hidden any new &lt;code&gt;src/**/corpus&lt;/code&gt; file, and a silent 1,000-row cap was truncating our corpus database stats. We also tackled UTF-8 mojibake across 27 files where special characters had turned into cryptographic-looking garbage during early AI prompt generation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Ensuring honest corpuses: paged corpus reads and strict URL hardening&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;readPagedCorpus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cursor&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="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;corpus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;take&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;skip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cursor&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cursor&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cursor&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;slice&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;We also built read-only Instagram and Facebook historical graph importers that backfill likes and comments onto existing posts idempotently across a 180-day window, and hardened our wing-insight URLs to prevent token-leaking query parameters.&lt;/p&gt;

&lt;p&gt;When your tooling lives inside your monorepo, marketing stops feeling like a distraction and starts feeling like an extension of your compiler. &lt;/p&gt;

&lt;p&gt;How do you balance writing code with getting it into the hands of users?&lt;/p&gt;

</description>
      <category>nandapersonal</category>
    </item>
    <item>
      <title>Engineering for Reality: What Managing 14 Live Systems Taught Me About Solo Shipping</title>
      <dc:creator>Nandawula Kabali-Kagwa</dc:creator>
      <pubDate>Thu, 15 Oct 2026 13:30:19 +0000</pubDate>
      <link>https://dev.to/nanda-regine/engineering-for-reality-what-managing-14-live-systems-taught-me-about-solo-shipping-5hj9</link>
      <guid>https://dev.to/nanda-regine/engineering-for-reality-what-managing-14-live-systems-taught-me-about-solo-shipping-5hj9</guid>
      <description>&lt;p&gt;There is a distinct kind of silence that settles over a desk at 2:00 AM in East London. The hum of the inverter, the rhythmic blinking of a terminal, and the quiet satisfaction of a passing test suite. This week, my codebase logs hit a milestone that still catches me by surprise: fourteen live production systems under my studio, Mirembe Muse, with total product reach crossing 1,384+ users.&lt;/p&gt;

&lt;p&gt;If you told me in June 2025, when I first touched code, that I would be shipping institutional platforms like the ICSA Summit OS, ECDC, ISD, and Green Diaspora alongside student tools like VarsityOS and SME solutions like AdminOS, I would have laughed. Yet, here we are.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Reality of Shipping Solo
&lt;/h3&gt;

&lt;p&gt;When you are a self-taught engineer building for African realities, you don't have the luxury of bloated teams or endless sprint retrospectives. Every line of code has to pull its weight. &lt;/p&gt;

&lt;p&gt;This week’s commits were all about institutional proof and system clarity. We pushed comprehensive case studies into MirembeApp, formalizing the work done for summits and regional development bodies. We also locked in the metrics: VarsityOS standing firm with offline capabilities through load shedding and 11-language support, and CreativelyNanda nurturing a growing creative community.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Architecture Under Constraints
&lt;/h3&gt;

&lt;p&gt;Building for environments marked by power outages and varied device specs changes how you think about frontend performance and backend reliability:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero Bloat:&lt;/strong&gt; Every dependency is interrogated. If a package can be written in twenty lines of vanilla TypeScript, it doesn't enter the dependency tree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Defensive UI:&lt;/strong&gt; Components must look pristine whether rendered on a flagship iPhone in Sandton or a budget Android device in a township.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transparent Pricing:&lt;/strong&gt; As a studio, we publish fixed prices. Software should not be an opaque mystery; clients deserve predictable economics.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Developer's Loop
&lt;/h3&gt;

&lt;p&gt;Being a developer who also writes poetry (yes, the two brains live happily in the same skull) changes how I view code. Code is syntax, yes, but it is also rhythm. It is the cadence of functions calling each other, the clean architecture of a well-structured component tree.&lt;/p&gt;

&lt;p&gt;Fourteen systems live. The studio is standing. The compiler is quiet. Back to the editor.&lt;/p&gt;

</description>
      <category>nandapersonal</category>
    </item>
    <item>
      <title>From 46 Raw Errors to Zero: Lessons in API Hardening and SAST Calendars</title>
      <dc:creator>Nandawula Kabali-Kagwa</dc:creator>
      <pubDate>Thu, 08 Oct 2026 13:31:13 +0000</pubDate>
      <link>https://dev.to/nanda-regine/from-46-raw-errors-to-zero-lessons-in-api-hardening-and-sast-calendars-3cip</link>
      <guid>https://dev.to/nanda-regine/from-46-raw-errors-to-zero-lessons-in-api-hardening-and-sast-calendars-3cip</guid>
      <description>&lt;p&gt;There is a specific kind of humility that comes from running 7 industry persona tenants against a live production environment and watching your authz matrix test every single role check. This week, our work on AdminOS and JarvisOS was less about shipping shiny new buttons and more about nailing the unglamorous plumbing that keeps real businesses running.&lt;/p&gt;

&lt;h3&gt;
  
  
  Catching What the Database Drops
&lt;/h3&gt;

&lt;p&gt;Let’s start with the database layer. In our API routes, we had a silent vulnerability: 46 distinct endpoints could potentially leak raw Postgres error strings back to the client if a query malformed or a constraint failed. That is a security risk and a terrible user experience. We audited every single one of those 46 routes, routing exceptions through a unified validation layer that returns graceful, type-safe responses.&lt;/p&gt;

&lt;p&gt;Next came the hard delete problem. When building rapidly, it is easy to write a quick &lt;code&gt;DELETE FROM&lt;/code&gt; query. But in enterprise systems, especially when managing finance books, client records, and POPIA compliance registers, deleting data is a cardinal sin. We hunted down six legacy hard-delete methods and refactored them into soft deletes, ensuring our audit gates remain unbroken.&lt;/p&gt;

&lt;h3&gt;
  
  
  The SAST Calendar Sweep
&lt;/h3&gt;

&lt;p&gt;One of the most insidious bugs we tackled this week involved dates. Compliance countdowns and licence expirations were drifting by an hour or two because of server-client timezone mismatches between 00:00 and 02:00. We implemented an app-wide SAST (South African Standard Time) sweep. &lt;/p&gt;

&lt;p&gt;We rebuilt our SA statutory calendar on SARS-verified dates that roll forward monthly. Persona provisioners now use this TypeScript calendar, ensuring that when an SME back-office agent checks a tax deadline or a sequence broadcast date, the calendar matches the physical reality of the business owner sitting across the screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making WhatsApp and Sequences Work
&lt;/h3&gt;

&lt;p&gt;Finally, we tackled the reach and messaging engine. POPIA section 69 requires explicit, verifiable consent for broadcasts and sequences. We wired up POPIA s69 consent checks directly into our broadcast enrolment pipelines. Combined with Meta Tech Provider coexistence plans and clean template execution, WhatsApp, NPS, and n8n automations finally click together without silent failures.&lt;/p&gt;

&lt;p&gt;When totals stop silently at 1,000 rows because of pagination oversights, you fix the pagination. When permissions need validation down to the method level, you write the matrix. That is what building real systems looks like.&lt;/p&gt;

</description>
      <category>nandapersonal</category>
    </item>
    <item>
      <title>Building for Real Realities: Why We Eliminated Em Dashes and Standardized Equal Thirds</title>
      <dc:creator>Nandawula Kabali-Kagwa</dc:creator>
      <pubDate>Thu, 01 Oct 2026 13:02:32 +0000</pubDate>
      <link>https://dev.to/nanda-regine/building-for-real-realities-why-we-eliminated-em-dashes-and-standardized-equal-thirds-15d9</link>
      <guid>https://dev.to/nanda-regine/building-for-real-realities-why-we-eliminated-em-dashes-and-standardized-equal-thirds-15d9</guid>
      <description>&lt;p&gt;When you are writing code that represents a physical and digital studio from East London, South Africa, aesthetic choices quickly become engineering constraints. This week, our main repository (&lt;code&gt;CreativelyNanda.co.za&lt;/code&gt;) underwent a massive structural refinement. We didn't add a flashy new database or migrate to an exotic edge runtime. Instead, we focused on the bones: equal thirds, strict color tokens, print stylesheets, and brutal typographic minimalism.&lt;/p&gt;

&lt;p&gt;Let’s talk about the stack decisions behind this week's commits.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Geometry of Equal Thirds
&lt;/h3&gt;

&lt;p&gt;Layout drift is the silent killer of frontend polish. Across our shop, Notion integration, business archives, and the Poem Wall, we enforced an equal-thirds layout structure. This wasn't just a whim; it was a deliberate move to create predictable mental models for users navigating heavy literary and commercial content simultaneously. We extended this rhythm to the legal pages, roots, testimonials, and imprints, ensuring that whether a user is reading a poem or buying a physical volume, the container feels anchored.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Styling for Paper: When CSS Meets the Physical World
&lt;/h3&gt;

&lt;p&gt;One of the most challenging frontend tasks this week was making the digital design system translate seamlessly to physical print. We configured the remaining navy pages to print accurately on paper, ensuring that coloured buttons retain stark white labels and Forge masthead pockets align precisely with the words beneath them. Stage reel captions were locked directly under their frames.&lt;/p&gt;

&lt;p&gt;In a region where load shedding is a structural reality, thinking about how a web page behaves when printed or saved offline isn't a retro novelty, it's robust engineering. It’s the same philosophy that drives VarsityOS to remain fully functional offline for students navigating NSFAS budgets.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Typography as an API
&lt;/h3&gt;

&lt;p&gt;We instituted a strict style rule: &lt;strong&gt;no em dashes anywhere on the site.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;From a parser and readability perspective, em dashes often act as a crutch for run-on architectural logic in copywriting. By stripping them out, we forced our prose to rely on hinge words, pauses, and clean pulse lines. We updated our X-ray poetry modes and garden layouts to respect this newfound restraint.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The Gotchas: JSX Comments and Selector Escaping
&lt;/h3&gt;

&lt;p&gt;No build log is complete without a bug post-mortem. Early in the week, a stray comment hidden inside a JSX conditional broke our product page build. It's a humbling reminder that even with mature tooling, JSX parsing edge cases will catch you off guard if your components mix declarative markup with raw scripting comments.&lt;/p&gt;

&lt;p&gt;We also escaped our CSS remap selectors (&lt;code&gt;fix(css): escape the remap selectors; journal §25&lt;/code&gt;), cleaning up specificity wars before they could compound. &lt;/p&gt;

&lt;p&gt;Building apps like AdminOS (our six-agent SME back office) and the K53 Drill Master (rigorously audited against official DLTC docs this week) teaches you one unchanging truth: discipline in the small files saves your life in the large ones. Clean up your tokens. Check your build logs. And let your code breathe.&lt;/p&gt;

</description>
      <category>nandapersonal</category>
    </item>
    <item>
      <title>VarsityOS: An AI Study OS Built for African Students</title>
      <dc:creator>Nandawula Kabali-Kagwa</dc:creator>
      <pubDate>Sun, 27 Sep 2026 16:03:11 +0000</pubDate>
      <link>https://dev.to/nanda-regine/varsityos-an-ai-study-os-built-for-african-students-3jcm</link>
      <guid>https://dev.to/nanda-regine/varsityos-an-ai-study-os-built-for-african-students-3jcm</guid>
      <description>&lt;h1&gt;
  
  
  VarsityOS: An AI Study OS Built for African Students
&lt;/h1&gt;

&lt;p&gt;South African students face study conditions Silicon Valley tools were never designed for. Load shedding, NSFAS deadlines, overcrowded libraries. VarsityOS was built to meet them where they are.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI-powered study sessions&lt;/strong&gt; that adapt to your energy levels and available time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline-first architecture&lt;/strong&gt; so load shedding never kills a session mid-flow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NSFAS deadline tracking&lt;/strong&gt; with intelligent reminders tied to the academic calendar&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Past paper engine&lt;/strong&gt; with 26 drill modes and step-by-step explanations&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The stack and one interesting decision
&lt;/h2&gt;

&lt;p&gt;Next.js 14 App Router, Supabase, and Claude AI (routed through a Groq adapter on the free tier). Every AI call routes through Inngest workers rather than route handlers, so a slow generation never times out on Vercel and retries happen automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is next
&lt;/h2&gt;

&lt;p&gt;Collaborative study rooms and a curriculum-linked flashcard engine ship in October.&lt;/p&gt;

&lt;p&gt;Try it free at varsityos.co.za&lt;/p&gt;

</description>
      <category>creativelynanda</category>
    </item>
    <item>
      <title>Building an AI-Powered Engineering OS: Cron Jobs, ADRs, and Autonomous Workflows</title>
      <dc:creator>Nandawula Kabali-Kagwa</dc:creator>
      <pubDate>Sun, 27 Sep 2026 15:31:33 +0000</pubDate>
      <link>https://dev.to/nanda-regine/building-an-ai-powered-engineering-os-cron-jobs-adrs-and-autonomous-workflows-4p3g</link>
      <guid>https://dev.to/nanda-regine/building-an-ai-powered-engineering-os-cron-jobs-adrs-and-autonomous-workflows-4p3g</guid>
      <description>&lt;p&gt;There is a distinct moment in software development when you realize you are spending more time maintaining your workflow than writing code. As a self-taught engineer managing 8 production apps and a registered studio, I hit that wall hard. When you're building for African realities from East London, South Africa—where reliability and efficiency are non-negotiable—manual project management wastes precious cycles.&lt;/p&gt;

&lt;p&gt;So, I decided to let JarvisOS manage itself.&lt;/p&gt;

&lt;p&gt;Over the past week, I engineered a suite of autonomous background systems and architectural pipelines inside JarvisOS. Let's look at how automation can replace administrative fatigue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating the Paper Trail: Commit to ADRs
&lt;/h2&gt;

&lt;p&gt;Documentation is where good software goes to die. Everyone intends to write comprehensive Architecture Decision Records, but life moves fast. To fix this, I built an automated pipeline that intercepts git pushes, analyzes the diff, and auto-suggests structured ADRs.&lt;/p&gt;

&lt;p&gt;Instead of hunting through months of git logs to figure out &lt;em&gt;why&lt;/em&gt; a database schema was refactored, the system documents the decision context at the exact moment the code is shipped. &lt;/p&gt;

&lt;h2&gt;
  
  
  The Autonomous Engineering Team
&lt;/h2&gt;

&lt;p&gt;When you don't have a large engineering team, you build synthetic guardrails. I set up five specialized cron jobs that act as automated colleagues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Cost Watchdog:&lt;/strong&gt; Runs every 2 hours, monitoring AI token expenditure to catch runaway recursive loops before they drain API budgets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Impact Detector:&lt;/strong&gt; Evaluates system health every 30 minutes following any production deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Decision Aging Monitor:&lt;/strong&gt; Daily alerts on technical debt and overdue architectural decisions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Dependency Brief:&lt;/strong&gt; A weekly pulse check on stale packages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Friday Retrospective:&lt;/strong&gt; Automatically compiles the week's output into a clean markdown retro every Friday at 17:00 SAST.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Content Systems at Scale: MirembeApp
&lt;/h2&gt;

&lt;p&gt;Automation isn't just for backend engineering; it applies to content delivery too. This week, MirembeApp migrated to a fully database-driven article architecture, seeding all 22 foundational pieces with a polished migration script. &lt;/p&gt;

&lt;p&gt;We paired this with robust ISR (Incremental Static Regeneration) and an hourly revalidating sitemap with article priorities set to 0.8/weekly. By ensuring analytics (GA4 with Consent Mode v2) load unconditionally and cleanly, we maintain strict visibility while respecting user privacy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;Engineering is ultimately about leverage. By turning administrative overhead into automated background pipelines, we free up cognitive bandwidth to focus on what matters: shipping resilient software that serves real people.&lt;/p&gt;

</description>
      <category>nandapersonal</category>
    </item>
    <item>
      <title>Article about how I actually build for Africa and why</title>
      <dc:creator>Nandawula Kabali-Kagwa</dc:creator>
      <pubDate>Sat, 26 Sep 2026 18:51:09 +0000</pubDate>
      <link>https://dev.to/nanda-regine/article-about-how-i-actually-build-for-africa-and-why-4oec</link>
      <guid>https://dev.to/nanda-regine/article-about-how-i-actually-build-for-africa-and-why-4oec</guid>
      <description>&lt;p&gt;TITLE: How I Build Software That Survives Load-Shedding and Spotty Connectivity in South Africa&lt;/p&gt;

&lt;p&gt;If you build software from a comfortable office with a gigabit fibre connection and uninterrupted grid power, your assumptions about network resilience are almost certainly broken.&lt;/p&gt;

&lt;p&gt;Over the last two years building apps like AdminOS and VarsityOS from East London, South Africa, I’ve had to throw away standard web development playbooks. When Eskom stage 4 load-shedding hits mid-afternoon, your fancy cloud-native microservices architecture doesn't matter if the local cellphone tower drops offline ten minutes later.&lt;/p&gt;

&lt;p&gt;Here is the exact technical stack and architectural pattern I use to build offline-first, resilient web applications that don't choke when the power goes out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Local-First Reality
&lt;/h2&gt;

&lt;p&gt;In San Francisco, local-first is a philosophical choice about data ownership. In the Eastern Cape, local-first is survival. If a spaza shop owner or a university student in Braamfontein loses their internet connection for three hours, they cannot afford an infinite loading spinner.&lt;/p&gt;

&lt;p&gt;Our architecture rests on three pillars:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;SQLite at the edge (via Turso or local IndexedDB adapters).&lt;/li&gt;
&lt;li&gt;Optimistic UI updates with zero-latency local state mutation.&lt;/li&gt;
&lt;li&gt;Background sync queues that retry silently when carrier signal returns.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s look at how we handle local mutations without losing transactional integrity.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;turso&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;SyncQueueItem&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&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;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSERT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UPDATE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DELETE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;table&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;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;any&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;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useOfflineSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;table&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isOnline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsOnline&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onLine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleOnline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
      &lt;span class="nf"&gt;setIsOnline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;flushQueue&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleOffline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setIsOnline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;online&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleOnline&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;offline&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleOffline&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;online&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleOnline&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;offline&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleOffline&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mutateLocally&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;localId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// Write immediately to local IndexedDB/SQLite cache&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;localDb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`INSERT INTO &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (id, data, synced) VALUES (?, ?, 0)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;localId&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="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isOnline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;syncToServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;queueForLater&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;queueForLater&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isOnline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mutateLocally&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;queueForLater&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sync_queue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sync_queue&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}]));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;flushQueue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SyncQueueItem&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sync_queue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/sync&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="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;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="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="c1"&gt;// Remove successfully synced item&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Stop flushing if connection drops again&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Handling Latency Spikes (3G vs Fibre)
&lt;/h2&gt;

&lt;p&gt;Most Western users are on broadband. In South Africa, a huge portion of users access web applications via mobile data on 3G or unstable 4G networks where latency can spike from 50ms to 4,000ms instantly.&lt;/p&gt;

&lt;p&gt;If you use heavy client-side hydration frameworks without aggressive code-splitting, your JavaScript bundle size will penalize users paying high rates per megabyte. We enforce a strict rule across Mirembe Muse products: initial JavaScript bundles must stay under 85KB compressed.&lt;/p&gt;

&lt;p&gt;We achieve this by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Eliminating heavy UI component libraries in favour of bespoke, headless Tailwind primitives.&lt;/li&gt;
&lt;li&gt;Server-side rendering critical shell layouts so the user sees painted pixels in under 300ms, even on a budget Android device.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why This Matters Beyond Africa
&lt;/h2&gt;

&lt;p&gt;When you build for extreme constraints—unstable power grids, high data costs, intermittent latency—you don't just build for Africa. You build better software. &lt;/p&gt;

&lt;p&gt;The same resilience that keeps an inventory app running during stage 3 load-shedding will keep your app running smoothly for a user on a shaky commuter train in London or a subway in New York.&lt;/p&gt;

&lt;p&gt;Stop designing for the ideal network condition. Build for the real world.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>webdev</category>
      <category>architecture</category>
      <category>opensource</category>
    </item>
    <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>
