<?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: Raja Abbas Affandi</title>
    <description>The latest articles on DEV Community by Raja Abbas Affandi (@raja-abbas-affandi).</description>
    <link>https://dev.to/raja-abbas-affandi</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%2F1131139%2F5b2fad8c-d411-46ec-91ce-6c462eb1bca3.png</url>
      <title>DEV Community: Raja Abbas Affandi</title>
      <link>https://dev.to/raja-abbas-affandi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/raja-abbas-affandi"/>
    <language>en</language>
    <item>
      <title>How I Built a Multi-Tenant RAG Knowledge Base with Source-Cited Answers — Pipeline, Multi-Tenancy, and Lessons</title>
      <dc:creator>Raja Abbas Affandi</dc:creator>
      <pubDate>Sat, 15 Aug 2026 08:41:03 +0000</pubDate>
      <link>https://dev.to/raja-abbas-affandi/how-i-built-a-multi-tenant-rag-knowledge-base-with-source-cited-answers-pipeline-multi-tenancy-22oo</link>
      <guid>https://dev.to/raja-abbas-affandi/how-i-built-a-multi-tenant-rag-knowledge-base-with-source-cited-answers-pipeline-multi-tenancy-22oo</guid>
      <description>&lt;p&gt;Every "build a RAG chatbot" tutorial ends the same way: embed a few paragraphs, call &lt;code&gt;similaritySearch&lt;/code&gt;, print the answer. That gets you a demo, not a product. The gap between a RAG demo and a RAG product you'd trust with a company's documents is where all the real engineering lives.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;KnowBase AI&lt;/strong&gt;, a multi-tenant SaaS knowledge base where businesses upload documents and an AI assistant answers questions &lt;strong&gt;grounded in their own content&lt;/strong&gt; — with source citations you can click. This post covers the RAG pipeline, how multi-tenancy changes the design, and the decisions I'd repeat.&lt;/p&gt;

&lt;p&gt;Live demo: &lt;a href="https://knowbase-ai.netlify.app" rel="noopener noreferrer"&gt;knowbase-ai.netlify.app&lt;/a&gt; — no login needed, fully functional (it runs in demo mode with mock responses).&lt;/p&gt;

&lt;h2&gt;
  
  
  What RAG actually requires
&lt;/h2&gt;

&lt;p&gt;RAG sounds simple: retrieve relevant context, feed it to the LLM, get a grounded answer. In production it means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ingestion:&lt;/strong&gt; upload documents, chunk them intelligently, store them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval:&lt;/strong&gt; find the chunks that actually answer the question&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grounding:&lt;/strong&gt; build a prompt from the retrieved context, no hallucinated answers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust:&lt;/strong&gt; show the user &lt;em&gt;why&lt;/em&gt; the model said that — source citations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each step is a small product on its own. Here's the pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ingestion pipeline
&lt;/h2&gt;

&lt;p&gt;Documents arrive as files, URLs, or manual entries. The key decision is chunking — too big and retrieval is fuzzy, too small and you lose context. The pipeline chunks text with overlap so no meaning falls through the gaps:&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;chunkText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;overlap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&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="nx"&gt;chunks&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;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&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;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;overlap&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;chunks&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;Each chunk becomes a &lt;code&gt;DocumentChunk&lt;/code&gt; tied to its source document, so retrieval can always trace back to where the information came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-tenancy changes everything
&lt;/h2&gt;

&lt;p&gt;A single-user RAG app and a multi-tenant SaaS share almost no code after the demo stage. Every query, chunk, and conversation must be scoped to a workspace:&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;// Every AI retrieval is scoped by workspaceId — a tenant can never&lt;/span&gt;
&lt;span class="c1"&gt;// retrieve another tenant's chunks, even if the embedding matches.&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;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workspaceId&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;query&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentChunk&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;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;document&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;workspaceId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="p"&gt;},&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="mi"&gt;5&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;p&gt;The data model enforces isolation at the schema level:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Workspace&lt;/code&gt; — tenant container&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WorkspaceMember&lt;/code&gt; — roles: Owner / Admin / Member (RBAC via NextAuth.js v5)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;KnowledgeSource&lt;/code&gt; + &lt;code&gt;Document&lt;/code&gt; + &lt;code&gt;DocumentChunk&lt;/code&gt; — the RAG layer, always under a workspace&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Conversation&lt;/code&gt; + &lt;code&gt;Message&lt;/code&gt; — chat sessions, scoped per workspace&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ApiUsage&lt;/code&gt; — token tracking per workspace&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The provider abstraction (OpenAI / Gemini / Claude)
&lt;/h2&gt;

