← Back to blog

launchthat

27 Apps, One Monorepo, Zero Regrets

We kept creating new repos for every project. Shared code drifted, configs diverged, and a single bug fix became a multi-day coordination exercise. Here is how a Turborepo monorepo fixed all of it.

Mar 28, 2026Desmond Tatilian

I want to tell you about the commit that broke four products at once.

We had a shared authentication library that lived in its own repo. It was imported by the portal, the trading platform, the bot platform, and a marketing site. Someone fixed a token refresh bug in the auth library, published a new version, and updated the portal to use it. The other three apps stayed on the old version because nobody remembered to update them.

Two weeks later, a user reported that their session expired every 30 minutes on the trading platform. The fix had been written, tested, and shipped — to the wrong repo. It took half a day to figure out which version each app was running and another half day to update them all.

That was the last week we used separate repositories.

LaunchThat Portal architecture

The problem with multiple repos

Separate repos sound clean until you need to:

  • Share UI components across five products
  • Fix a bug in shared authentication logic and have it propagate everywhere
  • Coordinate a breaking change across 12 apps
  • Keep TypeScript configs, ESLint rules, and Prettier settings consistent

We tried. We had a common-lib repo with shared code. We had a ui-kit repo for components. Both were perpetually out of sync. Updating one meant opening PRs in three others. Nobody wanted to be the person who triggered the cascade.

The breaking point was not dramatic. It was the cumulative weight of hundreds of small coordination failures that added up to real velocity loss.

The solution: Turborepo with enforced boundaries

We restructured everything into three layers:

apps/          → Deployable applications
packages/      → Shared packages (core, plugins, tooling)
tooling/       → Build and development configs

Apps layer

Each app in /apps is a deployable unit with its own Next.js config, its own routes, and its own deployment target:

  • portal — main SaaS platform (80+ internal dependencies)
  • launchthatbot — bot deployment platform
  • traderlaunchpad — trading platform with real-time data
  • portfolio — this site

Each app can use any combination of packages. The portal uses nearly everything. A marketing site uses only the UI package.

Packages layer

This is where shared code lives once:

  • @acme/ui — 50+ shadcn components, animation primitives, layout system
  • @acme/api — tRPC router definitions
  • @acme/auth — authentication implementation
  • @acme/db — Drizzle ORM schemas
  • 34 plugin packages, each integrating one external service

When I fix a bug in @acme/ui, every app that imports it gets the fix on the next build. No version bumping. No coordination PRs. No "which version is the portal running?"

Tooling layer

tooling/
├── tailwind/     → Shared Tailwind config with OKLCH colors
├── eslint/       → Fine-grained ESLint presets
├── prettier/     → Single Prettier config
└── typescript/   → Base tsconfig extending everywhere

The plugin architecture

Instead of a monolithic app that tries to do everything, we build small, focused packages:

// A plugin's structure
export { Component } from "./convex/component";
export { FrontendModule } from "./frontend";
export { nodeTypes } from "./nodes";

Apps install plugins by adding them to their Convex configuration:

// apps/portal/convex.config.ts
import { pluginCrm } from "launchthat-plugin-crm";
import { pluginStripe } from "launchthat-plugin-ecommerce-stripe";

export default defineConfig({
  plugins: [pluginCrm(), pluginStripe()],
});

34 plugins. Each one is a self-contained package with its own backend, frontend components, and type definitions. Any app in the monorepo can install any combination.

What we would do differently

Conventional commits from day one. With 27 apps, commit message chaos compounds fast. We retrofitted commitlint after six months of freeform messages and it was painful.

CODEOWNERS before adding new apps. We learned to enforce ownership at the package level after a shared component broke because nobody felt responsible for it.

Catalog dependencies immediately. Centralizing versions in pnpm-workspace.yaml prevents drift across 34 packages. We did this late and spent a week resolving mismatches.

The payoff

With this structure:

  • New app creation takes minutes, not days — scaffold from a working template that already imports the packages you need
  • Bug fixes propagate through pnpm --filter without merge conflicts or version coordination
  • Shared components are truly shared — one button implementation, one auth flow, one set of types
  • The plugin ecosystem lets us add features to all products by shipping one package

The monorepo went from ambitious experiment to obvious choice. The commit that broke four products? That cannot happen anymore. A fix in @acme/auth is a fix everywhere, verified by CI before it merges.

Want to see how this was built?

Explore the monorepo projects

Want to see how this was built?

Browse all posts