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/authdidn"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:
builddepends 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:
`` The most powerful piece is the
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
`
getTenantClientThe tenant middleware trick
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