&lt;p&gt;One of the best decisions: never hard-code a model. A thin provider interface means the product runs on OpenAI, Google Gemini, or Anthropic Claude by configuration, and it made the &lt;strong&gt;demo mode&lt;/strong&gt; trivial:&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;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AIProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;AsyncIterable&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="o"&gt;&amp;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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;providers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OpenAIProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;gemini&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GeminiProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;claude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ClaudeProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// No API keys configured? Run a fully functional mock.&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;getProvider&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;AIProvider&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;configured&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(([,&lt;/span&gt; &lt;span class="nx"&gt;P&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;P&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;isConfigured&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;configured&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;configured&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="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MockProvider&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;Demo mode was the reason I could publish a real demo without leaking keys or asking visitors to sign up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source citations — the feature that makes it trustworthy
&lt;/h2&gt;

&lt;p&gt;An AI answer with no receipts is just a guess. KnowBase streams responses over SSE and attaches the chunks that informed each answer, so users can click through to the source document:&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;// Simplified — stream tokens, then emit the citations that grounded them&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;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;conversationId&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;content&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="nx"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workspaceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&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;stream&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;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildRagPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunks&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="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/event-stream&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single feature separates a gimmick from a support tool. Customer-support teams don't trust "trust me" — they trust a cited answer they can verify in two clicks.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;Next.js 16 (App Router, Turbopack)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript (strict)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;SQLite + Prisma 7 (&lt;code&gt;@prisma/adapter-libsql&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;NextAuth.js v5 (Auth.js) + JWT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI&lt;/td&gt;
&lt;td&gt;OpenAI / Gemini / Claude (provider abstraction)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Styling&lt;/td&gt;
&lt;td&gt;Tailwind CSS v4 + shadcn/ui&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Charts / Markdown&lt;/td&gt;
&lt;td&gt;Recharts + React Markdown (remark-gfm)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forms&lt;/td&gt;
&lt;td&gt;React Hook Form + Zod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Netlify&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I'm a full-stack developer at &lt;a href="https://ratechnologies.netlify.app" rel="noopener noreferrer"&gt;RA Technologies&lt;/a&gt;, where we build SaaS and AI products with exactly this architecture — one codebase, strict types, a database that needs no server, and a demo anyone can click.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;SQLite-first was the right call.&lt;/strong&gt; &lt;code&gt;@prisma/adapter-libsql&lt;/code&gt; gives you a real production-ish setup with zero config. Swap to PostgreSQL later only if you actually need it — most knowledge-base products don't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoping is security.&lt;/strong&gt; Multi-tenant bugs are data-leak bugs. Enforce &lt;code&gt;workspaceId&lt;/code&gt; at the query layer, never rely on the UI to filter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A working demo beats a password gate.&lt;/strong&gt; Removing auth for the MVP made the demo clickable for everyone — and it still demonstrates the full RAG experience.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;Live demo: &lt;a href="https://knowbase-ai.netlify.app" rel="noopener noreferrer"&gt;knowbase-ai.netlify.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/Raja-Abbas/Knowbase-AI" rel="noopener noreferrer"&gt;github.com/Raja-Abbas/Knowbase-AI&lt;/a&gt; (MIT)&lt;/li&gt;
&lt;li&gt;Built at &lt;a href="https://ratechnologies.netlify.app" rel="noopener noreferrer"&gt;RA Technologies&lt;/a&gt; — we build SaaS and AI products like this for startups and businesses worldwide.&lt;/li&gt;
&lt;li&gt;Part of my &lt;a href="https://dev.to/raja-abbas-affandi/series/43209"&gt;Building AI Products&lt;/a&gt; series — previous post: building a visual AI agent builder with tool calling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've built RAG in production, what did I get wrong? I'd love to hear it in the comments.&lt;/p&gt;




</description>
      <category>ai</category>
      <category>webdev</category>
      <category>nextjs</category>
      <category>typescript</category>
    </item>
    <item>
      <title>How I Built a Visual AI Agent Builder with Tool Calling — Architecture, Tools, and Lessons</title>
      <dc:creator>Raja Abbas Affandi</dc:creator>
      <pubDate>Fri, 14 Aug 2026 22:01:20 +0000</pubDate>
      <link>https://dev.to/raja-abbas-affandi/how-i-built-a-visual-ai-agent-builder-with-tool-calling-architecture-tools-and-lessons-28pb</link>
      <guid>https://dev.to/raja-abbas-affandi/how-i-built-a-visual-ai-agent-builder-with-tool-calling-architecture-tools-and-lessons-28pb</guid>
      <description>&lt;p&gt;AI agents are the most exciting thing happening in software right now — but most agent tutorials stop at "call the LLM in a loop." The part that's actually hard is giving the agent real tools, watching what it does with them, and trusting it enough to let it run.&lt;/p&gt;

