BACK TO BLOG
2026-07-28Meghmalhar Bhowmick

Thirty Commits of Pure Monorepo Pain

TypeScriptMonorepoTurborepopnpmDeploymentLinuxDebugging

The Central Gardens web platform for the Rotary Club of Calcutta Central Gardens was my most ambitious deployment yet — a pnpm Turborepo monorepo with a shared database package, an Express API backend deploying to Render, and a React frontend deploying to Vercel. On my Windows laptop, everything built, ran, and worked perfectly.

Then I pushed to GitHub and kicked off CI.

Both pipelines immediately went red.

Not the same red. Different red. Multiple different reds.

Error 1: TypeScript can't find your package

The first thing Render told me:

error TS7016: Could not find a declaration file for module '@centralgardens/db'

The monorepo has a shared packages/db package that the Express backend (apps/api) imports as @centralgardens/db. On my machine this worked because TypeScript could read source files directly. On Render's build container, it was trying to resolve the package's type declarations — which didn't exist, because I never configured packages/db to emit them.

The fix was adding declaration emission to packages/db/tsconfig.json:

{
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true,
    "noEmitOnError": false,
    "outDir": "./dist"
  }
}

And adding explicit return type annotations to every exported function. TypeScript was previously inferring the return types of functions like createDbClient by reading Drizzle's complex generic internals. With noEmitOnError: false and explicit types, it could generate clean .d.ts files. Commit 4d658be: "fix: emit declarations with noEmitOnError false, explicit return type on createDbClient".

Error 2: pnpm hoisting in containers

With declaration files generating correctly, the Render build got further — and then crashed again. This time pnpm workspace symlinks were causing peer dependencies to resolve incorrectly in the container environment. Packages that apps/api legitimately depended on couldn't be found because pnpm's isolated node_modules structure doesn't hoist dependencies.

The fix: root-level .npmrc with one line:

shamefully-hoist=true

This forces pnpm to create a flat node_modules tree like npm's traditional structure, making all workspace packages visible to the container build tools without symlink resolution. Commit cd3b121: "fix: shamefully-hoist for render compatibility".

Named correctly. Shamefully.

Error 3: Windows lied about your filenames

With the backend deploying, I turned to the Vercel frontend. Fresh set of red. Different problem:

Module not found: Error: Can't resolve '@/components/layout/Footer'

On Windows, NTFS is case-insensitive. Footer.tsx and footer.tsx refer to the exact same file on disk. My imports said Footer.tsx but the actual files were footer.tsx, navbar.tsx, rootlayout.tsx. On my machine, vite build passed 100% without complaint. It happily matched the wrong case.

On Vercel (Linux, ext4 filesystem, strictly case-sensitive), those imports resolved to nothing. The build never had a chance.

The fix from the git diff:

apps/web/src/components/layout/{footer.tsx => Footer.tsx}
apps/web/src/components/layout/{navbar.tsx => Navbar.tsx}
apps/web/src/components/layout/{rootlayout.tsx => RootLayout.tsx}

Commit 2a8b118: "fix: correct file casing for Linux compatibility".

Error 4: CORS

After both apps deployed correctly, the frontend couldn't talk to the backend. Render had assigned the API a fresh deployment URL and it wasn't in the CORS allowlist. Commit b0e6ee5: "fix: add new vercel URL to CORS".

One line.

The commit message that tells the full story

Commit e4fdac3, somewhere in the middle of this entire ordeal: "plz work".

There was nothing technically interesting about that commit. It was just me at midnight, at the end of a long debugging session, hoping something would finally stick.

Lessons

Enable forceConsistentCasingInFileNames: true in your tsconfig.json before you start. Windows will deceive you about file paths. If you develop on Windows for Linux deployment targets, TypeScript's compiler can catch casing mismatches during local builds — but only if you ask it to.

Monorepos are separate compilation contracts. Adding a shared package to a monorepo means that package needs its own tsconfig, its own declaration emission, its own build step. You can't just import it and assume TypeScript will figure it out on a remote build container.

Container environments aren't your laptop. pnpm's default isolation is fine in development but can bite you in containerized builds where hoisting isn't available. Know your deployment target's dependency resolution behavior before you find out mid-CI.

[ GALLERY ]