<?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: Amon Ochuka</title>
    <description>The latest articles on DEV Community by Amon Ochuka (@amonoff).</description>
    <link>https://dev.to/amonoff</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%2F3718940%2Fbe2a72ca-7ab5-4417-8f38-6e11befa8321.png</url>
      <title>DEV Community: Amon Ochuka</title>
      <link>https://dev.to/amonoff</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/amonoff"/>
    <language>en</language>
    <item>
      <title>Building a RAG Knowledge Assistant: Chunking, Embeddings, Vector Search, and Answers That Cite Their Sources</title>
      <dc:creator>Amon Ochuka</dc:creator>
      <pubDate>Tue, 27 Oct 2026 21:23:00 +0000</pubDate>
      <link>https://dev.to/amonoff/building-a-rag-knowledge-assistant-chunking-embeddings-vector-search-and-answers-that-cite-4mbf</link>
      <guid>https://dev.to/amonoff/building-a-rag-knowledge-assistant-chunking-embeddings-vector-search-and-answers-that-cite-4mbf</guid>
      <description>&lt;p&gt;Ask a general-purpose LLM about your company's refund policy and it will answer confidently. It will also be wrong, because it has never seen your policy.&lt;/p&gt;

&lt;p&gt;That gap is why Retrieval-Augmented Generation (RAG) exists. Instead of hoping the model "knows" your data, you find the relevant text first and hand it to the model along with the question. The model stops guessing and starts reading.&lt;/p&gt;

&lt;p&gt;Guidely is an internal knowledge assistant I'm building at Zone01 Kisumu, with a FastAPI backend and a React/Vite frontend. You upload company docs, it indexes them, and when you ask a question it returns a short answer with the sources it used. In this post I'll break down how the pipeline works, what each stage costs you, and where RAG systems quietly fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With Asking an LLM Directly
&lt;/h2&gt;

&lt;p&gt;A bare LLM has two weaknesses for this job. It doesn't know your private documents, and when it doesn't know something it tends to produce a plausible answer anyway. For a support tool, a confident wrong answer is worse than no answer, because people act on it.&lt;/p&gt;

&lt;p&gt;Pasting every document into the prompt isn't the fix either. Context windows are limited, long prompts are slow and expensive, and models are worse at finding a needle in a huge haystack than in a small one. You want to send the model only the few passages that matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Just Use Keyword Search?
&lt;/h2&gt;

&lt;p&gt;Keyword search is the obvious shortcut, and it works until it doesn't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vocabulary mismatch breaks it.&lt;/strong&gt; "How do I reset my password?" and "I forgot my login credentials" share almost no words, yet they ask the same thing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It returns pages, not answers.&lt;/strong&gt; Users still dig through results to find the relevant paragraph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It can't explain itself in plain language.&lt;/strong&gt; You get a list of links, not a readable answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Semantic search fixes the first problem, and an LLM fixes the other two. RAG combines both.&lt;/p&gt;

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

&lt;p&gt;The design has two phases. Indexing happens offline, when documents are added or changed. Querying happens online, on every question.&lt;/p&gt;

&lt;p&gt;Indexing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Parse&lt;/strong&gt; the uploaded file into raw text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunk&lt;/strong&gt; the text into small overlapping pieces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embed&lt;/strong&gt; each chunk into a vector.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store&lt;/strong&gt; the vectors, plus the chunk text and file name, in a vector index.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Querying:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Embed&lt;/strong&gt; the question with the same model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search&lt;/strong&gt; the index for the top-k most similar chunks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate&lt;/strong&gt; an answer from those chunks and the question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return&lt;/strong&gt; the answer together with the sources that were used.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important property is that the model only sees what retrieval hands it. Everything downstream depends on that handoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chunking: The Most Underrated Decision
&lt;/h2&gt;

&lt;p&gt;Embedding a 40-page PDF as one vector produces mush. So documents are split into chunks, typically 500 to 1,000 tokens, with a small overlap between neighbours so a sentence cut at a boundary still has its context on the other side.&lt;/p&gt;

&lt;p&gt;Chunk size is a trade-off. Too big, and retrieval gets vague because one chunk covers several topics. Too small, and each chunk loses the context it needs to make sense on its own. Splitting on natural boundaries like headings and paragraphs beats cutting at a fixed character count, because it keeps ideas intact.&lt;/p&gt;

&lt;p&gt;Every chunk also keeps metadata: the file it came from and its position in that file. That metadata is what later lets the UI say exactly where an answer came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Embeddings and the Vector Store
&lt;/h2&gt;

&lt;p&gt;An embedding turns text into a list of numbers so that similar meaning means nearby vectors. That is what lets the password question match the login question despite sharing no words.&lt;/p&gt;

&lt;p&gt;For storage, Guidely uses FAISS: it runs locally, needs no account or external service, and is fast. With normalized vectors and inner-product search, the score is cosine similarity. An exact flat index is perfectly fine up to hundreds of thousands of chunks. Approximate indexes matter later, and hosted options like Pinecone or Weaviate make sense once you need managed infrastructure. Start with the simplest thing that works.&lt;/p&gt;

&lt;p&gt;One rule matters here: the question and the chunks must be embedded with the same model. Vectors from different models live in different spaces and can't be compared.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating a Grounded Answer
&lt;/h2&gt;

&lt;p&gt;The "G" in RAG is where you enforce honesty. The prompt gives the model the numbered chunks and a strict set of instructions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Answer using only the provided context.&lt;/li&gt;
&lt;li&gt;Cite sources by number.&lt;/li&gt;
&lt;li&gt;If the context doesn't contain the answer, say you don't know.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of those does real work. "Only the context" reduces hallucination. Numbered sources let the model cite, and let the UI map each citation back to a real file and snippet. The "I don't know" escape hatch stops the model from inventing an answer when retrieval fails. Setting the temperature to zero keeps the answers stable and reproducible.&lt;/p&gt;

&lt;p&gt;The API then returns clean JSON: the answer, plus the list of sources with file names and snippets. Always showing sources turns "trust me" into "check for yourself."&lt;/p&gt;