&lt;p&gt;I hit all three walls while building &lt;strong&gt;AgentForge&lt;/strong&gt;, a visual AI agent builder with tool calling, real-time execution visualization, and run analytics. This post covers the architecture, the tool-calling design that makes it work, and what I'd do differently next time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;AgentForge is a full SaaS-style product where you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create agents with custom system prompts, model, and temperature&lt;/li&gt;
&lt;li&gt;Attach tools from a library of &lt;strong&gt;8 built-in tools across 6 categories&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Chat with agents and &lt;strong&gt;watch every tool call execute in real time&lt;/strong&gt; — inputs, outputs, and durations&lt;/li&gt;
&lt;li&gt;Track every run with &lt;strong&gt;token usage, duration, and tool-call details&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Compare agent performance on a dashboard with charts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's live here: &lt;a href="https://agentforge-next.netlify.app" rel="noopener noreferrer"&gt;agentforge-next.netlify.app&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;Next.js 16 (App Router)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript (strict)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;SQLite + Prisma 7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Styling&lt;/td&gt;
&lt;td&gt;Tailwind CSS v4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Components&lt;/td&gt;
&lt;td&gt;shadcn/ui&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Charts&lt;/td&gt;
&lt;td&gt;Recharts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Icons&lt;/td&gt;
&lt;td&gt;Lucide React&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Netlify&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I'm a full-stack developer at &lt;a href="https://ratechnologies.netlify.app" rel="noopener noreferrer"&gt;RA Technologies&lt;/a&gt;, and this stack is exactly what we use for client SaaS and AI products — one codebase, strict types, and a database that doesn't fight you at the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool-calling design (the heart of the product)
&lt;/h2&gt;

&lt;p&gt;The core problem: an LLM gives you a structured request to call a function, and you have to execute it safely and return something the model can understand. Everything else is engineering around that contract.&lt;/p&gt;

&lt;h3&gt;
  
  
  The tool interface
&lt;/h3&gt;

&lt;p&gt;Every tool implements the same 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="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Tool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&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;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolCategory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;description&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;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSONSchema&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="nx"&gt;input&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="nx"&gt;unknown&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;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ToolResult&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;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ToolResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;error&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;durationMs&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The registry
&lt;/h3&gt;

&lt;p&gt;Tools are registered once and reused everywhere — the agent config screen, the chat executor, and the library page:&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nx"&gt;webSearch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// category: "search"&lt;/span&gt;
  &lt;span class="nx"&gt;calculator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// category: "utility"&lt;/span&gt;
  &lt;span class="nx"&gt;codeExecutor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// category: "code"&lt;/span&gt;
  &lt;span class="nx"&gt;weather&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// category: "data"&lt;/span&gt;
  &lt;span class="nx"&gt;urlFetch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// category: "network"&lt;/span&gt;
  &lt;span class="nx"&gt;dbQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// category: "data"&lt;/span&gt;
  &lt;span class="nx"&gt;emailSender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// category: "communication"&lt;/span&gt;
  &lt;span class="nx"&gt;fileReader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// category: "utility"&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;getToolsByCategory&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="nx"&gt;ToolCategory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Tool&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tool&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&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;acc&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;as&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="nx"&gt;ToolCategory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Tool&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The execution loop
&lt;/h3&gt;

&lt;p&gt;The agent loop is deliberately simple: send the conversation with the tool schema, read the tool calls from the response, execute, append results, repeat:&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;while &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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;openAISchema&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;tool_choice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toolCalls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&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="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool_calls&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;toolCalls&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&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="nx"&gt;message&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="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&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="nx"&gt;message&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;call&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;toolCalls&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;tool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&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;tool&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="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;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;tool_call_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;call&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="na"&gt;content&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The 8 tools
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;web_search&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Search&lt;/td&gt;
&lt;td&gt;Real-time web information&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;calculator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Utility&lt;/td&gt;
&lt;td&gt;Math operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;code_executor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Code&lt;/td&gt;
&lt;td&gt;Runs JavaScript safely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;weather&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Data&lt;/td&gt;
&lt;td&gt;Weather forecasts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;url_fetch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;Fetches URL content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;db_query&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Data&lt;/td&gt;
&lt;td&gt;Queries databases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;email_sender&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Communication&lt;/td&gt;
&lt;td&gt;Sends emails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;file_reader&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Utility&lt;/td&gt;
&lt;td&gt;Reads file contents&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The lesson here: tools with &lt;strong&gt;side effects&lt;/strong&gt; (email, DB, code) need confirmation UX, while &lt;strong&gt;read-only&lt;/strong&gt; tools (search, weather, fetch) can run automatically. AgentForge separates them by category so the UI can treat them differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-time tool-call visualization
&lt;/h2&gt;

