Back to blog
EngineeringTurborepoArchitecture

How we built a 12-package monorepo that powers 9 products

April 10, 20268 min read

The problem with building multiple products

When you're building a portfolio of software products, code duplication is the enemy. Every new product needs auth, billing, notifications, file storage, and internationalization. If you implement these from scratch for each product, you multiply your maintenance burden and your bug surface.

The solution is a monorepo with shared packages. But not just any monorepo — one where the build system is smart enough to only rebuild what changed.

Why Turborepo

We evaluated nx, Turborepo, and a custom shell script approach. Turborepo won because:

  • Remote caching: build artifacts are cached remotely. If @nixpx/auth didn"t change, it doesn"t rebuild — across every developer's machine and every CI run.
  • Affected-only builds: turbo build --filter=[HEAD^1] only builds packages touched since the last commit.
  • Task graph: build depends on ^build, meaning Turborepo automatically builds dependencies in the right order.
  • pnpm workspaces: native workspace linking with symlinks, no hoisting footguns.

The 12-package structure

Every Nixpx product inherits these packages:

`` packages/@nixpx/ ├── config ← TypeScript, ESLint, Tailwind, Prettier base configs ├── ui ← Component library + ThemeProvider (light/dark/system) ├── admin-ui ← AdminLayout, DataTable, AuditLogViewer ├── auth ← JWT RS256, Argon2id, OAuth, OTP ├── rbac ← 6 roles, 30 permissions, React hooks, server guards ├── db ← Prisma client, base schema (16 models), tenant middleware ├── billing ← Stripe, Paymob, Fawry, Tap, PayPal, license engine ├── notifications ← Resend email, Twilio SMS, FCM push ├── storage ← Cloudinary upload, transform, signed URLs ├── ai ← Claude API wrapper, streaming, prompt templates ├── i18n ← AR/EN/FR, RTL, formatters, Next.js locale routing ├── analytics ← Event tracking, MRR/ARR, retention cohorts └── onprem ← Docker Compose generator, RSA-2048 license engine `

The tenant middleware trick

The most powerful piece is the getTenantClient function in @nixpx/db. It wraps the Prisma client with a Prisma extension that injects tenantId into every query automatically:

`ts const tenantDb = getTenantClient(tenantId); const orders = await tenantDb.order.findMany(); // ↑ tenantId is injected — no chance of cross-tenant data leak ``

Combined with PostgreSQL Row-Level Security as a second layer, this gives us strong multi-tenant isolation with zero per-query boilerplate.

Results

  • Phase 01 (monorepo + all 12 packages): 3 weeks
  • Estimated Phase 04 (RestaurantOS full build): 4–6 weeks — because everything is inherited
  • Every subsequent vertical: 3–4 weeks — patterns are established
The investment in shared infrastructure pays back with every product we ship.

Nixpx

nixpx.com · Cairo, Egypt

← More posts