&lt;h2&gt;
  
  
  Things That Bite You in Production
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Re-embedding everything.&lt;/strong&gt; Embedding costs money and time, and documents rarely change. Hash each file's content at index time and skip it if the hash is unchanged. Log cache hits against new embeddings. If you re-ask a question and the embedding count goes up, something is broken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exact terms and IDs.&lt;/strong&gt; Pure vector search is weak on error codes, product names, and identifiers, which is exactly where keyword search is strong. Hybrid search runs both and merges the rankings, and it is often the single biggest quality upgrade you can make.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coarse ranking.&lt;/strong&gt; Vector search is fast but approximate. A common upgrade is to retrieve a wide net, say the top 20, then use a cross-encoder reranker that reads the question and each chunk together to pick the best few. You trade a little latency for better precision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow-up questions.&lt;/strong&gt; A user asks "what about for contractors?" and retrieval has nothing to search for. Rewrite follow-ups into standalone questions before embedding them, using the conversation history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt injection.&lt;/strong&gt; Your documents become part of the prompt, so a document containing "ignore previous instructions" is an attack vector. Keep system instructions separate from retrieved content, label the context clearly as data, and never let retrieved text trigger actions without validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure states.&lt;/strong&gt; Real systems hit empty queries, missing API keys, corrupted files, model timeouts, and searches that find nothing. Return proper 4xx and 5xx responses with clear messages, and show friendly error states in the UI. "No relevant documents found" is a good answer. A confident hallucination is the worst one.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Know It Works?
&lt;/h2&gt;

&lt;p&gt;"It seems to work" isn't a metric. Guidely tracks a small set of checks, some logged automatically by the backend and some validated by hand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval@3 (manual):&lt;/strong&gt; for 10 to 20 questions whose answers exist in the sample docs, how often does the correct passage appear in the top 3 chunks? Target: 80% or more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer reference coverage (manual):&lt;/strong&gt; does every answer cite at least one valid source? Target: 90% or more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source precision (manual):&lt;/strong&gt; does the highlighted snippet actually support the answer? Target: 80% or more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency (auto-logged):&lt;/strong&gt; time from question to final JSON. Target: median under 3 seconds and p95 under 5 seconds with a warm cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedding cache effectiveness (auto-logged):&lt;/strong&gt; repeated queries on unchanged docs should cause zero re-embedding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most useful habit is evaluating retrieval separately from generation. If the right chunk isn't in the top 3, no prompt will save you. Check retrieval first, then check the answers, and when quality drops you'll know which half of the pipeline to blame.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval quality is the ceiling.&lt;/strong&gt; The LLM can only be as good as the context you hand it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunking and prompts matter more than model choice.&lt;/strong&gt; Upgrading the model rarely fixes a bad pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test embedding and retrieval before the UI.&lt;/strong&gt; Wire up the interface last, once search returns the right chunks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache from day one.&lt;/strong&gt; Hash-based skipping is easy to build early and painful to retrofit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always show your sources.&lt;/strong&gt; Citations are what make the tool usable by real people.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log everything cheap.&lt;/strong&gt; Latency, cache hits, retrieved chunk IDs, and error counts make debugging far easier.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;RAG isn't magic. It's search plus a well-instructed reader, and once you see it that way, every improvement becomes an ordinary engineering problem: better chunks, hybrid search, reranking, query rewriting. If you're building your first one, start small: five documents, FAISS, and a strict prompt. Get retrieval working and measured before you touch anything else, and the rest of the system gets much easier to reason about.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>llm</category>
      <category>rag</category>
    </item>
    <item>
      <title>Building a Secure Auth System in Go: JWT, 2FA, OAuth, Sessions, and Rate Limiting</title>
      <dc:creator>Amon Ochuka</dc:creator>
      <pubDate>Tue, 27 Oct 2026 21:17:29 +0000</pubDate>
      <link>https://dev.to/amonoff/building-a-secure-auth-system-in-go-jwt-2fa-oauth-sessions-and-rate-limiting-4c91</link>
      <guid>https://dev.to/amonoff/building-a-secure-auth-system-in-go-jwt-2fa-oauth-sessions-and-rate-limiting-4c91</guid>
      <description>&lt;p&gt;Authentication seems simple at first glance. Then you add "remember me", OAuth, 2FA, and proper session revocation, and it gets complicated fast.&lt;/p&gt;

&lt;p&gt;Wapi is a live streaming app tailored for African content, with a backend written in Go using Gin. Its auth system covers many production concerns, from token rotation to two-factor login. In this post I'll walk through the pieces, why each one matters, and the mistakes that are easy to make along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Token Strategy: Access and Refresh Tokens
&lt;/h2&gt;

&lt;p&gt;Wapi uses the classic dual-token approach. Short-lived access tokens authenticate API requests, and longer-lived refresh tokens obtain new access tokens without forcing the user to log in again.&lt;/p&gt;

&lt;p&gt;The login endpoint returns both tokens along with the user profile. When the access token expires, the client calls the refresh endpoint, which issues a new access token and a new refresh token.&lt;/p&gt;

&lt;p&gt;That second part is the important one: &lt;strong&gt;refresh token rotation&lt;/strong&gt;. Every time a refresh token is used, it is invalidated and replaced. This limits the damage if a refresh token is stolen, because the thief and the real user can't both keep using the same token.&lt;/p&gt;

&lt;p&gt;Rotation also gives you theft detection. If someone presents a refresh token that has already been rotated, one of two things happened: a client retried badly, or a token was copied. Either way, the safe response is to revoke the entire session the token belonged to and force a fresh login.&lt;/p&gt;

&lt;h2&gt;
  
  
  JWTs Done Right
&lt;/h2&gt;

&lt;p&gt;JSON Web Tokens are convenient and easy to misuse. A few rules go a long way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep access tokens short-lived.&lt;/strong&gt; Minutes, not hours. The shorter the lifetime, the smaller the window an attacker has with a stolen token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sign properly.&lt;/strong&gt; Use a strong secret (HMAC) or an asymmetric key pair (RSA or ECDSA). Pin the expected signing algorithm when parsing, so a token can't claim a weaker one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never put sensitive data in the payload.&lt;/strong&gt; A JWT is signed, not encrypted. Anyone holding the token can read its claims.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate everything.&lt;/strong&gt; Check the signature, the expiry (&lt;code&gt;exp&lt;/code&gt;), and the issuer and audience where they apply.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store minimal claims.&lt;/strong&gt; Usually just the user ID, a token ID (&lt;code&gt;jti&lt;/code&gt;), and the expiry. Don't bloat the token, since it travels with every request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a Gin application this lives in middleware. It extracts the Bearer token from the &lt;code&gt;Authorization&lt;/code&gt; header, validates it, and attaches the user ID to the request context so downstream handlers never have to parse tokens themselves.&lt;/p&gt;