&lt;p&gt;The most requested feature by far. When an agent runs, the chat stream shows each tool call as a card with &lt;strong&gt;input → output → duration&lt;/strong&gt;, updating live over SSE.&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;// Simplified SSE emission&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;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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;encoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&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;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ReadableStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;)&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;step&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;runAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`data: &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;step&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;\n\n`&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/event-stream&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single feature is why people trust the product: &lt;strong&gt;an agent you can watch is an agent you can debug.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Run history and analytics
&lt;/h2&gt;

&lt;p&gt;Every run is persisted — tokens, duration, and every tool call with its inputs and outputs. The dashboard then answers real questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which tools get used the most?&lt;/li&gt;
&lt;li&gt;Which agents burn tokens fastest?&lt;/li&gt;
&lt;li&gt;Which runs fail and where?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Schema-first from day one.&lt;/strong&gt; I hand-wrote the tool parameter schemas. If I rebuilt this, I'd generate them from Zod types so the TypeScript type and the LLM schema can never drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sandbox the code executor harder.&lt;/strong&gt; It's isolated, but real deployments need per-user sandboxes (or WASM) — not just per-process isolation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add multi-agent orchestration earlier.&lt;/strong&gt; Single agents are easy; the interesting problems start when agents delegate to each other.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;Live demo: &lt;a href="https://agentforge-next.netlify.app" rel="noopener noreferrer"&gt;agentforge-next.netlify.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/Raja-Abbas/AgentForge" rel="noopener noreferrer"&gt;github.com/Raja-Abbas/AgentForge&lt;/a&gt; (MIT)&lt;/li&gt;
&lt;li&gt;Built at &lt;a href="https://ratechnologies.netlify.app" rel="noopener noreferrer"&gt;RA Technologies&lt;/a&gt; — we build SaaS and AI products like this for startups and businesses worldwide.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building agents, I'd love to hear how you handle tool execution and side effects in the comments.&lt;/p&gt;




</description>
      <category>ai</category>
      <category>nextjs</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Built 10 JavaScript Developer Tools in One Place — Here's How</title>
      <dc:creator>Raja Abbas Affandi</dc:creator>
      <pubDate>Thu, 16 Jul 2026 10:14:49 +0000</pubDate>
      <link>https://dev.to/raja-abbas-affandi/i-built-10-javascript-developer-tools-in-one-place-heres-how-5g22</link>
      <guid>https://dev.to/raja-abbas-affandi/i-built-10-javascript-developer-tools-in-one-place-heres-how-5g22</guid>
      <description>&lt;p&gt;I was tired of switching between linters, formatters, analyzers, and visualizers just to review my JavaScript code. So I built JSCodeCraft — a collection of 10 developer tools in one place.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code Analysis&lt;/strong&gt; — Scores your code quality across 10 dimensions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structure Mapping&lt;/strong&gt; — Visualizes your code as an interactive tree&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refactoring Engine&lt;/strong&gt; — Suggests improvements with before/after previews&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pattern Detection&lt;/strong&gt; — Catches anti-patterns and common mistakes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event Loop Visualizer&lt;/strong&gt; — Shows async execution step by step&lt;/li&gt;
&lt;li&gt;And 5 more tools&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 16 with App Router&lt;/li&gt;
&lt;li&gt;TypeScript strict mode&lt;/li&gt;
&lt;li&gt;Tailwind CSS v4&lt;/li&gt;
&lt;li&gt;Recharts for visualizations&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I Built It
&lt;/h2&gt;

&lt;p&gt;Every code review I did had the same issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nested callbacks that could be flattened&lt;/li&gt;
&lt;li&gt;Unused variables scattered across files&lt;/li&gt;
&lt;li&gt;Functions that were too long&lt;/li&gt;
&lt;li&gt;Missing error handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted a tool that catches all of this before anyone else sees my code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Event Loop Visualizer
&lt;/h2&gt;

&lt;p&gt;This was the hardest part to build. I wanted it to show the call stack, task queue, and microtask queue moving in real time.&lt;/p&gt;

&lt;p&gt;Most people learn async by reading articles. Actually seeing the execution order click by click makes it stick.&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="c1"&gt;// What the visualizer shows:&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;           &lt;span class="c1"&gt;// Call stack&lt;/span&gt;
&lt;span class="nf"&gt;setTimeout&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="c1"&gt;// Task queue&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&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="c1"&gt;// Microtask queue&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;3&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Output: 1, 4, 3, 2&lt;/span&gt;
&lt;span class="c1"&gt;// The visualizer shows WHY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Open Source
&lt;/h2&gt;

