OMAR
Field NotesCV
One Monorepo Per Client
← All Notes
Engineering24 March 2026 · 6 min read

One Monorepo Per Client

Public site, admin dashboard, API. Three apps that must agree on types and ship together. Splitting them across three repos is how small teams create integration bugs.

A typical client project of mine has three deployable pieces: a public site, a staff dashboard, and an API. They share types, they share validation rules, and they must agree with each other at every moment.

Split across three repositories, that agreement is a hope. In one repository it is enforced by the compiler.

The bug this prevents

The classic small-team integration failure goes like this. The API renames a field. The API repo is updated, tested, and deployed. The dashboard repo is untouched, so it still reads the old field name and now displays undefined on a screen nobody opened during testing.

Nothing failed loudly. No test broke, because each repository's tests passed against its own assumptions.

In a monorepo with shared types, that rename does not compile. The error appears in the same change, in the editor, before anything is deployed.

packages/
  shared/        # types, zod schemas, constants
apps/
  web/           # public site
  admin/         # staff dashboard
  api/           # express + prisma

Schemas are the shared artefact that matters

Types alone are compile-time only. The higher-value thing to share is the validation schema, because it is used at runtime on both ends:

// packages/shared/booking.ts
export const CreateBooking = z.object({
  serviceId: z.string().uuid(),
  date: z.coerce.date(),
  notes: z.string().max(500).optional()
})
export type CreateBooking = z.infer<typeof CreateBooking>

The API validates the request with it. The frontend validates the form with it. There is exactly one definition of what a valid booking is, so the two can never drift into disagreeing about it — which is the single most common source of "works locally, fails in production" on projects this size.

What actually goes in shared

Be strict about this or the shared package becomes a junk drawer that forces every app to rebuild whenever anything changes.

Belongs: types and schemas crossing the network boundary, domain constants (statuses, roles, currencies), and pure helpers with no environment assumptions — formatting money, computing a booking state transition.

Does not belong: React components (unless both frontends genuinely share a design system), anything importing process.env, and anything touching the database client.

Workspaces are enough

You do not need heavy tooling for three apps. npm workspaces handle it:

{
  "private": true,
  "workspaces": ["apps/*", "packages/*"]
}

One npm install at the root. One node_modules. Cross-package imports resolve by name. Add a build orchestrator later if builds get slow enough to justify the configuration — for three apps, they usually do not.

Deploying separate things from one repository

The objection I hear most is that a monorepo forces you to deploy everything together. It does not. Each app has its own build command and its own root directory, and every host I use supports pointing a deploy at a subdirectory.

What you do want is a filter so untouched apps are not rebuilt on every commit:

- uses: dorny/paths-filter@v3
  id: changes
  with:
    filters: |
      api: ['apps/api/**', 'packages/shared/**']
      web: ['apps/web/**', 'packages/shared/**']

Note that a change to shared rebuilds both. That is correct — a change to the contract affects everyone bound by it.

The handover argument

The reason I care about this most has nothing to do with build times. When a project ends, the client receives one repository containing everything: the site, the dashboard, the API, the schema, and the README.

Three repositories means three sets of access, three histories, and three chances for one of them to be forgotten until the day someone needs it. One repository is one thing to hand over and one thing to keep.

Choose the structure that makes disagreement between your apps impossible to compile, not the one that makes any single app marginally tidier.

When not to bother

A single site with no dashboard and no API does not need this. Neither do two apps built by two teams on separate release cycles, where the repository boundary is genuinely a coordination boundary.

The monorepo earns its place exactly when the same person ships all the parts and the parts must agree — which describes almost every client project a small studio takes on.

MonorepoTypeScriptPrismaDX

Need this built properly?

I build secure, fast, bilingual platforms for clients across Egypt, Saudi Arabia, the UAE and Kuwait.

Keep Reading