__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.
That project became PipelineCRM, and this is everything I learned building it.
Why I Built It
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.
I wanted to build that foundation once, properly, and package it so other developers don't have to repeat the same three weeks.
Tech Stack Decisions
I didn't pick this stack randomly — every choice traded something off:
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.
- MongoDB + Mongoose — CRMs have messy, evolving data shapes (custom fields, tags, notes). A flexible schema saved me from migration pain a relational DB would have caused.
3.*TanStack Query *— every dashboard needs live-feeling data. invalidateQueries made cache updates almost free.
shadcn/ui + Tailwind — I'm not a designer. This combination got me to a professional-looking UI without hiring one.
NextAuth v4 — I deliberately avoided the v5 beta. For a product going into other developers' hands, stability beat having the newest API.

The Biggest Challenges
1. Multi-tenancy
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:
Lead.find({ workspaceId })
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.

2. Kanban drag-and-drop with optimistic updates
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.
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:
Updating local state immediately on drop
Firing a PATCH request in the background just for the stage change
Syncing local state from the server via useEffect
Reverting to server state only if the request actually failed
3. Session and workspace sync
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.
What I'd Do Differently
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.
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.
Try It Yourself
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:
Click here
Happy to answer questions about any of the technical decisions above in the comments.

Top comments (0)