&lt;p&gt;There is one trade-off to be honest about. A pure JWT is stateless, so the server can't "un-issue" it before it expires. That is why access tokens must be short-lived, and why real revocation happens at the refresh token and session level. If you need instant revocation of access tokens, for example after a ban, you can check the token's session ID against a store on each request. That reintroduces state, so it's a deliberate trade of some of JWT's simplicity for control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Session Management
&lt;/h2&gt;

&lt;p&gt;Tokens alone aren't enough. Users and the system both need to be able to see and end sessions. Wapi exposes endpoints to list active sessions, revoke a specific session, and revoke all sessions at once.&lt;/p&gt;

&lt;p&gt;This matters for several scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Sign out of all devices"&lt;/strong&gt; after a lost phone or a password change&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Killing suspicious sessions&lt;/strong&gt; once a login looks wrong&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meeting security expectations&lt;/strong&gt; that users increasingly take for granted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To support it, you track each refresh token (or session ID) in the database along with metadata: device or user agent, IP address, creation time, and last-used time. Validating a refresh token then means checking that it exists and hasn't been revoked, and logging out means revoking it. The metadata is what turns a bare list of tokens into something a user can understand, such as "Chrome on Windows, last active two hours ago".&lt;/p&gt;

&lt;p&gt;Store a hash of the refresh token rather than the token itself, the same way you would treat a password. If the database leaks, the stored values shouldn't be usable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two-Factor Authentication (TOTP)
&lt;/h2&gt;

&lt;p&gt;Adding 2FA is one of the biggest security upgrades you can make. Wapi implements TOTP (Time-based One-Time Passwords), which works with Google Authenticator, Authy, 1Password, and similar apps.&lt;/p&gt;

&lt;p&gt;The flow has four stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Setup.&lt;/strong&gt; An authenticated user requests 2FA setup. The server generates a secret and returns an &lt;code&gt;otpauth://&lt;/code&gt; URI, which the client renders as a QR code. The secret is stored (encrypted) against the user but not yet active.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification.&lt;/strong&gt; The user scans the QR code and enters the code their app displays. The server checks it against the stored secret, and only then switches 2FA on. This confirms the user's app is configured correctly before they can lock themselves out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Login with 2FA.&lt;/strong&gt; When 2FA is enabled, a correct email and password no longer produce tokens. The server returns a short-lived challenge instead, and the user completes login by submitting a TOTP code. Only then are access and refresh tokens issued.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disabling.&lt;/strong&gt; Turning 2FA off requires re-authentication, so someone with a briefly unattended session can't quietly weaken the account.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two details deserve attention. First, the login challenge should be a single-purpose credential: short expiry, usable only to complete 2FA, and never accepted as an access token. Second, 2FA verification needs its own strict rate limit. A six-digit code has only a million possibilities, and brute-forcing that is realistic if attempts are unthrottled.&lt;/p&gt;

&lt;p&gt;Finally, plan for account recovery. Users lose phones, so you'll want backup codes, generated at setup, stored hashed, and single-use.&lt;/p&gt;

&lt;h2&gt;
  
  
  OAuth 2.0 (Google and Facebook)
&lt;/h2&gt;

&lt;p&gt;Social login is convenient for users and has its own pitfalls. Wapi uses the standard authorization code flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The client hits the server's login endpoint for a provider, which redirects to the provider's authorization page.&lt;/li&gt;
&lt;li&gt;The user authenticates with the provider and consents.&lt;/li&gt;
&lt;li&gt;The provider redirects back to the server's callback endpoint with an authorization code.&lt;/li&gt;
&lt;li&gt;The server exchanges the code for tokens with the provider and fetches the user's profile.&lt;/li&gt;
&lt;li&gt;The server finds or creates a local account linked to that provider identity.&lt;/li&gt;
&lt;li&gt;The server issues its own access and refresh tokens.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 6 is where Wapi does something smart. Instead of putting tokens in the redirect URL, it redirects the browser back to the frontend with a short-lived one-time code, and the frontend exchanges that code for tokens through a separate endpoint. URLs end up in browser history, server logs, and Referer headers, so tokens don't belong there. A code that is single-use and expires in seconds is a much safer thing to leak.&lt;/p&gt;

&lt;p&gt;A few OAuth details are easy to miss:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Validate the &lt;code&gt;state&lt;/code&gt; parameter&lt;/strong&gt; to protect against cross-site request forgery on the callback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use PKCE&lt;/strong&gt; where the provider supports it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be careful when linking accounts by email.&lt;/strong&gt; Only auto-link to an existing local account if the provider confirms the email is verified. Otherwise an attacker can register at a provider with someone else's address and take over their account.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rate Limiting for Abuse Prevention
&lt;/h2&gt;

&lt;p&gt;Auth endpoints are prime targets for credential stuffing and brute-force attacks. Wapi applies different limits depending on the endpoint:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint type&lt;/th&gt;
&lt;th&gt;Limit&lt;/th&gt;
&lt;th&gt;Keyed by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Signup, login, password reset&lt;/td&gt;
&lt;td&gt;10 per minute&lt;/td&gt;
&lt;td&gt;IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAuth initiation&lt;/td&gt;
&lt;td&gt;5 per minute&lt;/td&gt;
&lt;td&gt;IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2FA verification (suggested)&lt;/td&gt;
&lt;td&gt;Stricter than login, for example 5 attempts per 15 minutes&lt;/td&gt;
&lt;td&gt;IP and user or session&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The limiter is built on &lt;code&gt;golang.org/x/time/rate&lt;/code&gt;, with buckets stored per key. When a limit is hit, the API returns &lt;code&gt;429 Too Many Requests&lt;/code&gt; with a clear error code so clients can back off gracefully.&lt;/p&gt;