&lt;p&gt;The whole thing is free and open source. No signup required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://jscodecraft-next.netlify.app" rel="noopener noreferrer"&gt;https://jscodecraft-next.netlify.app&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/Raja-Abbas/JSCodeCraft" rel="noopener noreferrer"&gt;https://github.com/Raja-Abbas/JSCodeCraft&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Product Hunt:&lt;/strong&gt; &lt;a href="https://www.producthunt.com/products/jscodecraft?utm_source=other&amp;amp;utm_medium=social" rel="noopener noreferrer"&gt;https://www.producthunt.com/products/jscodecraft?utm_source=other&amp;amp;utm_medium=social&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;I'm building more tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI chatbot builder&lt;/li&gt;
&lt;li&gt;Knowledge base with RAG&lt;/li&gt;
&lt;li&gt;SaaS analytics dashboard&lt;/li&gt;
&lt;li&gt;Freelance finance tracker&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one solves a real problem. Each one built with the same stack.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start with your own pain.&lt;/strong&gt; I built this because I needed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make it visual.&lt;/strong&gt; Code is abstract. Visualizations make it concrete.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship fast, iterate.&lt;/strong&gt; MVP 1 is enough to get feedback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open source builds trust.&lt;/strong&gt; People try tools they can inspect.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you try it, let me know what you think. Feedback welcome.&lt;/p&gt;

&lt;p&gt;This is project 1 of 8. More coming soon.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>react</category>
      <category>typescript</category>
    </item>
    <item>
      <title>I Built a Full-Stack E-Commerce App with Admin Dashboard Using Next.js</title>
      <dc:creator>Raja Abbas Affandi</dc:creator>
      <pubDate>Mon, 02 Feb 2026 10:05:05 +0000</pubDate>
      <link>https://dev.to/raja-abbas-affandi/i-built-a-full-stack-e-commerce-app-with-admin-dashboard-using-nextjs-1j3l</link>
      <guid>https://dev.to/raja-abbas-affandi/i-built-a-full-stack-e-commerce-app-with-admin-dashboard-using-nextjs-1j3l</guid>
      <description>&lt;p&gt;Recently, I built a &lt;strong&gt;full-stack e-commerce application from scratch&lt;/strong&gt; with a &lt;strong&gt;powerful admin dashboard&lt;/strong&gt; — and I documented the entire project in this video: &lt;a href="https://www.youtube.com/watch?v=235k7QA-Jjs" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=235k7QA-Jjs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This project was an opportunity to combine modern web technologies and build something &lt;strong&gt;real-world developers can learn from&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Key Features&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User authentication &amp;amp; authorization&lt;/strong&gt; for secure login/signup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Product listing &amp;amp; management&lt;/strong&gt; for both users and admin&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure checkout flow&lt;/strong&gt; integrated with Paystack&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin dashboard&lt;/strong&gt; to manage products, orders, and users&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalable full-stack architecture&lt;/strong&gt; using Next.js &amp;amp; Supabase&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean, modern UI&lt;/strong&gt; for a smooth user experience&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js&lt;/strong&gt; – frontend &amp;amp; backend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase&lt;/strong&gt; – database &amp;amp; authentication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kinde&lt;/strong&gt; – authentication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paystack&lt;/strong&gt; – payment gateway&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uploadthing&lt;/strong&gt; – image uploads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimized APIs&lt;/strong&gt; – for performance and scalability&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why This Project Matters
&lt;/h2&gt;

&lt;p&gt;This project is a &lt;strong&gt;hands-on example for developers&lt;/strong&gt; who want to understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to build a secure e-commerce flow&lt;/li&gt;
&lt;li&gt;How to manage products and orders efficiently&lt;/li&gt;
&lt;li&gt;How to integrate modern tools like Supabase and Paystack&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Get the Code
&lt;/h2&gt;

&lt;p&gt;The full source code is available here: &lt;a href="https://github.com/WebDev-Insider/dustin-tech-hub-youtube" rel="noopener noreferrer"&gt;https://github.com/WebDev-Insider/dustin-tech-hub-youtube&lt;/a&gt;&lt;br&gt;
For direct questions or support, you can reach me on Telegram: &lt;a href="https://t.me/webdev_insider" rel="noopener noreferrer"&gt;https://t.me/webdev_insider&lt;/a&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>fullstack</category>
      <category>ecommerce</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
