<?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: Fahad Bashir</title>
    <description>The latest articles on DEV Community by Fahad Bashir (@fahadbashir).</description>
    <link>https://dev.to/fahadbashir</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%2F4062058%2Fc27a0cb3-f7eb-466e-b0bf-620700b1bb21.png</url>
      <title>DEV Community: Fahad Bashir</title>
      <link>https://dev.to/fahadbashir</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fahadbashir"/>
    <language>en</language>
    <item>
      <title>I built a production B2B CRM in 60 days with Next.js 16 — here's everything I learned</title>
      <dc:creator>Fahad Bashir</dc:creator>
      <pubDate>Tue, 11 Aug 2026 06:11:04 +0000</pubDate>
      <link>https://dev.to/fahadbashir/i-built-a-production-b2b-crm-in-60-dayswith-nextjs-16-heres-everything-i-learned-57ke</link>
      <guid>https://dev.to/fahadbashir/i-built-a-production-b2b-crm-in-60-dayswith-nextjs-16-heres-everything-i-learned-57ke</guid>
      <description>&lt;p&gt;&lt;strong&gt;__&lt;/strong&gt;Sixty days ago I decided to build something I'd actually want to sell: a production-ready B2B CRM. Not a toy project, not a tutorial clone — something with real auth, real multi-tenancy, and real edge cases.&lt;/p&gt;

&lt;p&gt;That project became PipelineCRM, and this is everything I learned building it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu6j99w9vovqm0md78zh1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu6j99w9vovqm0md78zh1.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why I Built It&lt;/strong&gt;&lt;br&gt;
Every developer who's ever done freelance or agency work has hit the same wall: a client needs a CRM, and you either bolt one onto a generic admin template or you burn three weeks building lead management, a pipeline, and auth from scratch — again.&lt;/p&gt;

&lt;p&gt;I wanted to build that foundation once, properly, and package it so other developers don't have to repeat the same three weeks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech Stack Decisions&lt;/strong&gt;&lt;br&gt;
I didn't pick this stack randomly — every choice traded something off:&lt;/p&gt;

&lt;p&gt;1.** Next.js 16 App Router** — server components meant less client-side JS shipped, and file-based routing kept the project structure predictable as it grew.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;MongoDB + Mongoose&lt;/strong&gt; — CRMs have messy, evolving data shapes (custom fields, tags, notes). A flexible schema saved me from migration pain a relational DB would have caused.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;3.*&lt;em&gt;TanStack Query *&lt;/em&gt;— every dashboard needs live-feeling data. invalidateQueries made cache updates almost free.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;shadcn/ui + Tailwind&lt;/strong&gt; — I'm not a designer. This combination got me to a professional-looking UI without hiring one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;NextAuth v4&lt;/strong&gt; — I deliberately avoided the v5 beta. For a product going into other developers' hands, stability beat having the newest API.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhu35ob65ojw9y4auz9yf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhu35ob65ojw9y4auz9yf.png" alt=" " width="800" height="581"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;The Biggest Challenges&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;&lt;em&gt;1. Multi-tenancy&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
This was less a technical problem and more a discipline problem. Every single document in MongoDB needed a workspaceId, and every query — reads, writes, updates — had to filter by it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lead.find({ workspaceId })
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Miss it once in a findOneAndUpdate, and you've got a data leak between two different companies' workspaces. I ended up treating workspaceId filtering as non-negotiable in every single API route, checked at the middleware level before a request even reaches a controller.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy1t4n00yq5r4kmgqtda8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy1t4n00yq5r4kmgqtda8.png" alt=" " width="693" height="684"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;&lt;em&gt;2. Kanban drag-and-drop with optimistic updates&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
The visual pipeline was the single most time-consuming feature. Using @hello-pangea/dnd got drag-and-drop working quickly — the hard part was making it feel instant.&lt;/p&gt;

&lt;p&gt;The naive approach (call the API, wait, then re-render) makes cards visibly snap back into place before jumping to their new column. I fixed this by:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Updating local state immediately on drop&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Firing a PATCH request in the background just for the stage change&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Syncing local state from the server via useEffect&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reverting to server state only if the request actually failed&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;3. Session and workspace sync&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
A smaller but sneaky bug: when a user's workspace settings changed (currency, name), their JWT session didn't reflect it until they logged out and back in. I had to explicitly trigger a session refresh on settings updates instead of assuming NextAuth would pick it up automatically.&lt;br&gt;
&lt;strong&gt;What I'd Do Differently&lt;/strong&gt;&lt;br&gt;
If I started over, I'd build the multi-tenancy layer as a single reusable data-access wrapper from day one instead of repeating the workspaceId filter by hand in every route. It would have caught mistakes earlier and saved a full week of later debugging.&lt;/p&gt;

&lt;p&gt;I'd also write the Kanban's optimistic-update logic as a small custom hook right away, instead of inlining it in the component — I ended up refactoring it out later anyway.&lt;br&gt;
&lt;strong&gt;Try It Yourself&lt;/strong&gt;&lt;br&gt;
If you're building a CRM for a client, or want a solid multi-tenant SaaS foundation to start from, PipelineCRM is available as a starter kit:&lt;br&gt;
&lt;a href="https://fahadventure084.gumroad.com/l/emzuuq?_gl=1*1g5pv84*_ga*MTU4NjYwMTExOC4xNzg1ODI0Njk2*_ga_6LJN6D94N6*czE3ODU4MjQ2OTUkbzEkZzEkdDE3ODU4Mjk3NzMkajI0JGwwJGgw" rel="noopener noreferrer"&gt;Click here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy to answer questions about any of the technical decisions above in the comments.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>mongodb</category>
      <category>webdev</category>
      <category>buildinpublic</category>
    </item>
  </channel>
</rss>