&lt;p&gt;An in-memory limiter is fine for a single instance, but note what happens when you scale out. Each instance counts separately, so your effective limit is multiplied by the number of instances. When you run more than one, move the counters to Redis, using a sliding window or token bucket, so every instance shares the same view. Also remember that IP-based limits are blunt: users behind a shared NAT share a bucket, and attackers rotate IPs. That matters even more for a mobile-heavy audience, where carrier-grade NAT can put many real users behind one IP. That is why the 2FA limit also keys on the user or session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Best Practices to Keep in Mind
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Password hashing.&lt;/strong&gt; Never store plaintext passwords. Use bcrypt, argon2, or scrypt with a sensible cost factor, and raise it over time as hardware improves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email verification.&lt;/strong&gt; Verify addresses before allowing sensitive actions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password reset.&lt;/strong&gt; Use time-limited, single-use tokens delivered by email, and invalidate existing sessions when a reset completes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Account enumeration.&lt;/strong&gt; Return the same response whether or not an email exists, for login errors and for password reset requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CORS.&lt;/strong&gt; Lock allowed origins down to the ones you actually use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure headers.&lt;/strong&gt; Add standard security headers in production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token storage on the client.&lt;/strong&gt; HttpOnly, Secure, SameSite cookies protect tokens from theft via XSS in a way localStorage can't. The trade-off is that cookie-based auth needs CSRF protection, which SameSite helps with. If you send tokens in an &lt;code&gt;Authorization&lt;/code&gt; header instead, keep them in memory and accept the refresh-on-reload cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;p&gt;A robust auth system isn't about implementing every feature at once. It's about layering defenses. Start with solid password authentication and short-lived JWTs. Add refresh token rotation and session management so users can end sessions remotely. Layer on 2FA for accounts that matter, add OAuth for convenience, and wrap everything in rate limiting.&lt;/p&gt;

&lt;p&gt;What stands out in Wapi's implementation is that these pieces are wired up and work together rather than existing as isolated features. The routes are organized logically, the middleware is reusable, and each flow closes the gaps the previous layer leaves open. It's a solid reference for anyone building auth in Go.&lt;/p&gt;

</description>
      <category>authentication</category>
      <category>backend</category>
      <category>go</category>
      <category>security</category>
    </item>
    <item>
      <title>Scaling Real-Time Stream Chat in Go: In-Memory Hubs, WebSockets, and Redis Pub/Sub</title>
      <dc:creator>Amon Ochuka</dc:creator>
      <pubDate>Tue, 27 Oct 2026 21:10:33 +0000</pubDate>
      <link>https://dev.to/amonoff/scaling-real-time-stream-chat-in-go-in-memory-hubs-websockets-and-redis-pubsub-4i04</link>
      <guid>https://dev.to/amonoff/scaling-real-time-stream-chat-in-go-in-memory-hubs-websockets-and-redis-pubsub-4i04</guid>
      <description>&lt;p&gt;Live stream chat is deceptively hard. What starts as "just broadcast messages to connected clients" becomes a distributed systems problem the moment you scale past a single server instance.&lt;/p&gt;

&lt;p&gt;Wapi is a live streaming app tailored for African content, with a backend written in Go. Its chat takes a pragmatic approach to scaling: an in-memory WebSocket hub per process, with an optional Redis Pub/Sub broadcaster for cross-instance fan-out. In this post I'll break down why the design works, what it costs you, and where it starts to strain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With Single-Instance Chat
&lt;/h2&gt;

&lt;p&gt;If you only ever run one backend instance, keeping all connected clients in memory and broadcasting to them directly is the simplest and fastest option. You get low latency, no external dependencies, and a system that is easy to reason about. Go is well suited to this because goroutines and channels make managing thousands of concurrent connections cheap.&lt;/p&gt;

&lt;p&gt;In production, though, you'll almost always want multiple instances behind a load balancer, for redundancy, for rolling deploys, and for capacity. With a pure in-memory hub, a client connected to instance A never receives messages sent by a client connected to instance B. Your "live" chat fragments based on which instance each user was routed to, and users in the same stream see different conversations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Just Use Sticky Sessions?
&lt;/h2&gt;

&lt;p&gt;The obvious shortcut is to route every viewer of a given stream to the same instance. It can work, but it has real drawbacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hot streams break it.&lt;/strong&gt; A popular stream can have more viewers than a single instance can hold, and you can't split it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploys get painful.&lt;/strong&gt; Restarting an instance drops every connection pinned to it, and all those clients reconnect at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load balancing gets complicated.&lt;/strong&gt; You need routing logic that understands stream IDs, not just connections.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fan-out between instances removes all three problems. Any instance can serve any viewer, and the instances keep each other informed.&lt;/p&gt;

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

&lt;p&gt;The design is a hybrid. Each instance runs its own hub that manages WebSocket connections and broadcasts locally. If &lt;code&gt;REDIS_URL&lt;/code&gt; is configured, each instance also publishes its messages to Redis and subscribes to messages from other instances. A message arriving from Redis is re-broadcast to local connections, but only if it originated on a different instance, so it never echoes back.&lt;/p&gt;

&lt;p&gt;This gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Local speed&lt;/strong&gt;: no network hop between clients on the same instance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Horizontal scalability&lt;/strong&gt;: add instances without breaking chat&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graceful degradation&lt;/strong&gt;: without Redis, it falls back to single-instance mode&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplicity&lt;/strong&gt;: no dedicated message queue, just Redis Pub/Sub&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How the In-Memory Hub Works
&lt;/h2&gt;

&lt;p&gt;The hub's job is to track which clients are watching which stream, and to deliver each message to everyone watching that stream. Internally it is a map from stream ID to a set of connected clients.&lt;/p&gt;

&lt;p&gt;The important design choice is how that map is protected. Instead of guarding it with locks, the hub runs in a single goroutine and receives all changes through channels: one for clients registering, one for clients leaving, and one for messages to broadcast. Because only that goroutine ever touches the map, there are no data races and no lock contention, and the event loop is easy to follow.&lt;/p&gt;

&lt;p&gt;Each client gets its own buffered outbound channel, drained by a dedicated write goroutine that pushes messages down the WebSocket. A matching read goroutine handles incoming messages. This read/write pump pair per connection is the standard pattern for WebSockets in Go, and it keeps slow network writes off the hub's critical path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Slow Client Problem
&lt;/h2&gt;

&lt;p&gt;Every real-time system eventually meets the slow consumer: a client on a bad mobile connection whose outbound buffer fills up. If the hub blocks while delivering to that client, one bad connection stalls chat for everyone in the stream.&lt;/p&gt;

&lt;p&gt;For an audience that is largely on mobile data, this isn't an edge case, it's the normal case.&lt;/p&gt;

&lt;p&gt;The fix is to never block. If a client's buffer is full, the hub drops that client, closes its channel, and lets the client reconnect. It sounds harsh, but it protects the many from the few. Combined with history loading on reconnect, the user barely notices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Redis for Cross-Instance Fan-Out
&lt;/h2&gt;

