ahmedallem.
Engineering · 6 min read

TypeScript Monorepo Patterns for Founders Managing 9 Products

How I structure a TypeScript monorepo across 9 live products - shared packages, independent deployments, and the patterns that prevent a monolith.

Ahmed Allem

Ahmed Allem

Founder & CTO · Aviation, AI & Startups

ShareShare
TypeScript Monorepo Patterns for Founders Managing 9 Products

When you maintain 9 products, code organization isn't an academic exercise. It's the difference between shipping features in an afternoon and spending three days untangling dependencies. Over the past few years, I've converged on a set of TypeScript monorepo patterns that keep my products independent where they need to be and shared where it saves time.

These are not patterns from a blog post about how Google does monorepos. These are patterns born from a solo founder managing a portfolio of real products across aviation, AI, legal tech, and travel.

The Problem with Separate Repos

Before I moved to a monorepo structure for related products, each product had its own repository. This was fine when I had three or four products. At 9, it became untenable.

The problems were predictable:

Dependency drift. Product A was on Next.js 14, Product B on 13, Product C was somehow still on 12. TypeScript versions diverged. Tailwind versions diverged. Every dependency was a potential compatibility issue when I tried to share code.

Copy-paste proliferation. I had the same authentication logic, the same API error handling, the same Tailwind configuration copied across a dozen repos. When I found a bug in one, I had to remember to fix it in all the others. I never remembered all of them.

Context switching cost. Opening a different repo, remembering its directory structure, its conventions, its quirks. The cognitive overhead of switching between separate repos was significant. Not individually, but across multiple products it accumulated into hours of lost productivity per week.

The Monorepo Structure

I don't put all 9 products in one monorepo. That would be chaos. Instead, I group related products into monorepos by domain:

  • Aviation monorepo: Aviation Infinity, AvioSharing, New Pilot Shop, Want To Be a Pilot
  • AI monorepo: ClickAi, HackIfy, shared AI infrastructure
  • Individual repos: Products that are truly standalone

Each monorepo follows the same structure:

/apps
  /product-a          # Next.js app
  /product-b          # Next.js app
/packages
  /ui                 # Shared UI components
  /utils              # Shared utilities
  /config             # Shared configuration (Tailwind, TypeScript, ESLint)
  /types              # Shared TypeScript types
  /api-client         # Shared API client patterns

Pattern 1: Shared Config, Independent Deployment

Every product in a monorepo shares base configurations for TypeScript, Tailwind CSS, and ESLint. But each product has its own next.config.ts, its own deployment pipeline, and its own environment variables.

The shared config lives in packages/config and is extended by each product:

// packages/config/tailwind.config.ts
export const baseConfig = {
  // Shared design tokens, plugins, utilities
}

// apps/product-a/tailwind.config.ts
import { baseConfig } from '@repo/config/tailwind'
export default {
  ...baseConfig,
  // Product-specific overrides
}

This pattern ensures visual consistency across related products while allowing each product to customize as needed. The aviation products share a design language. The AI products share a different one. But within each group, the consistency is automatic.

Pattern 2: Thin Shared Packages

The shared packages/ui directory contains only components that are used by at least two products. I am strict about this. A component that exists in only one product belongs in that product's codebase, not in the shared package.

I also keep shared packages thin. A shared Button component has the base styling and behavior. Product-specific variants are composed in the product, not added to the shared component with a product prop.

// packages/ui/Button.tsx - base component
export function Button({ variant, children, ...props }) {
  // Base implementation
}

// apps/clickai/components/EditorButton.tsx - product composition
import { Button } from '@repo/ui/Button'
export function EditorButton(props) {
  return <Button variant="primary" className="editor-specific-class" {...props} />
}

This prevents shared packages from becoming bloated with product-specific logic. It also means that modifying a shared component has a predictable, limited blast radius.

Pattern 3: Type Sharing Without Coupling

TypeScript types are the safest thing to share. They have no runtime cost, they catch errors at compile time, and they document interfaces between systems.

The packages/types directory contains types used across products: API response shapes, common data models, shared utility types. But I am careful to keep these types at the domain level, not the implementation level.

Good shared type: BlogPost, Contact, Product, domain concepts that are consistent across products.

Bad shared type: HomepageCardProps, SidebarNavItem, UI implementation details that should live in the product that uses them.

Pattern 4: Independent Versioning

I don't version shared packages separately. This is a conscious choice that goes against monorepo best practices from larger organizations. Here is why:

With only one developer (me), the overhead of publishing package versions, managing changelogs, and coordinating updates across consumers isn't worth it. I use workspace references, and when I change a shared package, I run the build across all consumers to make sure nothing breaks.

This works for a solo founder. It wouldn't work for a team. If I ever have collaborators, versioned shared packages would be the first thing I add.

Pattern 5: Test Isolation

Each product has its own test suite that runs independently. Shared packages also have their own tests. But I never write tests that cross product boundaries, no "integration test that starts Product A and calls Product B's API."

The interface between products is defined by types and contracts, and each side tests against those contracts independently. This keeps the test suite fast and prevents cascading test failures.

The Tools

I use Turborepo for build orchestration. It handles dependency-aware builds, caching, and parallel execution well. For a solo developer, the caching alone saves significant time. When I change a shared component, only the products that actually use it get rebuilt.

Package management is pnpm with workspaces. The strict dependency resolution prevents the phantom dependency issues that plague npm and yarn in monorepo setups.

What I Would Change

If I were starting over, I would establish the monorepo structure earlier. My first five products were built as separate repos, and migrating them was painful. Starting with a monorepo for related products, even when you only have one product, costs almost nothing and saves enormous headaches later.

I would also invest more in shared API client patterns earlier. Most of my products talk to similar backend services (MongoDB, external APIs, authentication), and the patterns for doing so reliably (error handling, retries, typing) should have been shared from day one.

The Takeaway

Monorepo patterns for a solo founder are different from monorepo patterns for a large organization. The goal isn't organizational scalability. It's personal productivity. Share what saves time. Isolate what preserves independence. Keep the tools simple and the conventions consistent.

After years of building and the evolution from scattered repos to organized monorepos, my velocity on new features across the portfolio is higher than it has ever been. The patterns are not glamorous, but they work.