Three months into building a SaaS product, I made the decision to tear down my carefully constructed Turborepo monorepo and convert everything back to a single Next.js application. This article covers why I made that choice, what went wrong, and when monorepos actually make sense.
The setup
I was building a coaching platform - a Next.js app with booking, payments, user accounts, and an admin panel. The tech stack: Next.js 15, React 19, Firebase Auth, Stripe, MongoDB, and several third-party integrations like Zoho Bookings.
When I started, the Turborepo documentation sold me on the dream: shared packages, faster builds through caching, clean separation of concerns. I'd have apps/web for the customer-facing site, apps/admin for the admin panel, and shared packages like @repo/ui and @repo/auth.
It sounded clean. It wasn't.
The initial structure
Here's what my monorepo looked like after running create-turbo:
toshalife-monorepo/
├── apps/
│ ├── admin/ # Admin dashboard
│ └── web/ # Customer-facing site
├── packages/
│ ├── auth/ # Shared Firebase auth
│ ├── ui/ # Shared components
│ └── design-system/ # Design tokens
├── turbo.json
└── package.jsonThe first few days felt productive. I had shared components working across both apps. The Turbo cache was speeding up builds. Everything was organized.
Then the problems started.
Problem 1: Module resolution hell
The first real headache came when I tried to import @repo/auth into my web app. The build failed with:
Module not found: Can't resolve '@repo/auth'I spent hours debugging. The issue? Next.js wasn't transpiling the workspace packages correctly. I had to add this to every app's next.config.js:
const nextConfig = {
transpilePackages: ['@repo/auth', '@repo/ui'],
// ... rest of config
};But that wasn't enough. The package itself was trying to export pre-built dist files that didn't exist. I had to change the package.json in @repo/auth to export source files directly:
{
"name": "@repo/auth",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
}
}
}This fix worked, but it felt wrong. Every new package required the same dance of configuration. And any developer joining the project would hit the same wall.
Problem 2: The component import cascade
Shared UI components sounded great in theory. In practice, every change to @repo/ui triggered a cascade of issues:
fix: resolve component import errors in admin and web apps
fix: update UI package exports for Next.js build compatibility
fix: standardize component exports to use const instead of functionI had to standardize how every component was exported. Function declarations that worked in isolation broke when imported through the package. The fix was mechanical but tedious:
// Before (broke in monorepo)
export function Button() { ... }
// After (worked)
export const Button = () => { ... }Three weeks in, I had more commits fixing import errors than actually building features.
Problem 3: Vercel deployment configuration
Deploying a monorepo to Vercel should be straightforward - they literally acquired Turborepo. It wasn't.
First attempt - build failed. Vercel couldn't find the dependencies. I added a vercel.json:
{
"buildCommand": "cd ../.. && npx turbo run build --filter=web",
"installCommand": "npm install",
"framework": "nextjs"
}Still broken. The install command ran in the wrong directory. Fixed it:
{
"installCommand": "cd ../.. && npm install"
}Now the build ran, but environment variables weren't being picked up correctly by the shared packages. Each package needed its own env configuration, and variables had to be explicitly passed through turbo.json:
{
"globalEnv": [
"FIREBASE_PROJECT_ID",
"FIREBASE_CLIENT_EMAIL",
"FIREBASE_PRIVATE_KEY",
"MONGODB_URI",
"STRIPE_SECRET_KEY"
// ... 20 more variables
]
}Every new environment variable meant updating this list. Miss one, and production breaks silently.
Problem 4: Mental overhead
This is the problem nobody talks about in the "why monorepos are great" articles.
When you're a solo developer or a small team, every minute spent on infrastructure is a minute not spent on the product. With the monorepo, I found myself constantly context-switching:
- "Wait, is this component in
@repo/uior local?" - "Did I run build in the package before testing the app?"
- "Which
node_modulesis this import resolving from?"
The cognitive load was real. I wasn't thinking about user problems anymore. I was thinking about workspace configuration.
The product changed
While I was fighting with build configuration, the product itself was evolving. My original vision was ambitious: a custom booking system with Stripe payments, user dashboards for viewing appointments and receipts, calendar integrations, an admin panel for the client to manage everything. The monorepo structure made sense for that scope - separate apps for customers and admin, shared authentication, shared UI components.
Then my client gave me feedback that changed everything. The booking flow felt too complex: customer selects a plan, pays on Stripe, views their package, then books an appointment. Too many steps. Too much friction. They worried it would hurt conversions. Instead of building more, we simplified: a free introductory call on the platform, then handle payments through personal conversation. No Stripe fees (3-4% saved per transaction). No complex user accounts. The elaborate system I was building was no longer needed.
The breaking point
The Zoho Bookings integration was the final straw. A simple feature that should have taken a day stretched into three because:
- The Zoho client needed to be shared between apps
- Creating a new package meant more configuration
- TypeScript kept complaining about module resolution
- The turbo cache was stale and I couldn't figure out why
I stepped back and asked myself: why am I doing this?
I had two apps that shared maybe 20% of their code. The admin panel was rarely updated. I was the only developer. The "benefits" of the monorepo were theoretical - I wasn't actually shipping faster.
The conversion
On January 16th, I made the call. Here's what the conversion commit looked like:
refactor: convert monorepo to single Next.js app
- Remove admin app and auth package (Zoho handles all bookings)
- Inline packages/ui to lib/ui
- Inline packages/zoho-bookings to lib/zoho-bookings
- Remove payment system (Stripe, packages, payment pages)
- Remove authentication (Firebase, user accounts, dashboard)
- Simplify booking to contact form (name, email, phone)
- Remove Redis caching (direct Zoho API calls)
Performance: Booking time reduced from 9.4s to 2-3sThe conversion took about 4 hours. Moving shared packages into a lib/ folder in the main app. Deleting the admin app entirely (Zoho's dashboard was good enough). Removing the authentication system that was overengineered for the actual use case.
The result? A single Next.js app that was easier to deploy, easier to maintain, and actually faster.
When monorepos make sense
I'm not saying monorepos are bad. They solve real problems - for the right teams. Here's when they make sense:
Multiple teams, clear ownership. If you have separate teams owning separate apps, a monorepo gives you atomic commits across shared code. But you need the team structure to support it.
Genuinely shared infrastructure. If you're building multiple products that share 50%+ of their code - like a web app, mobile app, and CLI that all use the same API client - the overhead pays off.
Dedicated DevOps capacity. Someone needs to own the build system. If that's not a real role on your team, you'll be the one debugging Turbo cache invalidation at 2am.
Mature tooling expertise. Monorepos require knowing how npm workspaces, TypeScript project references, and bundler configuration interact. If you're still learning these tools, a monorepo multiplies the complexity.
When monorepos don't make sense
If you're still figuring out what you're building, the monorepo structure can lock you into decisions too early.
Solo developers. The organizational benefits require organization. If you're one person, you're just adding friction.
Small teams (under 5 developers). Unless you have a dedicated infrastructure person, the maintenance cost outweighs the benefits.
Rapidly changing requirements. If you're still figuring out what you're building, the monorepo structure can lock you into decisions too early. I ended up deleting entire apps because the product direction changed.
Low code sharing. If your apps share less than 30% of their code, you're paying the monorepo tax for minimal benefit.
Lessons learned
Start simple. A single Next.js app with a good folder structure (/lib, /components, /app) handles more complexity than you'd expect. You can always extract packages later if you actually need them.
Architecture should follow product, not lead it. I built the monorepo for a product vision that changed. When requirements are still shifting, flexible architecture beats "correct" architecture. The elaborate structure I built became technical debt the moment the product simplified.
Measure the real benefits. I assumed the Turbo cache would save me time. In practice, I spent more time debugging cache issues than I saved on builds. Measure before you assume.
Infrastructure is a feature. Every hour spent on build configuration is an hour not spent on user-facing features. For an early-stage product, that trade-off rarely makes sense.
The "right" architecture depends on context. Turborepo is great software. It just wasn't right for my situation. There's no universal "best" architecture - only the one that matches your team size, product stage, and actual needs.
The current setup
Today, my project is a single Next.js app with this structure:
toshalife/
├── app/ # Next.js App Router pages
├── lib/
│ ├── ui/ # UI components (was @repo/ui)
│ ├── zoho/ # Zoho integration
│ └── utils/ # Shared utilities
├── components/ # Page-specific components
└── actions/ # Server actionsIt deploys in under a minute. New developers can clone it and run npm install && npm run dev. There's no Turbo cache to debug, no workspace configuration to maintain, no package resolution errors.
Is it less "clean" than a monorepo? Maybe. But it works. And for a product that's still finding its market, that matters more than architectural elegance.
Key takeaways
The best architecture is the one you don't have to think about.
- Monorepos solve real problems but add significant overhead for small teams
- Module resolution and build configuration eat more time than you'd expect
- Architecture should serve the product - when the product simplifies, the architecture can too
- Don't build infrastructure for a product vision that might change
- Start with a single app and extract packages only when you genuinely need them
The best architecture is the one you don't have to think about. And sometimes, the product changes in ways that make your "sophisticated" architecture unnecessary. For me, that turned out to be the simplest lesson of all.