&lt;p&gt;On startup, each instance generates a unique &lt;code&gt;instanceID&lt;/code&gt;, for example with &lt;code&gt;uuid.NewString()&lt;/code&gt;. When it publishes a message to Redis, it attaches this ID to the payload. When it receives a message from Redis, it checks the ID first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Same ID:&lt;/strong&gt; the message came from this instance, and local clients already got it directly from the hub. Ignore it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Different ID:&lt;/strong&gt; another instance produced it. Hand it to the local hub for delivery.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without that check, every message would be delivered twice on the instance that sent it.&lt;/p&gt;

&lt;p&gt;A design choice worth noting is how channels are laid out. Publishing to one channel per stream (something like &lt;code&gt;chat:stream:&amp;lt;id&amp;gt;&lt;/code&gt;) and subscribing with a single pattern subscription keeps the subscriber code simple. Each instance holds one subscription rather than managing a subscribe and unsubscribe for every stream as viewers come and go.&lt;/p&gt;

&lt;p&gt;One more detail matters for correctness: the Redis listener should not write to the hub's client map directly. It should send messages into the same broadcast channel that local messages use, so the single-goroutine guarantee holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Pub/Sub and Not Something Heavier
&lt;/h2&gt;

&lt;p&gt;Redis Pub/Sub is fire-and-forget. If a subscriber is disconnected when a message is published, that message is gone for that subscriber. For payments that would be unacceptable. For live chat it is usually fine, for three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Chat is ephemeral by nature, and a dropped message during a brief blip is rarely noticed.&lt;/li&gt;
&lt;li&gt;Every message is also persisted to the database, so history is complete even if live delivery isn't.&lt;/li&gt;
&lt;li&gt;The operational cost is tiny compared with running Kafka or a full message broker.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you later need replay, consumer groups, or delivery guarantees, Redis Streams, NATS JetStream, or Kafka are the natural next steps. Start simple and upgrade when a real requirement forces you to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It Together
&lt;/h2&gt;

&lt;p&gt;The full flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Client connects&lt;/strong&gt; via WebSocket to something like &lt;code&gt;/api/v1/streams/:id/ws&lt;/code&gt;, with auth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection is upgraded&lt;/strong&gt; and the client is registered with the local hub for that stream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client sends a message.&lt;/strong&gt; The service layer validates it (rate limit, ban check, permissions), persists it to the database for history, then pushes it to the local hub and publishes it to Redis with the instance ID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other local clients&lt;/strong&gt; receive it instantly from the hub.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other instances&lt;/strong&gt; receive it from Redis, see a different instance ID, and broadcast it to their own local clients.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Things That Bite You in Production
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ordering.&lt;/strong&gt; Within a single instance, messages are delivered in the order the hub processes them. Across instances there is no global ordering guarantee. Two users on different instances can send messages at nearly the same moment, and viewers may see them in different orders. For most chats this is acceptable, but if it matters, assign ordering on the server using database timestamps or sequence IDs and let clients sort on that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication and origin checks.&lt;/strong&gt; WebSocket upgrades bypass a lot of the protections you rely on for normal HTTP requests. Validate the token during the upgrade, check the &lt;code&gt;Origin&lt;/code&gt; header, and re-check permissions when bans or role changes happen mid-session, not just at connect time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reconnection storms.&lt;/strong&gt; When an instance restarts, every client it held reconnects at once. Clients should use exponential backoff with jitter so they don't all retry in lockstep and overwhelm the remaining instances.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heartbeats.&lt;/strong&gt; Half-open connections are common, especially on mobile. Use WebSocket ping/pong with read deadlines so dead connections get cleaned up rather than lingering in the hub's map and leaking memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Message size and rate limits.&lt;/strong&gt; Cap payload sizes and rate limit per user, for example 5 messages per second. Stream chat gets hot fast, and one spammer can saturate both the hub and Redis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Observability.&lt;/strong&gt; Track connected clients per instance, messages published and received per second, dropped slow clients, and Redis publish errors. When chat feels laggy, these numbers tell you whether the problem is the hub, the network, or Redis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Filter your echoes.&lt;/strong&gt; The instance ID check is non-negotiable. Without it, you get duplicate messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never block the hub.&lt;/strong&gt; Drop slow clients rather than letting them stall everyone else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persist for history.&lt;/strong&gt; WebSockets are ephemeral. Write every message to your database so clients can load history on reconnect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't let Redis block chat.&lt;/strong&gt; If a publish fails, log it and carry on. Local delivery should never depend on Redis being up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan for disconnects.&lt;/strong&gt; Networks drop. Build reconnection with backoff into the client from day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Know the limits.&lt;/strong&gt; Pub/Sub doesn't persist messages, and every instance receives every message for the streams it subscribes to. That is fine at moderate scale, but at very high volume you'll want to shard channels or move to a different transport.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The hybrid in-memory plus Redis approach is a sweet spot for most applications. It keeps the fast path local, scales horizontally with one small dependency, and degrades gracefully when Redis is unavailable. It doesn't overcomplicate your architecture before you need it, and the upgrade path to Streams or a full broker stays open. If you're building real-time features in Go, this pattern is worth stealing.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>go</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Why AI Can't Save You From Not Understanding Your Own System</title>
      <dc:creator>Amon Ochuka</dc:creator>
      <pubDate>Sat, 18 Jul 2026 17:01:40 +0000</pubDate>
      <link>https://dev.to/amonoff/why-ai-cant-save-you-from-not-understanding-your-own-system-39c4</link>
      <guid>https://dev.to/amonoff/why-ai-cant-save-you-from-not-understanding-your-own-system-39c4</guid>
      <description>&lt;p&gt;System design matters more than most people realize, and most of us practice it every day without noticing.&lt;/p&gt;

&lt;p&gt;AI has made building software faster than it's ever been. It can scaffold a project in seconds, generate a CRUD layer, explain a concept you're fuzzy on, and get you from idea to working prototype in minutes. If you're learning to code right now, this is an incredible advantage that developers five years ago simply didn't have.&lt;br&gt;
But there's a quiet trap hiding inside that speed: AI is only as useful as your ability to follow what it built.&lt;br&gt;
This week I ran into a bug that made that lesson very concrete.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Bug That Wasn't Where It Looked
&lt;/h2&gt;

