Couldn’t deploy Next.js app with pnpm workspaces to Firebase app hosting

Summary

This postmortem details a deployment failure of a Next.js application within a pnpm workspace to Firebase App Hosting. The build process encountered two distinct phases of failure, leading to an incomplete build artifact and deployment termination. The root cause was a combination of a misconfigured build command order and missing Prisma client generation before TypeScript compilation.

Root Cause

The failure stemmed from two primary issues in the apphosting.yaml configuration:

  1. Incorrect Build Order: In the first attempt, the buildCommand executed pnpm install at the root level before the Prisma client was generated. This led to the error ERROR  Cannot convert undefined or null to object, indicating that pnpm encountered an invalid state or dependency resolution issue, likely due to missing schema references.
  2. Missing Prisma Generation Step: In the second attempt, the build command attempted to compile the backend TypeScript code but failed to generate the Prisma client first. This resulted in TS2307: Cannot find module errors, as the TypeScript compiler could not locate the generated Prisma types and client code.

Why This Happens in Real Systems

Complex build pipelines often involve multiple steps: dependency installation, code generation, and compilation. When these steps are orchestrated manually or via scripts, order dependency is critical.

  • Monorepo Complexity: In pnpm workspaces, dependencies are hoisted. A root-level install might skip local package scripts if not explicitly configured, or fail if a dependency relies on generated code that does not yet exist.
  • Implicit Dependencies: Build tools like Next.js or tsc often assume that necessary generated files (like Prisma clients) are present. If the generation step is omitted or runs too late in the process, compilation fails. Firebase App Hosting builds typically occur in isolated environments where cached artifacts are not persisted between builds unless explicitly configured.

Real-World Impact

The impact of this build failure is severe for development teams:

  • Deployment Blockage: The application cannot be deployed, preventing new features or bug fixes from reaching production.
  • Loss of Productivity: Developers must manually debug build configurations and experiment with different apphosting.yaml setups, consuming significant time.
  • Inconsistent Environments: The local development environment (where Prisma might be pre-generated) differs from the CI/CD environment, leading to the “it works on my machine” syndrome.

Example or Code

Invalid Configuration (First Attempt)

This configuration triggered the pnpm install error because the environment was not prepared for the workspace logic.

scripts:
  buildCommand: pnpm run build:word-library
  runCommand: pnpm run start:word-library
  runConfig:
    minInstances: 0

Invalid Configuration (Second Attempt)

This configuration triggered TypeScript compilation errors because prisma:generate was executed, but the build context (likely the cd word-library scope) prevented the backend generation or the generation artifacts were not accessible to the compilation step.

build:
  buildCommand: cd word-library && pnpm run prisma:generate && pnpm run build:word-library
  outputDirectory: word-library/.next
runCommand: pnpm run start:word-library

How Senior Engineers Fix It

Senior engineers address this by ensuring a deterministic and complete build sequence.

  1. Global Prisma Generation: Instead of running generation in a subdirectory context which might be isolated, run the Prisma generation from the root where the schema likely resides.
  2. Unified Build Script: Consolidate logic into a single robust script rather than chaining commands in YAML, ensuring proper error handling and sequential execution.
  3. Dependency Verification: Ensure that the buildCommand explicitly installs dependencies for the target app if necessary, but more importantly, ensures that generated artifacts (Prisma client) are available before the TypeScript compilation starts.

Corrected Configuration:

scripts:
  # Run Prisma generation from the root to ensure paths resolve correctly,
  # then build the specific application.
  buildCommand: pnpm run prisma:generate && pnpm --filter word-library run build
  runCommand: pnpm --filter word-library start
  runConfig:
    minInstances: 0

Why Juniors Miss It

Junior engineers often struggle with this specific class of problems for several reasons:

  • Assumption of Order: They often assume that dependencies listed in package.json are sufficient, not realizing that tools like Prisma require an explicit code generation step at runtime.
  • Scope Confusion: In monorepos, the distinction between root-level commands and package-level commands (e.g., cd word-library vs. pnpm --filter) is subtle but critical. Running a command in the wrong directory can break path resolution for imports.
  • Blind Replication: Juniors might copy build configurations from smaller, single-package projects without adapting them to the complexities of a monorepo structure, missing the need for workspace-aware tooling.