Why a Monorepo for 9 SaaS Products
When we launched OmniRealm, the question came up immediately: one repo per product or a monorepo for the entire ecosystem? The answer came from arithmetic.
With 9 SaaS products (Wardek, FiscalPilot, OmniBookmark, OmniTask, OmniScan, FitRealm, IrisPro, Arena, Mission Brevet), each product shares common needs: authentication, UI components, error handling, logging, SEO, environment configuration, deployment.
In a multi-repo setup, each common need means either duplication (9 different implementations of the same thing) or a private NPM package (with its overhead of publishing, versioning, CI). Both options slow down development.
In a monorepo, a change in the authentication package is immediately available across all 9 products. A new UI component is usable everywhere with one import line. A security fix propagates instantly.
The Technical Stack
PNPM Workspaces
The monorepo uses PNPM with its workspace system. PNPM offers three critical advantages over npm or yarn:
Fast installation: PNPM's content-addressable store avoids package duplication on disk. With 9 apps and 48 internal packages, that is a multi-gigabyte difference.
Strict isolation: PNPM does not use the aggressive hoisting of npm/yarn. Each package has access only to the dependencies it explicitly declares. This prevents phantom dependencies, a common monorepo problem.
Workspace protocol: The workspace:* prefix allows referencing internal packages without version numbers. Resolution is automatic and instant.
Next.js for Each App
Each SaaS product is an independent Next.js application with App Router. The choice of Next.js is motivated by several factors:
- Server Components for optimal performance
- SSG for marketing pages (TTFB under 200ms)
- Integrated API Routes for lightweight backends
- A mature middleware ecosystem (auth, i18n, rate limiting)
Each app uses a standardized Next.js configuration including transpilePackages for internal packages and outputFileTracingRoot for Docker builds.
Strict TypeScript Everywhere
Zero compromise on types. TypeScript runs in strict mode across the entire monorepo: strict: true, noUncheckedIndexedAccess: true, no implicit any. Every internal package exports its types.
This represents a heavier initial investment, but the benefits are immense: errors are caught at build time, not in production. Refactoring is safe. Autocomplete is exhaustive.
The 48 Shared Packages
The monorepo's core is the package layer. 48 packages in packages/@omnirealm/ covering all cross-cutting needs.
Infrastructure Packages
@omnirealm/logger: Structured logging based on Pino. Server-side with rotation, client-side with console fallback. Interceptor pattern for automatic service logging.
@omnirealm/config: Environment validation with Zod. Each app declares its env schema and gets startup validation with explicit error messages.
@omnirealm/errors: Typed error hierarchy (NotFoundError, ValidationError, AuthorizationError) with consistent serialization and deserialization.
Business Packages
@omnirealm/auth: Shared authentication and authorization. Session management, JWT, roles and permissions via CASL.
@omnirealm/seo: Metadata generation, sitemaps, JSON-LD, Open Graph. Each product calls one function and gets complete SEO.
@omnirealm/ui: Shared UI components. Buttons, forms, modals, layouts. Built on Tailwind with consistent design tokens.
@omnirealm/testing: Centralized mocks for Prisma, Stripe, Resend, Anthropic. Shared test utilities. One mock written once, used across 9 apps.
The Reuse Rate
Across 9 apps, 66% of code is shared via internal packages. This means that when we create a new app, two-thirds of the code is already written. The time to create a new product dropped from 3 weeks to 3 days.
Unified Deployment
Each app has its own deploy.sh that calls a shared deploy-unified.sh script. This script handles:
- Docker build with monorepo context
- Vendored dependency preparation (for internal packages)
- Image push to registry
- Deploy to VPS via SSH
- Post-deploy health check
- Automatic rollback on failure
The Docker pattern is simple: each app produces a standalone container that depends on nothing else at runtime. The monorepo is a development tool, not a production constraint.
Ports are managed via a centralized standard (STANDARDS-ports.md) that assigns a unique port to each app. An Nginx reverse proxy routes traffic to the correct containers.
Challenges and Lessons
Build Times
With 9 apps and 48 packages, a full build takes time. Our solution: only build what changed. Turborepo's caching system (which we evaluated) was not adopted because PNPM with targeted scripts works well enough at our scale.
In practice, we never build all 9 apps at once. A change in a UI package triggers rebuilds only for apps that use it.
Git Hooks
With 9 apps and 48 packages, pre-commit hooks must be fast. We have 22 validators in .githooks/ but they only run on modified files. The secret: optimized Bash scripts that filter by scope before launching heavy checks.
Coordination
The risk of a monorepo is the "big bang merge": a PR that touches 5 apps and 10 packages. Our rule: PRs touch at most 2 apps. If a change impacts more, it gets split into atomic PRs.
Circular Dependencies
With 48 packages, the risk of circular dependencies is real. The rule is simple: the infrastructure layer never depends on the business layer. Business packages can depend on infrastructure but not on each other. Apps depend on packages but not on each other.
Does It Scale
At our scale (9 apps, 48 packages, 1 primary developer plus AI), the monorepo scales perfectly. The maintenance overhead is more than offset by the reuse rate.
The limits will probably appear around 20+ apps and 100+ packages, or when the team exceeds 10 people. At that point, a split into a federation of repos with published packages might become necessary. But we are not there yet.
What We Recommend
If you are building an ecosystem of related SaaS products, a monorepo with PNPM is an excellent choice. The conditions for success:
- Well-defined internal packages from the start
- Strict TypeScript as a safety net
- Autonomous deployment per app (no monolithic deploy)
- Strict conventions on imports and dependencies
- Git hooks to enforce quality
The monorepo is not a silver bullet. It is a multiplier. If your foundations are solid, it multiplies your productivity. If they are shaky, it multiplies your chaos.
Conclusion
Our OmniRealm monorepo hosts 9 SaaS products, 48 shared packages, and a 66% reuse rate. It allows us to launch a new product in 3 days instead of 3 weeks, with the same code quality and test coverage.
This is our technical competitive advantage. And everyone can achieve the same with the right tools and the right conventions.