&lt;p&gt;I was working on an escrow platform's backend — a Go API with layered architecture: handlers, services, repositories, middleware. One endpoint kept returning a plain 400 Bad Request, no error body, nothing useful in the response.&lt;br&gt;
The obvious place to look was the endpoint itself. The route existed. The handler looked correct. The service logic seemed fine. I stared at that one function for longer than I'd like to admit, because that's where the symptom was showing up.&lt;br&gt;
The actual problem wasn't there at all.&lt;br&gt;
It turned out a stale access token from an earlier test was silently failing verification before the request ever reached my handler. The middleware sitting in front of the route was rejecting it, quietly, without the kind of error message that would've pointed me anywhere useful.&lt;br&gt;
The fix wasn't in the endpoint. It was in understanding the full path a request takes through the system — middleware, then handler, then service, then repository,and tracing it one layer at a time, adding a log line at each boundary until I found exactly where execution stopped.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why AI Couldn't Just Solve This For Me
&lt;/h2&gt;

&lt;p&gt;Here's the part I want to sit with, because I think it's the actual lesson, not just an anecdote.&lt;br&gt;
If I'd asked an AI assistant "why is this endpoint returning 400," it could reason about the code I showed it,and it did, correctly, in this exact case. But it could only reason about what I gave it. It doesn't have a live view of my running server, my actual request/response cycle, or my database state. It can't run my curl command and watch what happens. &lt;br&gt;
It can suggest the most likely causes based on patterns, but it can't independently verify which one is actually true in my specific running system.&lt;br&gt;
That verification step — the actual debugging — still has to happen inside your own understanding of how the pieces fit together. AI can help you form hypotheses faster. It cannot replace tracing the real, live behavior of your own application.&lt;/p&gt;

&lt;p&gt;This is true at a small scale (one 400 error) and it's even more true at the system design level. AI is excellent at generating a plausible-looking architecture — a service layer here, a repository there, a middleware chain that looks clean on paper. But plausible and correct for your actual system are not the same thing. If you don't understand why the architecture is shaped the way it is, you won't notice when it's subtly wrong for your use case, and you definitely won't be able to debug it when something breaks in the seams between the pieces AI generated separately.&lt;/p&gt;
&lt;h2&gt;
  
  
  System Design Is Something You Already Practice
&lt;/h2&gt;

&lt;p&gt;Here's the thing I think gets missed: system design isn't some separate, advanced discipline you unlock later. If you've ever asked yourself questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Should this validation live in the handler or the service?"&lt;/li&gt;
&lt;li&gt;"What happens if this database call fails halfway through?"&lt;/li&gt;
&lt;li&gt;"Does this component need to know about that one, or should they stay decoupled?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;— you were already doing system design. Most developers do this daily, in small decisions, without ever labeling it that way. AI can generate answers to these questions for you. But if you don't understand why one answer is better than another for your specific system, you're not actually equipped to catch it when the AI's answer is wrong, or right in general but wrong for your case.&lt;/p&gt;
&lt;h2&gt;
  
  
  A Concrete Example: Layered Validation
&lt;/h2&gt;

&lt;p&gt;To make this less abstract, here's a real pattern from the same project. I have three related resources — a deal, an artifact belonging to that deal, and a verification belonging to that artifact. &lt;br&gt;
Early on, nothing stopped a client from requesting artifact ID from deal A while passing deal ID for deal B in the URL — the IDs would resolve independently, and as long as each one individually existed in the database, the request would "succeed," even though the artifact didn't actually belong to that deal.&lt;/p&gt;

&lt;p&gt;The fix wasn't complicated once it was understood:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;if artifact.DealID != dealID {&lt;br&gt;
    return nil, ErrArtifactNotFound&lt;br&gt;
}&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;Every layer that owns a nested resource needs to check that the parent chain actually matches, not just that each individual ID exists somewhere in the database. This is the kind of bug that's easy for AI to catch once you point at it directly, but it's very easy to miss in the first pass if you're not thinking about your system's ownership hierarchy at all, and just asking AI to "add an endpoint to get a verification by ID."&lt;br&gt;
The endpoint worked. It just wasn't correct, and "works" and "correct" diverge exactly at the boundaries AI can't see unless you show it the full picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  So What Does This Actually Mean Day to Day?
&lt;/h2&gt;

&lt;p&gt;I don't think the answer is "don't use AI for architecture", that would throw away a genuinely useful tool. I think the answer is closer to this:&lt;br&gt;
&lt;strong&gt;Use AI to move faster, not to move blind.&lt;/strong&gt;&lt;br&gt;
Concretely, that's looked like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Asking why a suggested pattern works, not just accepting that it does&lt;/li&gt;
&lt;li&gt;Deliberately tracing a request through every layer it passes through, even when things are working, so I actually know the shape of my own system&lt;/li&gt;
&lt;li&gt;Treating a bug as a chance to understand the architecture better, not just to patch the symptom&lt;/li&gt;
&lt;li&gt;Writing my own validation logic (like the ownership check above) by reasoning through the actual failure case, rather than only generating code and hoping it's complete&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is about distrusting AI. It's about recognizing that AI's usefulness for system design has a hard ceiling: it can only be as good as the mental model you bring to the conversation. If you don't understand your own system, you can't tell a subtly wrong suggestion from a genuinely good one, you can only tell "does this compile" from "does this error," and that's a much weaker signal than actually knowing your architecture.&lt;/p&gt;

&lt;p&gt;So remember...&lt;br&gt;
You don't need to memorize every line of code you write, and you shouldn't try to. But you do need to understand how your system flows — request in, through which layers, touching which data, back out — because that's the only thing that lets you debug with confidence instead of guessing, and it's the only thing that lets you actually evaluate whether an AI-suggested design is right for your system, not just right in general.&lt;br&gt;
That's what turns building software from assembling suggestions into actually engineering something.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>systemdesign</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>The Rise, Pause, and Rise of CRUD Apps</title>
      <dc:creator>Amon Ochuka</dc:creator>
      <pubDate>Wed, 27 May 2026 07:19:46 +0000</pubDate>
      <link>https://dev.to/amonoff/the-rise-pause-and-rise-of-crud-apps-349o</link>
      <guid>https://dev.to/amonoff/the-rise-pause-and-rise-of-crud-apps-349o</guid>
      <description>&lt;p&gt;In the early days of the web, websites were mostly digital brochures. They were static HTML pages. You could read them, but you couldn't interact with them. If a business wanted to change a price, a developer had to manually edit code and re-upload the file via FTP.&lt;br&gt;
It was slow, expensive, and didn't scale.&lt;/p&gt;

&lt;p&gt;Then came the legendary LAMP stack (Linux, Apache, MySQL, PHP). Suddenly, the web became dynamic. The industry quickly realized that almost every business problem on earth;whether running a bank, an online store, or a social network,boiled down to four fundamental actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create: Letting a user sign up or post a status.&lt;/li&gt;
&lt;li&gt;Read: Showing a user their profile or a product list.&lt;/li&gt;
&lt;li&gt;Update: Letting a user change their password.&lt;/li&gt;
&lt;li&gt;Delete: Letting a user close an account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the mid-2000s, frameworks like Ruby on Rails, Django, and Laravel arrived. They turned CRUD into a science. With a single command line prompt (rails generate scaffold), a developer could spin up a fully functioning database table, back-end logic, and front-end forms in seconds.&lt;br&gt;
This was the first great rise. CRUD apps became the undisputed infrastructure of the global economy. They weren't flashy, but they were predictable and deeply tied to the absolute truth of the database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Pause: The "CRUD is Boring" Mirage&lt;/strong&gt;&lt;br&gt;
Then, things got weird. Around 2015, the developer community collectively decided that simple, server-rendered CRUD apps were "boring." We entered the era of massive Single Page Applications (SPAs), microservices, GraphQL, and serverless hype.We started treating the database like something to be hidden behind ten layers of abstraction. Instead of a simple PHP or Rails script fetching a row from MySQL, we introduced a mountain of complexity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The SPA Takeover: We moved the entire application logic to the browser. Suddenly, a simple user profile required React, Redux for state management, Webpack for bundling, and a massive JavaScript file that took 5 seconds to load on a mobile phone.&lt;/li&gt;
&lt;li&gt;The API Chasm: We separated the front end and the back end entirely. Now, to display a simple list of products, you needed a REST or GraphQL API layer, complex CORS configurations, and token-based authentication (JWT) just to talk to your own database.&lt;/li&gt;
&lt;li&gt;Microservice Madness: We broke monolithic apps into dozens of tiny services. A basic update operation now had to hop across three different servers, manage distributed transactions, and use Kafka queues just to change a user's email address.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We spent a decade over-engineering the front end just to realize that users still just want to fill out a form and hit submit. We swapped reliable server logs for confusing JavaScript console errors. We paused the simplicity of CRUD, and in return, we got total JavaScript fatigue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Crypto &amp;amp; Blockchain Detour: An Append-Only Trap&lt;/strong&gt;&lt;br&gt;
As the web-development world was over-engineering the front end, the Web3 and blockchain movement emerged, promising to decentralize the entire internet. Enthusiasts claimed that the "shared ledger" would replace traditional databases entirely.&lt;/p&gt;

&lt;p&gt;But reality caught up quickly.&lt;/p&gt;

&lt;p&gt;A blockchain is fundamentally an append-only ledger. In CRUD terms, it is brilliant at Create and Read, but terrible at Update and Delete.&lt;/p&gt;

&lt;p&gt;Because data on a blockchain is immutable, it is incredibly slow and quite expensive for everyday operations. Worse, it creates a massive legal nightmare. Privacy frameworks like Europe's GDPR mandate that users have the "right to be forgotten"; a legal requirement to Delete data. Because you cannot delete data from a block, tech companies quickly realized they couldn't run a compliant business on a blockchain alone.&lt;/p&gt;

&lt;p&gt;The blockchain didn't kill the database; instead, decentralized applications (dApps) still had to be wrapped in traditional CRUD architectures to cache data, update states quickly, and manage local user profiles efficiently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Second Rise: AI and the Return to Absolute Truth&lt;/strong&gt;&lt;br&gt;
Following the blockchain hype came the AI boom. Large Language Models (LLMs) can write code, analyze data, and build entire workflows in seconds. Prominent tech voices started declaring that apps were dead, and the era of the user interface was over.&lt;/p&gt;

&lt;p&gt;But once again, the industry hit the same foundational wall: &lt;em&gt;AI is probabilistic, but business is deterministic.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;AI can write poetry, but it still needs a SQL database to know where to ship your package. An LLM cannot guess a bank balance. It cannot "predict" a shipping address. AI models are stateless; they don't know anything for sure; they just guess the next most likely word based on probability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech is the User, CRUD is the Foundation&lt;/strong&gt;&lt;br&gt;
This realization has triggered the second rise of the CRUD app. We aren't throwing CRUD away; we are realizing it is the guardrail that keeps both AI and Web3 tethered to reality.&lt;br&gt;
The relationship has completely flipped: AI and blockchain are no longer trying to replace the CRUD app; they are becoming the users of it.&lt;/p&gt;

&lt;p&gt;When an AI agent "cancels your subscription," it executes a standard Update or Delete command via a regular API to a traditional database.&lt;/p&gt;

&lt;p&gt;When a smart contract processes a transaction, a traditional CRUD system reads the event to update a user's dashboard instantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Ultimate Backup&lt;/strong&gt;&lt;br&gt;
Most importantly, CRUD apps are our absolute fallback. AI models hallucinate, prompt injections corrupt logic, and decentralized networks suffer from latency or high gas fees.&lt;br&gt;
When the advanced tech stack glitches or goes offline, humans cannot be left staring at a broken terminal. We need a direct, unabstracted, deterministic interface to view, edit, and fix the underlying data manually.&lt;/p&gt;

&lt;p&gt;We need a CRUD app as our ultimate backup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion: Boring is Beautiful Again&lt;/strong&gt;&lt;br&gt;
The tech industry went on a decade-long detour. We ran away from CRUD because it felt too simple, hiding it behind over-engineered JavaScript frameworks, distributed ledgers, and autonomous AI.&lt;/p&gt;

&lt;p&gt;But the loop has closed. We are entering an era where "boring" software is beautiful again. Frameworks like Next.js, Remix, and HTMX are booming because they bring us back to simple, server-rendered data management.&lt;/p&gt;

&lt;p&gt;No matter how smart the AI gets or how decentralized the ledger becomes, the world will always need a rock-solid database to store the absolute truth. And humans will always need a CRUD app to make sure that the database is backed up.&lt;/p&gt;

&lt;p&gt;Long live the CRUD app.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>architecture</category>
      <category>ai</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Kiboko to Sherehe(polite): From Strict Go Rules to JavaScript Chaos</title>
      <dc:creator>Amon Ochuka</dc:creator>
      <pubDate>Thu, 07 May 2026 17:53:56 +0000</pubDate>
      <link>https://dev.to/amonoff/kiboko-to-sherehepolite-from-strict-go-rules-to-javascript-chaos-350m</link>
      <guid>https://dev.to/amonoff/kiboko-to-sherehepolite-from-strict-go-rules-to-javascript-chaos-350m</guid>
      <description>&lt;p&gt;Growing up in a typical strict African household always had a very clear structure, well at least for me.&lt;br&gt;
There were rules. Not suggestions. Not “best practices.” Rules. You never really negotiated them, you just executed them.&lt;/p&gt;

&lt;p&gt;You woke up early. You went to school. You came back on time.  You didn’t always get the chance to “try things.” You mostly followed instructions.&lt;/p&gt;

&lt;p&gt;If you messed up, there was usually a very immediate feedback loop.&lt;br&gt;
Let’s just call it… &lt;em&gt;real-time debugging&lt;/em&gt;. ("Kiboko") &lt;/p&gt;

&lt;p&gt;Now fast forward to adulthood (or campus life, which feels like a soft launch into adulthood).&lt;br&gt;
Suddenly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No one is asking where you are
&lt;/li&gt;
&lt;li&gt;You can sleep at 3AM and call it “freedom” &lt;/li&gt;
&lt;li&gt;Food is no longer provided;it’s a budgeting problem
&lt;/li&gt;
&lt;li&gt;And somehow, people are going out on weekdays like it’s normal("sherehe polite")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It feels like moving from a tightly controlled environment into an open-world sandbox with no manual.&lt;/p&gt;

&lt;p&gt;And that’s exactly how learning JavaScript felt after coming from Go.&lt;/p&gt;


&lt;h2&gt;
  
  
  Go felt like home
&lt;/h2&gt;

&lt;p&gt;Go is structured, strict, and predictable.It feels like that kind of upbringing where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There is a correct way to do things
&lt;/li&gt;
&lt;li&gt;Mistakes are immediately pointed out
&lt;/li&gt;
&lt;li&gt;You don’t get “creative freedom” with rules
&lt;/li&gt;
&lt;li&gt;Everything has a clear, disciplined path
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you write something wrong, Go doesn’t argue with you.&lt;br&gt;
It simply refuses to proceed.&lt;br&gt;
Very “strict parent energy.”&lt;/p&gt;


&lt;h2&gt;
  
  
  JavaScript feels like campus life
&lt;/h2&gt;

&lt;p&gt;Suddenly, everything is allowed.&lt;br&gt;
Almost &lt;em&gt;too&lt;/em&gt; allowed.&lt;/p&gt;

&lt;p&gt;You can write things that technically work but feel emotionally wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="c1"&gt;// "51"&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="c1"&gt;// 4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the language just… accepts it.&lt;br&gt;
No questions. No intervention. No moral guidance.(This is where I would have been properly debugged)&lt;/p&gt;

&lt;p&gt;Coming from Go, it feels like moving from mama's hot home-cooked meals, to a hostel where someone is boiling noodles in a sufuria at 2AM, while watching YouTube at full volume.&lt;/p&gt;

&lt;p&gt;It works.But you’re not always sure it should.&lt;/p&gt;




&lt;h2&gt;
  
  
  The adjustment phase
&lt;/h2&gt;

&lt;p&gt;At first, JavaScript feels messy.&lt;br&gt;
Too flexible. Too forgiving. Too many ways to do the same thing.&lt;br&gt;
But slowly, something changes.&lt;br&gt;
You realize the language isn’t enforcing discipline on you, you’re expected to build it yourself.&lt;/p&gt;

&lt;p&gt;That’s the shift.Go gives you structure upfront.&lt;br&gt;
JavaScript forces you to learn structure through experience.&lt;/p&gt;

&lt;p&gt;One is guided discipline.The other is learned discipline&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Growing up strict teaches you order.Campus life teaches you independence.&lt;br&gt;
Go gives you rules.&lt;br&gt;
JavaScript gives you freedom, and occasionally lets you suffer for bad decisions.&lt;/p&gt;

&lt;p&gt;And maybe that’s the real lesson:&lt;br&gt;
Structure is important, but so is learning how to create it for yourself when no one is watching.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>software</category>
      <category>go</category>
    </item>
    <item>
      <title>Why AI Is Unlikely To Be A Game-Changer In African Economies</title>
      <dc:creator>Amon Ochuka</dc:creator>
      <pubDate>Tue, 17 Feb 2026 07:34:27 +0000</pubDate>
      <link>https://dev.to/amonoff/why-ai-is-unlikely-to-be-a-game-changer-in-african-economies-4g38</link>
      <guid>https://dev.to/amonoff/why-ai-is-unlikely-to-be-a-game-changer-in-african-economies-4g38</guid>
      <description>&lt;p&gt;Everyone is talking about AI transforming Africa, but the reality on the ground is very different, especially in countries facing electricity shortages and monopolies in key sectors. While AI has enormous potential globally, in many African contexts it is unlikely to become the economic miracle some people hope for, at least in the near term.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Electricity Shortages Are a Major Bottleneck&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI systems, whether cloud-hosted or on-device, consume a significant amount of electricity. Training even a single large AI model can use as much power as a small town in a month. Many African countries struggle with unreliable grids and frequent load shedding. Even highly skilled developers cannot run AI consistently without stable electricity, and relying on generators or solar power adds both cost and operational complexity, making many AI projects financially unviable.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Infrastructure Needs Go Beyond Power&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI requires more than electricity. Fast internet, reliable data storage, cloud computing, and ongoing maintenance are essential. Many regions still lack basic connectivity, and data centers are often concentrated in a few major cities. This makes AI adoption uneven and prevents smaller startups from scaling their solutions effectively.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Monopolies Stifle Innovation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Key sectors like telecommunications, banking, and energy are dominated by a few large players. Small AI startups often struggle to access APIs or market opportunities, as monopolies prioritize existing profit models over risky new technologies.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Talent Gaps Limit Scale&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI requires skilled engineers and data scientists. While Africa has a growing developer community, high-level AI talent remains limited, and access to training and compute resources is uneven, slowing adoption.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Localized Opportunities Exist&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI can succeed where infrastructure is reliable, such as mobile money in Kenya or agri-tech in parts of Nigeria. These are notable exceptions, not the rule.&lt;/p&gt;

&lt;p&gt;Conclusion&lt;/p&gt;

&lt;p&gt;AI is a powerful tool, but tools don’t operate in a vacuum. Without reliable electricity, fair competition, and solid infrastructure, AI is unlikely to drive broad economic growth. Prioritizing fundamental challenges should come first, before expecting AI to revolutionize economies.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>discuss</category>
      <category>machinelearning</category>
      <category>news</category>
    </item>
  </channel>
</rss>
